Skip to content

new interleaving prover/circuit matching the compiler/runtime - #232

Merged
ecioppettini merged 47 commits into
mainfrom
enzo/new-interleaving-prover
Oct 7, 2026
Merged

ecioppettini merged 47 commits into
mainfrom
enzo/new-interleaving-prover

Conversation

@ecioppettini

@ecioppettini ecioppettini commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

I wouldn't merge this yet, mainly because it depends on things in Nightstream that are unmerged yet (and some things that won't even be merged, since they were already rewritten in one of the ongoing branches)

However, I think it can be reviewed right now.

This adds mainly a "replacement" for starstream-interleaving-proof-legacy.

It also adds some missing things to the quint spec, since it was needed to get a few tests going.

Limitations:

  • No inputs yet (although technically set-storage is somewhat handled, but there are still multiple missing things)
  • The host-call scripts (which are called templates right now) are still WIP, and based somewhat on some heuristics. Eventually we'll probably need more compiler-generated metadata to infer some things.

@ecioppettini ecioppettini self-assigned this Sep 17, 2026
@ecioppettini
ecioppettini marked this pull request as ready for review September 23, 2026 03:09

@rvolosatovs rvolosatovs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can one try this out? Is it at a point where it could make sense to integrate into the ledger runtime?

proof: Box::default(), // TODO: add proof
(possibly just logging errors for now)

@ecioppettini

Copy link
Copy Markdown
Contributor Author

How can one try this out?

Well, right now only the tests (plus there will be a demo soonish)

Is it at a point where it could make sense to integrate into the ledger runtime?

Good question, maybe? I'll try it. In theory it should not be that far away. The only thing I'm still not really sure about is the Wizer part. This branch currently has support for inputs technically, in the circuit and instrumentation. But it's driven entirely though the set-storage call.

So to verify the proof, the ledger needs to be able to provide the same thing that is passed to set-storage, and check that it is the current storage (somehow).

(Also, while technically we support data segments in neo-wasm, there are some details that need to be sorted out because how it could be implemented for this depends on still unimplemented apis in Nightstream)

code.instructions()
.i32_const(0)
.return_call(self.current_resource.as_ref().unwrap().resource_new_fn)
.call(self.current_resource.as_ref().unwrap().resource_new_fn)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SpaceManiac thoughts on this?

I could technically get this to work, but it's less trivial than I originally I thought for a few reasons (tail calls into host functions rather than guest). Not really about proving it, but about tracing it. And for proving it I think the way to do it is to re-write it into this anyway, so it also doesn't change that much in that regard I think.

Comment thread starstream-ledger/src/client/runtime.rs Outdated

use bytes::{Bytes, BytesMut};
use sha2::{Digest as _, Sha256};
#[cfg(feature = "proving-instrumentation")]

@ecioppettini ecioppettini Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rvolosatovs could you take a quick look at this (the file, not this line)? there are still many things to do, but I wanted to at least sanity check this (and also since you asked about it)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do a deeper look tomorrow, but is there a particular reason we don't want to always enable this and just integrate this directly into call_coordination_script?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be by default I guess... mainly it was to not pull Nightstream unnecessarily, since it's not the smallest dep, but I guess for most people it will be already there

also partially because the ledger probably supports more things than the instrumentation/interleaving circuit (for example, I don't think the sha256 circuit would work right now), and I'm almost sure it will lag behind eventually, so I didn't want to block things

that said, we could remove the feature and just route things through a normal runtime flag I guess

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that said, we could remove the feature and just route things through a normal runtime flag I guess

and which could be on by default and just provide a bypass

because I think tracing errors shouldn't stop the execution, but missing things on the setup side could

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fear that with the feature sprinkled around the file we will inevitably break stuff, could we have just one call_coordination_script with a boolean flag?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I unified them, and made all the instrumentation + interleaving checking have its own result type instead (which also makes guest_debug that boolean flag, technically)

@rvolosatovs rvolosatovs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the first glance, this looks good to me overall, and I am wondering if we could just have one call_coordination_script function always producing the trace

Comment thread starstream-ledger/src/client/runtime.rs Outdated

use bytes::{Bytes, BytesMut};
use sha2::{Digest as _, Sha256};
#[cfg(feature = "proving-instrumentation")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do a deeper look tomorrow, but is there a particular reason we don't want to always enable this and just integrate this directly into call_coordination_script?

inputs,
outputs: tx_outputs,
events,
proof: Box::default(), // TODO: add proof

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we encode the proof and submit it to the ledger?

@ecioppettini ecioppettini Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not right now, or at least not in a way it makes sense

first of all currently this integration is not proving anything, it's just generating the witnesses and running them through the circuit equations

if there is something wrong it will fail (and so that thing is not provable right now)

but even if we do run the prover, the nightstream version we are pointing to right now doesn't expose an easy way of serializing the proof

and even if we do that (which is quite hardware intensive), it's not the actual proof we'd want here (it's not a succinct folding proof, that's currently not supported on main)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something we could just debug-print here with format!("{:?}")?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something we could just debug-print here with format!("{:?}")?

I was thinking we may want Transaction<CapturedExecution> and Transaction<Proof> tbh (plus there may be other things that could be parametrized like whether it is signed or something)

Or we can just have UnprovenTransaction and Transaction. With fn prove(UnprovenTransaction) -> Transaction.

The midnight codebase has something like this for example (Transaction<ProofPreimage> I think?), since it's not that rare to have an unproven transaction (for example, you can send it to a proof server, or you could merge it with another or idk).

Right now call_coordination_script returns wasmtime::Result<(Transaction, wasmtime::Result<CapturedExecution>)>

But I was considering putting the second element of the tuple in there directly. I didn't do it because I'd need to derive all the cbor traits for it, but I could do it if you think that could be useful.

Comment thread starstream-ledger/Cargo.toml Outdated
Comment thread starstream-ledger/Cargo.toml Outdated
Comment thread starstream-ledger/src/client/runtime.rs Outdated

use bytes::{Bytes, BytesMut};
use sha2::{Digest as _, Sha256};
#[cfg(feature = "proving-instrumentation")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fear that with the feature sprinkled around the file we will inevitably break stuff, could we have just one call_coordination_script with a boolean flag?

Comment thread starstream-ledger/src/client/runtime.rs Outdated
Comment on lines +366 to +367
| Type::Float32
| Type::Float64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we support floats

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't, but this is checking for scalar types... I guess I could remove them, the error would be misleading but it won't happen anyway

Comment thread starstream-ledger/src/client/runtime.rs Outdated
Comment on lines +375 to +377
let [root, rest @ ..] = components.as_slice() else {
unreachable!("root component is always registered");
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, I'd like to remove the need for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe done? or well... not sure, the split is gone, the unreachable is there, although I could just let it panic without message, unless I don't use a vec?


/// Register the contract's core module and create a single-step tracing store.
/// Neo-Wasm discovers each instance and captures its entry inputs automatically.
pub fn new_tracing_wasmtime_store<T: neo_wasm::WasmTraceSink + Send + 'static>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we rename this function to something like enable_tracing and take &mut Store<T> as parameter instead of calling Store::new?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Direct scalar/resource parameters each lower to _one_ core local.
// TODO: Support composite and indirectly lowered coordinator arguments.
// (the indexes of utxo inputs will be invalid as computed right now otherwise)
ensure!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we move this check to the top of the function to fail early?

Comment on lines +321 to +347
if let ResolvedCoordinationScriptArg::Utxo(input) = arg {
input_locals.push(index);
input_methods.push(
input
.output
.methods
.iter()
.map(|&(a, b, c, d)| {
starstream_proving_runtime::MethodHash::from_u64_words([a, b, c, d])
})
.collect::<Vec<_>>(),
);
let (_, instrumented) = wizer.instrument_component(&input.wasm)?;
let scripts = input
.contract
.coordination_scripts()
.map(|(name, export)| export.map(|_| name))
.collect::<wasmtime::Result<Vec<_>>>()?;
let templates =
build_component_templates(&instrumented, &scripts).map_err(|error| {
wasmtime::format_err!("failed to build input trace templates: {error}")
})?;
components.push(TracingComponent {
instrumented,
templates,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we do this while iterating the args during resolution?
If we unify all of this in call_coordination_script we probably also would not need the ResolvedCoordinationScriptArg enum

Comment thread starstream-ledger/src/client/runtime.rs Outdated
Comment on lines +292 to +293
script: &str,
export: &CoordinationScriptExport,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add a TODO here to only require one of these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, I put the name in CoordinationScriptExport now... I don't see another easy way of recovering it otherwise

Comment thread starstream-runtime-next/src/lib.rs Outdated
Comment on lines +1975 to +1980
// tracked for the proving instrumentation
//
// we use this as a key into the core-wasm function namespace, since
// wasmtime doesn't expose enough information to get the core module id for
// the export otherwise
name: String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// tracked for the proving instrumentation
//
// we use this as a key into the core-wasm function namespace, since
// wasmtime doesn't expose enough information to get the core module id for
// the export otherwise
name: String,
name: Arc<str>,

Comment on lines +11 to +12
#[allow(dead_code)]
mod common;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(dead_code)]
mod common;
pub mod common;

Comment on lines +60 to +71
enum ResolvedCoordinationScriptArg {
Val(Val),
Utxo(ResolvedUtxoInput),
}

struct ResolvedUtxoInput {
input: TransactionInput,
output: TransactionOutput,
contract: starstream_runtime_next::Contract<Ctx>,
external_id: Option<Arc<str>>,
wasm: Vec<u8>,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l still feel like these should not be necessary, if we just inline everything in call_coordination_script, at least to begin with, we can always refactor later.

That would also minimize the diff in the PR making it easier to review

@rvolosatovs rvolosatovs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ledger/runtime part looks good to me, and I am happy to refactor things later

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
…ected)

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
add a few more tests in test.qnt

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
it makes it much easier to the circuit to just ignore a duplicated
registration than to forbid it, and it shouldn't change things
semantically (although performance may degrade)

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
…call-method)

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
…ents

(limited to at most one coord script)

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
…o mapping

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
provides instrumentation on top of the runtime, plus host-abi
interleaving semantics/embeddings

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
… zeroing from the templates

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
switch the proving paths to the audit path (there is no NIFS path
currently implemented in main)

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
instrumenting tail calls into host functions has quite a few corner
cases (for neo-wasm), this should eventually be supported (although it
may very well be by re-writing into call . return), but in the meantime
this is a much simpler change (and it shouldn't change much in terms of
performance at least)

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
…habetically

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
and make it receive a mutable Store

also remove the implicit root component registration

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
… functions

rather than adding a flag, the instrumentation + tracing + circuit
checks have an independent error from the normal execution, which can be
ignored by the caller

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
…xport

Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
@ecioppettini
ecioppettini force-pushed the enzo/new-interleaving-prover branch from baa3b8b to cf1b145 Compare October 7, 2026 14:45
Signed-off-by: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com>
@ecioppettini
ecioppettini merged commit 2e9a9be into main Oct 7, 2026
7 checks passed
@ecioppettini
ecioppettini deleted the enzo/new-interleaving-prover branch October 7, 2026 15:53
rvolosatovs added a commit that referenced this pull request Oct 7, 2026
Address the review comments on #232 that were left open and bring the
ledger and runtime back close to their shape before that PR:

- drop the single-use `resolve_coordination_script_args`,
  `prepare_tracing`, `postprocess_trace`,
  `run_resolved_coordination_script` and `tracing_components` helpers
  together with the `ResolvedCoordinationScriptArg`, `ResolvedUtxoInput`,
  `TracingComponent` and `TracingSetup` types; the original argument loop
  now registers each input component and records its parameter index and
  method hashes in place, so the input Wasm is no longer copied
- keep one `register_tracing` helper shared by the root contract, its
  imports and the loaded inputs
- remove the `unreachable!` on the root component and the float types
  from the direct-argument check
- use `.context` instead of `format_err!` closures and fix the literal
  `{error}` in the trace capture context
- only do the tracing work when `enable_tracing` succeeds; its error is
  surfaced through the returned execution result as before
- `CoordinationScriptExport::name` is an `Arc<str>` and `name()` returns
  a reference to it

Assisted-by: anthropic:claude-fable-5-1
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
rvolosatovs added a commit that referenced this pull request Oct 7, 2026
Address the review comments on #232 that were left open and bring the
ledger and runtime back close to their shape before that PR:

- drop the single-use `resolve_coordination_script_args`,
  `prepare_tracing`, `postprocess_trace`,
  `run_resolved_coordination_script` and `tracing_components` helpers
  together with the `ResolvedCoordinationScriptArg`, `ResolvedUtxoInput`,
  `TracingComponent` and `TracingSetup` types; the original argument loop
  now registers each input component and records its parameter index and
  method hashes in place, so the input Wasm is no longer copied
- keep one `register_tracing` helper shared by the root contract, its
  imports and the loaded inputs
- remove the `unreachable!` on the root component and the float types
  from the direct-argument check
- use `.context` instead of `format_err!` closures and fix the literal
  `{error}` in the trace capture context
- only do the tracing work when `enable_tracing` succeeds; its error is
  surfaced through the returned execution result as before
- `CoordinationScriptExport::name` is an `Arc<str>` and `name()` returns
  a reference to it

Assisted-by: anthropic:claude-fable-5-1
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
rvolosatovs added a commit that referenced this pull request Oct 8, 2026
Address the review comments on #232 that were left open and bring the
ledger and runtime back close to their shape before that PR:

- drop the single-use `resolve_coordination_script_args`,
  `prepare_tracing`, `postprocess_trace`,
  `run_resolved_coordination_script` and `tracing_components` helpers
  together with the `ResolvedCoordinationScriptArg`, `ResolvedUtxoInput`,
  `TracingComponent` and `TracingSetup` types; the original argument loop
  now registers each input component and records its parameter index and
  method hashes in place, so the input Wasm is no longer copied
- keep one `register_tracing` helper shared by the root contract, its
  imports and the loaded inputs
- build the root trace templates before enabling tracing; input and
  import templates are built while tracing is already enabled and a
  failure there leaves the store single-stepping for the rest of the
  call, which is fine since every transaction will produce a trace
  eventually
- register the imports resolved for the root together with it, and the
  imports each input pulls in before that input is loaded, since the
  registry refuses a module that already executed untraced
- remove the `unreachable!` on the root component
- use `.context` instead of `format_err!` closures and fix the literal
  `{error}` in the trace capture context
- only do the tracing work when `enable_tracing` succeeds; its error is
  surfaced through the returned execution result as before, which the
  doc comment now explains
- `CoordinationScriptExport::name` is an `Arc<str>` and `name()` returns
  a reference to it

Assisted-by: anthropic:claude-fable-5-1
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
rvolosatovs added a commit that referenced this pull request Oct 8, 2026
Simplify the ledger tracing integration, bringing the ledger client
runtime closer to pre-#232 state and minimizing the diff

```diff
diff --git a/starstream-ledger/src/client/runtime.rs b/starstream-ledger/src/client/runtime.rs
index 64295f7..66cf74e 100644
--- a/starstream-ledger/src/client/runtime.rs
+++ b/starstream-ledger/src/client/runtime.rs
@@ -1,4 +1,5 @@
 use core::mem;
+use core::ops::Deref;
 use core::pin::Pin;
 
 use std::collections::{BTreeSet, HashMap, HashSet};
@@ -6,6 +7,11 @@ use std::sync::Arc;
 
 use bytes::{Bytes, BytesMut};
 use sha2::{Digest as _, Sha256};
+use starstream_proving_runtime::{
+    CapturedExecution, ComponentTemplates, WasmTraceSink, WasmtimeTraceRegistry,
+    build_component_templates, check_captured_transaction, enable_tracing,
+    register_tracing_component,
+};
 use starstream_runtime::bindings::starstream;
 use starstream_runtime::{
     CoordinationScriptExport, CoordinationScriptImport, Token, Utxo, UtxoExport, UtxoImport,
@@ -25,22 +31,24 @@ use crate::{
     Transaction, TransactionEvent, TransactionInput, TransactionOutput, encode_digest, parse_digest,
 };
 
-pub fn compile_component(
-    engine: &Engine,
-    wizer: &Wizer,
-    wasm: &[u8],
-) -> wasmtime::Result<Component> {
-    let (_wizer_cx, instrumented) = wizer
-        .instrument_component(wasm)
-        .context("failed to instrument component")?;
-    let component = wasmtime::component::Component::from_binary(engine, &instrumented)
-        .context("failed to compile component")?;
-    Ok(component)
+/// Contract compiled from the instrumented component bytes
+#[derive(Clone)]
+pub struct CompiledContract {
+    pub contract: starstream_runtime::Contract<Ctx>,
+    pub instrumented: Bytes,
+}
+
+impl Deref for CompiledContract {
+    type Target = starstream_runtime::Contract<Ctx>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.contract
+    }
 }
 
 #[derive(Clone)]
 pub struct Contract {
-    pub contract: Option<starstream_runtime::Contract<Ctx>>,
+    pub contract: Option<CompiledContract>,
     pub wasm: Bytes,
 }
 
@@ -53,13 +61,15 @@ pub trait Client {
     ) -> impl Future<Output = anyhow::Result<TransactionOutput>>;
 }
 
-pub async fn new_contract(
+/// Instrument and compile `wasm`, compiling the imports missing from `imports` via `client`
+pub async fn compile_contract(
     client: &(impl Client + ?Sized),
+    engine: &Engine,
     wizer: &Wizer,
-    component: &Component,
+    wasm: &[u8],
     external_id: Option<&str>,
     imports: &mut HashMap<[u8; 32], Contract>,
-) -> wasmtime::Result<starstream_runtime::Contract<Ctx>> {
+) -> wasmtime::Result<CompiledContract> {
     struct ContractLookup<'a>(pub &'a HashMap<[u8; 32], Contract>);
     impl starstream_runtime::ContractLookup<Ctx> for ContractLookup<'_> {
         fn get_contract(
@@ -71,14 +81,19 @@ pub async fn new_contract(
                 error!(external_id, "unresolved contract import");
                 format!("contract identified by `external-id` `{external_id}` not found")
             })?;
-            contract.contract.clone().with_context(|| {
+            let contract = contract.contract.as_ref().with_context(|| {
                 error!(external_id, "uncompiled contract import");
                 format!("contract identified by `external-id` `{external_id}` was not compiled")
-            })
+            })?;
+            Ok(contract.contract.clone())
         }
     }
 
-    let engine = component.engine();
+    let (.., instrumented) = wizer
+        .instrument_component(wasm)
+        .context("failed to instrument component")?;
+    let component =
+        Component::from_binary(engine, &instrumented).context("failed to compile component")?;
     let ty = component.component_type();
     let script_instance = get_coordination_script_instance_import(engine, &ty);
     let script_external_ids = script_instance.as_ref().map(|instance| {
@@ -106,11 +121,11 @@ pub async fn new_contract(
                 .await
                 .map_err(wasmtime::Error::from_anyhow)?,
         };
-        let component = compile_component(engine, wizer, wasm.as_ref())?;
-        let contract = Box::pin(new_contract(
+        let contract = Box::pin(compile_contract(
             client,
+            engine,
             wizer,
-            &component,
+            &wasm,
             Some(external_id),
             imports,
         ))
@@ -123,27 +138,112 @@ pub async fn new_contract(
             },
         );
     }
-    starstream_runtime::Contract::new(component, external_id, ContractLookup(imports))
+    let contract =
+        starstream_runtime::Contract::new(&component, external_id, ContractLookup(imports))?;
+    Ok(CompiledContract {
+        contract,
+        instrumented: instrumented.into(),
+    })
+}
+
+fn build_templates(contract: &CompiledContract) -> wasmtime::Result<ComponentTemplates> {
+    let scripts = contract
+        .contract
+        .coordination_script_names()
+        .collect::<Vec<_>>();
+    build_component_templates(&contract.instrumented, &scripts)
+        .context("failed to build trace templates")
+}
+
+fn register_tracing(cx: &mut Ctx, contract: &CompiledContract) -> wasmtime::Result<()> {
+    let templates = build_templates(contract)?;
+    register_tracing_component(cx, &contract.instrumented, &templates.bindings)
 }
 
+/// Register compiled imports that have not been registered yet
+fn register_imports(
+    cx: &mut Ctx,
+    imports: &HashMap<[u8; 32], Contract>,
+    registered: &mut HashSet<[u8; 32]>,
+) -> wasmtime::Result<()> {
+    for (digest, Contract { contract, .. }) in imports {
+        let Some(contract) = contract else {
+            continue;
+        };
+        if registered.insert(*digest) {
+            register_tracing(cx, contract)?;
+        }
+    }
+    Ok(())
+}
+
+/// Call the coordination script `export` exported by `contract` with `args`,
+/// loading UTXO arguments through `client`.
+///
+/// `wasm` must be the original component bytes `contract` was compiled from.
+///
+/// The execution check is best-effort for now, mainly to not block execution
+/// on configurations not supported by the current instrumentation, so its
+/// result is returned next to the transaction and it is up to the caller to
+/// report the error, if any.
 #[allow(clippy::too_many_arguments)]
 pub async fn call_coordination_script(
     store: &mut Store<Ctx>,
     client: &(impl Client + ?Sized),
     wizer: &Wizer,
-    contract: &starstream_runtime::Contract<Ctx>,
+    contract: &CompiledContract,
     wasm: &[u8],
     export: &CoordinationScriptExport,
     imports: &mut HashMap<[u8; 32], Contract>,
     args: impl IntoIterator<Item = CoordinationScriptArg>,
     results: &mut [Val],
     utxos: &mut Vec<Utxo<Arc<std::sync::Mutex<UtxoCtx>>>>,
-) -> wasmtime::Result<Transaction> {
+) -> wasmtime::Result<(Transaction, wasmtime::Result<CapturedExecution>)> {
     let engine = contract.component().engine();
+    let digest: [u8; 32] = Sha256::digest(wasm).into();
+    let args = args.into_iter().collect::<Vec<_>>();
+    let mut registered = HashSet::from([digest]);
+    let mut tracing = (|| {
+        if args
+            .iter()
+            .any(|arg| matches!(arg, CoordinationScriptArg::Utxo(_)))
+        {
+            // Direct scalar/resource parameters each lower to _one_ core local.
+            // TODO: Support composite and indirectly lowered coordinator arguments.
+            ensure!(
+                export.ty().params().len() <= 16
+                    && export.ty().params().all(|(_, ty)| matches!(
+                        ty,
+                        Type::Bool
+                            | Type::S8
+                            | Type::U8
+                            | Type::S16
+                            | Type::U16
+                            | Type::S32
+                            | Type::U32
+                            | Type::S64
+                            | Type::U64
+                            | Type::Float32
+                            | Type::Float64
+                            | Type::Char
+                            | Type::Own(_)
+                            | Type::Borrow(_)
+                    )),
+                "traced input loading requires direct scalar/resource arguments"
+            );
+        }
+        let templates = build_templates(contract)?;
+        enable_tracing(store)?;
+        let cx = store.data_mut();
+        register_tracing_component(cx, &contract.instrumented, &templates.bindings)?;
+        register_imports(cx, imports, &mut registered)?;
+        Ok(templates)
+    })();
     let instance = contract.instantiate(&mut *store).await?;
 
-    let digest = Sha256::digest(wasm).into();
     let mut inputs = Vec::default();
+    let mut input_locals = Vec::default();
+    let mut input_methods = Vec::default();
     let mut params = Vec::with_capacity(export.ty().params().len());
     let mut args = args.into_iter();
     for (name, _ty) in export.ty().params() {
@@ -163,19 +263,16 @@ pub async fn call_coordination_script(
                 let (external_id, wasm) = if utxo_contract_digest == digest {
                     let wasm =
                         apply_state(wasm, &utxo.state).map_err(wasmtime::Error::from_anyhow)?;
-                    (None, Bytes::from(wasm))
+                    (None, wasm)
                 } else if let Some(Contract { wasm, .. }) = imports.get(&utxo_contract_digest) {
                     let wasm =
                         apply_state(wasm, &utxo.state).map_err(wasmtime::Error::from_anyhow)?;
-                    (Some(Arc::from(utxo.contract)), Bytes::from(wasm))
+                    (Some(Arc::from(utxo.contract)), wasm)
                 } else {
                     let wasm = client
                         .get_contract_wasm(utxo_contract_digest)
                         .await
                         .map_err(wasmtime::Error::from_anyhow)?;
-                    let wasm =
-                        apply_state(&wasm, &utxo.state).map_err(wasmtime::Error::from_anyhow)?;
-                    let wasm = Bytes::from(wasm);
                     imports.insert(
                         utxo_contract_digest,
                         Contract {
@@ -183,17 +280,34 @@ pub async fn call_coordination_script(
                             wasm: wasm.clone(),
                         },
                     );
+                    let wasm =
+                        apply_state(&wasm, &utxo.state).map_err(wasmtime::Error::from_anyhow)?;
                     (Some(Arc::from(utxo.contract)), wasm)
                 };
-                let component = compile_component(engine, wizer, &wasm)?;
-                let contract = new_contract(
+                let contract = compile_contract(
                     client,
+                    engine,
                     wizer,
-                    &component,
+                    &wasm,
                     external_id.as_deref(),
                     &mut *imports,
                 )
                 .await?;
+                tracing = tracing.and_then(|templates| {
+                    let cx = store.data_mut();
+                    register_tracing(cx, &contract)?;
+                    register_imports(cx, imports, &mut registered)?;
+                    Ok(templates)
+                });
+                input_locals.push(params.len());
+                input_methods.push(
+                    utxo.methods
+                        .iter()
+                        .map(|&(a, b, c, d)| {
+                            starstream_proving_runtime::MethodHash::from_u64_words([a, b, c, d])
+                        })
+                        .collect::<Vec<_>>(),
+                );
                 let utxo_export = contract.get_utxo(&utxo.instance)?;
                 let storage_export = utxo_export.storage().context("UTXO has no storage")?;
                 let mut storage = Val::Record(Vec::default());
@@ -252,7 +366,7 @@ pub async fn call_coordination_script(
             cx.clone()
         };
         let mut instance = WasmtimeWizerComponent {
-            store: &mut *store,
+            store,
             instance: utxo.instance(),
         };
         let (contract, wasm) = if let Some(external_id) = cx.external_id.as_deref() {
@@ -295,12 +409,46 @@ pub async fn call_coordination_script(
             state,
         });
     }
-    Ok(Transaction {
-        inputs,
-        outputs: tx_outputs,
-        events,
-        proof: Box::default(), // TODO: add proof
-    })
+    let execution = tracing.and_then(|templates| {
+        let mut handles = Vec::with_capacity(input_locals.len());
+        if !input_locals.is_empty() {
+            // The root coordinator is instantiated before input UTXOs;
+            // the registry iterates in instance-index order.
+            let (.., coordinator) = store
+                .data()
+                .traces
+                .instances()
+                .context("failed to capture trace")?
+                .next()
+                .context("missing coordinator trace")?;
+            let fref = templates
+                .export_fref(export.name())
+                .context("missing coordinator export")?;
+            let entry = coordinator
+                .steps()
+                .iter()
+                .find(|step| step.current_function_ref == Some(fref))
+                .context("missing coordinator entry arguments")?;
+            for local in input_locals {
+                let &(handle, ..) = entry
+                    .locals_words
+                    .get(local)
+                    .context("missing input handle local")?;
+                handles.push(starstream_proving_runtime::ResourceHandle(handle));
+            }
+        }
+        // TODO: Authenticate these input handles against the coordinator argument root.
+        check_captured_transaction(&store.data().traces, handles, &input_methods)
+    });
+    Ok((
+        Transaction {
+            inputs,
+            outputs: tx_outputs,
+            events,
+            proof: Box::default(), // TODO: add proof
+        },
+        execution,
+    ))
 }
 
 #[derive(Debug, Default)]
@@ -308,6 +456,7 @@ pub struct Ctx {
     pub table: ResourceTable,
     pub events: Vec<TransactionEvent>,
     pub outputs: Vec<starstream_runtime::Utxo<<Self as starstream_runtime::Host>::UtxoContext>>,
+    pub traces: WasmtimeTraceRegistry,
 }
 
 #[derive(Clone, Debug)]
@@ -323,6 +472,16 @@ pub fn lock<T>(mu: &std::sync::Mutex<T>) -> wasmtime::Result<std::sync::MutexGua
     mu.lock().map_err(|err| format_err!("{err}"))
 }
 
+impl WasmTraceSink for Ctx {
+    fn wasm_trace_registry(&self) -> &WasmtimeTraceRegistry {
+        &self.traces
+    }
+
+    fn wasm_trace_registry_mut(&mut self) -> &mut WasmtimeTraceRegistry {
+        &mut self.traces
+    }
+}
+
 impl starstream::std::cardano::Host for Ctx {
     fn block_height(&mut self) -> wasmtime::Result<i64> {
         bail!("TODO")

```

---------

Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants