diff --git a/Cargo.lock b/Cargo.lock index 2b6c37c4..3e2548c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7617,6 +7617,7 @@ dependencies = [ "bitcoin", "borsh", "serde", + "serde_json", "ssz", "ssz_codegen", "ssz_derive", @@ -7725,6 +7726,7 @@ dependencies = [ "serde_json", "ssz", "ssz_derive", + "strata-asm-common", "strata-asm-proto-bridge-v1-types", "strata-btc-types", "strata-btc-verification", diff --git a/bin/asm-runner/src/bootstrap.rs b/bin/asm-runner/src/bootstrap.rs index 0c29f1ed..75d486b6 100644 --- a/bin/asm-runner/src/bootstrap.rs +++ b/bin/asm-runner/src/bootstrap.rs @@ -74,16 +74,16 @@ pub(crate) async fn bootstrap( // on a runtime worker thread here, so wrap the build in `block_in_place` to allow blocking; // the worker's own loop runs on a dedicated sync thread where blocking is already fine. let asm_worker = task::block_in_place(|| { - AsmWorkerBuilder::new() + AsmWorkerBuilder::<_, StrataAsmSpec>::new() .with_context(worker_context) - .with_asm_spec(StrataAsmSpec) - .with_params(params.clone()) + .with_params(params) .launch(&executor) })?; let asm_worker = Arc::new(asm_worker); // 6. Finish orchestrator wiring if it was configured. + let genesis_block = asm_worker.genesis_block(); let proof_rpc_deps = if let Some((orch_config, proof_db, moho_state_db, backend)) = orch_prep { let rpc_deps = AsmProofRpcDeps { proof_db: proof_db.clone(), @@ -118,7 +118,7 @@ pub(crate) async fn bootstrap( let moho_worker = MohoWorkerBuilder::new() .with_context(moho_context) .with_subscription(asm_worker.subscribe_blocks()) - .with_genesis_block(params.anchor.block) + .with_genesis_block(genesis_block) .with_asm_predicate(asm_predicate.clone()) .launch(&executor) .await?; @@ -133,7 +133,7 @@ pub(crate) async fn bootstrap( aux_db.clone(), bitcoin_client.clone(), ); - let input_builder = InputBuilder::new(params.anchor.block, asm_predicate, moho_predicate); + let input_builder = InputBuilder::new(genesis_block, asm_predicate, moho_predicate); // Drive the prover from the *Moho* worker's commit stream, not the ASM // worker's: the Moho worker emits a block only after it has persisted diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 87df6064..57a7c602 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -32,5 +32,6 @@ zkaleido-logging.workspace = true ssz_codegen.workspace = true [dev-dependencies] +serde_json.workspace = true strata-identifiers.workspace = true strata-test-utils-arb.workspace = true diff --git a/crates/common/src/fork.rs b/crates/common/src/fork.rs new file mode 100644 index 00000000..8cf8e882 --- /dev/null +++ b/crates/common/src/fork.rs @@ -0,0 +1,221 @@ +//! Fork-based upgradeability primitives. +//! +//! The ASM upgrades EVM-style: STF logic is gated on named forks with L1 +//! activation heights, so a single binary can execute both sides of an upgrade +//! boundary. The schedule is *not* part of committed state — it is baked into +//! each proving artifact (guest ELF / native host) and supplied to the worker +//! via params, with the invariant that every artifact agrees on the gate's +//! outcome at every height it executes (see `AsmStfParams`). + +use serde::{Deserialize, Serialize}; +use strata_identifiers::L1Height; + +/// Identifies a named fork. +/// +/// One variant per protocol upgrade, in activation order. The numeric +/// discriminant is the stable identity: it keys persisted fork-activation +/// records (stored as the raw discriminant byte) and is the raw id carried in +/// ASM VK upgrade actions. Neither path goes through this type's serde, so a +/// persisted record or an in-flight action is unaffected by a variant being +/// renamed — the byte and the id stay the same. +/// +/// The variant name is the human-readable form: it is this type's serde +/// representation (snake_case) and is mirrored by the [`ForkSchedule`] params +/// field. Names are meant to change — `Fork1` is a placeholder for an upgrade +/// not yet defined — so renaming one once defined is a routine migration of the +/// human-facing config, leaving persisted records and wire actions untouched. +/// +/// The id crosses two boundaries with opposite tolerances: +/// +/// - Parse-time: ASM VK upgrade actions carry the raw id, not this enum, so an artifact predating a +/// fork can still parse and enact the upgrade that activates it — the wire format never requires +/// knowing the fork. +/// - Act-time: a consumer that must *apply* the fork's rules (the worker) maps the id via +/// [`TryFrom`]. An id it does not know is not skipped: it means the worker is running old +/// software past an upgrade it cannot execute, so it MUST halt rather than silently limp along on +/// stale rules. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[repr(u8)] +pub enum ForkId { + /// Placeholder for the first protocol upgrade; renamed once that upgrade is + /// defined. The rename is a migration of the human-readable name (this + /// variant's serde form and the [`ForkSchedule`] params field); persisted + /// records and wire actions key on the numeric discriminant and are + /// unaffected. + Fork1 = 0, +} + +impl From for u8 { + fn from(fork: ForkId) -> Self { + fork as u8 + } +} + +impl From for u16 { + fn from(fork: ForkId) -> Self { + fork as u16 + } +} + +impl TryFrom for ForkId { + type Error = u8; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(ForkId::Fork1), + invalid => Err(invalid), + } + } +} + +impl TryFrom for ForkId { + type Error = u16; + + fn try_from(value: u16) -> Result { + u8::try_from(value) + .ok() + .and_then(|v| ForkId::try_from(v).ok()) + .ok_or(value) + } +} + +/// Activation heights for every named fork. +/// +/// A fork with activation height `Some(n)` is active at L1 height `h` iff +/// `h >= n` — so `Some(0)` means active since genesis. `None` means disabled. +/// Proving artifacts bake one of the two extremes (`Some(0)` or `None` — an +/// artifact only ever executes one side of an upgrade boundary), while the +/// worker tracks the real activation height discovered from the ASM VK +/// upgrade log. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ForkSchedule { + /// Activation height of [`ForkId::Fork1`], or `None` if disabled. + pub fork1: Option, +} + +impl ForkSchedule { + /// Schedule with every fork disabled (no activation height). + pub const fn all_disabled() -> Self { + Self { fork1: None } + } + + /// Returns the activation height of `fork`, or `None` if disabled. + pub fn activation_height_of(&self, fork: ForkId) -> Option { + match fork { + ForkId::Fork1 => self.fork1, + } + } + + /// Returns whether `fork` is active at L1 `height`. + pub fn is_active(&self, fork: ForkId, height: L1Height) -> bool { + self.activation_height_of(fork) + .is_some_and(|activation| height >= activation) + } + + /// Sets the activation height of `fork`. + pub fn set_fork_activation(&mut self, fork: ForkId, height: L1Height) { + match fork { + ForkId::Fork1 => self.fork1 = Some(height), + } + } +} + +impl Default for ForkSchedule { + fn default() -> Self { + Self::all_disabled() + } +} + +/// Protocol-rule parameters consumed by the STF, as opposed to the genesis +/// params that only seed the initial state. +/// +/// Every executor of the STF carries its own copy: guest programs hardcode it +/// (so the proof's verifying key commits to it), native proving hosts bake it +/// into their closure, and the worker derives an effective copy from params +/// plus discovered fork activations. +/// +/// `Default` inherits [`ForkSchedule`]'s default: everything disabled. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AsmStfParams { + /// Fork activation schedule. + pub forks: ForkSchedule, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_active_boundaries() { + let sched = ForkSchedule { fork1: Some(100) }; + assert!(!sched.is_active(ForkId::Fork1, 99)); + assert!(sched.is_active(ForkId::Fork1, 100)); + assert!(sched.is_active(ForkId::Fork1, 101)); + } + + #[test] + fn zero_means_always_active() { + let sched = ForkSchedule { fork1: Some(0) }; + assert!(sched.is_active(ForkId::Fork1, 0)); + assert!(sched.is_active(ForkId::Fork1, L1Height::MAX)); + } + + #[test] + fn none_means_never_active() { + let sched = ForkSchedule::all_disabled(); + assert_eq!(sched.activation_height_of(ForkId::Fork1), None); + assert!(!sched.is_active(ForkId::Fork1, 0)); + assert!(!sched.is_active(ForkId::Fork1, L1Height::MAX)); + } + + #[test] + fn set_fork_activation_overrides() { + let mut sched = ForkSchedule::all_disabled(); + sched.set_fork_activation(ForkId::Fork1, 42); + assert_eq!(sched.activation_height_of(ForkId::Fork1), Some(42)); + assert!(sched.is_active(ForkId::Fork1, 42)); + assert!(!sched.is_active(ForkId::Fork1, 41)); + } + + #[test] + fn serde_roundtrip() { + let params = AsmStfParams { + forks: ForkSchedule { fork1: Some(7) }, + }; + let json = serde_json::to_string(¶ms).unwrap(); + assert_eq!(json, r#"{"forks":{"fork1":7}}"#); + let back: AsmStfParams = serde_json::from_str(&json).unwrap(); + assert_eq!(back, params); + + let disabled = AsmStfParams { + forks: ForkSchedule::all_disabled(), + }; + let json = serde_json::to_string(&disabled).unwrap(); + assert_eq!(json, r#"{"forks":{"fork1":null}}"#); + let back: AsmStfParams = serde_json::from_str(&json).unwrap(); + assert_eq!(back, disabled); + } + + /// Serde is the human-readable form (the variant name). The stable numeric + /// identity used for persistence and the wire is exercised by + /// [`fork_id_u16_roundtrip`] instead. + #[test] + fn fork_id_serde_is_the_variant_name() { + assert_eq!(serde_json::to_string(&ForkId::Fork1).unwrap(), r#""fork1""#); + assert_eq!( + serde_json::from_str::(r#""fork1""#).unwrap(), + ForkId::Fork1 + ); + assert!(serde_json::from_str::(r#""nope""#).is_err()); + } + + /// Raw fork ids on the wire round-trip through the enum; unknown ids + /// surface as errors instead of misparsing. + #[test] + fn fork_id_u16_roundtrip() { + assert_eq!(u16::from(ForkId::Fork1), 0); + assert_eq!(ForkId::try_from(0u16).unwrap(), ForkId::Fork1); + assert_eq!(ForkId::try_from(0xFFFFu16), Err(0xFFFF)); + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 1134ea44..b1c950ec 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,6 +4,7 @@ mod aux; mod constants; mod errors; +mod fork; mod log; mod manifest; mod mmr; @@ -28,6 +29,7 @@ mod ssz_generated { pub use aux::*; pub use constants::*; pub use errors::*; +pub use fork::*; pub use log::*; pub use manifest::*; pub use mmr::*; diff --git a/crates/common/src/spec.rs b/crates/common/src/spec.rs index ba2d6e50..340012a5 100644 --- a/crates/common/src/spec.rs +++ b/crates/common/src/spec.rs @@ -1,34 +1,83 @@ -use crate::{AnchorState, Subprotocol}; +use std::fmt::Debug; -/// Specification for a concrete ASM instantiation describing the subprotocols we -/// want to invoke and in what order. +use crate::{AnchorState, AsmStfParams, Subprotocol}; + +/// Specification for a concrete ASM instantiation: the subprotocols we intend +/// to invoke and the order to invoke them in, plus the parameter set an +/// instance is configured with. /// -/// This way, we only have to declare the subprotocols a single time and they -/// will always be processed in a consistent order as defined by an `AsmSpec`. +/// The pipeline is declared as a type-level list rather than a method so the +/// invocation order is a compile-time constant of the spec type: it cannot +/// vary per stage, per execution, or with any runtime configuration — the +/// determinism the STF requires. Everything that must traverse the +/// subprotocols (loading, processing, finishing, genesis construction) drives +/// off this single declaration. pub trait AsmSpec { - /// The parameters type used to construct the genesis state. - type Params; + /// The subprotocols processed by this ASM, in invocation order. + type Subprotocols: SubprotoList; - /// Function that calls the stage with each subprotocol we intend to - /// process, in the order we intend to process them. + /// The full parameter set an instance of this ASM is configured with. /// - /// This MUST NOT change its behavior depending on the stage we're - /// processing. - fn call_subprotocols(&self, stage: &mut impl Stage); + /// Owning the params type ties everything derived from configuration — + /// the genesis layout, the STF params — to the same spec that declares + /// the pipeline, so a worker instantiated with this spec cannot be handed + /// a genesis state built for a different one. + type Params: Debug; - /// Builds the genesis [`AnchorState`] from the given parameters. - fn construct_genesis_state(&self, params: &Self::Params) -> AnchorState; + /// Builds the genesis anchor state from the params. + fn construct_genesis_state(params: &Self::Params) -> AnchorState; - /// Returns the L1 block height of the chain genesis (anchor) block. - /// - /// Used by the worker to align the height-indexed manifest MMR with L1 - /// block heights (positions `0..=genesis_l1_height` are sentinel-prefilled). - fn genesis_l1_height(&self, params: &Self::Params) -> u64; + /// Extracts the base STF params every transition executes under. + fn stf_params(params: &Self::Params) -> AsmStfParams; + + /// Invokes the stage with each subprotocol, in the declared order. + fn call_subprotocols(stage: &mut impl Stage) { + Self::Subprotocols::for_each(stage); + } +} + +/// A type-level list of [`Subprotocol`]s, traversed left to right. +/// +/// Implemented for tuples of subprotocols up to 8 elements (e.g. +/// `(AdminSubproto, BridgeSubproto)`); the unit type is the empty list. +pub trait SubprotoList { + /// Invokes the stage with each subprotocol in the list, in order. + fn for_each(stage: &mut impl Stage); } +impl SubprotoList for () { + fn for_each(_stage: &mut impl Stage) {} +} + +/// Generates the [`SubprotoList`] impl for one tuple arity. +/// +/// Rust has no variadic generics, so tuples of arbitrary length cannot be +/// covered by a single impl. Like the standard library's tuple impls of +/// `Hash`/`Debug`, we stamp out one impl per arity up to a cap. Growing an +/// ASM past the cap fails to compile ("SubprotoList is not implemented"); +/// the fix is one more invocation below. +macro_rules! impl_subproto_list { + ($($s:ident),+) => { + impl<$($s: Subprotocol),+> SubprotoList for ($($s,)+) { + fn for_each(stage: &mut impl Stage) { + $(stage.invoke_subprotocol::<$s>();)+ + } + } + }; +} + +impl_subproto_list!(A); +impl_subproto_list!(A, B); +impl_subproto_list!(A, B, C); +impl_subproto_list!(A, B, C, D); +impl_subproto_list!(A, B, C, D, E); +impl_subproto_list!(A, B, C, D, E, F); +impl_subproto_list!(A, B, C, D, E, F, G); +impl_subproto_list!(A, B, C, D, E, F, G, H); + /// Impl of a subprotocol execution stage. pub trait Stage { - /// Invoked by the ASM spec to perform a the stage's logic with respect to + /// Invoked by the ASM spec to perform the stage's logic with respect to /// the subprotocol. fn invoke_subprotocol(&mut self); } diff --git a/crates/common/src/subprotocol.rs b/crates/common/src/subprotocol.rs index b388a073..412c5000 100644 --- a/crates/common/src/subprotocol.rs +++ b/crates/common/src/subprotocol.rs @@ -7,14 +7,77 @@ use std::any::Any; use ssz::{Decode, Encode}; -use strata_identifiers::L1BlockCommitment; +use strata_identifiers::{L1BlockCommitment, L1Height}; pub use strata_l1_txfmt::SubprotocolId; use crate::{ - AsmError, AsmLogEntry, AuxRequestCollector, HeaderVerificationState, SectionState, TxInputRef, - VerifiedAuxData, msg::InterprotoMsg, + AsmError, AsmLogEntry, AsmStfParams, AuxRequestCollector, HeaderVerificationState, + SectionState, TxInputRef, VerifiedAuxData, msg::InterprotoMsg, }; +/// Context for [`Subprotocol::pre_process_txs`]. +/// +/// Aux-data requests declared here MUST be gated on exactly the same fork +/// conditions as the [`Subprotocol::process_txs`] phase that later consumes +/// them, otherwise aux data is requested but never consumed — or consumed but +/// never requested, failing at runtime. +#[derive(Debug)] +pub struct PreProcessTxsCtx<'a> { + /// Height of the L1 block whose transactions are being pre-processed. + /// + /// Needed to evaluate fork gates: pre-processing runs against the + /// *previous* block's state and, unlike `process_txs`, receives no + /// [`HeaderVerificationState`] to read the height from, so it is not + /// otherwise derivable in this phase. + pub block_height: L1Height, + + /// STF params the transition executes under. + /// + /// Needed for the fork schedule that gates aux requests, evaluated + /// against [`Self::block_height`]. + pub stf_params: &'a AsmStfParams, +} + +/// Context for [`Subprotocol::process_txs`]. +#[derive(Debug)] +pub struct ProcessTxsCtx<'a> { + /// Verification state of the L1 block being processed. + /// + /// Needed as the source of the block commitment — its height drives fork + /// gates and height-based logic (e.g. assignment expiry), and it carries + /// PoW data such as the accumulated work. + pub header_vs: &'a HeaderVerificationState, + + /// Aux data requested during [`Subprotocol::pre_process_txs`], verified + /// against the anchor state. + /// + /// Needed so processing can consume the external data (manifest hashes, + /// proofs, ...) it declared a dependency on during pre-processing. + pub verified_aux_data: &'a VerifiedAuxData, + + /// STF params the transition executes under. + /// + /// Needed for the fork schedule that gates processing logic, evaluated + /// against the height of `header_vs.last_verified_block`. + pub stf_params: &'a AsmStfParams, +} + +/// Context for [`Subprotocol::process_msgs`]. +#[derive(Debug)] +pub struct ProcessMsgsCtx<'a> { + /// Commitment of the L1 block being processed. + /// + /// Needed to anchor message effects to the block (e.g. withdrawal + /// assignments record the L1 block they were created at). + pub l1ref: &'a L1BlockCommitment, + + /// STF params the transition executes under. + /// + /// Needed so message handling can be fork-gated on the same conditions as + /// the tx-processing phase that produced the messages. + pub stf_params: &'a AsmStfParams, +} + /// Trait for defining subprotocol behavior within the ASM framework. /// /// Subprotocols are modular components that can be plugged into the ASM to handle @@ -52,6 +115,7 @@ use crate::{ /// state: &Self::State, /// txs: &[TxInputRef], /// collector: &mut AuxRequestCollector, +/// ctx: &PreProcessTxsCtx<'_>, /// ) { /// // Pre-process transactions and request auxiliary data /// } @@ -59,14 +123,13 @@ use crate::{ /// fn process_txs( /// state: &mut Self::State, /// txs: &[TxInputRef], -/// header_vs: &HeaderVerificationState, -/// verified_aux_data: &VerifiedAuxData, /// relayer: &mut impl MsgRelayer, +/// ctx: &ProcessTxsCtx<'_>, /// ) { /// // Process transactions /// } /// -/// fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], l1ref: &L1BlockCommitment) { +/// fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], ctx: &ProcessMsgsCtx<'_>) { /// // Process messages /// } /// } @@ -76,7 +139,10 @@ pub trait Subprotocol: 'static { const ID: SubprotocolId; /// Configuration used to initialize the subprotocol's state. - type InitConfig; + /// + /// `Any` so genesis construction can locate a subprotocol's config in a + /// heterogeneous params list by its type. + type InitConfig: Any; /// State type serialized into the ASM state structure. type State: Any + Decode + Encode; @@ -110,10 +176,12 @@ pub trait Subprotocol: 'static { /// * `state` - Current state of the subprotocol /// * `txs` - Slice of L1 transactions relevant to this subprotocol /// * `collector` - Interface for registering auxiliary input requirements + /// * `ctx` - Ambient context; see [`PreProcessTxsCtx`] for the fork-gating invariant fn pre_process_txs( _state: &Self::State, _txs: &[TxInputRef<'_>], _collector: &mut AuxRequestCollector, + _ctx: &PreProcessTxsCtx<'_>, ) { // default nothing } @@ -128,16 +196,13 @@ pub trait Subprotocol: 'static { /// # Arguments /// * `state` - Mutable reference to the subprotocol's state /// * `txs` - Slice of L1 transactions relevant to this subprotocol - /// * `header_vs` - Verification state of the L1 block being processed; subprotocols can read - /// `header_vs.last_verified_block` for the block commitment, or any other field they need - /// * `verified_aux_data` - Verified auxiliary data previously requested and validated /// * `relayer` - Interface for sending messages to other subprotocols and emitting logs + /// * `ctx` - Ambient context; see [`ProcessTxsCtx`] for what each field provides fn process_txs( state: &mut Self::State, txs: &[TxInputRef<'_>], - header_vs: &HeaderVerificationState, - verified_aux_data: &VerifiedAuxData, relayer: &mut impl MsgRelayer, + ctx: &ProcessTxsCtx<'_>, ); /// Processes messages received from other subprotocols. @@ -148,11 +213,11 @@ pub trait Subprotocol: 'static { /// # Arguments /// * `state` - Mutable reference to the subprotocol's state /// * `msgs` - Slice of messages received from other subprotocols - /// * `l1ref` - L1 block being processed + /// * `ctx` - Ambient context; see [`ProcessMsgsCtx`] for what each field provides /// /// TODO(STR-3028): Enable log emission from process_msgs to support multi-round /// inter-subprotocol messaging - fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], l1ref: &L1BlockCommitment); + fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], ctx: &ProcessMsgsCtx<'_>); } /// Generic message relayer interface which subprotocols can use to interact @@ -179,7 +244,12 @@ pub trait SubprotoHandler { /// /// Any required auxiliary data should be registered via the provided `AuxRequestCollector` for /// the subsequent processing phase. - fn pre_process_txs(&mut self, txs: &[TxInputRef<'_>], collector: &mut AuxRequestCollector); + fn pre_process_txs( + &mut self, + txs: &[TxInputRef<'_>], + collector: &mut AuxRequestCollector, + ctx: &PreProcessTxsCtx<'_>, + ); /// Processes a batch of L1 transactions by delegating to the underlying subprotocol's /// `process_txs` implementation. @@ -189,8 +259,7 @@ pub trait SubprotoHandler { &mut self, txs: &[TxInputRef<'_>], relayer: &mut dyn MsgRelayer, - header_vs: &HeaderVerificationState, - verified_aux_data: &VerifiedAuxData, + ctx: &ProcessTxsCtx<'_>, ); /// Accepts a message. This is called while processing other subprotocols. @@ -205,7 +274,7 @@ pub trait SubprotoHandler { fn accept_msg(&mut self, msg: &dyn InterprotoMsg); /// Processes the buffered messages stored in the handler. - fn process_buffered_msgs(&mut self, l1ref: &L1BlockCommitment); + fn process_buffered_msgs(&mut self, ctx: &ProcessMsgsCtx<'_>); /// Repacks the state into a [`SectionState`] instance. /// diff --git a/crates/extensions/moho/worker/src/state.rs b/crates/extensions/moho/worker/src/state.rs index f22e0981..830b05a4 100644 --- a/crates/extensions/moho/worker/src/state.rs +++ b/crates/extensions/moho/worker/src/state.rs @@ -257,7 +257,7 @@ mod tests { /// Builds a genesis anchor state and its commitment from arbitrary params. fn genesis_anchor() -> (L1BlockCommitment, AnchorState) { let params: AsmParams = ArbitraryGenerator::new().generate(); - let anchor = construct_genesis_state(¶ms); + let anchor = construct_genesis_state(¶ms.genesis); let commitment = anchor.chain_view.pow_state.last_verified_block; (commitment, anchor) } diff --git a/crates/extensions/prover/worker/src/backend/native.rs b/crates/extensions/prover/worker/src/backend/native.rs index 35292e73..e2f1613f 100644 --- a/crates/extensions/prover/worker/src/backend/native.rs +++ b/crates/extensions/prover/worker/src/backend/native.rs @@ -39,11 +39,16 @@ pub(super) async fn build_native_hosts( // across runs, so we construct `NativeHost` directly with the keys // supplied by config. use moho_recursive_proof::process_recursive_moho_proof; + use strata_asm_common::AsmStfParams; use strata_asm_proof_impl::statements::process_asm_stf; use zkaleido_native_adapter::NativeHost; + // Matches the schedule baked into the production ASM guest. + let stf_params = AsmStfParams::default(); Ok(( - NativeHost::new(asm_signing_key.clone(), process_asm_stf), + NativeHost::new(asm_signing_key.clone(), move |env| { + process_asm_stf(env, stf_params.clone()) + }), NativeHost::new(moho_signing_key.clone(), process_recursive_moho_proof), )) } diff --git a/crates/params/Cargo.toml b/crates/params/Cargo.toml index d2a3300a..2aa6ba52 100644 --- a/crates/params/Cargo.toml +++ b/crates/params/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" workspace = true [dependencies] +strata-asm-common.workspace = true strata-asm-proto-bridge-v1-types.workspace = true strata-btc-types.workspace = true strata-btc-verification.workspace = true diff --git a/crates/params/src/genesis.rs b/crates/params/src/genesis.rs new file mode 100644 index 00000000..80f7723f --- /dev/null +++ b/crates/params/src/genesis.rs @@ -0,0 +1,89 @@ +//! Parameters consumed once, at genesis state construction. + +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; +use serde::{Deserialize, Serialize}; +use strata_btc_verification::L1Anchor; +#[cfg(feature = "arbitrary")] +use strata_identifiers::L1BlockCommitment; +use strata_l1_txfmt::MagicBytes; + +use crate::subprotocols::{ + AdministrationInitConfig, BridgeV1InitConfig, CheckpointInitConfig, SubprotocolInstance, +}; + +/// Parameters used to construct the genesis anchor state. +/// +/// Combines the SPS-50 magic bytes used to tag L1 transactions, the genesis +/// L1 view that bootstraps header verification, and the set of active +/// subprotocol configurations. After genesis everything here lives on in the +/// anchor state itself; the STF never reads these again. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AsmGenesisParams { + /// SPS-50 magic bytes that identify protocol transactions on L1. + pub magic: MagicBytes, + + /// L1 anchor point after which L1 processing begins. + /// + /// Captures everything needed to initialize + /// [`HeaderVerificationState`](strata_btc_verification::HeaderVerificationState) and + /// begin validating subsequent L1 headers. + pub anchor: L1Anchor, + + /// Ordered list of subprotocol configurations active in this ASM. + pub subprotocols: Vec, +} + +impl AsmGenesisParams { + pub fn admin_config(&self) -> Option<&AdministrationInitConfig> { + self.subprotocols.iter().find_map(|s| match s { + SubprotocolInstance::Admin(cfg) => Some(cfg), + _ => None, + }) + } + + pub fn bridge_config(&self) -> Option<&BridgeV1InitConfig> { + self.subprotocols.iter().find_map(|s| match s { + SubprotocolInstance::Bridge(cfg) => Some(cfg), + _ => None, + }) + } + + pub fn checkpoint_config(&self) -> Option<&CheckpointInitConfig> { + self.subprotocols.iter().find_map(|s| match s { + SubprotocolInstance::Checkpoint(cfg) => Some(cfg), + _ => None, + }) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for AsmGenesisParams { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let networks = [ + bitcoin::Network::Bitcoin, + bitcoin::Network::Testnet, + bitcoin::Network::Signet, + bitcoin::Network::Regtest, + ]; + let network = *u.choose(&networks)?; + + let block = L1BlockCommitment::arbitrary(u)?; + let anchor = L1Anchor { + block, + next_target: u.arbitrary()?, + epoch_start_timestamp: u.arbitrary()?, + network, + }; + + Ok(Self { + magic: MagicBytes::new(*b"ALPN"), + anchor, + subprotocols: vec![ + SubprotocolInstance::Admin(AdministrationInitConfig::arbitrary(u)?), + SubprotocolInstance::Checkpoint(CheckpointInitConfig::arbitrary(u)?), + SubprotocolInstance::Bridge(BridgeV1InitConfig::arbitrary(u)?), + ], + }) + } +} diff --git a/crates/params/src/lib.rs b/crates/params/src/lib.rs index 45eaaeaa..87ceb2fe 100644 --- a/crates/params/src/lib.rs +++ b/crates/params/src/lib.rs @@ -1,14 +1,164 @@ //! Configuration parameters for the Anchor State Machine (ASM). //! -//! Provides [`AsmParams`], which bundles the L1 magic bytes, genesis L1 view, -//! and per-subprotocol configuration (admin, bridge, checkpoint) needed to -//! initialize and run an ASM instance. +//! Provides [`AsmParams`], split into [`AsmGenesisParams`] (L1 magic bytes, +//! genesis L1 view and per-subprotocol configuration, consumed once to build +//! the genesis state) and [`AsmRuntimeParams`] (fork schedule driving the +//! per-block state transition function). -mod params; +mod genesis; +mod runtime; mod subprotocols; -pub use params::AsmParams; +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; +pub use genesis::AsmGenesisParams; +pub use runtime::AsmRuntimeParams; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "arbitrary")] +use strata_asm_common::ForkSchedule; pub use subprotocols::{ AdminTxType, AdministrationInitConfig, BridgeV1InitConfig, CheckpointInitConfig, ConfirmationDepths, Role, SubprotocolInstance, UpdateTxType, }; + +/// Top-level parameters for an ASM instance. +/// +/// Split by consumer: [`AsmGenesisParams`] is only used to construct the +/// genesis anchor state, while [`AsmRuntimeParams`] configures the state +/// transition function for every block. Both are flattened in the serialized +/// form, so the params file is a single flat object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AsmParams { + /// Parameters consumed once, at genesis state construction. + #[serde(flatten)] + pub genesis: AsmGenesisParams, + + /// Parameters of the per-block state transition function. + #[serde(flatten)] + pub runtime: AsmRuntimeParams, +} + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for AsmParams { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + Ok(Self { + genesis: AsmGenesisParams::arbitrary(u)?, + runtime: AsmRuntimeParams { + forks: ForkSchedule::all_disabled(), + }, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_asm_params_deserialize_from_raw_json() { + // Static JSON generated from arbitrary instance with seed [0..256] + let raw_json = r#" +{ + "magic": "ALPN", + "anchor": { + "block": { + "height": 50462976, + "blkid": "0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223" + }, + "next_target": 656811300, + "epoch_start_timestamp": 724183336, + "network": "regtest" + }, + "subprotocols": [ + { + "Admin": { + "strata_administrator": { + "keys": [ + "02bedfa2fa42d906565519bee43875608a09e06640203a6c7a43569150c7cbe7c5" + ], + "threshold": 1 + }, + "strata_sequencer_manager": { + "keys": [ + "03cf59a1a5ef092ced386f2651b610d3dd2cc6806bb74a8eab95c1f3b2f3d81772", + "02343edde4a056e00af99aa49de60df03859d1b79ebbc4f3f6da8fbd0053565de3" + ], + "threshold": 1 + }, + "alpen_administrator": { + "keys": [ + "02bedfa2fa42d906565519bee43875608a09e06640203a6c7a43569150c7cbe7c5" + ], + "threshold": 1 + }, + "strata_security_council": { + "keys": [ + "02bedfa2fa42d906565519bee43875608a09e06640203a6c7a43569150c7cbe7c5" + ], + "threshold": 1 + }, + "confirmation_depths": { + "strata_admin_multisig_update": 144, + "strata_seq_manager_multisig_update": 144, + "alpen_admin_multisig_update": 144, + "strata_security_council_multisig_update": 144, + "operator_update": 144, + "sequencer_update": 144, + "ol_stf_vk_update": 144, + "asm_stf_vk_update": 144, + "ee_stf_vk_update": 144, + "defcon3": 144, + "safe_harbour_address_update": 144 + }, + "max_seqno_gap": 10 + } + }, + { + "Checkpoint": { + "sequencer_predicate": "Sp1Groth16", + "checkpoint_predicate": "AlwaysAccept", + "genesis_l1_height": 3334849731, + "genesis_ol_blkid": "c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6" + } + }, + { + "Bridge": { + "operators": [ + "02becdf7aab195ab0a42ba2f2eca5b7fa5a246267d802c627010e1672f08657f70" + ], + "denomination": 0, + "assignment_duration": 0, + "operator_fee": 0, + "recovery_delay": 0, + "safe_harbour_address": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + } + ], + "forks": { + "fork1": 0 + } +} +"#; + + let params: AsmParams = + serde_json::from_str(raw_json).expect("deserialization from raw JSON should succeed"); + assert_eq!(params.runtime.forks.fork1, Some(0)); + } + + #[cfg(feature = "arbitrary")] + mod proptest_arbitrary { + use arbitrary::{Arbitrary, Unstructured}; + use proptest::{collection, prelude::*}; + + use super::*; + + proptest! { + #[test] + fn test_arbitrary(seed in collection::vec(any::(), 0..4096)) { + let mut u = Unstructured::new(&seed); + let res = AsmParams::arbitrary(&mut u); + prop_assert!(res.is_ok()); + } + } + } +} diff --git a/crates/params/src/params.rs b/crates/params/src/params.rs deleted file mode 100644 index a7a8da6a..00000000 --- a/crates/params/src/params.rs +++ /dev/null @@ -1,200 +0,0 @@ -#[cfg(feature = "arbitrary")] -use arbitrary::{Arbitrary, Unstructured}; -use serde::{Deserialize, Serialize}; -use strata_btc_verification::L1Anchor; -use strata_l1_txfmt::MagicBytes; - -use crate::subprotocols::{ - AdministrationInitConfig, BridgeV1InitConfig, CheckpointInitConfig, SubprotocolInstance, -}; - -/// Top-level parameters for an ASM instance. -/// -/// Combines the SPS-50 magic bytes used to tag L1 transactions, the genesis -/// L1 view that bootstraps header verification, and the set of active -/// subprotocol configurations. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AsmParams { - /// SPS-50 magic bytes that identify protocol transactions on L1. - pub magic: MagicBytes, - - /// L1 anchor point after which L1 processing begins. - /// - /// Captures everything needed to initialize - /// [`HeaderVerificationState`](strata_btc_verification::HeaderVerificationState) and - /// begin validating subsequent L1 headers. - pub anchor: L1Anchor, - - /// Ordered list of subprotocol configurations active in this ASM. - pub subprotocols: Vec, -} - -impl AsmParams { - pub fn admin_config(&self) -> Option<&AdministrationInitConfig> { - self.subprotocols.iter().find_map(|s| match s { - SubprotocolInstance::Admin(cfg) => Some(cfg), - _ => None, - }) - } - - pub fn bridge_config(&self) -> Option<&BridgeV1InitConfig> { - self.subprotocols.iter().find_map(|s| match s { - SubprotocolInstance::Bridge(cfg) => Some(cfg), - _ => None, - }) - } - - pub fn checkpoint_config(&self) -> Option<&CheckpointInitConfig> { - self.subprotocols.iter().find_map(|s| match s { - SubprotocolInstance::Checkpoint(cfg) => Some(cfg), - _ => None, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for AsmParams { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - use strata_btc_verification::L1Anchor; - use strata_identifiers::L1BlockCommitment; - - use crate::subprotocols::{ - AdministrationInitConfig, BridgeV1InitConfig, CheckpointInitConfig, - }; - - let networks = [ - bitcoin::Network::Bitcoin, - bitcoin::Network::Testnet, - bitcoin::Network::Signet, - bitcoin::Network::Regtest, - ]; - let network = *u.choose(&networks)?; - - let block = L1BlockCommitment::arbitrary(u)?; - let anchor = L1Anchor { - block, - next_target: u.arbitrary()?, - epoch_start_timestamp: u.arbitrary()?, - network, - }; - - Ok(Self { - magic: MagicBytes::new(*b"ALPN"), - anchor, - subprotocols: vec![ - SubprotocolInstance::Admin(AdministrationInitConfig::arbitrary(u)?), - SubprotocolInstance::Checkpoint(CheckpointInitConfig::arbitrary(u)?), - SubprotocolInstance::Bridge(BridgeV1InitConfig::arbitrary(u)?), - ], - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_asm_params_deserialize_from_raw_json() { - // Static JSON generated from arbitrary instance with seed [0..256] - let raw_json = r#" -{ - "magic": "ALPN", - "anchor": { - "block": { - "height": 50462976, - "blkid": "0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223" - }, - "next_target": 656811300, - "epoch_start_timestamp": 724183336, - "network": "regtest" - }, - "subprotocols": [ - { - "Admin": { - "strata_administrator": { - "keys": [ - "02bedfa2fa42d906565519bee43875608a09e06640203a6c7a43569150c7cbe7c5" - ], - "threshold": 1 - }, - "strata_sequencer_manager": { - "keys": [ - "03cf59a1a5ef092ced386f2651b610d3dd2cc6806bb74a8eab95c1f3b2f3d81772", - "02343edde4a056e00af99aa49de60df03859d1b79ebbc4f3f6da8fbd0053565de3" - ], - "threshold": 1 - }, - "alpen_administrator": { - "keys": [ - "02bedfa2fa42d906565519bee43875608a09e06640203a6c7a43569150c7cbe7c5" - ], - "threshold": 1 - }, - "strata_security_council": { - "keys": [ - "02bedfa2fa42d906565519bee43875608a09e06640203a6c7a43569150c7cbe7c5" - ], - "threshold": 1 - }, - "confirmation_depths": { - "strata_admin_multisig_update": 144, - "strata_seq_manager_multisig_update": 144, - "alpen_admin_multisig_update": 144, - "strata_security_council_multisig_update": 144, - "operator_update": 144, - "sequencer_update": 144, - "ol_stf_vk_update": 144, - "asm_stf_vk_update": 144, - "ee_stf_vk_update": 144, - "defcon3": 144, - "safe_harbour_address_update": 144 - }, - "max_seqno_gap": 10 - } - }, - { - "Checkpoint": { - "sequencer_predicate": "Sp1Groth16", - "checkpoint_predicate": "AlwaysAccept", - "genesis_l1_height": 3334849731, - "genesis_ol_blkid": "c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6" - } - }, - { - "Bridge": { - "operators": [ - "02becdf7aab195ab0a42ba2f2eca5b7fa5a246267d802c627010e1672f08657f70" - ], - "denomination": 0, - "assignment_duration": 0, - "operator_fee": 0, - "recovery_delay": 0, - "safe_harbour_address": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" - } - } - ] -} -"#; - - let _params: AsmParams = - serde_json::from_str(raw_json).expect("deserialization from raw JSON should succeed"); - } - - #[cfg(feature = "arbitrary")] - mod proptest_arbitrary { - use arbitrary::{Arbitrary, Unstructured}; - use proptest::{collection, prelude::*}; - - use super::*; - - proptest! { - #[test] - fn test_arbitrary(seed in collection::vec(any::(), 0..4096)) { - let mut u = Unstructured::new(&seed); - let res = AsmParams::arbitrary(&mut u); - prop_assert!(res.is_ok()); - } - } - } -} diff --git a/crates/params/src/runtime.rs b/crates/params/src/runtime.rs new file mode 100644 index 00000000..ebf68e1f --- /dev/null +++ b/crates/params/src/runtime.rs @@ -0,0 +1,36 @@ +//! Parameters of the per-block state transition function. + +use serde::{Deserialize, Serialize}; +use strata_asm_common::{AsmStfParams, ForkSchedule}; + +/// Runtime parameters of the state transition function. +/// +/// `forks` is the base fork schedule the worker starts from — the part that, +/// on the proving side, is baked into guest programs as [`AsmStfParams`]. The +/// worker overlays it with activations discovered from enacted ASM VK +/// upgrades, each of which names the fork it activates. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AsmRuntimeParams { + /// Base fork activation schedule. + pub forks: ForkSchedule, +} + +impl AsmRuntimeParams { + /// The STF-facing view of these params, before any dynamic activations. + pub fn stf_params(&self) -> AsmStfParams { + AsmStfParams { + forks: self.forks.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_runtime_params_deserialize() { + let params: AsmRuntimeParams = serde_json::from_str(r#"{"forks":{"fork1":5}}"#).unwrap(); + assert_eq!(params.stf_params().forks.fork1, Some(5)); + } +} diff --git a/crates/proof/statements/src/lib.rs b/crates/proof/statements/src/lib.rs index 39cf89e8..c687f785 100644 --- a/crates/proof/statements/src/lib.rs +++ b/crates/proof/statements/src/lib.rs @@ -9,3 +9,7 @@ pub mod program; pub mod statements; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; + +// Re-exported so guest programs can construct their hardcoded params without +// depending on strata-asm-common directly. +pub use strata_asm_common::{AsmStfParams, ForkId, ForkSchedule}; diff --git a/crates/proof/statements/src/moho_program/program.rs b/crates/proof/statements/src/moho_program/program.rs index 0990a3c8..d201d69b 100644 --- a/crates/proof/statements/src/moho_program/program.rs +++ b/crates/proof/statements/src/moho_program/program.rs @@ -99,8 +99,8 @@ impl MohoProgram for AsmStfProgram { spec: &StrataAsmSpec, input: &AsmStepInput, ) -> AsmStfOutput { - compute_asm_transition( - spec, + compute_asm_transition::( + spec.stf_params(), pre_state, input.block(), input.aux_data(), diff --git a/crates/proof/statements/src/program.rs b/crates/proof/statements/src/program.rs index 2974f654..9ffb46ca 100644 --- a/crates/proof/statements/src/program.rs +++ b/crates/proof/statements/src/program.rs @@ -3,6 +3,7 @@ use moho_runtime_impl::RuntimeInput; use moho_types::StepMohoAttestation; use ssz::{decode::Decode, encode::Encode}; +use strata_asm_common::AsmStfParams; use zkaleido::{ DataFormatError, ProofType, PublicValues, ZkVmError, ZkVmHost, ZkVmInputBuilder, ZkVmInputResult, ZkVmProgram, ZkVmResult, @@ -53,17 +54,18 @@ impl ZkVmProgram for AsmStfProofProgram { } impl AsmStfProofProgram { - /// Native host that can be used for testing - pub fn native_host() -> NativeHost { - NativeHost::new_with_random_key(process_asm_stf) + /// Native host executing under the given STF params; usable for testing. + pub fn native_host(stf_params: AsmStfParams) -> NativeHost { + NativeHost::new_with_random_key(move |env| process_asm_stf(env, stf_params.clone())) } - /// Executes the program using the native host. + /// Executes the program under the given STF params using the native host. pub fn execute( input: &::Input, + stf_params: AsmStfParams, ) -> ZkVmResult<::Output> { // Get the native host and delegate to the trait's execute method - let host = Self::native_host(); + let host = Self::native_host(stf_params); let summary = ::execute(input, &host)?; ::process_output::(summary.public_values()) } @@ -74,6 +76,7 @@ mod tests { use moho_runtime_impl::RuntimeInput; use ssz::Encode; + use strata_asm_common::AsmStfParams; use strata_predicate::PredicateKey; use crate::{ @@ -97,7 +100,7 @@ mod tests { fn test_stf() { let runtime_input = create_runtime_input(); - let output = AsmStfProofProgram::execute(&runtime_input).unwrap(); + let output = AsmStfProofProgram::execute(&runtime_input, AsmStfParams::default()).unwrap(); dbg!(output); } } diff --git a/crates/proof/statements/src/statements.rs b/crates/proof/statements/src/statements.rs index 03f1e91d..190932cf 100644 --- a/crates/proof/statements/src/statements.rs +++ b/crates/proof/statements/src/statements.rs @@ -2,6 +2,7 @@ use moho_runtime_impl::{compute_moho_attestation, RuntimeInput}; use ssz::{Decode, Encode}; +use strata_asm_common::AsmStfParams; use strata_asm_spec::StrataAsmSpec; use zkaleido::ZkVmEnv; @@ -15,14 +16,16 @@ use crate::moho_program::program::AsmStfProgram; /// /// # Note /// -/// The `spec` must be hardcoded by the outer guest program rather than read from the ZKVM input, -/// as it defines the trusted chain parameters that the proof is verified against. -pub fn process_asm_stf(zkvm: &impl ZkVmEnv) { +/// `stf_params` must be hardcoded by the outer guest program rather than read from the +/// ZKVM input: together with the spec it defines the trusted chain parameters that the +/// proof is verified against, so the verifying key must commit to it. +pub fn process_asm_stf(zkvm: &impl ZkVmEnv, stf_params: AsmStfParams) { let runtime_input_bytes = zkvm.read_buf(); let runtime_input = RuntimeInput::from_ssz_bytes(&runtime_input_bytes) .expect("failed to deserialize runtime input for SSZ bytes"); - let attestation = compute_moho_attestation::(runtime_input, &StrataAsmSpec); + let spec = StrataAsmSpec::new(stf_params); + let attestation = compute_moho_attestation::(runtime_input, &spec); let attestation_bytes = attestation.as_ssz_bytes(); zkvm.commit_buf(&attestation_bytes); diff --git a/crates/proof/statements/src/test_utils.rs b/crates/proof/statements/src/test_utils.rs index ff7f563b..02bbff44 100644 --- a/crates/proof/statements/src/test_utils.rs +++ b/crates/proof/statements/src/test_utils.rs @@ -8,7 +8,7 @@ use bitcoin::Block; use moho_runtime_interface::MohoProgram; use moho_types::{ExportState, MohoState}; use strata_asm_common::{AnchorState, AuxData}; -use strata_asm_params::AsmParams; +use strata_asm_params::AsmGenesisParams; use strata_asm_spec::construct_genesis_state; use strata_btc_types::BlockHashExt; use strata_btc_verification::{L1Anchor, TxidInclusionProof}; @@ -43,11 +43,11 @@ pub fn create_l1_anchor_to_process_block(block: &Block) -> L1Anchor { } } -/// Note: the returned state is **non-deterministic** because `AsmParams` fields -/// (magic, subprotocols) are generated randomly via [`ArbitraryGenerator`]. +/// Note: the returned state is **non-deterministic** because `AsmGenesisParams` +/// fields (magic, subprotocols) are generated randomly via [`ArbitraryGenerator`]. /// Use [`create_deterministic_genesis_anchor_state`] when reproducibility matters. pub fn create_genesis_anchor_state(block: &Block) -> AnchorState { - let mut params: AsmParams = ArbitraryGenerator::new().generate(); + let mut params: AsmGenesisParams = ArbitraryGenerator::new().generate(); let anchor = create_l1_anchor_to_process_block(block); params.anchor = anchor; construct_genesis_state(¶ms) @@ -60,7 +60,7 @@ pub fn create_genesis_anchor_state(block: &Block) -> AnchorState { pub fn create_deterministic_genesis_anchor_state(block: &Block) -> AnchorState { let buf = [42u8; 65_536]; let mut u = Unstructured::new(&buf); - let mut params = AsmParams::arbitrary(&mut u).expect("deterministic AsmParams"); + let mut params = AsmGenesisParams::arbitrary(&mut u).expect("deterministic AsmGenesisParams"); let anchor = create_l1_anchor_to_process_block(block); params.anchor = anchor; construct_genesis_state(¶ms) diff --git a/crates/spec/src/genesis.rs b/crates/spec/src/genesis.rs index e5c9bf77..407620ee 100644 --- a/crates/spec/src/genesis.rs +++ b/crates/spec/src/genesis.rs @@ -1,46 +1,36 @@ -//! Genesis anchor state construction from [`AsmParams`]. +//! Genesis anchor state construction from [`AsmGenesisParams`]. + +use std::any::Any; use strata_asm_common::{ - AnchorState, AsmHistoryAccumulatorState, ChainViewState, HeaderVerificationState, SectionState, + AnchorState, AsmHistoryAccumulatorState, AsmSpec, ChainViewState, HeaderVerificationState, + SectionState, Stage, Subprotocol, }; -use strata_asm_params::AsmParams; -use strata_asm_proto_admin::{AdministrationSubprotoState, AdministrationSubprotocol}; -use strata_asm_proto_bridge_v1::{BridgeV1State, BridgeV1Subproto}; -use strata_asm_proto_checkpoint::{CheckpointState, CheckpointSubprotocol}; +use strata_asm_params::{AsmGenesisParams, SubprotocolInstance}; use strata_btc_verification::HeaderVerificationState as NativeHeaderVerificationState; -/// Builds the genesis [`AnchorState`] from the given [`AsmParams`]. +use crate::StrataAsmSpec; + +/// Builds the genesis [`AnchorState`] from the given [`AsmGenesisParams`]. /// -/// Initialises every subprotocol's state from its config in `params` and -/// assembles the chain view (PoW header verification + history accumulator). -pub fn construct_genesis_state(params: &AsmParams) -> AnchorState { - let genesis_admin_subprotocol_state = AdministrationSubprotoState::new( - params - .admin_config() - .expect("asm: missing Admin subprotocol config in params"), - ); - let admin_subprotocol_section = - SectionState::from_state::(&genesis_admin_subprotocol_state) - .expect("asm: Admin subprotocol genesis state fits section data capacity"); - - let genesis_checkpoint_subprotocol_state = CheckpointState::init( - params - .checkpoint_config() - .expect("asm: missing Checkpoint subprotocol config in params") - .clone(), - ); - let checkpoint_subprotocol_section = - SectionState::from_state::(&genesis_checkpoint_subprotocol_state) - .expect("asm: Checkpoint subprotocol genesis state fits section data capacity"); - - let genesis_bridge_subprotocol_state = BridgeV1State::new( - params - .bridge_config() - .expect("asm: missing Bridge subprotocol config in params"), +/// Initialises every subprotocol's state from its config in `params` — driven +/// by the same [`AsmSpec`] subprotocol list every execution stage traverses, +/// so the pipeline and the genesis layout cannot drift apart — and assembles +/// the chain view (PoW header verification + history accumulator). +pub fn construct_genesis_state(params: &AsmGenesisParams) -> AnchorState { + let mut stage = GenesisStateStage { + params, + sections: Vec::new(), + }; + StrataAsmSpec::call_subprotocols(&mut stage); + + // Post-transition exports emit sections in ascending subprotocol-ID order + // (see the STF's section export); genesis must produce the same layout or + // the first transition would silently reorder the state. + assert!( + stage.sections.is_sorted_by_key(|s| s.id), + "asm: genesis sections not sorted by subprotocol id" ); - let bridge_subprotocol_section = - SectionState::from_state::(&genesis_bridge_subprotocol_state) - .expect("asm: Bridge subprotocol genesis state fits section data capacity"); let native_header_vs = NativeHeaderVerificationState::init(params.anchor.clone()); let history_accumulator = AsmHistoryAccumulatorState::new(params.anchor.block.height() as u64); @@ -52,12 +42,47 @@ pub fn construct_genesis_state(params: &AsmParams) -> AnchorState { AnchorState { magic: AnchorState::magic_ssz(params.magic), chain_view, - sections: vec![ - admin_subprotocol_section, - checkpoint_subprotocol_section, - bridge_subprotocol_section, - ] - .try_into() - .expect("asm: genesis sections fit within capacity"), + sections: stage + .sections + .try_into() + .expect("asm: genesis sections fit within capacity"), + } +} + +/// [`Stage`] that builds each subprotocol's genesis state from its config, +/// packed into its [`SectionState`] envelope. +/// +/// Configs are located in the params' heterogeneous list by their type: each +/// subprotocol's `InitConfig` type appears in exactly one +/// [`SubprotocolInstance`] variant. +struct GenesisStateStage<'p> { + params: &'p AsmGenesisParams, + sections: Vec, +} + +impl Stage for GenesisStateStage<'_> { + fn invoke_subprotocol(&mut self) { + let config = self + .params + .subprotocols + .iter() + .find_map(|instance| { + let config: &dyn Any = match instance { + SubprotocolInstance::Admin(config) => config, + SubprotocolInstance::Bridge(config) => config, + SubprotocolInstance::Checkpoint(config) => config, + }; + config.downcast_ref::() + }) + .unwrap_or_else(|| panic!("asm: missing config for subprotocol {} in params", S::ID)); + + let state = S::init(config); + let section = SectionState::from_state::(&state).unwrap_or_else(|e| { + panic!( + "asm: genesis state for subprotocol {} exceeds section data capacity: {e}", + S::ID + ) + }); + self.sections.push(section); } } diff --git a/crates/spec/src/lib.rs b/crates/spec/src/lib.rs index 600c164b..fb988721 100644 --- a/crates/spec/src/lib.rs +++ b/crates/spec/src/lib.rs @@ -5,7 +5,7 @@ //! - [`StrataAsmSpec`] — declares which subprotocols are active and their invocation order. //! - [`construct_genesis_state`] — builds the genesis //! [`AnchorState`](strata_asm_common::AnchorState) from -//! [`AsmParams`](strata_asm_params::AsmParams). +//! [`AsmGenesisParams`](strata_asm_params::AsmGenesisParams). mod genesis; mod spec; diff --git a/crates/spec/src/spec.rs b/crates/spec/src/spec.rs index 5523ea13..93affd38 100644 --- a/crates/spec/src/spec.rs +++ b/crates/spec/src/spec.rs @@ -1,33 +1,54 @@ //! Strata ASM specification defining the subprotocol pipeline. -use strata_asm_common::{AnchorState, AsmSpec, Stage}; +use strata_asm_common::{AnchorState, AsmSpec, AsmStfParams}; use strata_asm_params::AsmParams; use strata_asm_proto_admin::AdministrationSubprotocol; use strata_asm_proto_bridge_v1::BridgeV1Subproto; use strata_asm_proto_checkpoint::CheckpointSubprotocol; +use crate::genesis; + /// Strata ASM specification. /// /// Declares which subprotocols participate in the ASM and the order in which /// they are invoked. The same ordering is used for every execution stage -/// (load, preprocess, process, finish). -#[derive(Debug)] -pub struct StrataAsmSpec; +/// (load, preprocess, process, finish) and for genesis construction. +/// +/// The pipeline itself is type-level (see [`AsmSpec`]); the struct exists to +/// carry the [`AsmStfParams`] through interfaces that thread a single spec value +/// (the Moho runtime). Guest programs construct it with their hardcoded +/// params — the verifying key thereby commits to them. +#[derive(Debug, Clone)] +pub struct StrataAsmSpec { + stf_params: AsmStfParams, +} -impl AsmSpec for StrataAsmSpec { - type Params = AsmParams; +impl StrataAsmSpec { + /// Creates a spec executing under the given STF params. + pub fn new(stf_params: AsmStfParams) -> Self { + Self { stf_params } + } - fn call_subprotocols(&self, stage: &mut impl Stage) { - stage.invoke_subprotocol::(); - stage.invoke_subprotocol::(); - stage.invoke_subprotocol::(); + /// Returns the STF params this executor runs under. + pub fn stf_params(&self) -> &AsmStfParams { + &self.stf_params } +} + +impl AsmSpec for StrataAsmSpec { + type Subprotocols = ( + AdministrationSubprotocol, + CheckpointSubprotocol, + BridgeV1Subproto, + ); + + type Params = AsmParams; - fn construct_genesis_state(&self, params: &Self::Params) -> AnchorState { - crate::construct_genesis_state(params) + fn construct_genesis_state(params: &AsmParams) -> AnchorState { + genesis::construct_genesis_state(¶ms.genesis) } - fn genesis_l1_height(&self, params: &Self::Params) -> u64 { - params.anchor.block.height() as u64 + fn stf_params(params: &AsmParams) -> AsmStfParams { + params.runtime.stf_params() } } diff --git a/crates/stf/src/manager.rs b/crates/stf/src/manager.rs index 0acf8e74..d155c873 100644 --- a/crates/stf/src/manager.rs +++ b/crates/stf/src/manager.rs @@ -3,10 +3,10 @@ use std::{any::Any, collections::BTreeMap, marker}; use strata_asm_common::{ - AsmError, AsmLogEntry, AuxRequestCollector, HeaderVerificationState, InterprotoMsg, MsgRelayer, - SectionState, SubprotoHandler, Subprotocol, SubprotocolId, TxInputRef, VerifiedAuxData, + AsmError, AsmLogEntry, AuxRequestCollector, InterprotoMsg, MsgRelayer, PreProcessTxsCtx, + ProcessMsgsCtx, ProcessTxsCtx, SectionState, SubprotoHandler, Subprotocol, SubprotocolId, + TxInputRef, }; -use strata_identifiers::L1BlockCommitment; /// Wrapper around the common subprotocol interface that handles the common /// buffering logic for interproto messages. @@ -41,28 +41,32 @@ impl SubprotoHandler for HandlerImpl { } // TODO(STR-3065): make this just return the aux request - fn pre_process_txs(&mut self, txs: &[TxInputRef<'_>], collector: &mut AuxRequestCollector) { - S::pre_process_txs(&self.state, txs, collector); + fn pre_process_txs( + &mut self, + txs: &[TxInputRef<'_>], + collector: &mut AuxRequestCollector, + ctx: &PreProcessTxsCtx<'_>, + ) { + S::pre_process_txs(&self.state, txs, collector, ctx); } fn process_txs( &mut self, txs: &[TxInputRef<'_>], relayer: &mut dyn MsgRelayer, - header_vs: &HeaderVerificationState, - verified_aux_data: &VerifiedAuxData, + ctx: &ProcessTxsCtx<'_>, ) { let relayer = relayer .as_mut_any() .downcast_mut::() .expect("asm: handler"); - S::process_txs(&mut self.state, txs, header_vs, verified_aux_data, relayer); + S::process_txs(&mut self.state, txs, relayer, ctx); } - fn process_buffered_msgs(&mut self, l1ref: &L1BlockCommitment) { + fn process_buffered_msgs(&mut self, ctx: &ProcessMsgsCtx<'_>) { // TODO(STR-2416): allow multi rounds of interproto msg passing - S::process_msgs(&mut self.state, &self.interproto_msg_buf, l1ref) + S::process_msgs(&mut self.state, &self.interproto_msg_buf, ctx) } fn to_section(&self) -> Result { @@ -97,6 +101,7 @@ impl SubprotoManager { &mut self, aux_collector: &mut AuxRequestCollector, txs: &[TxInputRef<'_>], + ctx: &PreProcessTxsCtx<'_>, ) { // We temporarily take the handler out of the map so we can call // `process_txs` with `self` as the relayer without violating the @@ -106,7 +111,7 @@ impl SubprotoManager { .expect("asm: unloaded subprotocol"); // Invoke the preprocess function. - h.pre_process_txs(txs, aux_collector); + h.pre_process_txs(txs, aux_collector, ctx); self.insert_handler(h); } @@ -118,8 +123,7 @@ impl SubprotoManager { pub(crate) fn invoke_process_txs( &mut self, txs: &[TxInputRef<'_>], - header_vs: &HeaderVerificationState, - verified_aux_data: &VerifiedAuxData, + ctx: &ProcessTxsCtx<'_>, ) { // We temporarily take the handler out of the map so we can call // `process_txs` with `self` as the relayer without violating the @@ -127,16 +131,16 @@ impl SubprotoManager { let mut h = self .remove_handler(S::ID) .expect("asm: unloaded subprotocol"); - h.process_txs(txs, self, header_vs, verified_aux_data); + h.process_txs(txs, self, ctx); self.insert_handler(h); } /// Dispatches buffered inter-protocol message processing to the handler. - pub(crate) fn invoke_process_msgs(&mut self, l1ref: &L1BlockCommitment) { + pub(crate) fn invoke_process_msgs(&mut self, ctx: &ProcessMsgsCtx<'_>) { let h = self .get_handler_mut(S::ID) .expect("asm: unloaded subprotocol"); - h.process_buffered_msgs(l1ref) + h.process_buffered_msgs(ctx) } fn insert_handler(&mut self, handler: Box) { diff --git a/crates/stf/src/preprocess.rs b/crates/stf/src/preprocess.rs index c4342e11..b262f121 100644 --- a/crates/stf/src/preprocess.rs +++ b/crates/stf/src/preprocess.rs @@ -3,7 +3,9 @@ //! view into a single deterministic state transition. use bitcoin::block::Block; -use strata_asm_common::{AnchorState, AsmError, AsmResult, AsmSpec}; +use strata_asm_common::{ + AnchorState, AsmError, AsmResult, AsmSpec, AsmStfParams, PreProcessTxsCtx, +}; use crate::{ manager::SubprotoManager, @@ -27,6 +29,9 @@ use crate::{ /// /// # Arguments /// +/// * `stf_params` - The STF params the transition executes under; MUST be the same params the +/// subsequent [`compute_asm_transition`](crate::compute_asm_transition) call runs with, so +/// fork-gated aux requests stay in lockstep with fork-gated processing /// * `pre_state` - The previous anchor state to transition from /// * `block` - The new L1 Bitcoin block to process /// @@ -44,11 +49,10 @@ use crate::{ /// /// # Type Parameters /// -/// * `S` - The ASM specification type that defines magic bytes, subprotocol behavior, and genesis -/// configs +/// * `S` - The ASM specification type declaring the subprotocol pipeline /// * `'b` - Lifetime parameter tied to the input block reference pub fn pre_process_asm<'b, S: AsmSpec>( - spec: &S, + stf_params: &AsmStfParams, pre_state: &AnchorState, block: &'b Block, ) -> AsmResult> { @@ -67,13 +71,21 @@ pub fn pre_process_asm<'b, S: AsmSpec>( // 3. LOAD: Initialize each subprotocol in the subproto manager. let mut loader_stage = LoaderStage::new(&mut manager, pre_state); - spec.call_subprotocols(&mut loader_stage); + S::call_subprotocols(&mut loader_stage); // 4. PROCESS: Feed each subprotocol its filtered transactions for pre-processing. // This stage extracts auxiliary requests that will be needed for the main STF execution. + // `pow_state` was advanced above, so its last verified block IS the block being + // pre-processed — the same height `process_txs` later sees, keeping fork gates in + // both phases in lockstep. + let block_height = pow_state.last_verified_block.height(); + let ctx = PreProcessTxsCtx { + block_height, + stf_params, + }; let mut pre_process_stage = - PreProcessStage::new(&mut manager, pre_state, &grouped_relevant_txs); - spec.call_subprotocols(&mut pre_process_stage); + PreProcessStage::new(&mut manager, pre_state, &grouped_relevant_txs, ctx); + S::call_subprotocols(&mut pre_process_stage); // 5. Export auxiliary requests collected during pre-processing. // These requests will be fulfilled before running the main ASM state transition. diff --git a/crates/stf/src/stage.rs b/crates/stf/src/stage.rs index 3ec27bd3..132b90ce 100644 --- a/crates/stf/src/stage.rs +++ b/crates/stf/src/stage.rs @@ -3,10 +3,9 @@ use std::collections::BTreeMap; use strata_asm_common::{ - AnchorState, AuxRequestCollector, AuxRequests, HeaderVerificationState, Stage, Subprotocol, - SubprotocolId, TxInputRef, VerifiedAuxData, + AnchorState, AuxRequestCollector, AuxRequests, PreProcessTxsCtx, ProcessMsgsCtx, ProcessTxsCtx, + Stage, Subprotocol, SubprotocolId, TxInputRef, }; -use strata_identifiers::L1BlockCommitment; use crate::manager::SubprotoManager; @@ -42,6 +41,7 @@ pub(crate) struct PreProcessStage<'c> { manager: &'c mut SubprotoManager, tx_bufs: &'c BTreeMap>>, aux_collector: AuxRequestCollector, + ctx: PreProcessTxsCtx<'c>, } impl<'c> PreProcessStage<'c> { @@ -49,6 +49,7 @@ impl<'c> PreProcessStage<'c> { manager: &'c mut SubprotoManager, anchor_state: &'c AnchorState, tx_bufs: &'c BTreeMap>>, + ctx: PreProcessTxsCtx<'c>, ) -> Self { let accumulator = &anchor_state.chain_view.history_accumulator; // The MMR is height-indexed (sentinel-prefilled at and before genesis), @@ -63,6 +64,7 @@ impl<'c> PreProcessStage<'c> { manager, tx_bufs, aux_collector, + ctx, } } @@ -80,30 +82,27 @@ impl Stage for PreProcessStage<'_> { .unwrap_or(&[]); self.manager - .invoke_pre_process_txs::(&mut self.aux_collector, txs); + .invoke_pre_process_txs::(&mut self.aux_collector, txs, &self.ctx); } } /// Stage to process txs pre-extracted from the block for each subprotocol. pub(crate) struct ProcessStage<'c> { manager: &'c mut SubprotoManager, - header_vs: &'c HeaderVerificationState, tx_bufs: BTreeMap>>, - verified_aux_data: VerifiedAuxData, + ctx: ProcessTxsCtx<'c>, } impl<'c> ProcessStage<'c> { pub(crate) fn new( manager: &'c mut SubprotoManager, - header_vs: &'c HeaderVerificationState, tx_bufs: BTreeMap>>, - verified_aux_data: VerifiedAuxData, + ctx: ProcessTxsCtx<'c>, ) -> Self { Self { manager, - header_vs, tx_bufs, - verified_aux_data, + ctx, } } } @@ -116,25 +115,24 @@ impl Stage for ProcessStage<'_> { .map(|v| v.as_slice()) .unwrap_or(&[]); - self.manager - .invoke_process_txs::(txs, self.header_vs, &self.verified_aux_data); + self.manager.invoke_process_txs::(txs, &self.ctx); } } /// Stage to handle messages exchanged between subprotocols in execution. pub(crate) struct FinishStage<'m> { manager: &'m mut SubprotoManager, - l1ref: &'m L1BlockCommitment, + ctx: ProcessMsgsCtx<'m>, } impl<'m> FinishStage<'m> { - pub(crate) fn new(manager: &'m mut SubprotoManager, l1ref: &'m L1BlockCommitment) -> Self { - Self { manager, l1ref } + pub(crate) fn new(manager: &'m mut SubprotoManager, ctx: ProcessMsgsCtx<'m>) -> Self { + Self { manager, ctx } } } impl Stage for FinishStage<'_> { fn invoke_subprotocol(&mut self) { - self.manager.invoke_process_msgs::(self.l1ref); + self.manager.invoke_process_msgs::(&self.ctx); } } diff --git a/crates/stf/src/transition.rs b/crates/stf/src/transition.rs index d384c309..d02f235e 100644 --- a/crates/stf/src/transition.rs +++ b/crates/stf/src/transition.rs @@ -5,8 +5,8 @@ use bitcoin::Block; use ssz_types::VariableList; use strata_asm_common::{ - AnchorState, AsmError, AsmManifest, AsmResult, AsmSpec, AuxData, ChainViewState, - VerifiedAuxData, + AnchorState, AsmError, AsmManifest, AsmResult, AsmSpec, AsmStfParams, AuxData, ChainViewState, + ProcessMsgsCtx, ProcessTxsCtx, VerifiedAuxData, }; use strata_btc_verification::{TxidInclusionProof, check_block_integrity}; @@ -24,8 +24,14 @@ use crate::{ /// witness commitment) and header continuity, loading subprotocols with auxiliary input data, /// processing protocol-specific transactions, handling inter-protocol communication, and /// constructing the final state with logs. +/// +/// `S` declares the subprotocol pipeline; `stf_params` are the protocol-rule +/// parameters the transition executes under (e.g. the fork schedule). Every +/// executor passes its own: guest programs hardcode them (so the verifying key +/// commits to them), while the worker passes its effective params with +/// discovered fork activations applied. pub fn compute_asm_transition( - spec: &S, + stf_params: &AsmStfParams, pre_state: &AnchorState, block: &Block, aux_data: &AuxData, @@ -57,21 +63,29 @@ pub fn compute_asm_transition( // 4. LOAD: Initialize each subprotocol in the subproto manager. let mut loader = LoaderStage::new(&mut manager, pre_state); - spec.call_subprotocols(&mut loader); + S::call_subprotocols(&mut loader); // 5. PROCESS: Feed each subprotocol its filtered transactions for execution. // This stage performs the actual state transitions for each subprotocol. - let mut process_stage = - ProcessStage::new(&mut manager, &pow_state, protocol_txs, verified_aux_data); - spec.call_subprotocols(&mut process_stage); + let process_ctx = ProcessTxsCtx { + header_vs: &pow_state, + verified_aux_data: &verified_aux_data, + stf_params, + }; + let mut process_stage = ProcessStage::new(&mut manager, protocol_txs, process_ctx); + S::call_subprotocols(&mut process_stage); // 6. FINISH: Allow each subprotocol to process buffered inter-protocol messages. // This stage handles cross-protocol communication and finalizes state changes. // TODO(STR-2416): probably will have change this to repeat the interproto message // processing phase until we have no more messages to deliver, or some // bounded number of times - let mut finish_stage = FinishStage::new(&mut manager, &pow_state.last_verified_block); - spec.call_subprotocols(&mut finish_stage); + let finish_ctx = ProcessMsgsCtx { + l1ref: &pow_state.last_verified_block, + stf_params, + }; + let mut finish_stage = FinishStage::new(&mut manager, finish_ctx); + S::call_subprotocols(&mut finish_stage); // 7. Construct the manifest with the logs. let (sections, logs) = manager.export_sections_and_logs()?; diff --git a/crates/subprotocols/admin/subprotocol/src/subprotocol.rs b/crates/subprotocols/admin/subprotocol/src/subprotocol.rs index 4d9cab8d..da76eca6 100644 --- a/crates/subprotocols/admin/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/admin/subprotocol/src/subprotocol.rs @@ -4,12 +4,11 @@ //! with the Strata Anchor State Machine (ASM) for managing protocol governance and updates. use strata_asm_common::{ - HeaderVerificationState, MsgRelayer, NullMsg, Subprotocol, SubprotocolId, TxInputRef, - VerifiedAuxData, logging::warn, + MsgRelayer, NullMsg, ProcessMsgsCtx, ProcessTxsCtx, Subprotocol, SubprotocolId, TxInputRef, + logging::warn, }; use strata_asm_params::AdministrationInitConfig; use strata_asm_proto_admin_txs::{constants::ADMINISTRATION_SUBPROTOCOL_ID, parser::parse_tx}; -use strata_identifiers::L1BlockCommitment; use crate::{ handler::{handle_action, handle_pending_updates}, @@ -45,11 +44,10 @@ impl Subprotocol for AdministrationSubprotocol { fn process_txs( state: &mut AdministrationSubprotoState, txs: &[TxInputRef<'_>], - header_vs: &HeaderVerificationState, - _verified_aux_data: &VerifiedAuxData, relayer: &mut impl MsgRelayer, + ctx: &ProcessTxsCtx<'_>, ) { - let current_height = header_vs.last_verified_block.height(); + let current_height = ctx.header_vs.last_verified_block.height(); // Phase 1: Execute any pending updates that have reached their activation height handle_pending_updates(state, relayer, current_height); @@ -80,7 +78,7 @@ impl Subprotocol for AdministrationSubprotocol { fn process_msgs( _state: &mut AdministrationSubprotoState, _msgs: &[Self::Msg], - _l1ref: &L1BlockCommitment, + _ctx: &ProcessMsgsCtx<'_>, ) { } } diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index 94d0be35..0c54c1d5 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -4,15 +4,14 @@ //! with the Strata Anchor State Machine (ASM). use strata_asm_common::{ - AsmLogEntry, AuxRequestCollector, HeaderVerificationState, MsgRelayer, Subprotocol, - SubprotocolId, TxInputRef, VerifiedAuxData, + AsmLogEntry, AuxRequestCollector, MsgRelayer, PreProcessTxsCtx, ProcessMsgsCtx, ProcessTxsCtx, + Subprotocol, SubprotocolId, TxInputRef, logging::{debug, error, info}, }; use strata_asm_logs::ExportExtraDataUpdate; use strata_asm_params::BridgeV1InitConfig; use strata_asm_proto_bridge_v1_msgs::BridgeIncomingMsg; use strata_asm_proto_bridge_v1_txs::{BRIDGE_V1_SUBPROTOCOL_ID, parser::parse_tx}; -use strata_identifiers::L1BlockCommitment; use crate::{ handler::{handle_parsed_tx, preprocess_parsed_tx}, @@ -48,6 +47,7 @@ impl Subprotocol for BridgeV1Subproto { state: &Self::State, txs: &[TxInputRef<'_>], collector: &mut AuxRequestCollector, + _ctx: &PreProcessTxsCtx<'_>, ) { // Pre-Process each transaction for tx in txs { @@ -77,10 +77,11 @@ impl Subprotocol for BridgeV1Subproto { fn process_txs( state: &mut Self::State, txs: &[TxInputRef<'_>], - header_vs: &HeaderVerificationState, - verified_aux_data: &VerifiedAuxData, relayer: &mut impl MsgRelayer, + ctx: &ProcessTxsCtx<'_>, ) { + let header_vs = ctx.header_vs; + // Process each transaction for tx in txs { // Parse transaction to extract structured data (deposit/withdrawal info) @@ -90,7 +91,7 @@ impl Subprotocol for BridgeV1Subproto { let Some(parsed_tx) = parse_tx(tx) else { continue; }; - match handle_parsed_tx(state, parsed_tx, verified_aux_data, relayer) { + match handle_parsed_tx(state, parsed_tx, ctx.verified_aux_data, relayer) { // `handle_parsed_tx` already emits a type-specific info log on success, so this // is only a coarse trace marker. `txid` is computed inside the macro, because // logging is compiled to noop in ZkVM. @@ -162,7 +163,8 @@ impl Subprotocol for BridgeV1Subproto { /// /// Both conditions represent unrecoverable protocol violations where continued operation /// poses significant risk of fund loss. - fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], l1ref: &L1BlockCommitment) { + fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], ctx: &ProcessMsgsCtx<'_>) { + let l1ref = ctx.l1ref; for msg in msgs { match msg { BridgeIncomingMsg::DispatchWithdrawal(payload) => { @@ -210,7 +212,7 @@ impl Subprotocol for BridgeV1Subproto { #[cfg(test)] mod tests { - use strata_asm_common::Subprotocol; + use strata_asm_common::{AsmStfParams, ProcessMsgsCtx, Subprotocol}; use strata_asm_proto_bridge_v1_msgs::{BridgeIncomingMsg, DefconPayload}; use strata_asm_proto_bridge_v1_types::SafeHarbourAddress; use strata_identifiers::L1BlockCommitment; @@ -237,7 +239,12 @@ mod tests { let msgs = vec![BridgeIncomingMsg::UpdateSafeHarbourAddress( new_address.clone(), )]; - BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + let stf_params = AsmStfParams::default(); + let ctx = ProcessMsgsCtx { + l1ref: &l1ref, + stf_params: &stf_params, + }; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &ctx); assert_eq!(state.safe_harbour().address(), &new_address); // Address updates alone must not activate the safe harbour. @@ -250,7 +257,12 @@ mod tests { let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); let msgs = vec![BridgeIncomingMsg::Defcon(DefconPayload::default())]; - BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + let stf_params = AsmStfParams::default(); + let ctx = ProcessMsgsCtx { + l1ref: &l1ref, + stf_params: &stf_params, + }; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &ctx); assert!(state.safe_harbour().is_activated()); assert_eq!( @@ -275,7 +287,12 @@ mod tests { BridgeIncomingMsg::Defcon(DefconPayload::default()), BridgeIncomingMsg::UpdateSafeHarbourAddress(rejected_address), ]; - BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + let stf_params = AsmStfParams::default(); + let ctx = ProcessMsgsCtx { + l1ref: &l1ref, + stf_params: &stf_params, + }; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &ctx); assert!(state.safe_harbour().is_activated()); // Address must be unchanged from before the rejected update. diff --git a/crates/subprotocols/checkpoint/subprotocol/src/subprotocol.rs b/crates/subprotocols/checkpoint/subprotocol/src/subprotocol.rs index 1b8e6024..585cab77 100644 --- a/crates/subprotocols/checkpoint/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/checkpoint/subprotocol/src/subprotocol.rs @@ -1,8 +1,8 @@ //! Checkpoint Subprotocol Implementation use strata_asm_common::{ - AuxRequestCollector, HeaderVerificationState, MsgRelayer, Subprotocol, SubprotocolId, - TxInputRef, VerifiedAuxData, logging, + AuxRequestCollector, MsgRelayer, PreProcessTxsCtx, ProcessMsgsCtx, ProcessTxsCtx, Subprotocol, + SubprotocolId, TxInputRef, logging, }; use strata_asm_params::CheckpointInitConfig; use strata_asm_proto_checkpoint_msgs::CheckpointIncomingMsg; @@ -10,7 +10,6 @@ use strata_asm_proto_checkpoint_txs::{ CHECKPOINT_SUBPROTOCOL_ID, OL_STF_CHECKPOINT_TX_TYPE, extract_checkpoint_from_envelope, }; use strata_checkpoint_verification::CheckpointState; -use strata_identifiers::L1BlockCommitment; use crate::handler::handle_checkpoint_tx; @@ -41,6 +40,7 @@ impl Subprotocol for CheckpointSubprotocol { state: &Self::State, txs: &[TxInputRef<'_>], collector: &mut AuxRequestCollector, + _ctx: &PreProcessTxsCtx<'_>, ) { for tx in txs { if tx.tag().tx_type() == OL_STF_CHECKPOINT_TX_TYPE { @@ -81,20 +81,19 @@ impl Subprotocol for CheckpointSubprotocol { fn process_txs( state: &mut Self::State, txs: &[TxInputRef<'_>], - header_vs: &HeaderVerificationState, - verified_aux_data: &VerifiedAuxData, relayer: &mut impl MsgRelayer, + ctx: &ProcessTxsCtx<'_>, ) { - let current_l1_height = header_vs.last_verified_block.height(); + let current_l1_height = ctx.header_vs.last_verified_block.height(); for tx in txs { if tx.tag().tx_type() == OL_STF_CHECKPOINT_TX_TYPE { - handle_checkpoint_tx(state, tx, current_l1_height, verified_aux_data, relayer) + handle_checkpoint_tx(state, tx, current_l1_height, ctx.verified_aux_data, relayer) } } } - fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], _l1ref: &L1BlockCommitment) { + fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], _ctx: &ProcessMsgsCtx<'_>) { // ASM design assumes subprotocols are not adversarial against each other, // so no additional validation is performed on incoming messages. for msg in msgs { diff --git a/crates/worker/src/builder.rs b/crates/worker/src/builder.rs index d9bc3e0a..418ea815 100644 --- a/crates/worker/src/builder.rs +++ b/crates/worker/src/builder.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use strata_asm_common::AsmSpec; use strata_service::ServiceBuilder; use strata_tasks::TaskExecutor; @@ -14,14 +16,14 @@ use crate::{ /// from leaking into the caller. The builder launches the service and returns /// a handle to it. /// -/// Generic over the worker context `W` and the ASM spec `S`, so callers can -/// inject alternative specs (e.g. a debug-wrapped spec for testing) without -/// forking the worker. +/// Generic over the worker context `W` and the ASM spec `S`. The spec is a +/// pure type-level declaration of the pipeline and its params type, so +/// callers name it explicitly, e.g. `AsmWorkerBuilder::<_, StrataAsmSpec>::new()`. #[derive(Debug)] pub struct AsmWorkerBuilder { context: Option, params: Option, - spec: Option, + _spec: PhantomData, } impl AsmWorkerBuilder { @@ -30,7 +32,7 @@ impl AsmWorkerBuilder { Self { context: None, params: None, - spec: None, + _spec: PhantomData, } } @@ -40,21 +42,15 @@ impl AsmWorkerBuilder { self } - /// Set the ASM parameters used to construct the genesis state. + /// Set the ASM params. The spec derives everything else from these: the + /// genesis anchor state, adopted (after validation against the L1 source) + /// when the store holds no prior state, and the base STF params the + /// worker starts from. pub fn with_params(mut self, params: S::Params) -> Self { self.params = Some(params); self } - /// Set the ASM spec driving the subprotocol pipeline. - /// - /// Production deployments pass `StrataAsmSpec`; tests can pass a wrapped - /// debug spec to inject extra subprotocols. - pub fn with_asm_spec(mut self, spec: S) -> Self { - self.spec = Some(spec); - self - } - /// Launch the ASM worker service and return a handle to it. /// /// This method validates all required dependencies, creates the service state, @@ -64,7 +60,6 @@ impl AsmWorkerBuilder { where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { let context = self .context @@ -72,7 +67,13 @@ impl AsmWorkerBuilder { let params = self .params .ok_or(WorkerError::MissingDependency("params"))?; - let spec = self.spec.ok_or(WorkerError::MissingDependency("spec"))?; + + let genesis_state = S::construct_genesis_state(¶ms); + let stf_params = S::stf_params(¶ms); + // Exposed on the handle so downstream services (Moho worker, prover + // input builder) read the chain's genesis point from the worker + // rather than re-deriving it from params. + let genesis_block = genesis_state.chain_view.pow_state.last_verified_block; // Shared between the service state (which emits) and the handle (which // hands out subscriptions), so a `subscribe_blocks()` on the handle @@ -80,7 +81,12 @@ impl AsmWorkerBuilder { let subscribers = Subscribers::default(); // Create the service state. - let service_state = AsmWorkerServiceState::new(context, spec, params, subscribers.clone())?; + let service_state = AsmWorkerServiceState::::new( + context, + genesis_state, + stf_params, + subscribers.clone(), + )?; // Create the service builder and get command handle. let mut service_builder = @@ -93,7 +99,8 @@ impl AsmWorkerBuilder { let service_monitor = service_builder.launch_sync(constants::SERVICE_NAME, executor)?; // Create and return the handle. - let handle = AsmWorkerHandle::new(command_handle, service_monitor, subscribers); + let handle = + AsmWorkerHandle::new(command_handle, service_monitor, subscribers, genesis_block); Ok(handle) } diff --git a/crates/worker/src/handle.rs b/crates/worker/src/handle.rs index 1ea30874..b4dae594 100644 --- a/crates/worker/src/handle.rs +++ b/crates/worker/src/handle.rs @@ -12,6 +12,7 @@ pub struct AsmWorkerHandle { command_handle: CommandHandle, monitor: ServiceMonitor, subscribers: Subscribers, + genesis_block: L1BlockCommitment, } impl AsmWorkerHandle { @@ -19,18 +20,30 @@ impl AsmWorkerHandle { /// /// `subscribers` is the same registry the service state emits into, so /// handles created here can hand out [`Subscription`]s wired to the worker. + /// `genesis_block` is the L1 block the genesis anchor is pinned to. pub(crate) fn new( command_handle: CommandHandle, monitor: ServiceMonitor, subscribers: Subscribers, + genesis_block: L1BlockCommitment, ) -> Self { Self { command_handle, monitor, subscribers, + genesis_block, } } + /// The L1 block the genesis anchor is pinned to. + /// + /// The worker is the component that owns the params, so downstream + /// services needing the chain's genesis point (the Moho worker, the + /// prover input builder) read it from here. + pub fn genesis_block(&self) -> L1BlockCommitment { + self.genesis_block + } + /// Subscribes to per-block notifications. /// /// Returns a [`Subscription`] that yields each [`L1BlockCommitment`] the diff --git a/crates/worker/src/service.rs b/crates/worker/src/service.rs index 213ee9d3..08886f71 100644 --- a/crates/worker/src/service.rs +++ b/crates/worker/src/service.rs @@ -24,7 +24,6 @@ impl Service for AsmWorkerService where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { type State = AsmWorkerServiceState; type Msg = AsmWorkerMessage; @@ -43,7 +42,6 @@ impl SyncService for AsmWorkerService where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { fn process_input( state: &mut AsmWorkerServiceState, @@ -117,7 +115,6 @@ fn sync_to_block( where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { // Resolve the submitted id to a height-tagged commitment. This is the only // height the worker takes from outside; every later height is derived from @@ -259,7 +256,6 @@ fn apply_block( where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { // Fetch the full block now, one height at a time, so only a single block is // resident at any point during the forward pass. @@ -301,7 +297,7 @@ mod tests { use std::thread; use bitcoind_async_client::traits::Reader; - use strata_asm_common::{AsmManifestHash, AuxRequestCollector}; + use strata_asm_common::{AsmManifestHash, AsmStfParams, AuxRequestCollector}; use strata_btc_types::L1BlockIdBitcoinExt; use strata_identifiers::{Buf32, L1BlockId}; use strata_service::CommandCompletionSender; @@ -533,10 +529,14 @@ mod tests { // ...so a restart over the same store resumes at the tip. let context = fx.state.context.clone(); - let params = fixtures::genesis_params(&fx.client, 101).await; - let reloaded = - AsmWorkerServiceState::new(context, TestAsmSpec, params, Subscribers::default()) - .unwrap(); + let genesis = fixtures::genesis_state(&fx.client, 101).await; + let reloaded = AsmWorkerServiceState::<_, TestAsmSpec>::new( + context, + genesis, + AsmStfParams::default(), + Subscribers::default(), + ) + .unwrap(); assert_eq!( reloaded.blkid, tip, "restart resumes from the tip, not the stale notification", diff --git a/crates/worker/src/state.rs b/crates/worker/src/state.rs index 5e26eef0..847ce21a 100644 --- a/crates/worker/src/state.rs +++ b/crates/worker/src/state.rs @@ -1,5 +1,7 @@ +use std::marker::PhantomData; + use bitcoin::{Block, CompactTarget, params::Params}; -use strata_asm_common::{AnchorState, AsmSpec, AuxData, HeaderVerificationState}; +use strata_asm_common::{AnchorState, AsmSpec, AsmStfParams, AuxData, HeaderVerificationState}; use strata_asm_stf::AsmStfOutput; use strata_btc_types::BlockHashExt; use strata_btc_verification::{ @@ -17,16 +19,13 @@ use crate::{ /// Service state for the ASM worker. /// /// Generic over the worker context `W` and the ASM spec `S`, so callers can -/// inject alternative specs wrapping `StrataAsmSpec` (e.g. for testing) without -/// forking the worker. +/// inject alternative specs (e.g. for testing) without forking the worker. +/// The spec is purely a type-level pipeline declaration, so no value is held. #[derive(Debug)] pub struct AsmWorkerServiceState { /// Context for the state to interact with outer world. pub(crate) context: W, - /// ASM spec driving the subprotocol pipeline. - pub(crate) spec: S, - /// Current ASM anchor state. pub anchor: AnchorState, @@ -42,25 +41,39 @@ pub struct AsmWorkerServiceState { /// the service fans the new commitment out to these; see /// [`crate::AsmWorkerHandle::subscribe_blocks`]. pub(crate) subscribers: Subscribers, + + /// STF params every transition executes under. + pub(crate) stf_params: AsmStfParams, + + /// ASM spec driving the subprotocol pipeline (type-level only). + _spec: PhantomData, } impl AsmWorkerServiceState where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { - /// Creates a new service state, loading the latest anchor or creating genesis. + /// Creates a new service state, loading the latest anchor or adopting the + /// given genesis state. + /// + /// `stf_params` are the params every transition executes under; + /// `genesis_state` is the genesis anchor the worker adopts when the store + /// holds no prior state. /// /// Construction goes through [`crate::AsmWorkerBuilder`], which owns the /// shared [`Subscribers`] registry — hence `pub(crate)`. pub(crate) fn new( context: W, - spec: S, - params: S::Params, + genesis_state: AnchorState, + stf_params: AsmStfParams, subscribers: Subscribers, ) -> WorkerResult { - let genesis_height = spec.genesis_l1_height(¶ms); + let genesis_height = genesis_state + .chain_view + .pow_state + .last_verified_block + .height() as u64; // Align the manifest MMR with L1 heights before processing any block: // it is height-indexed, prefilled with sentinels for heights @@ -70,11 +83,10 @@ where // The configured anchor is otherwise trusted blindly: a wrong block, // target, epoch timestamp, or network would only surface one L1 block - // later when header verification rejects the anchor's successor. Build - // the genesis state once (it carries the anchor-derived header - // verification fields) and validate it against the L1 source on every - // startup, before adopting either stored or genesis state. - let genesis_state = spec.construct_genesis_state(¶ms); + // later when header verification rejects the anchor's successor. The + // genesis state carries the anchor-derived header verification fields; + // validate them against the L1 source on every startup, before + // adopting either stored or genesis state. validate_anchor_against_l1(&context, &genesis_state.chain_view.pow_state)?; let (anchor, blkid) = match context.get_latest_asm_state()? { @@ -93,11 +105,12 @@ where Ok(Self { context, - spec, + _spec: PhantomData, anchor, blkid, genesis_height, subscribers, + stf_params, }) } @@ -117,7 +130,7 @@ where let span = tracing::debug_span!("asm.stf.pre_process", protocol_txs = Empty); let _guard = span.enter(); - let result = strata_asm_stf::pre_process_asm(&self.spec, cur_state, block) + let result = strata_asm_stf::pre_process_asm::(&self.stf_params, cur_state, block) .map_err(WorkerError::AsmError)?; span.record("protocol_txs", result.txs.len()); @@ -143,8 +156,8 @@ where let coinbase_inclusion_proof = TxidInclusionProof::generate(&block.txdata, 0); - strata_asm_stf::compute_asm_transition( - &self.spec, + strata_asm_stf::compute_asm_transition::( + &self.stf_params, cur_state, block, &aux_data, @@ -165,7 +178,6 @@ impl ServiceState for AsmWorkerServiceState where W: WorkerContext + Send + Sync + 'static, S: AsmSpec + Send + Sync + 'static, - S::Params: Send + Sync + 'static, { fn name(&self) -> &str { constants::SERVICE_NAME @@ -320,10 +332,14 @@ mod tests { .store_anchor_state(&advanced, &seed.state.anchor) .unwrap(); - let params = fixtures::genesis_params(&seed.client, 101).await; - let reloaded = - AsmWorkerServiceState::new(context, TestAsmSpec, params, Subscribers::default()) - .unwrap(); + let genesis = fixtures::genesis_state(&seed.client, 101).await; + let reloaded = AsmWorkerServiceState::<_, TestAsmSpec>::new( + context, + genesis, + AsmStfParams::default(), + Subscribers::default(), + ) + .unwrap(); assert_eq!( reloaded.blkid, advanced, @@ -341,12 +357,7 @@ mod tests { let hash = client.get_block_hash(height).await.unwrap(); let mut anchor = get_l1_anchor(client, &hash).await.unwrap(); tamper(&mut anchor); - let params = fixtures::TestAsmParams { - anchor, - magic: strata_l1_txfmt::MagicBytes::new(*b"ALPN"), - }; - TestAsmSpec - .construct_genesis_state(¶ms) + fixtures::genesis_state_from_anchor(anchor) .chain_view .pow_state } @@ -446,8 +457,14 @@ mod tests { assert_eq!(fx.state.context.mmr_leaf_count(), 102); let context = fx.state.context.clone(); - let params = fixtures::genesis_params(&fx.client, 101).await; - AsmWorkerServiceState::new(context, TestAsmSpec, params, Subscribers::default()).unwrap(); + let genesis = fixtures::genesis_state(&fx.client, 101).await; + AsmWorkerServiceState::<_, TestAsmSpec>::new( + context, + genesis, + AsmStfParams::default(), + Subscribers::default(), + ) + .unwrap(); assert_eq!( fx.state.context.mmr_leaf_count(), diff --git a/crates/worker/src/test_utils.rs b/crates/worker/src/test_utils.rs index 09072b8b..efc4aeac 100644 --- a/crates/worker/src/test_utils.rs +++ b/crates/worker/src/test_utils.rs @@ -313,8 +313,8 @@ pub(crate) mod fixtures { use bitcoind_async_client::{Client, traits::Reader}; use corepc_node::Node; use strata_asm_common::{ - AnchorState, AsmHistoryAccumulatorState, AsmSpec, ChainViewState, HeaderVerificationState, - Stage, + AnchorState, AsmHistoryAccumulatorState, AsmSpec, AsmStfParams, ChainViewState, + HeaderVerificationState, }; use strata_btc_types::BlockHashExt; use strata_btc_verification::L1Anchor; @@ -327,41 +327,38 @@ pub(crate) mod fixtures { use super::{TestAsmWorkerContext, get_l1_anchor}; use crate::{AsmWorkerServiceState, Subscribers}; - /// Minimal [`AsmSpec::Params`] for the worker's own tests: just the L1 anchor - /// the genesis state pins to, plus a magic. The production `AsmParams` also - /// carries per-subprotocol configs, which [`TestAsmSpec`] has no use for. - #[derive(Debug)] - pub(crate) struct TestAsmParams { - pub anchor: L1Anchor, - pub magic: MagicBytes, - } - /// A no-subprotocol [`AsmSpec`] for exercising the worker in isolation. #[derive(Debug)] pub(crate) struct TestAsmSpec; impl AsmSpec for TestAsmSpec { - type Params = TestAsmParams; - - fn call_subprotocols(&self, _stage: &mut impl Stage) {} - - fn construct_genesis_state(&self, params: &Self::Params) -> AnchorState { - let genesis_height = params.anchor.block.height() as u64; - let chain_view = ChainViewState { - history_accumulator: AsmHistoryAccumulatorState::new(genesis_height), - pow_state: HeaderVerificationState::init(params.anchor.clone()), - }; - AnchorState { - magic: AnchorState::magic_ssz(params.magic), - chain_view, - sections: Vec::new() - .try_into() - .expect("empty dummy sections fit within capacity"), - } + type Subprotocols = (); + + type Params = L1Anchor; + + fn construct_genesis_state(anchor: &L1Anchor) -> AnchorState { + genesis_state_from_anchor(anchor.clone()) + } + + fn stf_params(_anchor: &L1Anchor) -> AsmStfParams { + AsmStfParams::default() } + } - fn genesis_l1_height(&self, params: &Self::Params) -> u64 { - params.anchor.block.height() as u64 + /// A no-sections genesis [`AnchorState`] pinned to `anchor` — the + /// [`TestAsmSpec`] counterpart of the production genesis construction. + pub(crate) fn genesis_state_from_anchor(anchor: L1Anchor) -> AnchorState { + let genesis_height = anchor.block.height() as u64; + let chain_view = ChainViewState { + history_accumulator: AsmHistoryAccumulatorState::new(genesis_height), + pow_state: HeaderVerificationState::init(anchor), + }; + AnchorState { + magic: AnchorState::magic_ssz(MagicBytes::new(*b"ALPN")), + chain_view, + sections: Vec::new() + .try_into() + .expect("empty dummy sections fit within capacity"), } } @@ -375,7 +372,7 @@ pub(crate) mod fixtures { } /// Builds a worker state with genesis at `genesis_height`: mine that many - /// blocks, point the ASM params' anchor at the tip, and run + /// blocks, pin the genesis anchor at the tip, and run /// [`AsmWorkerServiceState::new`] (which stores the genesis anchor and /// prefills the manifest MMR). pub(crate) async fn setup_state(genesis_height: u64) -> StateFixture { @@ -385,11 +382,15 @@ pub(crate) mod fixtures { .await .expect("mine genesis blocks"); - let params = genesis_params(&client, genesis_height).await; + let genesis = genesis_state(&client, genesis_height).await; let context = TestAsmWorkerContext::new((*client).clone()); - let state = - AsmWorkerServiceState::new(context, TestAsmSpec, params, Subscribers::default()) - .expect("create service state"); + let state = AsmWorkerServiceState::new( + context, + genesis, + AsmStfParams::default(), + Subscribers::default(), + ) + .expect("create service state"); StateFixture { node, @@ -398,18 +399,15 @@ pub(crate) mod fixtures { } } - /// [`TestAsmParams`] with the anchor pinned to the block at `genesis_height`, - /// so [`AsmWorkerServiceState::new`] genesis lands there. - pub(crate) async fn genesis_params(client: &Client, genesis_height: u64) -> TestAsmParams { + /// Genesis [`AnchorState`] with the anchor pinned to the block at + /// `genesis_height`, so [`AsmWorkerServiceState::new`] genesis lands there. + pub(crate) async fn genesis_state(client: &Client, genesis_height: u64) -> AnchorState { let tip = client .get_block_hash(genesis_height) .await .expect("genesis tip hash"); let anchor = get_l1_anchor(client, &tip).await.expect("genesis anchor"); - TestAsmParams { - anchor, - magic: MagicBytes::new(*b"ALPN"), - } + genesis_state_from_anchor(anchor) } /// A running regtest node with a bare worker context (no anchors stored, no diff --git a/functional-tests/factory/common/asm_params.py b/functional-tests/factory/common/asm_params.py index 730e39cc..46435ab8 100644 --- a/functional-tests/factory/common/asm_params.py +++ b/functional-tests/factory/common/asm_params.py @@ -11,6 +11,10 @@ # tests. Address `bc1ppuxgmd6n4j73wdp688p08a8rte97dkn5n70r2ym6kgsw0v3c5ensrytduf`. DEFAULT_SAFE_HARBOUR_ADDRESS = "040f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667" +# Fork activation height meaning "disabled". `None` serializes to JSON `null`, +# matching the `Option` on the Rust side. +FORK_DISABLED = None + @dataclass class Block: @@ -80,12 +84,17 @@ class AsmParams: magic: str anchor: L1Anchor subprotocols: list[dict[str, Any]] + # STF config: base fork schedule. `None` disables the fork. Dynamic + # activations come from enacted ASM VK upgrades, which name the fork they + # activate. + fork1_height: int | None = 0 def to_dict(self) -> dict[str, Any]: return { "magic": self.magic, "anchor": asdict(self.anchor), "subprotocols": self.subprotocols, + "forks": {"fork1": self.fork1_height}, } @@ -139,9 +148,11 @@ def build_subprotocols( operator_fee: int = 100_000_000, recovery_delay: int = 1_008, safe_harbour_address: str = DEFAULT_SAFE_HARBOUR_ADDRESS, + confirmation_depth: int = 144, ) -> list[dict[str, Any]]: - compressed_keys = [f"02{key}" for key in musig2_keys] - confirmation_depth = 144 + # Accept either x-only keys (64 hex chars, assumed even-Y and prefixed + # with 02) or full compressed keys (66 hex chars, used as-is). + compressed_keys = [key if len(key) == 66 else f"02{key}" for key in musig2_keys] admin = { "Admin": asdict( @@ -207,6 +218,8 @@ def build_asm_params( operator_fee: int = 100_000_000, recovery_delay: int = 1_008, safe_harbour_address: str = DEFAULT_SAFE_HARBOUR_ADDRESS, + confirmation_depth: int = 144, + fork1_height: int | None = 0, ) -> AsmParams: anchor = build_l1_anchor(genesis_height, block_hash, header, epoch_start_header) subprotocols = build_subprotocols( @@ -217,11 +230,13 @@ def build_asm_params( operator_fee=operator_fee, recovery_delay=recovery_delay, safe_harbour_address=safe_harbour_address, + confirmation_depth=confirmation_depth, ) return AsmParams( magic=magic, anchor=anchor, subprotocols=subprotocols, + fork1_height=fork1_height, ) diff --git a/guest-builder/sp1/guest-asm/Cargo.lock b/guest-builder/sp1/guest-asm/Cargo.lock index 0667b14c..ff3f55d9 100644 --- a/guest-builder/sp1/guest-asm/Cargo.lock +++ b/guest-builder/sp1/guest-asm/Cargo.lock @@ -1823,6 +1823,7 @@ dependencies = [ "serde", "ssz", "ssz_derive", + "strata-asm-common", "strata-asm-proto-bridge-v1-types", "strata-btc-types", "strata-btc-verification", diff --git a/guest-builder/sp1/guest-asm/src/main.rs b/guest-builder/sp1/guest-asm/src/main.rs index c6ddc824..88f56a9a 100644 --- a/guest-builder/sp1/guest-asm/src/main.rs +++ b/guest-builder/sp1/guest-asm/src/main.rs @@ -1,9 +1,10 @@ #![no_main] zkaleido_sp1_guest_env::entrypoint!(main); -use strata_asm_proof_impl::statements::process_asm_stf; +use strata_asm_proof_impl::{statements::process_asm_stf, AsmStfParams}; use zkaleido_sp1_guest_env::Sp1ZkVmEnv; fn main() { - process_asm_stf(&Sp1ZkVmEnv) + // Hardcoded on purpose: the verifying key must commit to the STF params. + process_asm_stf(&Sp1ZkVmEnv, AsmStfParams::default()) } diff --git a/tests/asm/admin.rs b/tests/asm/admin.rs index bd42ea5a..5865fd3a 100644 --- a/tests/asm/admin.rs +++ b/tests/asm/admin.rs @@ -630,7 +630,7 @@ async fn test_multiple_zero_depth_updates_same_block() { // Verify all 3 transactions were included in the block let block = harness.client.get_block(&block_hash).await.unwrap(); - let parser = ParseConfig::new(harness.asm_params.magic); + let parser = ParseConfig::new(harness.asm_params.genesis.magic); let admin_tx_count = block .txdata .iter() diff --git a/tests/asm/admin_to_stf.rs b/tests/asm/admin_to_stf.rs index c0b4b794..9026037c 100644 --- a/tests/asm/admin_to_stf.rs +++ b/tests/asm/admin_to_stf.rs @@ -16,7 +16,7 @@ use integration_tests::harness; use moho_runtime_impl::RuntimeInput; use moho_types::ExportState; use ssz::Encode; -use strata_asm_common::AuxData; +use strata_asm_common::{AsmStfParams, AuxData}; use strata_asm_logs::AsmStfUpdate; use strata_asm_proof_impl::{ moho_program::{input::AsmStepInput, program::advance_export_state_with_logs}, @@ -157,12 +157,12 @@ async fn test_proof_program_reflects_predicate_update() { pre_anchor_state.as_ssz_bytes(), step_input.as_ssz_bytes(), ); - let attestation = - AsmStfProofProgram::execute(&runtime_input).expect("AsmStfProofProgram::execute failed"); + let attestation = AsmStfProofProgram::execute(&runtime_input, AsmStfParams::default()) + .expect("AsmStfProofProgram::execute failed"); // Independently compute the expected post-state. - let stf_output = compute_asm_transition( - &StrataAsmSpec, + let stf_output = compute_asm_transition::( + &AsmStfParams::default(), &pre_anchor_state, &activation_block, step_input.aux_data(), diff --git a/tests/harness/bridge.rs b/tests/harness/bridge.rs index e5369f61..99bb98cf 100644 --- a/tests/harness/bridge.rs +++ b/tests/harness/bridge.rs @@ -233,7 +233,7 @@ impl AsmTestHarness { // Build the SPS-50 OP_RETURN tag let tag_data = drt_aux.build_tag_data(); - let parse_config = ParseConfig::new(self.asm_params.magic); + let parse_config = ParseConfig::new(self.asm_params.genesis.magic); let op_return_script = parse_config.encode_script_buf(&tag_data.as_ref())?; // Build the P2TR deposit request locking script @@ -313,7 +313,7 @@ impl AsmTestHarness { // Build the SPS-50 OP_RETURN tag for deposit let dt_aux = DepositTxHeaderAux::new(deposit_idx); let tag_data = dt_aux.build_tag_data(); - let parse_config = ParseConfig::new(self.asm_params.magic); + let parse_config = ParseConfig::new(self.asm_params.genesis.magic); let op_return_script = parse_config.encode_script_buf(&tag_data.as_ref())?; // Build the P2TR deposit output (key-path only with operator multisig) @@ -466,7 +466,7 @@ pub async fn submit_forged_unstake_tx( let aux = UnstakeTxHeaderAux::new(victim_idx); let tag_data = aux.build_tag_data(); let op_return_script = - ParseConfig::new(harness.asm_params.magic).encode_script_buf(&tag_data.as_ref())?; + ParseConfig::new(harness.asm_params.genesis.magic).encode_script_buf(&tag_data.as_ref())?; // 6. Send the leftover sats back to a wallet address so the tx pays a fee without producing a // dust violation. @@ -548,7 +548,7 @@ pub async fn submit_attacker_keyed_unstake_tx( let aux = UnstakeTxHeaderAux::new(victim_idx); let tag_data = aux.build_tag_data(); let op_return_script = - ParseConfig::new(harness.asm_params.magic).encode_script_buf(&tag_data.as_ref())?; + ParseConfig::new(harness.asm_params.genesis.magic).encode_script_buf(&tag_data.as_ref())?; let fee = AsmTestHarness::DEFAULT_FEE; let change_address = harness.client.get_new_address().await?; diff --git a/tests/harness/test_harness.rs b/tests/harness/test_harness.rs index 98863b6f..3639f27a 100644 --- a/tests/harness/test_harness.rs +++ b/tests/harness/test_harness.rs @@ -431,8 +431,8 @@ impl AsmTestHarness { let commit_outpoint = OutPoint::new(commit_txid, commit_vout); // Build SPS-50 compliant OP_RETURN tag - let op_return_script = - ParseConfig::new(self.asm_params.magic).encode_script_buf(&sps50_tag.as_ref())?; + let op_return_script = ParseConfig::new(self.asm_params.genesis.magic) + .encode_script_buf(&sps50_tag.as_ref())?; let op_return_output = TxOut { value: Amount::ZERO, @@ -691,8 +691,8 @@ impl AsmTestHarnessBuilder { // 4. Build AsmParams (arbitrary for non-subprotocol fields) and install our configs. let mut asm_params: AsmParams = ArbitraryGenerator::new().generate(); - asm_params.anchor = genesis_view; - for instance in &mut asm_params.subprotocols { + asm_params.genesis.anchor = genesis_view; + for instance in &mut asm_params.genesis.subprotocols { match instance { SubprotocolInstance::Admin(cfg) => *cfg = admin_config.clone(), SubprotocolInstance::Bridge(cfg) => *cfg = bridge_config.clone(), @@ -713,10 +713,9 @@ impl AsmTestHarnessBuilder { let executor = task_manager.create_executor(); // 7. Launch ASM worker service - let asm_handle = AsmWorkerBuilder::new() + let asm_handle = AsmWorkerBuilder::<_, StrataAsmSpec>::new() .with_context(context.clone()) - .with_asm_spec(StrataAsmSpec) - .with_params((*asm_params).clone()) + .with_params(asm_params.as_ref().clone()) .launch(&executor)?; let harness = AsmTestHarness {