Skip to content

Commit 357e0bb

Browse files
authored
feat(admin): emit AsmStfUpdate log on ASM verifying key update (#63)
* feat: emit AsmStfUpdate log from admin handler * test: asm predicate update emits logs
1 parent ac8bc94 commit 357e0bb

File tree

8 files changed

+264
-2
lines changed

8 files changed

+264
-2
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/subprotocols/admin/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ workspace = true
1010
strata-asm-bridge-msgs.workspace = true
1111
strata-asm-checkpoint-msgs.workspace = true
1212
strata-asm-common.workspace = true
13+
strata-asm-logs.workspace = true
1314
strata-asm-params.workspace = true
1415
strata-asm-txs-admin.workspace = true
1516
strata-crypto.workspace = true

crates/subprotocols/admin/src/handler.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use strata_asm_bridge_msgs::{BridgeIncomingMsg, UpdateOperatorSetPayload};
22
use strata_asm_checkpoint_msgs::CheckpointIncomingMsg;
33
use strata_asm_common::{
4-
MsgRelayer,
4+
AsmLogEntry, MsgRelayer,
55
logging::{error, info},
66
};
7+
use strata_asm_logs::AsmStfUpdate;
78
use strata_asm_txs_admin::{
89
actions::{MultisigAction, UpdateAction, updates::predicate::ProofType},
910
parser::SignedPayload,
@@ -57,7 +58,13 @@ pub(crate) fn handle_pending_updates(
5758
let (key, kind) = update.into_inner();
5859
match kind {
5960
ProofType::Asm => {
60-
// TODO(STR-1721): Emit ASM Log
61+
let log_entry = AsmLogEntry::from_log(&AsmStfUpdate::new(key))
62+
.expect("AsmStfUpdate encoding is infallible");
63+
relayer.emit_log(log_entry);
64+
info!(
65+
%update_id,
66+
"Emitted ASM STF verifying key update log",
67+
);
6168
}
6269
ProofType::OLStf => {
6370
relay_checkpoint_predicate(relayer, key);
@@ -201,6 +208,7 @@ mod tests {
201208
use rand::{rngs::OsRng, seq::SliceRandom, thread_rng};
202209
use strata_asm_checkpoint_msgs::CheckpointIncomingMsg;
203210
use strata_asm_common::{AsmLogEntry, InterprotoMsg, MsgRelayer};
211+
use strata_asm_logs::AsmStfUpdate;
204212
use strata_asm_params::{AdministrationInitConfig, Role};
205213
use strata_asm_txs_admin::{
206214
actions::{
@@ -520,6 +528,38 @@ mod tests {
520528
}
521529
}
522530

531+
#[test]
532+
fn test_asm_verifying_key_update_emits_log() {
533+
let (params, _, _) = create_test_params();
534+
let mut state = AdministrationSubprotoState::new(&params);
535+
let mut relayer = MockRelayer::<CheckpointIncomingMsg>::new();
536+
537+
let predicate = PredicateKey::always_accept();
538+
539+
let update = PredicateUpdate::new(predicate.clone(), ProofType::Asm);
540+
let update_id = state.next_update_id();
541+
let activation_height = 42;
542+
state.enqueue(QueuedUpdate::new(
543+
update_id,
544+
update.into(),
545+
activation_height,
546+
));
547+
548+
handle_pending_updates(&mut state, &mut relayer, activation_height);
549+
550+
assert!(state.queued().is_empty());
551+
// No inter-protocol messages should be sent for ASM updates
552+
assert!(relayer.messages().is_empty());
553+
// Exactly one log should be emitted
554+
assert_eq!(relayer.logs.len(), 1);
555+
556+
let log_entry = &relayer.logs[0];
557+
let asm_update = log_entry
558+
.try_into_log::<AsmStfUpdate>()
559+
.expect("log should deserialize as AsmStfUpdate");
560+
assert_eq!(asm_update.new_predicate(), &predicate);
561+
}
562+
523563
/// Test that cancel actions properly remove queued updates:
524564
/// - First queue 5 update actions.
525565
/// - Then cancel each one individually.

crates/subprotocols/admin/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ mod queued_update;
1212
mod state;
1313
mod subprotocol;
1414

15+
pub use queued_update::QueuedUpdate;
1516
pub use state::AdministrationSubprotoState;
1617
pub use subprotocol::AdministrationSubprotocol;

guest-builder/sp1/guest-asm/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ version = "0.1.0"
77
workspace = true
88

99
[dependencies]
10+
moho-runtime-impl.workspace = true
11+
moho-runtime-interface.workspace = true
12+
moho-types.workspace = true
1013
strata-asm-common.workspace = true
14+
strata-asm-logs.workspace = true
1115
strata-asm-manifest-types.workspace = true
1216
strata-asm-params = { workspace = true, features = ["arbitrary"] }
17+
strata-asm-proof-impl = { workspace = true, features = ["test-utils"] }
1318
strata-asm-proto-administration.workspace = true
1419
strata-asm-proto-bridge-v1.workspace = true
1520
strata-asm-proto-checkpoint.workspace = true
1621
strata-asm-spec.workspace = true
22+
strata-asm-stf.workspace = true
1723
strata-asm-txs-admin = { workspace = true, features = ["test-utils"] }
1824
strata-asm-txs-bridge-v1 = { workspace = true, features = ["test-utils"] }
1925
strata-asm-worker.workspace = true
@@ -31,6 +37,7 @@ strata-merkle.workspace = true
3137
strata-predicate.workspace = true
3238
strata-tasks.workspace = true
3339
strata-test-utils-arb.workspace = true
40+
strata-test-utils-btc.workspace = true
3441
strata-test-utils-btcio.workspace = true
3542
strata-test-utils-checkpoint.workspace = true
3643

@@ -78,3 +85,7 @@ path = "asm/admin_to_bridge.rs"
7885
[[test]]
7986
name = "asm_bridge_to_checkpoint"
8087
path = "asm/bridge_to_checkpoint.rs"
88+
89+
[[test]]
90+
name = "asm_admin_to_stf"
91+
path = "asm/admin_to_stf.rs"

tests/asm/admin_to_stf.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//! Admin → ASM STF interaction tests
2+
//!
3+
//! Tests the propagation of ASM verifying key updates as `AsmStfUpdate` logs
4+
//! in the manifest, which the `MohoProgram` uses to set the next predicate key.
5+
6+
#![allow(
7+
unused_crate_dependencies,
8+
reason = "test dependencies shared across test suite"
9+
)]
10+
11+
use harness::{
12+
admin::{create_test_admin_setup, predicate_update, AdminExt},
13+
test_harness::AsmTestHarnessBuilder,
14+
};
15+
use integration_tests::harness;
16+
use moho_runtime_impl::RuntimeInput;
17+
use ssz::Encode;
18+
use strata_asm_common::AuxData;
19+
use strata_asm_logs::AsmStfUpdate;
20+
use strata_asm_proof_impl::{
21+
moho_program::input::AsmStepInput, program::AsmStfProofProgram, test_utils::create_moho_state,
22+
};
23+
use strata_asm_spec::StrataAsmSpec;
24+
use strata_asm_stf::compute_asm_transition;
25+
use strata_asm_txs_admin::actions::updates::predicate::ProofType;
26+
use strata_btc_verification::TxidInclusionProof;
27+
use strata_predicate::PredicateKey;
28+
29+
/// Verifies ASM predicate updates emit an `AsmStfUpdate` log in the manifest after activation.
30+
///
31+
/// Flow:
32+
/// 1. Submit predicate update with `ProofType::Asm` (gets queued)
33+
/// 2. Mine blocks to trigger activation (confirmation_depth=2)
34+
/// 3. Verify the manifest contains an `AsmStfUpdate` log with the correct predicate
35+
#[tokio::test(flavor = "multi_thread")]
36+
async fn test_asm_predicate_update_emits_log() {
37+
let (admin_config, mut ctx) = create_test_admin_setup(2);
38+
let harness = AsmTestHarnessBuilder::default()
39+
.with_admin_config(admin_config)
40+
.build()
41+
.await
42+
.unwrap();
43+
44+
// Initialize subprotocols (genesis state has no sections)
45+
harness.mine_block(None).await.unwrap();
46+
47+
// Submit an ASM predicate update (gets queued for StrataAdministrator role)
48+
let new_predicate = PredicateKey::always_accept();
49+
harness
50+
.submit_admin_action(
51+
&mut ctx,
52+
predicate_update(new_predicate.clone(), ProofType::Asm),
53+
)
54+
.await
55+
.unwrap();
56+
57+
// Verify it's queued, not applied yet
58+
let state = harness.admin_state().unwrap();
59+
assert_eq!(state.queued().len(), 1, "Predicate update should be queued");
60+
61+
// Mine blocks to trigger activation (confirmation_depth=2)
62+
harness.mine_block(None).await.unwrap();
63+
harness.mine_block(None).await.unwrap();
64+
65+
// Admin queue should be empty
66+
let final_state = harness.admin_state().unwrap();
67+
assert_eq!(
68+
final_state.queued().len(),
69+
0,
70+
"Queue should be empty after activation"
71+
);
72+
73+
// Find the AsmStfUpdate log in the stored manifests
74+
let manifests = harness.get_stored_manifests();
75+
let asm_stf_update = manifests
76+
.iter()
77+
.flat_map(|m| &m.logs)
78+
.find_map(|log| log.try_into_log::<AsmStfUpdate>().ok())
79+
.expect("expected an AsmStfUpdate log in manifests");
80+
81+
assert_eq!(
82+
asm_stf_update.new_predicate(),
83+
&new_predicate,
84+
"AsmStfUpdate log should contain the new predicate"
85+
);
86+
}
87+
88+
/// Verifies that `AsmStfProofProgram::execute()` produces a `MohoAttestation` whose post-state
89+
/// commitment reflects the updated predicate key.
90+
///
91+
/// Uses the full test harness (bitcoind regtest) to naturally submit an admin predicate update,
92+
/// mine blocks for activation, and then replays the activation block through
93+
/// `AsmStfProofProgram::execute()` to verify the proof output.
94+
///
95+
/// Flow:
96+
/// 1. Set up harness with `confirmation_depth=2`, submit predicate update (always_accept →
97+
/// never_accept)
98+
/// 2. Mine blocks to trigger activation, capturing the pre-state and activation block
99+
/// 3. Build `RuntimeInput` from the captured state/block and run `AsmStfProofProgram::execute()`
100+
/// 4. Verify the output attestation's post-state commitment reflects the new predicate
101+
#[tokio::test(flavor = "multi_thread")]
102+
async fn test_proof_program_reflects_predicate_update() {
103+
let (admin_config, mut ctx) = create_test_admin_setup(2);
104+
let harness = AsmTestHarnessBuilder::default()
105+
.with_admin_config(admin_config)
106+
.build()
107+
.await
108+
.unwrap();
109+
110+
// Initialize subprotocols (genesis state has no sections yet).
111+
harness.mine_block(None).await.unwrap();
112+
113+
// Submit an ASM predicate update (gets queued for StrataAdministrator role).
114+
let new_predicate = PredicateKey::never_accept();
115+
harness
116+
.submit_admin_action(
117+
&mut ctx,
118+
predicate_update(new_predicate.clone(), ProofType::Asm),
119+
)
120+
.await
121+
.unwrap();
122+
123+
// Verify it's queued.
124+
let state = harness.admin_state().unwrap();
125+
assert_eq!(state.queued().len(), 1, "Predicate update should be queued");
126+
127+
// Mine first confirmation block.
128+
harness.mine_block(None).await.unwrap();
129+
130+
// Capture the pre-state before the activation block.
131+
let (_, pre_asm_state) = harness
132+
.get_latest_asm_state()
133+
.unwrap()
134+
.expect("ASM state must exist before activation block");
135+
let pre_anchor_state = pre_asm_state.state().clone();
136+
137+
// Mine the activation block (confirmation_depth=2 reached).
138+
let activation_block_hash = harness.mine_block(None).await.unwrap();
139+
140+
// Admin queue should be empty after activation.
141+
let final_state = harness.admin_state().unwrap();
142+
assert_eq!(
143+
final_state.queued().len(),
144+
0,
145+
"Queue should be empty after activation"
146+
);
147+
148+
// Fetch the activation block.
149+
let activation_block = harness.get_block(activation_block_hash).await.unwrap();
150+
let coinbase_inclusion_proof = TxidInclusionProof::generate(&activation_block.txdata, 0);
151+
152+
// Build AsmStepInput from the real activation block.
153+
let step_input = AsmStepInput::new(
154+
activation_block.clone(),
155+
AuxData::default(),
156+
Some(coinbase_inclusion_proof.clone()),
157+
);
158+
159+
// Build MohoState pre-state with always_accept (the initial predicate).
160+
let initial_predicate = PredicateKey::always_accept();
161+
let moho_pre_state = create_moho_state(&pre_anchor_state, initial_predicate);
162+
163+
// Construct RuntimeInput and execute the proof program.
164+
let runtime_input = RuntimeInput::new(
165+
moho_pre_state,
166+
pre_anchor_state.as_ssz_bytes(),
167+
step_input.as_ssz_bytes(),
168+
);
169+
let attestation =
170+
AsmStfProofProgram::execute(&runtime_input).expect("AsmStfProofProgram::execute failed");
171+
172+
// Independently compute the expected post-state.
173+
let stf_output = compute_asm_transition(
174+
&StrataAsmSpec,
175+
&pre_anchor_state,
176+
&activation_block,
177+
step_input.aux_data(),
178+
Some(&coinbase_inclusion_proof),
179+
)
180+
.expect("compute_asm_transition failed");
181+
182+
// The post MohoState should carry `never_accept` as the next predicate,
183+
// because the queued AsmStfUpdate log was emitted during the transition.
184+
let expected_post_moho = create_moho_state(&stf_output.state, new_predicate);
185+
186+
// The proven commitment in the attestation must match.
187+
assert_eq!(
188+
attestation.proven().commitment(),
189+
&expected_post_moho.compute_commitment(),
190+
"post-state commitment should reflect the updated predicate (never_accept)"
191+
);
192+
}

tests/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ use bitcoin_bosd as _;
1111
use bitcoind_async_client as _;
1212
use borsh as _;
1313
use corepc_node as _;
14+
use moho_runtime_impl as _;
15+
use moho_runtime_interface as _;
16+
use moho_types as _;
1417
use rand as _;
1518
use rand_chacha as _;
1619
use ssz as _;
1720
use strata_asm_common as _;
21+
use strata_asm_logs as _;
1822
use strata_asm_manifest_types as _;
23+
use strata_asm_proof_impl as _;
1924
use strata_asm_proto_administration as _;
25+
use strata_asm_spec as _;
26+
use strata_asm_stf as _;
2027
use strata_asm_txs_admin as _;
2128
use strata_asm_worker as _;
2229
use strata_bridge_types as _;
@@ -29,5 +36,6 @@ use strata_l1_txfmt as _;
2936
use strata_merkle as _;
3037
use strata_predicate as _;
3138
use strata_tasks as _;
39+
use strata_test_utils_btc as _;
3240
use strata_test_utils_btcio as _;
3341
use strata_test_utils_checkpoint as _;

0 commit comments

Comments
 (0)