From c44c76df477419cccb457b9df2448d4b89e3940f Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Mon, 6 Jul 2026 10:06:32 +0545 Subject: [PATCH 01/12] feat(common): add fork schedule primitives EVM-style named forks with L1 activation heights. The schedule is not committed state: proving artifacts bake 0/MAX extremes (each only ever executes one side of an upgrade boundary) while the worker tracks the real activation height, so every executor agrees on the gate outcome at every height. Upgrade actions will carry forks as raw u16 ids, because the artifact that enacts a fork's activation predates the fork and cannot know it; ForkId maps the ids a binary knows and leaves the rest to be skipped by the consumer. --- Cargo.lock | 1 + crates/common/Cargo.toml | 1 + crates/common/src/fork.rs | 184 ++++++++++++++++++++++++++++++++++++++ crates/common/src/lib.rs | 2 + 4 files changed, 188 insertions(+) create mode 100644 crates/common/src/fork.rs diff --git a/Cargo.lock b/Cargo.lock index 2b6c37c4..9fd26237 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7617,6 +7617,7 @@ dependencies = [ "bitcoin", "borsh", "serde", + "serde_json", "ssz", "ssz_codegen", "ssz_derive", 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..0f4e26b7 --- /dev/null +++ b/crates/common/src/fork.rs @@ -0,0 +1,184 @@ +//! 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 `StfParams`). + +use serde::{Deserialize, Serialize}; + +/// Identifies a named fork. +/// +/// One variant per protocol upgrade, in activation order. Discriminants are +/// stable: they key persisted fork-activation records and are the raw fork +/// ids carried in ASM VK upgrade actions. Actions carry the raw id rather +/// than this enum so that artifacts predating a fork can still parse and +/// enact the upgrade that activates it; consumers that act on the id (the +/// worker) map the ones they know via [`TryFrom`] and skip the rest. +#[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. + 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 is active at L1 height `h` iff `h >= activation_height`. `0` means +/// active since genesis; [`u64::MAX`] means never active. Proving artifacts +/// bake one of those two extremes (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`]. + pub fork1: u64, +} + +impl ForkSchedule { + /// Schedule with every fork disabled (activation at [`u64::MAX`]). + pub const fn all_disabled() -> Self { + Self { fork1: u64::MAX } + } + + /// Returns the activation height of `fork`. + pub fn activation_height(&self, fork: ForkId) -> u64 { + match fork { + ForkId::Fork1 => self.fork1, + } + } + + /// Returns whether `fork` is active at L1 `height`. + pub fn is_active(&self, fork: ForkId, height: u64) -> bool { + height >= self.activation_height(fork) + } + + /// Sets the activation height of `fork`. + pub fn activate_at(&mut self, fork: ForkId, height: u64) { + match fork { + ForkId::Fork1 => self.fork1 = 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 StfParams { + /// Fork activation schedule. + pub forks: ForkSchedule, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_active_boundaries() { + let sched = ForkSchedule { fork1: 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: 0 }; + assert!(sched.is_active(ForkId::Fork1, 0)); + assert!(sched.is_active(ForkId::Fork1, u64::MAX)); + } + + #[test] + fn max_means_never_active() { + let sched = ForkSchedule::all_disabled(); + assert!(!sched.is_active(ForkId::Fork1, 0)); + assert!(!sched.is_active(ForkId::Fork1, u64::MAX - 1)); + // Degenerate boundary: is_active is a plain >= comparison. + assert!(sched.is_active(ForkId::Fork1, u64::MAX)); + } + + #[test] + fn activate_at_overrides() { + let mut sched = ForkSchedule::all_disabled(); + sched.activate_at(ForkId::Fork1, 42); + assert_eq!(sched.activation_height(ForkId::Fork1), 42); + assert!(sched.is_active(ForkId::Fork1, 42)); + assert!(!sched.is_active(ForkId::Fork1, 41)); + } + + #[test] + fn serde_roundtrip() { + let params = StfParams { + forks: ForkSchedule { fork1: 7 }, + }; + let json = serde_json::to_string(¶ms).unwrap(); + assert_eq!(json, r#"{"forks":{"fork1":7}}"#); + let back: StfParams = serde_json::from_str(&json).unwrap(); + assert_eq!(back, params); + } + + #[test] + fn fork_id_serde_snake_case() { + assert_eq!(serde_json::to_string(&ForkId::Fork1).unwrap(), r#""fork1""#); + } + + /// 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::*; From fb177abc27e50c08f97e8e24c202ea0dde4f9c7e Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 7 Jul 2026 10:01:15 +0545 Subject: [PATCH 02/12] feat(params)!: split AsmParams into genesis and STF sections AsmParams was genesis-only, leaving no home for configuration of the per-block state transition. Split it by consumer: GenesisParams is consumed once to build the genesis anchor state, StfConfig (the base fork schedule) configures the state transition function for every block. Both sections are serde-flattened, so the params file stays a single flat object: the split is a property of the Rust types, not something operators need to spell out. --- Cargo.lock | 1 + bin/asm-runner/src/bootstrap.rs | 7 +- crates/extensions/moho/worker/src/state.rs | 2 +- crates/params/Cargo.toml | 1 + crates/params/src/genesis.rs | 89 ++++++++ crates/params/src/lib.rs | 160 +++++++++++++- crates/params/src/params.rs | 200 ------------------ crates/params/src/stf.rs | 36 ++++ crates/proof/statements/src/test_utils.rs | 10 +- crates/spec/src/genesis.rs | 8 +- crates/spec/src/lib.rs | 2 +- crates/spec/src/spec.rs | 4 +- functional-tests/factory/common/asm_params.py | 19 +- guest-builder/sp1/guest-asm/Cargo.lock | 1 + tests/asm/admin.rs | 2 +- tests/harness/bridge.rs | 8 +- tests/harness/test_harness.rs | 10 +- 17 files changed, 327 insertions(+), 233 deletions(-) create mode 100644 crates/params/src/genesis.rs delete mode 100644 crates/params/src/params.rs create mode 100644 crates/params/src/stf.rs diff --git a/Cargo.lock b/Cargo.lock index 9fd26237..3e2548c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7726,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..e0f9ec4e 100644 --- a/bin/asm-runner/src/bootstrap.rs +++ b/bin/asm-runner/src/bootstrap.rs @@ -77,7 +77,7 @@ pub(crate) async fn bootstrap( AsmWorkerBuilder::new() .with_context(worker_context) .with_asm_spec(StrataAsmSpec) - .with_params(params.clone()) + .with_params(params.genesis.clone()) .launch(&executor) })?; @@ -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(params.genesis.anchor.block) .with_asm_predicate(asm_predicate.clone()) .launch(&executor) .await?; @@ -133,7 +133,8 @@ 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(params.genesis.anchor.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/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/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..301fa358 --- /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 GenesisParams { + /// 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 GenesisParams { + 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 GenesisParams { + 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..57061885 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 [`GenesisParams`] (L1 magic bytes, +//! genesis L1 view and per-subprotocol configuration, consumed once to build +//! the genesis state) and [`StfConfig`] (fork schedule driving the per-block +//! state transition function). -mod params; +mod genesis; +mod stf; mod subprotocols; -pub use params::AsmParams; +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; +pub use genesis::GenesisParams; +use serde::{Deserialize, Serialize}; +pub use stf::StfConfig; +#[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: [`GenesisParams`] is only used to construct the genesis +/// anchor state, while [`StfConfig`] 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: GenesisParams, + + /// Parameters of the per-block state transition function. + #[serde(flatten)] + pub stf: StfConfig, +} + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for AsmParams { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + Ok(Self { + genesis: GenesisParams::arbitrary(u)?, + stf: StfConfig { + 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.stf.forks.fork1, 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/stf.rs b/crates/params/src/stf.rs new file mode 100644 index 00000000..f40b65d0 --- /dev/null +++ b/crates/params/src/stf.rs @@ -0,0 +1,36 @@ +//! Configuration of the per-block state transition function. + +use serde::{Deserialize, Serialize}; +use strata_asm_common::{ForkSchedule, StfParams}; + +/// Configuration 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 [`StfParams`]. 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 StfConfig { + /// Base fork activation schedule. + pub forks: ForkSchedule, +} + +impl StfConfig { + /// The STF-facing view of this config, before any dynamic activations. + pub fn stf_params(&self) -> StfParams { + StfParams { + forks: self.forks.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stf_config_deserialize() { + let cfg: StfConfig = serde_json::from_str(r#"{"forks":{"fork1":5}}"#).unwrap(); + assert_eq!(cfg.stf_params().forks.fork1, 5); + } +} diff --git a/crates/proof/statements/src/test_utils.rs b/crates/proof/statements/src/test_utils.rs index ff7f563b..0d38bd1e 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::GenesisParams; 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 `GenesisParams` +/// 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: GenesisParams = 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 = GenesisParams::arbitrary(&mut u).expect("deterministic GenesisParams"); 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..6fda63bd 100644 --- a/crates/spec/src/genesis.rs +++ b/crates/spec/src/genesis.rs @@ -1,19 +1,19 @@ -//! Genesis anchor state construction from [`AsmParams`]. +//! Genesis anchor state construction from [`GenesisParams`]. use strata_asm_common::{ AnchorState, AsmHistoryAccumulatorState, ChainViewState, HeaderVerificationState, SectionState, }; -use strata_asm_params::AsmParams; +use strata_asm_params::GenesisParams; use strata_asm_proto_admin::{AdministrationSubprotoState, AdministrationSubprotocol}; use strata_asm_proto_bridge_v1::{BridgeV1State, BridgeV1Subproto}; use strata_asm_proto_checkpoint::{CheckpointState, CheckpointSubprotocol}; use strata_btc_verification::HeaderVerificationState as NativeHeaderVerificationState; -/// Builds the genesis [`AnchorState`] from the given [`AsmParams`]. +/// Builds the genesis [`AnchorState`] from the given [`GenesisParams`]. /// /// 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 { +pub fn construct_genesis_state(params: &GenesisParams) -> AnchorState { let genesis_admin_subprotocol_state = AdministrationSubprotoState::new( params .admin_config() diff --git a/crates/spec/src/lib.rs b/crates/spec/src/lib.rs index 600c164b..30201a6a 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). +//! [`GenesisParams`](strata_asm_params::GenesisParams). mod genesis; mod spec; diff --git a/crates/spec/src/spec.rs b/crates/spec/src/spec.rs index 5523ea13..c55c4c75 100644 --- a/crates/spec/src/spec.rs +++ b/crates/spec/src/spec.rs @@ -1,7 +1,7 @@ //! Strata ASM specification defining the subprotocol pipeline. use strata_asm_common::{AnchorState, AsmSpec, Stage}; -use strata_asm_params::AsmParams; +use strata_asm_params::GenesisParams; use strata_asm_proto_admin::AdministrationSubprotocol; use strata_asm_proto_bridge_v1::BridgeV1Subproto; use strata_asm_proto_checkpoint::CheckpointSubprotocol; @@ -15,7 +15,7 @@ use strata_asm_proto_checkpoint::CheckpointSubprotocol; pub struct StrataAsmSpec; impl AsmSpec for StrataAsmSpec { - type Params = AsmParams; + type Params = GenesisParams; fn call_subprotocols(&self, stage: &mut impl Stage) { stage.invoke_subprotocol::(); diff --git a/functional-tests/factory/common/asm_params.py b/functional-tests/factory/common/asm_params.py index 730e39cc..f8f2aad7 100644 --- a/functional-tests/factory/common/asm_params.py +++ b/functional-tests/factory/common/asm_params.py @@ -11,6 +11,11 @@ # tests. Address `bc1ppuxgmd6n4j73wdp688p08a8rte97dkn5n70r2ym6kgsw0v3c5ensrytduf`. DEFAULT_SAFE_HARBOUR_ADDRESS = "040f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667" +# Fork activation height meaning "never". Capped at i64::MAX rather than +# u64::MAX because the value also rides through the prover's TOML config, +# and TOML integers are signed 64-bit. Equally unreachable in practice. +FORK_NEVER = 2**63 - 1 + @dataclass class Block: @@ -80,12 +85,16 @@ class AsmParams: magic: str anchor: L1Anchor subprotocols: list[dict[str, Any]] + # STF config: base fork schedule. Dynamic activations come from enacted + # ASM VK upgrades, which name the fork they activate. + fork1_height: int = 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 = 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/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/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..53757892 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(), @@ -716,7 +716,7 @@ impl AsmTestHarnessBuilder { let asm_handle = AsmWorkerBuilder::new() .with_context(context.clone()) .with_asm_spec(StrataAsmSpec) - .with_params((*asm_params).clone()) + .with_params(asm_params.genesis.clone()) .launch(&executor)?; let harness = AsmTestHarness { From d09a20c170c8d0e188c4357663540d59b7f3a558 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 7 Jul 2026 10:09:12 +0545 Subject: [PATCH 03/12] refactor!: reduce AsmSpec to a type-level pipeline declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trait declared the pipeline as an instance method and built genesis through &self, so every executor had to thread a spec value and the invocation order was only a doc-comment invariant ("MUST NOT change behavior per stage"). The pipeline is now a type-level subprotocol list, making invocation order a compile-time constant of the spec type: it cannot vary per stage, per execution, or with runtime configuration. The spec also owns its Params type and derives everything configuration-dependent from it (genesis state, base STF params) through pure functions, so the worker takes the single params value at its boundary and constructs genesis behind the spec — a genesis built by a different spec can no longer be adopted silently. The handle exposes the genesis block so downstream services (the Moho worker, the prover input builder) read the chain's genesis point from the worker rather than re-deriving it from params. --- bin/asm-runner/src/bootstrap.rs | 11 ++- crates/common/src/spec.rs | 89 ++++++++++++++----- .../statements/src/moho_program/program.rs | 5 +- crates/spec/src/spec.rs | 26 +++--- crates/stf/src/preprocess.rs | 8 +- crates/stf/src/transition.rs | 7 +- crates/worker/src/builder.rs | 39 ++++---- crates/worker/src/handle.rs | 13 +++ crates/worker/src/service.rs | 8 +- crates/worker/src/state.rs | 56 ++++++------ crates/worker/src/test_utils.rs | 75 +++++++--------- tests/asm/admin_to_stf.rs | 3 +- tests/harness/test_harness.rs | 5 +- 13 files changed, 195 insertions(+), 150 deletions(-) diff --git a/bin/asm-runner/src/bootstrap.rs b/bin/asm-runner/src/bootstrap.rs index e0f9ec4e..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.genesis.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.genesis.anchor.block) + .with_genesis_block(genesis_block) .with_asm_predicate(asm_predicate.clone()) .launch(&executor) .await?; @@ -133,8 +133,7 @@ pub(crate) async fn bootstrap( aux_db.clone(), bitcoin_client.clone(), ); - let input_builder = - InputBuilder::new(params.genesis.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/src/spec.rs b/crates/common/src/spec.rs index ba2d6e50..11ca0e3d 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, StfParams, 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) -> StfParams; + + /// 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/proof/statements/src/moho_program/program.rs b/crates/proof/statements/src/moho_program/program.rs index 0990a3c8..cb9f5ecb 100644 --- a/crates/proof/statements/src/moho_program/program.rs +++ b/crates/proof/statements/src/moho_program/program.rs @@ -96,11 +96,10 @@ impl MohoProgram for AsmStfProgram { fn process_transition( pre_state: &AnchorState, - spec: &StrataAsmSpec, + _spec: &StrataAsmSpec, input: &AsmStepInput, ) -> AsmStfOutput { - compute_asm_transition( - spec, + compute_asm_transition::( pre_state, input.block(), input.aux_data(), diff --git a/crates/spec/src/spec.rs b/crates/spec/src/spec.rs index c55c4c75..52ecbac8 100644 --- a/crates/spec/src/spec.rs +++ b/crates/spec/src/spec.rs @@ -1,11 +1,13 @@ //! Strata ASM specification defining the subprotocol pipeline. -use strata_asm_common::{AnchorState, AsmSpec, Stage}; -use strata_asm_params::GenesisParams; +use strata_asm_common::{AnchorState, AsmSpec, StfParams}; +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 @@ -15,19 +17,19 @@ use strata_asm_proto_checkpoint::CheckpointSubprotocol; pub struct StrataAsmSpec; impl AsmSpec for StrataAsmSpec { - type Params = GenesisParams; + type Subprotocols = ( + AdministrationSubprotocol, + CheckpointSubprotocol, + BridgeV1Subproto, + ); - fn call_subprotocols(&self, stage: &mut impl Stage) { - stage.invoke_subprotocol::(); - stage.invoke_subprotocol::(); - stage.invoke_subprotocol::(); - } + 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) -> StfParams { + params.stf.stf_params() } } diff --git a/crates/stf/src/preprocess.rs b/crates/stf/src/preprocess.rs index c4342e11..47948f0c 100644 --- a/crates/stf/src/preprocess.rs +++ b/crates/stf/src/preprocess.rs @@ -44,11 +44,9 @@ 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, pre_state: &AnchorState, block: &'b Block, ) -> AsmResult> { @@ -67,13 +65,13 @@ 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. let mut pre_process_stage = PreProcessStage::new(&mut manager, pre_state, &grouped_relevant_txs); - spec.call_subprotocols(&mut pre_process_stage); + 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/transition.rs b/crates/stf/src/transition.rs index d384c309..53a711ab 100644 --- a/crates/stf/src/transition.rs +++ b/crates/stf/src/transition.rs @@ -25,7 +25,6 @@ use crate::{ /// processing protocol-specific transactions, handling inter-protocol communication, and /// constructing the final state with logs. pub fn compute_asm_transition( - spec: &S, pre_state: &AnchorState, block: &Block, aux_data: &AuxData, @@ -57,13 +56,13 @@ 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); + 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. @@ -71,7 +70,7 @@ pub fn compute_asm_transition( // 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); + 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/worker/src/builder.rs b/crates/worker/src/builder.rs index d9bc3e0a..0e0f7074 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,14 @@ 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. 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 +59,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 +66,12 @@ 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); + // 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 +79,8 @@ 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, subscribers.clone())?; // Create the service builder and get command handle. let mut service_builder = @@ -93,7 +93,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..ddf6cc5e 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. @@ -533,9 +529,9 @@ 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 genesis = fixtures::genesis_state(&fx.client, 101).await; let reloaded = - AsmWorkerServiceState::new(context, TestAsmSpec, params, Subscribers::default()) + AsmWorkerServiceState::<_, TestAsmSpec>::new(context, genesis, Subscribers::default()) .unwrap(); assert_eq!( reloaded.blkid, tip, diff --git a/crates/worker/src/state.rs b/crates/worker/src/state.rs index 5e26eef0..1a863666 100644 --- a/crates/worker/src/state.rs +++ b/crates/worker/src/state.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use bitcoin::{Block, CompactTarget, params::Params}; use strata_asm_common::{AnchorState, AsmSpec, AuxData, HeaderVerificationState}; use strata_asm_stf::AsmStfOutput; @@ -17,15 +19,15 @@ 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, + /// ASM spec driving the subprotocol pipeline (type-level only). + _spec: PhantomData, /// Current ASM anchor state. pub anchor: AnchorState, @@ -48,19 +50,22 @@ 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 (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, 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 +75,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,7 +97,7 @@ where Ok(Self { context, - spec, + _spec: PhantomData, anchor, blkid, genesis_height, @@ -117,7 +121,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::(cur_state, block) .map_err(WorkerError::AsmError)?; span.record("protocol_txs", result.txs.len()); @@ -143,8 +147,7 @@ where let coinbase_inclusion_proof = TxidInclusionProof::generate(&block.txdata, 0); - strata_asm_stf::compute_asm_transition( - &self.spec, + strata_asm_stf::compute_asm_transition::( cur_state, block, &aux_data, @@ -165,7 +168,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,9 +322,9 @@ mod tests { .store_anchor_state(&advanced, &seed.state.anchor) .unwrap(); - let params = fixtures::genesis_params(&seed.client, 101).await; + let genesis = fixtures::genesis_state(&seed.client, 101).await; let reloaded = - AsmWorkerServiceState::new(context, TestAsmSpec, params, Subscribers::default()) + AsmWorkerServiceState::<_, TestAsmSpec>::new(context, genesis, Subscribers::default()) .unwrap(); assert_eq!( @@ -341,12 +343,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 +443,9 @@ 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, 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..e3fa300f 100644 --- a/crates/worker/src/test_utils.rs +++ b/crates/worker/src/test_utils.rs @@ -314,7 +314,7 @@ pub(crate) mod fixtures { use corepc_node::Node; use strata_asm_common::{ AnchorState, AsmHistoryAccumulatorState, AsmSpec, ChainViewState, HeaderVerificationState, - Stage, + StfParams, }; 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 genesis_l1_height(&self, params: &Self::Params) -> u64 { - params.anchor.block.height() as u64 + fn stf_params(_anchor: &L1Anchor) -> StfParams { + StfParams::default() + } + } + + /// 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,10 @@ 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, Subscribers::default()) + .expect("create service state"); StateFixture { node, @@ -398,18 +394,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/tests/asm/admin_to_stf.rs b/tests/asm/admin_to_stf.rs index c0b4b794..396c3fa5 100644 --- a/tests/asm/admin_to_stf.rs +++ b/tests/asm/admin_to_stf.rs @@ -161,8 +161,7 @@ async fn test_proof_program_reflects_predicate_update() { AsmStfProofProgram::execute(&runtime_input).expect("AsmStfProofProgram::execute failed"); // Independently compute the expected post-state. - let stf_output = compute_asm_transition( - &StrataAsmSpec, + let stf_output = compute_asm_transition::( &pre_anchor_state, &activation_block, step_input.aux_data(), diff --git a/tests/harness/test_harness.rs b/tests/harness/test_harness.rs index 53757892..3639f27a 100644 --- a/tests/harness/test_harness.rs +++ b/tests/harness/test_harness.rs @@ -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.genesis.clone()) + .with_params(asm_params.as_ref().clone()) .launch(&executor)?; let harness = AsmTestHarness { From 8924b5909e2ff3ae632043259f2aa0672f071d26 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 7 Jul 2026 10:10:26 +0545 Subject: [PATCH 04/12] refactor(spec): derive genesis sections from the spec's subprotocol list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Genesis hand-rolled the same three subprotocols the spec already declares, calling their state constructors directly (leaving Subprotocol::init dead framework surface) and hand-ordering the sections to match the ascending-ID layout the STF's section export asserts — an invariant nothing checked at genesis. Drive it through the same Stage traversal as every execution stage, locating each config in the params list by its InitConfig type (hence the new Any bound), so the pipeline and the genesis layout cannot drift apart. --- crates/common/src/subprotocol.rs | 5 +- crates/spec/src/genesis.rs | 104 +++++++++++++++++++------------ 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/crates/common/src/subprotocol.rs b/crates/common/src/subprotocol.rs index b388a073..024b2a10 100644 --- a/crates/common/src/subprotocol.rs +++ b/crates/common/src/subprotocol.rs @@ -76,7 +76,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; diff --git a/crates/spec/src/genesis.rs b/crates/spec/src/genesis.rs index 6fda63bd..d00aeceb 100644 --- a/crates/spec/src/genesis.rs +++ b/crates/spec/src/genesis.rs @@ -1,46 +1,36 @@ //! Genesis anchor state construction from [`GenesisParams`]. +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::GenesisParams; -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::{GenesisParams, SubprotocolInstance}; use strata_btc_verification::HeaderVerificationState as NativeHeaderVerificationState; +use crate::StrataAsmSpec; + /// Builds the genesis [`AnchorState`] from the given [`GenesisParams`]. /// -/// Initialises every subprotocol's state from its config in `params` and -/// assembles the chain view (PoW header verification + history accumulator). +/// 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: &GenesisParams) -> 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"), + let mut stage = GenesisSectionStage { + 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,46 @@ pub fn construct_genesis_state(params: &GenesisParams) -> 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 section from its config. +/// +/// Configs are located in the params' heterogeneous list by their type: each +/// subprotocol's `InitConfig` type appears in exactly one +/// [`SubprotocolInstance`] variant. +struct GenesisSectionStage<'p> { + params: &'p GenesisParams, + sections: Vec, +} + +impl Stage for GenesisSectionStage<'_> { + 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); } } From f51831d10de3842a5ce90fc5ff8edc7b64567079 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 7 Jul 2026 10:14:11 +0545 Subject: [PATCH 05/12] refactor(common)!: bundle subprotocol hook inputs into per-phase ctx structs process_txs and process_msgs took loose ambient args (header verification state, verified aux data, L1 block ref). Bundle the read-only inputs of each phase into a method-aligned context struct (ProcessTxsCtx / ProcessMsgsCtx) passed as the final parameter: each field's purpose gets a documented home, and future context can grow without breaking every implementor's signature again. Capabilities (collector, relayer) stay as plain args; only read-only inputs live in ctx. --- crates/common/src/subprotocol.rs | 49 ++++++++++++++----- crates/stf/src/manager.rs | 23 ++++----- crates/stf/src/stage.rs | 25 ++++------ crates/stf/src/transition.rs | 14 ++++-- .../admin/subprotocol/src/subprotocol.rs | 12 ++--- .../bridge-v1/subprotocol/src/subprotocol.rs | 26 +++++----- .../checkpoint/subprotocol/src/subprotocol.rs | 14 +++--- 7 files changed, 92 insertions(+), 71 deletions(-) diff --git a/crates/common/src/subprotocol.rs b/crates/common/src/subprotocol.rs index 024b2a10..b1a274aa 100644 --- a/crates/common/src/subprotocol.rs +++ b/crates/common/src/subprotocol.rs @@ -15,6 +15,34 @@ use crate::{ VerifiedAuxData, msg::InterprotoMsg, }; +/// 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, +} + +/// 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, +} + /// Trait for defining subprotocol behavior within the ASM framework. /// /// Subprotocols are modular components that can be plugged into the ASM to handle @@ -59,14 +87,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 /// } /// } @@ -131,16 +158,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. @@ -151,11 +175,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 @@ -192,8 +216,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. @@ -208,7 +231,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/stf/src/manager.rs b/crates/stf/src/manager.rs index 0acf8e74..6f6dc28b 100644 --- a/crates/stf/src/manager.rs +++ b/crates/stf/src/manager.rs @@ -3,10 +3,9 @@ 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, 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. @@ -49,20 +48,19 @@ impl SubprotoHandler for HandlerImpl { &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 { @@ -118,8 +116,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 +124,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/stage.rs b/crates/stf/src/stage.rs index 3ec27bd3..4f193528 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, ProcessMsgsCtx, ProcessTxsCtx, Stage, + Subprotocol, SubprotocolId, TxInputRef, }; -use strata_identifiers::L1BlockCommitment; use crate::manager::SubprotoManager; @@ -87,23 +86,20 @@ impl Stage for PreProcessStage<'_> { /// 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 +112,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 53a711ab..96f0bf56 100644 --- a/crates/stf/src/transition.rs +++ b/crates/stf/src/transition.rs @@ -6,7 +6,7 @@ use bitcoin::Block; use ssz_types::VariableList; use strata_asm_common::{ AnchorState, AsmError, AsmManifest, AsmResult, AsmSpec, AuxData, ChainViewState, - VerifiedAuxData, + ProcessMsgsCtx, ProcessTxsCtx, VerifiedAuxData, }; use strata_btc_verification::{TxidInclusionProof, check_block_integrity}; @@ -60,8 +60,11 @@ pub fn compute_asm_transition( // 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); + let process_ctx = ProcessTxsCtx { + header_vs: &pow_state, + verified_aux_data: &verified_aux_data, + }; + 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. @@ -69,7 +72,10 @@ pub fn compute_asm_transition( // 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); + let finish_ctx = ProcessMsgsCtx { + l1ref: &pow_state.last_verified_block, + }; + let mut finish_stage = FinishStage::new(&mut manager, finish_ctx); S::call_subprotocols(&mut finish_stage); // 7. Construct the manifest with the 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..52ae9f5e 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, 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}, @@ -77,10 +76,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 +90,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 +162,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 +211,7 @@ impl Subprotocol for BridgeV1Subproto { #[cfg(test)] mod tests { - use strata_asm_common::Subprotocol; + use strata_asm_common::{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 +238,8 @@ mod tests { let msgs = vec![BridgeIncomingMsg::UpdateSafeHarbourAddress( new_address.clone(), )]; - BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + let ctx = ProcessMsgsCtx { l1ref: &l1ref }; + 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 +252,8 @@ mod tests { let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); let msgs = vec![BridgeIncomingMsg::Defcon(DefconPayload::default())]; - BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + let ctx = ProcessMsgsCtx { l1ref: &l1ref }; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &ctx); assert!(state.safe_harbour().is_activated()); assert_eq!( @@ -275,7 +278,8 @@ mod tests { BridgeIncomingMsg::Defcon(DefconPayload::default()), BridgeIncomingMsg::UpdateSafeHarbourAddress(rejected_address), ]; - BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + let ctx = ProcessMsgsCtx { l1ref: &l1ref }; + 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..b435d769 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, 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; @@ -81,20 +80,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 { From 32bd1d300cda34031598c24eb98ddd8d55d4a63e Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 7 Jul 2026 10:15:10 +0545 Subject: [PATCH 06/12] feat!: thread StfParams from every executor to the subprotocol hooks The spec type is stateless, so params reach the STF entry points as explicit arguments; the StrataAsmSpec struct carries them only across interfaces that thread a single spec value (the Moho runtime). Guest programs hardcode their params, making the verifying key commit to them; the native prover host bakes the same schedule its guest counterpart would; the worker passes the base params the spec derives from its params file. Each hook ctx gains the params, and pre-processing gets its own ctx carrying the target block height, not otherwise derivable in that phase: aux-data requests must be gateable on exactly the fork conditions that gate the processing which consumes them, and message handling in lockstep with the tx processing that produced the messages. Nothing consumes the params yet; the first fork gate will. --- crates/common/src/subprotocol.rs | 49 +++++++++++++++++-- .../prover/worker/src/backend/native.rs | 7 ++- crates/proof/statements/src/lib.rs | 4 ++ .../statements/src/moho_program/program.rs | 3 +- crates/proof/statements/src/program.rs | 15 +++--- crates/proof/statements/src/statements.rs | 11 +++-- crates/spec/src/spec.rs | 25 ++++++++-- crates/stf/src/manager.rs | 17 +++++-- crates/stf/src/preprocess.rs | 16 +++++- crates/stf/src/stage.rs | 9 ++-- crates/stf/src/transition.rs | 11 ++++- .../bridge-v1/subprotocol/src/subprotocol.rs | 25 +++++++--- .../checkpoint/subprotocol/src/subprotocol.rs | 5 +- crates/worker/src/builder.rs | 12 +++-- crates/worker/src/service.rs | 12 +++-- crates/worker/src/state.rs | 41 +++++++++++----- crates/worker/src/test_utils.rs | 9 +++- guest-builder/sp1/guest-asm/src/main.rs | 5 +- tests/asm/admin_to_stf.rs | 7 +-- 19 files changed, 221 insertions(+), 62 deletions(-) diff --git a/crates/common/src/subprotocol.rs b/crates/common/src/subprotocol.rs index b1a274aa..b92922d5 100644 --- a/crates/common/src/subprotocol.rs +++ b/crates/common/src/subprotocol.rs @@ -11,10 +11,33 @@ use strata_identifiers::L1BlockCommitment; pub use strata_l1_txfmt::SubprotocolId; use crate::{ - AsmError, AsmLogEntry, AuxRequestCollector, HeaderVerificationState, SectionState, TxInputRef, - VerifiedAuxData, msg::InterprotoMsg, + AsmError, AsmLogEntry, AuxRequestCollector, HeaderVerificationState, SectionState, StfParams, + 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 target_height: u64, + + /// STF params the transition executes under. + /// + /// Needed for the fork schedule that gates aux requests, evaluated + /// against [`Self::target_height`]. + pub stf_params: &'a StfParams, +} + /// Context for [`Subprotocol::process_txs`]. #[derive(Debug)] pub struct ProcessTxsCtx<'a> { @@ -31,6 +54,12 @@ pub struct ProcessTxsCtx<'a> { /// 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 StfParams, } /// Context for [`Subprotocol::process_msgs`]. @@ -41,6 +70,12 @@ pub struct ProcessMsgsCtx<'a> { /// 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 StfParams, } /// Trait for defining subprotocol behavior within the ASM framework. @@ -80,6 +115,7 @@ pub struct ProcessMsgsCtx<'a> { /// state: &Self::State, /// txs: &[TxInputRef], /// collector: &mut AuxRequestCollector, +/// ctx: &PreProcessTxsCtx<'_>, /// ) { /// // Pre-process transactions and request auxiliary data /// } @@ -140,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 } @@ -206,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. diff --git a/crates/extensions/prover/worker/src/backend/native.rs b/crates/extensions/prover/worker/src/backend/native.rs index 35292e73..fe28171e 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::StfParams; 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 = StfParams::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/proof/statements/src/lib.rs b/crates/proof/statements/src/lib.rs index 39cf89e8..43e3b989 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::{ForkId, ForkSchedule, StfParams}; diff --git a/crates/proof/statements/src/moho_program/program.rs b/crates/proof/statements/src/moho_program/program.rs index cb9f5ecb..d201d69b 100644 --- a/crates/proof/statements/src/moho_program/program.rs +++ b/crates/proof/statements/src/moho_program/program.rs @@ -96,10 +96,11 @@ impl MohoProgram for AsmStfProgram { fn process_transition( pre_state: &AnchorState, - _spec: &StrataAsmSpec, + spec: &StrataAsmSpec, input: &AsmStepInput, ) -> AsmStfOutput { 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..1381921d 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::StfParams; 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: StfParams) -> 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: StfParams, ) -> 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::StfParams; 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, StfParams::default()).unwrap(); dbg!(output); } } diff --git a/crates/proof/statements/src/statements.rs b/crates/proof/statements/src/statements.rs index 03f1e91d..bc461904 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::StfParams; 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: StfParams) { 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/spec/src/spec.rs b/crates/spec/src/spec.rs index 52ecbac8..6f6638ea 100644 --- a/crates/spec/src/spec.rs +++ b/crates/spec/src/spec.rs @@ -12,9 +12,28 @@ use crate::genesis; /// /// 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 [`StfParams`] 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: StfParams, +} + +impl StrataAsmSpec { + /// Creates a spec executing under the given STF params. + pub fn new(stf_params: StfParams) -> Self { + Self { stf_params } + } + + /// Returns the STF params this executor runs under. + pub fn stf_params(&self) -> &StfParams { + &self.stf_params + } +} impl AsmSpec for StrataAsmSpec { type Subprotocols = ( diff --git a/crates/stf/src/manager.rs b/crates/stf/src/manager.rs index 6f6dc28b..d155c873 100644 --- a/crates/stf/src/manager.rs +++ b/crates/stf/src/manager.rs @@ -3,8 +3,9 @@ use std::{any::Any, collections::BTreeMap, marker}; use strata_asm_common::{ - AsmError, AsmLogEntry, AuxRequestCollector, InterprotoMsg, MsgRelayer, ProcessMsgsCtx, - ProcessTxsCtx, SectionState, SubprotoHandler, Subprotocol, SubprotocolId, TxInputRef, + AsmError, AsmLogEntry, AuxRequestCollector, InterprotoMsg, MsgRelayer, PreProcessTxsCtx, + ProcessMsgsCtx, ProcessTxsCtx, SectionState, SubprotoHandler, Subprotocol, SubprotocolId, + TxInputRef, }; /// Wrapper around the common subprotocol interface that handles the common @@ -40,8 +41,13 @@ 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( @@ -95,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 @@ -104,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); } diff --git a/crates/stf/src/preprocess.rs b/crates/stf/src/preprocess.rs index 47948f0c..bd31a480 100644 --- a/crates/stf/src/preprocess.rs +++ b/crates/stf/src/preprocess.rs @@ -3,7 +3,7 @@ //! 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, PreProcessTxsCtx, StfParams}; use crate::{ manager::SubprotoManager, @@ -27,6 +27,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 /// @@ -47,6 +50,7 @@ use crate::{ /// * `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>( + stf_params: &StfParams, pre_state: &AnchorState, block: &'b Block, ) -> AsmResult> { @@ -69,8 +73,16 @@ pub fn pre_process_asm<'b, S: AsmSpec>( // 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 target_height = pow_state.last_verified_block.height() as u64; + let ctx = PreProcessTxsCtx { + target_height, + stf_params, + }; let mut pre_process_stage = - PreProcessStage::new(&mut manager, pre_state, &grouped_relevant_txs); + 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. diff --git a/crates/stf/src/stage.rs b/crates/stf/src/stage.rs index 4f193528..132b90ce 100644 --- a/crates/stf/src/stage.rs +++ b/crates/stf/src/stage.rs @@ -3,8 +3,8 @@ use std::collections::BTreeMap; use strata_asm_common::{ - AnchorState, AuxRequestCollector, AuxRequests, ProcessMsgsCtx, ProcessTxsCtx, Stage, - Subprotocol, SubprotocolId, TxInputRef, + AnchorState, AuxRequestCollector, AuxRequests, PreProcessTxsCtx, ProcessMsgsCtx, ProcessTxsCtx, + Stage, Subprotocol, SubprotocolId, TxInputRef, }; use crate::manager::SubprotoManager; @@ -41,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> { @@ -48,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), @@ -62,6 +64,7 @@ impl<'c> PreProcessStage<'c> { manager, tx_bufs, aux_collector, + ctx, } } @@ -79,7 +82,7 @@ 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); } } diff --git a/crates/stf/src/transition.rs b/crates/stf/src/transition.rs index 96f0bf56..c1bd3a7c 100644 --- a/crates/stf/src/transition.rs +++ b/crates/stf/src/transition.rs @@ -6,7 +6,7 @@ use bitcoin::Block; use ssz_types::VariableList; use strata_asm_common::{ AnchorState, AsmError, AsmManifest, AsmResult, AsmSpec, AuxData, ChainViewState, - ProcessMsgsCtx, ProcessTxsCtx, VerifiedAuxData, + ProcessMsgsCtx, ProcessTxsCtx, StfParams, VerifiedAuxData, }; use strata_btc_verification::{TxidInclusionProof, check_block_integrity}; @@ -24,7 +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( + stf_params: &StfParams, pre_state: &AnchorState, block: &Block, aux_data: &AuxData, @@ -63,6 +70,7 @@ pub fn compute_asm_transition( 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); @@ -74,6 +82,7 @@ pub fn compute_asm_transition( // bounded number of times 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); diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index 52ae9f5e..f6bcf068 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -4,8 +4,8 @@ //! with the Strata Anchor State Machine (ASM). use strata_asm_common::{ - AsmLogEntry, AuxRequestCollector, MsgRelayer, ProcessMsgsCtx, ProcessTxsCtx, Subprotocol, - SubprotocolId, TxInputRef, + AsmLogEntry, AuxRequestCollector, MsgRelayer, PreProcessTxsCtx, ProcessMsgsCtx, ProcessTxsCtx, + Subprotocol, SubprotocolId, TxInputRef, logging::{debug, error, info}, }; use strata_asm_logs::ExportExtraDataUpdate; @@ -47,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 { @@ -211,7 +212,7 @@ impl Subprotocol for BridgeV1Subproto { #[cfg(test)] mod tests { - use strata_asm_common::{ProcessMsgsCtx, Subprotocol}; + use strata_asm_common::{ProcessMsgsCtx, StfParams, Subprotocol}; use strata_asm_proto_bridge_v1_msgs::{BridgeIncomingMsg, DefconPayload}; use strata_asm_proto_bridge_v1_types::SafeHarbourAddress; use strata_identifiers::L1BlockCommitment; @@ -238,7 +239,11 @@ mod tests { let msgs = vec![BridgeIncomingMsg::UpdateSafeHarbourAddress( new_address.clone(), )]; - let ctx = ProcessMsgsCtx { l1ref: &l1ref }; + let stf_params = StfParams::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); @@ -252,7 +257,11 @@ mod tests { let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); let msgs = vec![BridgeIncomingMsg::Defcon(DefconPayload::default())]; - let ctx = ProcessMsgsCtx { l1ref: &l1ref }; + let stf_params = StfParams::default(); + let ctx = ProcessMsgsCtx { + l1ref: &l1ref, + stf_params: &stf_params, + }; BridgeV1Subproto::process_msgs(&mut state, &msgs, &ctx); assert!(state.safe_harbour().is_activated()); @@ -278,7 +287,11 @@ mod tests { BridgeIncomingMsg::Defcon(DefconPayload::default()), BridgeIncomingMsg::UpdateSafeHarbourAddress(rejected_address), ]; - let ctx = ProcessMsgsCtx { l1ref: &l1ref }; + let stf_params = StfParams::default(); + let ctx = ProcessMsgsCtx { + l1ref: &l1ref, + stf_params: &stf_params, + }; BridgeV1Subproto::process_msgs(&mut state, &msgs, &ctx); assert!(state.safe_harbour().is_activated()); diff --git a/crates/subprotocols/checkpoint/subprotocol/src/subprotocol.rs b/crates/subprotocols/checkpoint/subprotocol/src/subprotocol.rs index b435d769..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, MsgRelayer, ProcessMsgsCtx, ProcessTxsCtx, Subprotocol, SubprotocolId, - TxInputRef, logging, + AuxRequestCollector, MsgRelayer, PreProcessTxsCtx, ProcessMsgsCtx, ProcessTxsCtx, Subprotocol, + SubprotocolId, TxInputRef, logging, }; use strata_asm_params::CheckpointInitConfig; use strata_asm_proto_checkpoint_msgs::CheckpointIncomingMsg; @@ -40,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 { diff --git a/crates/worker/src/builder.rs b/crates/worker/src/builder.rs index 0e0f7074..418ea815 100644 --- a/crates/worker/src/builder.rs +++ b/crates/worker/src/builder.rs @@ -44,7 +44,8 @@ impl AsmWorkerBuilder { /// 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. + /// 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 @@ -68,6 +69,7 @@ impl AsmWorkerBuilder { .ok_or(WorkerError::MissingDependency("params"))?; 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. @@ -79,8 +81,12 @@ impl AsmWorkerBuilder { let subscribers = Subscribers::default(); // Create the service state. - let service_state = - AsmWorkerServiceState::::new(context, genesis_state, 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 = diff --git a/crates/worker/src/service.rs b/crates/worker/src/service.rs index ddf6cc5e..82386038 100644 --- a/crates/worker/src/service.rs +++ b/crates/worker/src/service.rs @@ -297,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, AuxRequestCollector, StfParams}; use strata_btc_types::L1BlockIdBitcoinExt; use strata_identifiers::{Buf32, L1BlockId}; use strata_service::CommandCompletionSender; @@ -530,9 +530,13 @@ mod tests { // ...so a restart over the same store resumes at the tip. let context = fx.state.context.clone(); let genesis = fixtures::genesis_state(&fx.client, 101).await; - let reloaded = - AsmWorkerServiceState::<_, TestAsmSpec>::new(context, genesis, Subscribers::default()) - .unwrap(); + let reloaded = AsmWorkerServiceState::<_, TestAsmSpec>::new( + context, + genesis, + StfParams::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 1a863666..fbfb189f 100644 --- a/crates/worker/src/state.rs +++ b/crates/worker/src/state.rs @@ -1,7 +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, AuxData, HeaderVerificationState, StfParams}; use strata_asm_stf::AsmStfOutput; use strata_btc_types::BlockHashExt; use strata_btc_verification::{ @@ -26,9 +26,6 @@ pub struct AsmWorkerServiceState { /// Context for the state to interact with outer world. pub(crate) context: W, - /// ASM spec driving the subprotocol pipeline (type-level only). - _spec: PhantomData, - /// Current ASM anchor state. pub anchor: AnchorState, @@ -44,6 +41,12 @@ 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: StfParams, + + /// ASM spec driving the subprotocol pipeline (type-level only). + _spec: PhantomData, } impl AsmWorkerServiceState @@ -52,13 +55,18 @@ where S: AsmSpec + Send + Sync + 'static, { /// Creates a new service state, loading the latest anchor or adopting the - /// given genesis state (when the store holds no prior state). + /// 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, genesis_state: AnchorState, + stf_params: StfParams, subscribers: Subscribers, ) -> WorkerResult { let genesis_height = genesis_state @@ -102,6 +110,7 @@ where blkid, genesis_height, subscribers, + stf_params, }) } @@ -121,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::(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()); @@ -148,6 +157,7 @@ where let coinbase_inclusion_proof = TxidInclusionProof::generate(&block.txdata, 0); strata_asm_stf::compute_asm_transition::( + &self.stf_params, cur_state, block, &aux_data, @@ -323,9 +333,13 @@ mod tests { .unwrap(); let genesis = fixtures::genesis_state(&seed.client, 101).await; - let reloaded = - AsmWorkerServiceState::<_, TestAsmSpec>::new(context, genesis, Subscribers::default()) - .unwrap(); + let reloaded = AsmWorkerServiceState::<_, TestAsmSpec>::new( + context, + genesis, + StfParams::default(), + Subscribers::default(), + ) + .unwrap(); assert_eq!( reloaded.blkid, advanced, @@ -444,8 +458,13 @@ mod tests { let context = fx.state.context.clone(); let genesis = fixtures::genesis_state(&fx.client, 101).await; - AsmWorkerServiceState::<_, TestAsmSpec>::new(context, genesis, Subscribers::default()) - .unwrap(); + AsmWorkerServiceState::<_, TestAsmSpec>::new( + context, + genesis, + StfParams::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 e3fa300f..5c74bfdf 100644 --- a/crates/worker/src/test_utils.rs +++ b/crates/worker/src/test_utils.rs @@ -384,8 +384,13 @@ pub(crate) mod fixtures { let genesis = genesis_state(&client, genesis_height).await; let context = TestAsmWorkerContext::new((*client).clone()); - let state = AsmWorkerServiceState::new(context, genesis, Subscribers::default()) - .expect("create service state"); + let state = AsmWorkerServiceState::new( + context, + genesis, + StfParams::default(), + Subscribers::default(), + ) + .expect("create service state"); StateFixture { node, diff --git a/guest-builder/sp1/guest-asm/src/main.rs b/guest-builder/sp1/guest-asm/src/main.rs index c6ddc824..b7e1fccf 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, StfParams}; 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, StfParams::default()) } diff --git a/tests/asm/admin_to_stf.rs b/tests/asm/admin_to_stf.rs index 396c3fa5..e1af915a 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::{AuxData, StfParams}; use strata_asm_logs::AsmStfUpdate; use strata_asm_proof_impl::{ moho_program::{input::AsmStepInput, program::advance_export_state_with_logs}, @@ -157,11 +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, StfParams::default()) + .expect("AsmStfProofProgram::execute failed"); // Independently compute the expected post-state. let stf_output = compute_asm_transition::( + &StfParams::default(), &pre_anchor_state, &activation_block, step_input.aux_data(), From 3db9ecc67e208d15bef06628f069f32ac21628bf Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Mon, 13 Jul 2026 16:37:33 +0545 Subject: [PATCH 07/12] refactor!: use L1Height for fork gates and align params naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on #187: fork activation heights move from raw u64 to L1Height so gates share the height domain the rest of the code uses (L1Height::MAX takes over as the never-active sentinel), and several names are clarified — activation_height_of / set_fork_activation (it is a plain setter, not a scheduler), PreProcessTxsCtx::block_height (target_height read like a destination), and AsmGenesisParams / AsmRuntimeParams to match OL-side params naming. --- crates/common/src/fork.rs | 39 ++++++++++++----------- crates/common/src/subprotocol.rs | 6 ++-- crates/params/src/genesis.rs | 6 ++-- crates/params/src/lib.rs | 30 ++++++++--------- crates/params/src/{stf.rs => runtime.rs} | 16 +++++----- crates/proof/statements/src/test_utils.rs | 8 ++--- crates/spec/src/genesis.rs | 10 +++--- crates/spec/src/lib.rs | 2 +- crates/spec/src/spec.rs | 2 +- crates/stf/src/preprocess.rs | 4 +-- 10 files changed, 63 insertions(+), 60 deletions(-) rename crates/params/src/{stf.rs => runtime.rs} (61%) diff --git a/crates/common/src/fork.rs b/crates/common/src/fork.rs index 0f4e26b7..bfbbcf74 100644 --- a/crates/common/src/fork.rs +++ b/crates/common/src/fork.rs @@ -8,6 +8,7 @@ //! outcome at every height it executes (see `StfParams`). use serde::{Deserialize, Serialize}; +use strata_identifiers::L1Height; /// Identifies a named fork. /// @@ -62,37 +63,39 @@ impl TryFrom for ForkId { /// Activation heights for every named fork. /// -/// A fork is active at L1 height `h` iff `h >= activation_height`. `0` means -/// active since genesis; [`u64::MAX`] means never active. Proving artifacts -/// bake one of those two extremes (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. +/// A fork is active at L1 height `h` iff `h >= activation_height_of`. `0` +/// means active since genesis; [`L1Height::MAX`] means never active. Proving +/// artifacts bake one of those two extremes (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`]. - pub fork1: u64, + pub fork1: L1Height, } impl ForkSchedule { - /// Schedule with every fork disabled (activation at [`u64::MAX`]). + /// Schedule with every fork disabled (activation at [`L1Height::MAX`]). pub const fn all_disabled() -> Self { - Self { fork1: u64::MAX } + Self { + fork1: L1Height::MAX, + } } /// Returns the activation height of `fork`. - pub fn activation_height(&self, fork: ForkId) -> u64 { + pub fn activation_height_of(&self, fork: ForkId) -> L1Height { match fork { ForkId::Fork1 => self.fork1, } } /// Returns whether `fork` is active at L1 `height`. - pub fn is_active(&self, fork: ForkId, height: u64) -> bool { - height >= self.activation_height(fork) + pub fn is_active(&self, fork: ForkId, height: L1Height) -> bool { + height >= self.activation_height_of(fork) } /// Sets the activation height of `fork`. - pub fn activate_at(&mut self, fork: ForkId, height: u64) { + pub fn set_fork_activation(&mut self, fork: ForkId, height: L1Height) { match fork { ForkId::Fork1 => self.fork1 = height, } @@ -136,23 +139,23 @@ mod tests { fn zero_means_always_active() { let sched = ForkSchedule { fork1: 0 }; assert!(sched.is_active(ForkId::Fork1, 0)); - assert!(sched.is_active(ForkId::Fork1, u64::MAX)); + assert!(sched.is_active(ForkId::Fork1, L1Height::MAX)); } #[test] fn max_means_never_active() { let sched = ForkSchedule::all_disabled(); assert!(!sched.is_active(ForkId::Fork1, 0)); - assert!(!sched.is_active(ForkId::Fork1, u64::MAX - 1)); + assert!(!sched.is_active(ForkId::Fork1, L1Height::MAX - 1)); // Degenerate boundary: is_active is a plain >= comparison. - assert!(sched.is_active(ForkId::Fork1, u64::MAX)); + assert!(sched.is_active(ForkId::Fork1, L1Height::MAX)); } #[test] - fn activate_at_overrides() { + fn set_fork_activation_overrides() { let mut sched = ForkSchedule::all_disabled(); - sched.activate_at(ForkId::Fork1, 42); - assert_eq!(sched.activation_height(ForkId::Fork1), 42); + sched.set_fork_activation(ForkId::Fork1, 42); + assert_eq!(sched.activation_height_of(ForkId::Fork1), 42); assert!(sched.is_active(ForkId::Fork1, 42)); assert!(!sched.is_active(ForkId::Fork1, 41)); } diff --git a/crates/common/src/subprotocol.rs b/crates/common/src/subprotocol.rs index b92922d5..c6c9ddc2 100644 --- a/crates/common/src/subprotocol.rs +++ b/crates/common/src/subprotocol.rs @@ -7,7 +7,7 @@ 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::{ @@ -29,12 +29,12 @@ pub struct PreProcessTxsCtx<'a> { /// *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 target_height: u64, + pub block_height: L1Height, /// STF params the transition executes under. /// /// Needed for the fork schedule that gates aux requests, evaluated - /// against [`Self::target_height`]. + /// against [`Self::block_height`]. pub stf_params: &'a StfParams, } diff --git a/crates/params/src/genesis.rs b/crates/params/src/genesis.rs index 301fa358..80f7723f 100644 --- a/crates/params/src/genesis.rs +++ b/crates/params/src/genesis.rs @@ -19,7 +19,7 @@ use crate::subprotocols::{ /// 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 GenesisParams { +pub struct AsmGenesisParams { /// SPS-50 magic bytes that identify protocol transactions on L1. pub magic: MagicBytes, @@ -34,7 +34,7 @@ pub struct GenesisParams { pub subprotocols: Vec, } -impl GenesisParams { +impl AsmGenesisParams { pub fn admin_config(&self) -> Option<&AdministrationInitConfig> { self.subprotocols.iter().find_map(|s| match s { SubprotocolInstance::Admin(cfg) => Some(cfg), @@ -58,7 +58,7 @@ impl GenesisParams { } #[cfg(feature = "arbitrary")] -impl<'a> Arbitrary<'a> for GenesisParams { +impl<'a> Arbitrary<'a> for AsmGenesisParams { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { let networks = [ bitcoin::Network::Bitcoin, diff --git a/crates/params/src/lib.rs b/crates/params/src/lib.rs index 57061885..f6996b2c 100644 --- a/crates/params/src/lib.rs +++ b/crates/params/src/lib.rs @@ -1,19 +1,19 @@ //! Configuration parameters for the Anchor State Machine (ASM). //! -//! Provides [`AsmParams`], split into [`GenesisParams`] (L1 magic bytes, +//! Provides [`AsmParams`], split into [`AsmGenesisParams`] (L1 magic bytes, //! genesis L1 view and per-subprotocol configuration, consumed once to build -//! the genesis state) and [`StfConfig`] (fork schedule driving the per-block -//! state transition function). +//! the genesis state) and [`AsmRuntimeParams`] (fork schedule driving the +//! per-block state transition function). mod genesis; -mod stf; +mod runtime; mod subprotocols; #[cfg(feature = "arbitrary")] use arbitrary::{Arbitrary, Unstructured}; -pub use genesis::GenesisParams; +pub use genesis::AsmGenesisParams; +pub use runtime::AsmRuntimeParams; use serde::{Deserialize, Serialize}; -pub use stf::StfConfig; #[cfg(feature = "arbitrary")] use strata_asm_common::ForkSchedule; pub use subprotocols::{ @@ -23,27 +23,27 @@ pub use subprotocols::{ /// Top-level parameters for an ASM instance. /// -/// Split by consumer: [`GenesisParams`] is only used to construct the genesis -/// anchor state, while [`StfConfig`] configures the state transition function -/// for every block. Both are flattened in the serialized form, so the params -/// file is a single flat object. +/// 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: GenesisParams, + pub genesis: AsmGenesisParams, /// Parameters of the per-block state transition function. #[serde(flatten)] - pub stf: StfConfig, + pub runtime: AsmRuntimeParams, } #[cfg(feature = "arbitrary")] impl<'a> Arbitrary<'a> for AsmParams { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { Ok(Self { - genesis: GenesisParams::arbitrary(u)?, - stf: StfConfig { + genesis: AsmGenesisParams::arbitrary(u)?, + runtime: AsmRuntimeParams { forks: ForkSchedule::all_disabled(), }, }) @@ -142,7 +142,7 @@ mod tests { let params: AsmParams = serde_json::from_str(raw_json).expect("deserialization from raw JSON should succeed"); - assert_eq!(params.stf.forks.fork1, 0); + assert_eq!(params.runtime.forks.fork1, 0); } #[cfg(feature = "arbitrary")] diff --git a/crates/params/src/stf.rs b/crates/params/src/runtime.rs similarity index 61% rename from crates/params/src/stf.rs rename to crates/params/src/runtime.rs index f40b65d0..d1e1aa64 100644 --- a/crates/params/src/stf.rs +++ b/crates/params/src/runtime.rs @@ -1,22 +1,22 @@ -//! Configuration of the per-block state transition function. +//! Parameters of the per-block state transition function. use serde::{Deserialize, Serialize}; use strata_asm_common::{ForkSchedule, StfParams}; -/// Configuration of the state transition function. +/// 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 [`StfParams`]. 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 StfConfig { +pub struct AsmRuntimeParams { /// Base fork activation schedule. pub forks: ForkSchedule, } -impl StfConfig { - /// The STF-facing view of this config, before any dynamic activations. +impl AsmRuntimeParams { + /// The STF-facing view of these params, before any dynamic activations. pub fn stf_params(&self) -> StfParams { StfParams { forks: self.forks.clone(), @@ -29,8 +29,8 @@ mod tests { use super::*; #[test] - fn test_stf_config_deserialize() { - let cfg: StfConfig = serde_json::from_str(r#"{"forks":{"fork1":5}}"#).unwrap(); - assert_eq!(cfg.stf_params().forks.fork1, 5); + fn test_runtime_params_deserialize() { + let params: AsmRuntimeParams = serde_json::from_str(r#"{"forks":{"fork1":5}}"#).unwrap(); + assert_eq!(params.stf_params().forks.fork1, 5); } } diff --git a/crates/proof/statements/src/test_utils.rs b/crates/proof/statements/src/test_utils.rs index 0d38bd1e..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::GenesisParams; +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 `GenesisParams` +/// 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: GenesisParams = 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 = GenesisParams::arbitrary(&mut u).expect("deterministic GenesisParams"); + 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 d00aeceb..284e0a86 100644 --- a/crates/spec/src/genesis.rs +++ b/crates/spec/src/genesis.rs @@ -1,4 +1,4 @@ -//! Genesis anchor state construction from [`GenesisParams`]. +//! Genesis anchor state construction from [`AsmGenesisParams`]. use std::any::Any; @@ -6,18 +6,18 @@ use strata_asm_common::{ AnchorState, AsmHistoryAccumulatorState, AsmSpec, ChainViewState, HeaderVerificationState, SectionState, Stage, Subprotocol, }; -use strata_asm_params::{GenesisParams, SubprotocolInstance}; +use strata_asm_params::{AsmGenesisParams, SubprotocolInstance}; use strata_btc_verification::HeaderVerificationState as NativeHeaderVerificationState; use crate::StrataAsmSpec; -/// Builds the genesis [`AnchorState`] from the given [`GenesisParams`]. +/// Builds the genesis [`AnchorState`] from the given [`AsmGenesisParams`]. /// /// 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: &GenesisParams) -> AnchorState { +pub fn construct_genesis_state(params: &AsmGenesisParams) -> AnchorState { let mut stage = GenesisSectionStage { params, sections: Vec::new(), @@ -55,7 +55,7 @@ pub fn construct_genesis_state(params: &GenesisParams) -> AnchorState { /// subprotocol's `InitConfig` type appears in exactly one /// [`SubprotocolInstance`] variant. struct GenesisSectionStage<'p> { - params: &'p GenesisParams, + params: &'p AsmGenesisParams, sections: Vec, } diff --git a/crates/spec/src/lib.rs b/crates/spec/src/lib.rs index 30201a6a..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 -//! [`GenesisParams`](strata_asm_params::GenesisParams). +//! [`AsmGenesisParams`](strata_asm_params::AsmGenesisParams). mod genesis; mod spec; diff --git a/crates/spec/src/spec.rs b/crates/spec/src/spec.rs index 6f6638ea..32f184ed 100644 --- a/crates/spec/src/spec.rs +++ b/crates/spec/src/spec.rs @@ -49,6 +49,6 @@ impl AsmSpec for StrataAsmSpec { } fn stf_params(params: &AsmParams) -> StfParams { - params.stf.stf_params() + params.runtime.stf_params() } } diff --git a/crates/stf/src/preprocess.rs b/crates/stf/src/preprocess.rs index bd31a480..f482018a 100644 --- a/crates/stf/src/preprocess.rs +++ b/crates/stf/src/preprocess.rs @@ -76,9 +76,9 @@ pub fn pre_process_asm<'b, S: AsmSpec>( // `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 target_height = pow_state.last_verified_block.height() as u64; + let block_height = pow_state.last_verified_block.height(); let ctx = PreProcessTxsCtx { - target_height, + block_height, stf_params, }; let mut pre_process_stage = From 924b4aa1f8aa7e470f4be3e00ce1a6218edf1763 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Mon, 13 Jul 2026 16:40:49 +0545 Subject: [PATCH 08/12] refactor!: rename StfParams to AsmStfParams Completes the Asm* naming alignment from review: the executor-facing params now match AsmParams/AsmGenesisParams/AsmRuntimeParams and the existing AsmStf* convention (AsmStfProgram, AsmStfUpdate). --- crates/common/src/fork.rs | 8 ++++---- crates/common/src/spec.rs | 4 ++-- crates/common/src/subprotocol.rs | 10 +++++----- .../extensions/prover/worker/src/backend/native.rs | 4 ++-- crates/params/src/runtime.rs | 8 ++++---- crates/proof/statements/src/lib.rs | 2 +- crates/proof/statements/src/program.rs | 10 +++++----- crates/proof/statements/src/statements.rs | 4 ++-- crates/spec/src/spec.rs | 12 ++++++------ crates/stf/src/preprocess.rs | 6 ++++-- crates/stf/src/transition.rs | 6 +++--- .../bridge-v1/subprotocol/src/subprotocol.rs | 8 ++++---- crates/worker/src/service.rs | 4 ++-- crates/worker/src/state.rs | 10 +++++----- crates/worker/src/test_utils.rs | 10 +++++----- guest-builder/sp1/guest-asm/src/main.rs | 4 ++-- tests/asm/admin_to_stf.rs | 6 +++--- 17 files changed, 59 insertions(+), 57 deletions(-) diff --git a/crates/common/src/fork.rs b/crates/common/src/fork.rs index bfbbcf74..889ade04 100644 --- a/crates/common/src/fork.rs +++ b/crates/common/src/fork.rs @@ -5,7 +5,7 @@ //! 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 `StfParams`). +//! outcome at every height it executes (see `AsmStfParams`). use serde::{Deserialize, Serialize}; use strata_identifiers::L1Height; @@ -118,7 +118,7 @@ impl Default for ForkSchedule { /// /// `Default` inherits [`ForkSchedule`]'s default: everything disabled. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct StfParams { +pub struct AsmStfParams { /// Fork activation schedule. pub forks: ForkSchedule, } @@ -162,12 +162,12 @@ mod tests { #[test] fn serde_roundtrip() { - let params = StfParams { + let params = AsmStfParams { forks: ForkSchedule { fork1: 7 }, }; let json = serde_json::to_string(¶ms).unwrap(); assert_eq!(json, r#"{"forks":{"fork1":7}}"#); - let back: StfParams = serde_json::from_str(&json).unwrap(); + let back: AsmStfParams = serde_json::from_str(&json).unwrap(); assert_eq!(back, params); } diff --git a/crates/common/src/spec.rs b/crates/common/src/spec.rs index 11ca0e3d..340012a5 100644 --- a/crates/common/src/spec.rs +++ b/crates/common/src/spec.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use crate::{AnchorState, StfParams, Subprotocol}; +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 @@ -28,7 +28,7 @@ pub trait AsmSpec { fn construct_genesis_state(params: &Self::Params) -> AnchorState; /// Extracts the base STF params every transition executes under. - fn stf_params(params: &Self::Params) -> StfParams; + fn stf_params(params: &Self::Params) -> AsmStfParams; /// Invokes the stage with each subprotocol, in the declared order. fn call_subprotocols(stage: &mut impl Stage) { diff --git a/crates/common/src/subprotocol.rs b/crates/common/src/subprotocol.rs index c6c9ddc2..412c5000 100644 --- a/crates/common/src/subprotocol.rs +++ b/crates/common/src/subprotocol.rs @@ -11,8 +11,8 @@ use strata_identifiers::{L1BlockCommitment, L1Height}; pub use strata_l1_txfmt::SubprotocolId; use crate::{ - AsmError, AsmLogEntry, AuxRequestCollector, HeaderVerificationState, SectionState, StfParams, - TxInputRef, VerifiedAuxData, msg::InterprotoMsg, + AsmError, AsmLogEntry, AsmStfParams, AuxRequestCollector, HeaderVerificationState, + SectionState, TxInputRef, VerifiedAuxData, msg::InterprotoMsg, }; /// Context for [`Subprotocol::pre_process_txs`]. @@ -35,7 +35,7 @@ pub struct PreProcessTxsCtx<'a> { /// /// Needed for the fork schedule that gates aux requests, evaluated /// against [`Self::block_height`]. - pub stf_params: &'a StfParams, + pub stf_params: &'a AsmStfParams, } /// Context for [`Subprotocol::process_txs`]. @@ -59,7 +59,7 @@ pub struct ProcessTxsCtx<'a> { /// /// Needed for the fork schedule that gates processing logic, evaluated /// against the height of `header_vs.last_verified_block`. - pub stf_params: &'a StfParams, + pub stf_params: &'a AsmStfParams, } /// Context for [`Subprotocol::process_msgs`]. @@ -75,7 +75,7 @@ pub struct ProcessMsgsCtx<'a> { /// /// 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 StfParams, + pub stf_params: &'a AsmStfParams, } /// Trait for defining subprotocol behavior within the ASM framework. diff --git a/crates/extensions/prover/worker/src/backend/native.rs b/crates/extensions/prover/worker/src/backend/native.rs index fe28171e..e2f1613f 100644 --- a/crates/extensions/prover/worker/src/backend/native.rs +++ b/crates/extensions/prover/worker/src/backend/native.rs @@ -39,12 +39,12 @@ 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::StfParams; + 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 = StfParams::default(); + let stf_params = AsmStfParams::default(); Ok(( NativeHost::new(asm_signing_key.clone(), move |env| { process_asm_stf(env, stf_params.clone()) diff --git a/crates/params/src/runtime.rs b/crates/params/src/runtime.rs index d1e1aa64..b2f62aae 100644 --- a/crates/params/src/runtime.rs +++ b/crates/params/src/runtime.rs @@ -1,12 +1,12 @@ //! Parameters of the per-block state transition function. use serde::{Deserialize, Serialize}; -use strata_asm_common::{ForkSchedule, StfParams}; +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 [`StfParams`]. The +/// 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)] @@ -17,8 +17,8 @@ pub struct AsmRuntimeParams { impl AsmRuntimeParams { /// The STF-facing view of these params, before any dynamic activations. - pub fn stf_params(&self) -> StfParams { - StfParams { + pub fn stf_params(&self) -> AsmStfParams { + AsmStfParams { forks: self.forks.clone(), } } diff --git a/crates/proof/statements/src/lib.rs b/crates/proof/statements/src/lib.rs index 43e3b989..c687f785 100644 --- a/crates/proof/statements/src/lib.rs +++ b/crates/proof/statements/src/lib.rs @@ -12,4 +12,4 @@ 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::{ForkId, ForkSchedule, StfParams}; +pub use strata_asm_common::{AsmStfParams, ForkId, ForkSchedule}; diff --git a/crates/proof/statements/src/program.rs b/crates/proof/statements/src/program.rs index 1381921d..9ffb46ca 100644 --- a/crates/proof/statements/src/program.rs +++ b/crates/proof/statements/src/program.rs @@ -3,7 +3,7 @@ use moho_runtime_impl::RuntimeInput; use moho_types::StepMohoAttestation; use ssz::{decode::Decode, encode::Encode}; -use strata_asm_common::StfParams; +use strata_asm_common::AsmStfParams; use zkaleido::{ DataFormatError, ProofType, PublicValues, ZkVmError, ZkVmHost, ZkVmInputBuilder, ZkVmInputResult, ZkVmProgram, ZkVmResult, @@ -55,14 +55,14 @@ impl ZkVmProgram for AsmStfProofProgram { impl AsmStfProofProgram { /// Native host executing under the given STF params; usable for testing. - pub fn native_host(stf_params: StfParams) -> NativeHost { + 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 under the given STF params using the native host. pub fn execute( input: &::Input, - stf_params: StfParams, + stf_params: AsmStfParams, ) -> ZkVmResult<::Output> { // Get the native host and delegate to the trait's execute method let host = Self::native_host(stf_params); @@ -76,7 +76,7 @@ mod tests { use moho_runtime_impl::RuntimeInput; use ssz::Encode; - use strata_asm_common::StfParams; + use strata_asm_common::AsmStfParams; use strata_predicate::PredicateKey; use crate::{ @@ -100,7 +100,7 @@ mod tests { fn test_stf() { let runtime_input = create_runtime_input(); - let output = AsmStfProofProgram::execute(&runtime_input, StfParams::default()).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 bc461904..190932cf 100644 --- a/crates/proof/statements/src/statements.rs +++ b/crates/proof/statements/src/statements.rs @@ -2,7 +2,7 @@ use moho_runtime_impl::{compute_moho_attestation, RuntimeInput}; use ssz::{Decode, Encode}; -use strata_asm_common::StfParams; +use strata_asm_common::AsmStfParams; use strata_asm_spec::StrataAsmSpec; use zkaleido::ZkVmEnv; @@ -19,7 +19,7 @@ use crate::moho_program::program::AsmStfProgram; /// `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: StfParams) { +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"); diff --git a/crates/spec/src/spec.rs b/crates/spec/src/spec.rs index 32f184ed..93affd38 100644 --- a/crates/spec/src/spec.rs +++ b/crates/spec/src/spec.rs @@ -1,6 +1,6 @@ //! Strata ASM specification defining the subprotocol pipeline. -use strata_asm_common::{AnchorState, AsmSpec, StfParams}; +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; @@ -15,22 +15,22 @@ use crate::genesis; /// (load, preprocess, process, finish) and for genesis construction. /// /// The pipeline itself is type-level (see [`AsmSpec`]); the struct exists to -/// carry the [`StfParams`] through interfaces that thread a single spec value +/// 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: StfParams, + stf_params: AsmStfParams, } impl StrataAsmSpec { /// Creates a spec executing under the given STF params. - pub fn new(stf_params: StfParams) -> Self { + pub fn new(stf_params: AsmStfParams) -> Self { Self { stf_params } } /// Returns the STF params this executor runs under. - pub fn stf_params(&self) -> &StfParams { + pub fn stf_params(&self) -> &AsmStfParams { &self.stf_params } } @@ -48,7 +48,7 @@ impl AsmSpec for StrataAsmSpec { genesis::construct_genesis_state(¶ms.genesis) } - fn stf_params(params: &AsmParams) -> StfParams { + fn stf_params(params: &AsmParams) -> AsmStfParams { params.runtime.stf_params() } } diff --git a/crates/stf/src/preprocess.rs b/crates/stf/src/preprocess.rs index f482018a..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, PreProcessTxsCtx, StfParams}; +use strata_asm_common::{ + AnchorState, AsmError, AsmResult, AsmSpec, AsmStfParams, PreProcessTxsCtx, +}; use crate::{ manager::SubprotoManager, @@ -50,7 +52,7 @@ use crate::{ /// * `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>( - stf_params: &StfParams, + stf_params: &AsmStfParams, pre_state: &AnchorState, block: &'b Block, ) -> AsmResult> { diff --git a/crates/stf/src/transition.rs b/crates/stf/src/transition.rs index c1bd3a7c..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, - ProcessMsgsCtx, ProcessTxsCtx, StfParams, VerifiedAuxData, + AnchorState, AsmError, AsmManifest, AsmResult, AsmSpec, AsmStfParams, AuxData, ChainViewState, + ProcessMsgsCtx, ProcessTxsCtx, VerifiedAuxData, }; use strata_btc_verification::{TxidInclusionProof, check_block_integrity}; @@ -31,7 +31,7 @@ use crate::{ /// commits to them), while the worker passes its effective params with /// discovered fork activations applied. pub fn compute_asm_transition( - stf_params: &StfParams, + stf_params: &AsmStfParams, pre_state: &AnchorState, block: &Block, aux_data: &AuxData, diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index f6bcf068..0c54c1d5 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -212,7 +212,7 @@ impl Subprotocol for BridgeV1Subproto { #[cfg(test)] mod tests { - use strata_asm_common::{ProcessMsgsCtx, StfParams, 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; @@ -239,7 +239,7 @@ mod tests { let msgs = vec![BridgeIncomingMsg::UpdateSafeHarbourAddress( new_address.clone(), )]; - let stf_params = StfParams::default(); + let stf_params = AsmStfParams::default(); let ctx = ProcessMsgsCtx { l1ref: &l1ref, stf_params: &stf_params, @@ -257,7 +257,7 @@ mod tests { let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); let msgs = vec![BridgeIncomingMsg::Defcon(DefconPayload::default())]; - let stf_params = StfParams::default(); + let stf_params = AsmStfParams::default(); let ctx = ProcessMsgsCtx { l1ref: &l1ref, stf_params: &stf_params, @@ -287,7 +287,7 @@ mod tests { BridgeIncomingMsg::Defcon(DefconPayload::default()), BridgeIncomingMsg::UpdateSafeHarbourAddress(rejected_address), ]; - let stf_params = StfParams::default(); + let stf_params = AsmStfParams::default(); let ctx = ProcessMsgsCtx { l1ref: &l1ref, stf_params: &stf_params, diff --git a/crates/worker/src/service.rs b/crates/worker/src/service.rs index 82386038..08886f71 100644 --- a/crates/worker/src/service.rs +++ b/crates/worker/src/service.rs @@ -297,7 +297,7 @@ mod tests { use std::thread; use bitcoind_async_client::traits::Reader; - use strata_asm_common::{AsmManifestHash, AuxRequestCollector, StfParams}; + use strata_asm_common::{AsmManifestHash, AsmStfParams, AuxRequestCollector}; use strata_btc_types::L1BlockIdBitcoinExt; use strata_identifiers::{Buf32, L1BlockId}; use strata_service::CommandCompletionSender; @@ -533,7 +533,7 @@ mod tests { let reloaded = AsmWorkerServiceState::<_, TestAsmSpec>::new( context, genesis, - StfParams::default(), + AsmStfParams::default(), Subscribers::default(), ) .unwrap(); diff --git a/crates/worker/src/state.rs b/crates/worker/src/state.rs index fbfb189f..847ce21a 100644 --- a/crates/worker/src/state.rs +++ b/crates/worker/src/state.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use bitcoin::{Block, CompactTarget, params::Params}; -use strata_asm_common::{AnchorState, AsmSpec, AuxData, HeaderVerificationState, StfParams}; +use strata_asm_common::{AnchorState, AsmSpec, AsmStfParams, AuxData, HeaderVerificationState}; use strata_asm_stf::AsmStfOutput; use strata_btc_types::BlockHashExt; use strata_btc_verification::{ @@ -43,7 +43,7 @@ pub struct AsmWorkerServiceState { pub(crate) subscribers: Subscribers, /// STF params every transition executes under. - pub(crate) stf_params: StfParams, + pub(crate) stf_params: AsmStfParams, /// ASM spec driving the subprotocol pipeline (type-level only). _spec: PhantomData, @@ -66,7 +66,7 @@ where pub(crate) fn new( context: W, genesis_state: AnchorState, - stf_params: StfParams, + stf_params: AsmStfParams, subscribers: Subscribers, ) -> WorkerResult { let genesis_height = genesis_state @@ -336,7 +336,7 @@ mod tests { let reloaded = AsmWorkerServiceState::<_, TestAsmSpec>::new( context, genesis, - StfParams::default(), + AsmStfParams::default(), Subscribers::default(), ) .unwrap(); @@ -461,7 +461,7 @@ mod tests { AsmWorkerServiceState::<_, TestAsmSpec>::new( context, genesis, - StfParams::default(), + AsmStfParams::default(), Subscribers::default(), ) .unwrap(); diff --git a/crates/worker/src/test_utils.rs b/crates/worker/src/test_utils.rs index 5c74bfdf..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, - StfParams, + AnchorState, AsmHistoryAccumulatorState, AsmSpec, AsmStfParams, ChainViewState, + HeaderVerificationState, }; use strata_btc_types::BlockHashExt; use strata_btc_verification::L1Anchor; @@ -340,8 +340,8 @@ pub(crate) mod fixtures { genesis_state_from_anchor(anchor.clone()) } - fn stf_params(_anchor: &L1Anchor) -> StfParams { - StfParams::default() + fn stf_params(_anchor: &L1Anchor) -> AsmStfParams { + AsmStfParams::default() } } @@ -387,7 +387,7 @@ pub(crate) mod fixtures { let state = AsmWorkerServiceState::new( context, genesis, - StfParams::default(), + AsmStfParams::default(), Subscribers::default(), ) .expect("create service state"); diff --git a/guest-builder/sp1/guest-asm/src/main.rs b/guest-builder/sp1/guest-asm/src/main.rs index b7e1fccf..88f56a9a 100644 --- a/guest-builder/sp1/guest-asm/src/main.rs +++ b/guest-builder/sp1/guest-asm/src/main.rs @@ -1,10 +1,10 @@ #![no_main] zkaleido_sp1_guest_env::entrypoint!(main); -use strata_asm_proof_impl::{statements::process_asm_stf, StfParams}; +use strata_asm_proof_impl::{statements::process_asm_stf, AsmStfParams}; use zkaleido_sp1_guest_env::Sp1ZkVmEnv; fn main() { // Hardcoded on purpose: the verifying key must commit to the STF params. - process_asm_stf(&Sp1ZkVmEnv, StfParams::default()) + process_asm_stf(&Sp1ZkVmEnv, AsmStfParams::default()) } diff --git a/tests/asm/admin_to_stf.rs b/tests/asm/admin_to_stf.rs index e1af915a..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, StfParams}; +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, StfParams::default()) + let attestation = AsmStfProofProgram::execute(&runtime_input, AsmStfParams::default()) .expect("AsmStfProofProgram::execute failed"); // Independently compute the expected post-state. let stf_output = compute_asm_transition::( - &StfParams::default(), + &AsmStfParams::default(), &pre_anchor_state, &activation_block, step_input.aux_data(), From 5c4cffd41e66a2bfd489b80b1cb03eeaa661d313 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 14 Jul 2026 08:24:39 +0545 Subject: [PATCH 09/12] refactor(spec): rename GenesisSectionStage to GenesisStateStage The stage's product is each subprotocol's genesis state; the section is just the envelope it gets packed into. --- crates/spec/src/genesis.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/spec/src/genesis.rs b/crates/spec/src/genesis.rs index 284e0a86..407620ee 100644 --- a/crates/spec/src/genesis.rs +++ b/crates/spec/src/genesis.rs @@ -18,7 +18,7 @@ use crate::StrataAsmSpec; /// 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 = GenesisSectionStage { + let mut stage = GenesisStateStage { params, sections: Vec::new(), }; @@ -49,17 +49,18 @@ pub fn construct_genesis_state(params: &AsmGenesisParams) -> AnchorState { } } -/// [`Stage`] that builds each subprotocol's genesis section from its config. +/// [`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 GenesisSectionStage<'p> { +struct GenesisStateStage<'p> { params: &'p AsmGenesisParams, sections: Vec, } -impl Stage for GenesisSectionStage<'_> { +impl Stage for GenesisStateStage<'_> { fn invoke_subprotocol(&mut self) { let config = self .params From 322587a695fa3e7c778d61ee5ae7d728a1fbb122 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Fri, 17 Jul 2026 17:07:23 +0545 Subject: [PATCH 10/12] refactor(common)!: represent disabled forks as None, not a sentinel Model fork activation as `Option` so a disabled fork is `None` rather than the `L1Height::MAX` sentinel. The sentinel forced a degenerate boundary (a "never" fork was still active at MAX under the plain `>=` comparison) and required test fixtures to carry a magic max-int that also had to be capped at i64::MAX to survive the prover's signed-64-bit TOML. `None` removes both hazards and reads as intent on the wire (`"fork1": null`). --- crates/common/src/fork.rs | 53 +++++++++++-------- crates/params/src/lib.rs | 2 +- crates/params/src/runtime.rs | 2 +- functional-tests/factory/common/asm_params.py | 16 +++--- 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/crates/common/src/fork.rs b/crates/common/src/fork.rs index 889ade04..dca33cbd 100644 --- a/crates/common/src/fork.rs +++ b/crates/common/src/fork.rs @@ -63,27 +63,26 @@ impl TryFrom for ForkId { /// Activation heights for every named fork. /// -/// A fork is active at L1 height `h` iff `h >= activation_height_of`. `0` -/// means active since genesis; [`L1Height::MAX`] means never active. Proving -/// artifacts bake one of those two extremes (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. +/// 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`]. - pub fork1: L1Height, + /// Activation height of [`ForkId::Fork1`], or `None` if disabled. + pub fork1: Option, } impl ForkSchedule { - /// Schedule with every fork disabled (activation at [`L1Height::MAX`]). + /// Schedule with every fork disabled (no activation height). pub const fn all_disabled() -> Self { - Self { - fork1: L1Height::MAX, - } + Self { fork1: None } } - /// Returns the activation height of `fork`. - pub fn activation_height_of(&self, fork: ForkId) -> L1Height { + /// 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, } @@ -91,13 +90,14 @@ impl ForkSchedule { /// Returns whether `fork` is active at L1 `height`. pub fn is_active(&self, fork: ForkId, height: L1Height) -> bool { - height >= self.activation_height_of(fork) + 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 = height, + ForkId::Fork1 => self.fork1 = Some(height), } } } @@ -129,7 +129,7 @@ mod tests { #[test] fn is_active_boundaries() { - let sched = ForkSchedule { fork1: 100 }; + 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)); @@ -137,25 +137,24 @@ mod tests { #[test] fn zero_means_always_active() { - let sched = ForkSchedule { fork1: 0 }; + let sched = ForkSchedule { fork1: Some(0) }; assert!(sched.is_active(ForkId::Fork1, 0)); assert!(sched.is_active(ForkId::Fork1, L1Height::MAX)); } #[test] - fn max_means_never_active() { + 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 - 1)); - // Degenerate boundary: is_active is a plain >= comparison. - assert!(sched.is_active(ForkId::Fork1, L1Height::MAX)); + 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), 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)); } @@ -163,12 +162,20 @@ mod tests { #[test] fn serde_roundtrip() { let params = AsmStfParams { - forks: ForkSchedule { fork1: 7 }, + 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); } #[test] diff --git a/crates/params/src/lib.rs b/crates/params/src/lib.rs index f6996b2c..87ceb2fe 100644 --- a/crates/params/src/lib.rs +++ b/crates/params/src/lib.rs @@ -142,7 +142,7 @@ mod tests { let params: AsmParams = serde_json::from_str(raw_json).expect("deserialization from raw JSON should succeed"); - assert_eq!(params.runtime.forks.fork1, 0); + assert_eq!(params.runtime.forks.fork1, Some(0)); } #[cfg(feature = "arbitrary")] diff --git a/crates/params/src/runtime.rs b/crates/params/src/runtime.rs index b2f62aae..ebf68e1f 100644 --- a/crates/params/src/runtime.rs +++ b/crates/params/src/runtime.rs @@ -31,6 +31,6 @@ mod tests { #[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, 5); + assert_eq!(params.stf_params().forks.fork1, Some(5)); } } diff --git a/functional-tests/factory/common/asm_params.py b/functional-tests/factory/common/asm_params.py index f8f2aad7..46435ab8 100644 --- a/functional-tests/factory/common/asm_params.py +++ b/functional-tests/factory/common/asm_params.py @@ -11,10 +11,9 @@ # tests. Address `bc1ppuxgmd6n4j73wdp688p08a8rte97dkn5n70r2ym6kgsw0v3c5ensrytduf`. DEFAULT_SAFE_HARBOUR_ADDRESS = "040f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667" -# Fork activation height meaning "never". Capped at i64::MAX rather than -# u64::MAX because the value also rides through the prover's TOML config, -# and TOML integers are signed 64-bit. Equally unreachable in practice. -FORK_NEVER = 2**63 - 1 +# Fork activation height meaning "disabled". `None` serializes to JSON `null`, +# matching the `Option` on the Rust side. +FORK_DISABLED = None @dataclass @@ -85,9 +84,10 @@ class AsmParams: magic: str anchor: L1Anchor subprotocols: list[dict[str, Any]] - # STF config: base fork schedule. Dynamic activations come from enacted - # ASM VK upgrades, which name the fork they activate. - fork1_height: int = 0 + # 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 { @@ -219,7 +219,7 @@ def build_asm_params( recovery_delay: int = 1_008, safe_harbour_address: str = DEFAULT_SAFE_HARBOUR_ADDRESS, confirmation_depth: int = 144, - fork1_height: int = 0, + fork1_height: int | None = 0, ) -> AsmParams: anchor = build_l1_anchor(genesis_height, block_hash, header, epoch_start_header) subprotocols = build_subprotocols( From 59d3408862636e10cb6dcf68d4dc335062b47c3e Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Fri, 17 Jul 2026 17:16:43 +0545 Subject: [PATCH 11/12] docs(common): clarify unknown fork ids halt the worker, not skip The ForkId doc conflated parse-time tolerance with act-time policy, reading as if the worker skips activations for fork ids it doesn't know. It doesn't: an unknown id means the worker is running old software past an upgrade it cannot execute, and it halts rather than limp along on stale rules. Separate the two boundaries so the doc can't be read as endorsing the silent-limp failure. --- crates/common/src/fork.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/common/src/fork.rs b/crates/common/src/fork.rs index dca33cbd..08d15a3d 100644 --- a/crates/common/src/fork.rs +++ b/crates/common/src/fork.rs @@ -14,10 +14,17 @@ use strata_identifiers::L1Height; /// /// One variant per protocol upgrade, in activation order. Discriminants are /// stable: they key persisted fork-activation records and are the raw fork -/// ids carried in ASM VK upgrade actions. Actions carry the raw id rather -/// than this enum so that artifacts predating a fork can still parse and -/// enact the upgrade that activates it; consumers that act on the id (the -/// worker) map the ones they know via [`TryFrom`] and skip the rest. +/// ids carried in ASM VK upgrade actions. +/// +/// 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)] From 0683d284fb4ab768a17fbc87a6a161139e249001 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Fri, 17 Jul 2026 17:23:42 +0545 Subject: [PATCH 12/12] docs(common): clarify ForkId's numeric-vs-name identity split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reviewer read the "stable discriminant" line as contradicting the name-based serde. It doesn't: persisted fork-activation records key on the raw discriminant byte and VK actions carry the numeric id — neither uses serde — so both survive a variant rename. The variant name is the human-readable form (serde plus the mirrored ForkSchedule params field) and is meant to change: Fork1 is a placeholder, and renaming it once the upgrade is defined is a routine config migration that leaves persisted and wire data untouched. Spell that split out rather than implying the name is either the stable id or an unused label. --- crates/common/src/fork.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/crates/common/src/fork.rs b/crates/common/src/fork.rs index 08d15a3d..8cf8e882 100644 --- a/crates/common/src/fork.rs +++ b/crates/common/src/fork.rs @@ -12,9 +12,18 @@ use strata_identifiers::L1Height; /// Identifies a named fork. /// -/// One variant per protocol upgrade, in activation order. Discriminants are -/// stable: they key persisted fork-activation records and are the raw fork -/// ids carried in ASM VK upgrade actions. +/// 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: /// @@ -29,8 +38,11 @@ use strata_identifiers::L1Height; #[serde(rename_all = "snake_case")] #[repr(u8)] pub enum ForkId { - /// Placeholder for the first protocol upgrade; renamed once that upgrade - /// is defined. + /// 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, } @@ -185,9 +197,17 @@ mod tests { 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_snake_case() { + 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