From 4cc945ea3acf07f6accb1969aab0869d9773e128 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Thu, 23 Apr 2026 11:23:28 -0400 Subject: [PATCH 01/13] feat: define safe harbour address --- .../subprotocols/bridge-v1/types/src/lib.rs | 2 + .../bridge-v1/types/src/safe_harbour.rs | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 crates/subprotocols/bridge-v1/types/src/safe_harbour.rs diff --git a/crates/subprotocols/bridge-v1/types/src/lib.rs b/crates/subprotocols/bridge-v1/types/src/lib.rs index df4ff948..86677eaa 100644 --- a/crates/subprotocols/bridge-v1/types/src/lib.rs +++ b/crates/subprotocols/bridge-v1/types/src/lib.rs @@ -30,11 +30,13 @@ use strata_identifiers::{AccountId, AccountSerial}; mod operator; +mod safe_harbour; mod withdrawal; pub use operator::{ OperatorBitmap, OperatorBitmapError, OperatorIdx, OperatorSelection, filter_eligible_operators, }; +pub use safe_harbour::SafeHarbour; pub use withdrawal::{WithdrawOutput, WithdrawalCommand}; const BRIDGE_GATEWAY_REF: u8 = 0x10; diff --git a/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs b/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs new file mode 100644 index 00000000..5d9ea0b6 --- /dev/null +++ b/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs @@ -0,0 +1,51 @@ +//! Safe harbour address management. +//! +//! A safe harbour is a predefined Bitcoin output script descriptor that can be +//! activated by the security council (admin multisig) to redirect flows under +//! emergency conditions. The address is fixed at bridge initialization; only the +//! activation flag changes at runtime. + +use arbitrary::Arbitrary; +use bitcoin_bosd::Descriptor; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; + +/// A predefined safe harbour address with an activation flag. +/// +/// The [`Descriptor`] is set once (via the bridge init config) and cannot be +/// changed at runtime. Activation is toggled by the admin multisig. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Arbitrary, Encode, Decode)] +pub struct SafeHarbour { + address: Descriptor, + activated: bool, +} + +impl SafeHarbour { + /// Creates a new deactivated safe harbour for the given address. + pub fn new(address: Descriptor) -> Self { + Self { + address, + activated: false, + } + } + + /// Returns the configured safe harbour address. + pub fn address(&self) -> &Descriptor { + &self.address + } + + /// Returns `Some(&address)` when activated, otherwise `None`. + pub fn active_address(&self) -> Option<&Descriptor> { + self.activated.then_some(&self.address) + } + + /// Returns whether the safe harbour is currently activated. + pub fn is_activated(&self) -> bool { + self.activated + } + + /// Sets the activation flag. + pub fn set_activated(&mut self, activated: bool) { + self.activated = activated; + } +} From 86d0bc1a11ae2d3e86aef8c31020c695f1cfb3d1 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Thu, 23 Apr 2026 11:37:09 -0400 Subject: [PATCH 02/13] feat(asm-params): add safe harbour config to bridge params --- Cargo.lock | 1 + crates/params/Cargo.toml | 3 ++- crates/params/src/params.rs | 3 ++- crates/params/src/subprotocols/bridge.rs | 4 ++++ guest-builder/sp1/guest-asm/Cargo.lock | 2 ++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6af4497..1fbf2419 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7725,6 +7725,7 @@ version = "0.1.0" dependencies = [ "arbitrary", "bitcoin", + "bitcoin-bosd 0.10.0", "proptest", "serde", "serde_json", diff --git a/crates/params/Cargo.toml b/crates/params/Cargo.toml index 27b041b9..5f92833f 100644 --- a/crates/params/Cargo.toml +++ b/crates/params/Cargo.toml @@ -16,6 +16,7 @@ strata-predicate.workspace = true arbitrary = { workspace = true, optional = true } bitcoin = { workspace = true, optional = true } +bitcoin-bosd = { workspace = true, features = ["serde"] } serde.workspace = true ssz.workspace = true ssz_derive.workspace = true @@ -25,4 +26,4 @@ proptest.workspace = true serde_json.workspace = true [features] -arbitrary = ["dep:arbitrary", "dep:bitcoin", "strata-predicate/arbitrary"] +arbitrary = ["dep:arbitrary", "dep:bitcoin", "bitcoin-bosd/arbitrary", "strata-predicate/arbitrary"] diff --git a/crates/params/src/params.rs b/crates/params/src/params.rs index 648a0c0c..0767ab16 100644 --- a/crates/params/src/params.rs +++ b/crates/params/src/params.rs @@ -160,7 +160,8 @@ mod tests { "denomination": 0, "assignment_duration": 0, "operator_fee": 0, - "recovery_delay": 0 + "recovery_delay": 0, + "safe_harbour_address": "03671041727b982843f7e3db4669c2f542e05096fb" } } ] diff --git a/crates/params/src/subprotocols/bridge.rs b/crates/params/src/subprotocols/bridge.rs index 95b5100c..e0c9b4b2 100644 --- a/crates/params/src/subprotocols/bridge.rs +++ b/crates/params/src/subprotocols/bridge.rs @@ -1,3 +1,4 @@ +use bitcoin_bosd::Descriptor; use serde::{Deserialize, Serialize}; use strata_btc_types::BitcoinAmount; use strata_crypto::EvenPublicKey; @@ -17,6 +18,8 @@ pub struct BridgeV1InitConfig { /// Number of Bitcoin blocks after Deposit Request Transaction that the depositor can reclaim /// funds if operators fail to process the deposit. pub recovery_delay: u16, + /// Predefined safe harbour address. Deactivated at init; the admin multisig toggles activation. + pub safe_harbour_address: Descriptor, } #[cfg(feature = "arbitrary")] @@ -34,6 +37,7 @@ impl<'a> arbitrary::Arbitrary<'a> for BridgeV1InitConfig { assignment_duration: u.arbitrary()?, operator_fee: u.arbitrary()?, recovery_delay: u.arbitrary()?, + safe_harbour_address: u.arbitrary()?, }) } } diff --git a/guest-builder/sp1/guest-asm/Cargo.lock b/guest-builder/sp1/guest-asm/Cargo.lock index b7cf20e5..1a4e7e72 100644 --- a/guest-builder/sp1/guest-asm/Cargo.lock +++ b/guest-builder/sp1/guest-asm/Cargo.lock @@ -2132,6 +2132,7 @@ version = "0.1.0" dependencies = [ "arbitrary", "bitcoin", + "bitcoin-bosd 0.10.0", "serde", "ssz", "ssz_derive", @@ -2208,6 +2209,7 @@ version = "0.1.0" dependencies = [ "arbitrary", "bitcoin", + "bitcoin-bosd 0.10.0", "rand_chacha 0.9.0", "serde", "ssz", From 08bedf662b15961ed5861a3033862bdbcc851c7b Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Thu, 23 Apr 2026 11:39:27 -0400 Subject: [PATCH 03/13] feat: add safe harbour address to the bridge state --- .../bridge-v1/subprotocol/src/state/bridge.rs | 16 +++++++++++++++- .../bridge-v1/subprotocol/src/test_utils.rs | 1 + guest-builder/sp1/guest-asm/Cargo.lock | 1 - 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs index ce9463a6..63b2c9bf 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs @@ -1,7 +1,7 @@ use ssz_derive::{Decode, Encode}; use strata_asm_params::BridgeV1InitConfig; use strata_asm_proto_bridge_v1_txs::{deposit::DepositInfo, errors::Mismatch}; -use strata_asm_proto_bridge_v1_types::{OperatorIdx, WithdrawOutput, WithdrawalCommand}; +use strata_asm_proto_bridge_v1_types::{OperatorIdx, SafeHarbour, WithdrawOutput, WithdrawalCommand}; use strata_btc_types::BitcoinAmount; use strata_identifiers::L1BlockCommitment; @@ -38,6 +38,9 @@ pub struct BridgeV1State { /// Number of blocks after Deposit Request Transaction that the depositor can reclaim /// funds if operators fail to process the deposit. recovery_delay: u16, + + /// Predefined safe harbour address, activated by the admin multisig. + safe_harbour: SafeHarbour, } impl BridgeV1State { @@ -64,6 +67,7 @@ impl BridgeV1State { denomination: config.denomination, operator_fee: config.operator_fee, recovery_delay: config.recovery_delay, + safe_harbour: SafeHarbour::new(config.safe_harbour_address.clone()), } } @@ -92,6 +96,16 @@ impl BridgeV1State { self.recovery_delay } + /// Returns the safe harbour address if it is currently activated. + pub fn safe_harbour(&self) -> &SafeHarbour { + &self.safe_harbour + } + + /// Sets the safe harbour activation flag. Invoked by the admin multisig. + pub fn set_safe_harbour_activated(&mut self, activated: bool) { + self.safe_harbour.set_activated(activated); + } + /// Processes a deposit transaction by validating and adding it to the deposits table. /// /// This function takes already parsed deposit transaction information, validates it against the diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/test_utils.rs b/crates/subprotocols/bridge-v1/subprotocol/src/test_utils.rs index 41316366..7db7ceeb 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/test_utils.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/test_utils.rs @@ -60,6 +60,7 @@ pub(crate) fn create_test_state() -> (BridgeV1State, Vec) { assignment_duration: 144, // ~24 hours operator_fee: BitcoinAmount::from_sat(100_000), recovery_delay: 1008, + safe_harbour_address: ArbitraryGenerator::new().generate(), }; let bridge_state = BridgeV1State::new(&config); (bridge_state, privkeys) diff --git a/guest-builder/sp1/guest-asm/Cargo.lock b/guest-builder/sp1/guest-asm/Cargo.lock index 1a4e7e72..4b9c5796 100644 --- a/guest-builder/sp1/guest-asm/Cargo.lock +++ b/guest-builder/sp1/guest-asm/Cargo.lock @@ -2209,7 +2209,6 @@ version = "0.1.0" dependencies = [ "arbitrary", "bitcoin", - "bitcoin-bosd 0.10.0", "rand_chacha 0.9.0", "serde", "ssz", From 5b31ffff81481fe5af0ddddc0be6c0a3a811b625 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Thu, 23 Apr 2026 11:47:01 -0400 Subject: [PATCH 04/13] feat: support defcon msg --- crates/subprotocols/bridge-v1/msgs/src/lib.rs | 7 +++++++ .../subprotocols/bridge-v1/subprotocol/src/subprotocol.rs | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/crates/subprotocols/bridge-v1/msgs/src/lib.rs b/crates/subprotocols/bridge-v1/msgs/src/lib.rs index e3e645b3..72d0f143 100644 --- a/crates/subprotocols/bridge-v1/msgs/src/lib.rs +++ b/crates/subprotocols/bridge-v1/msgs/src/lib.rs @@ -26,6 +26,9 @@ pub enum BridgeIncomingMsg { /// Emitted by the admin subprotocol when the operator set is updated. /// Adds new operators by public key and removes existing operators by index. UpdateOperatorSet(UpdateOperatorSetPayload), + + /// Emergency signal from the admin subprotocol that activates the safe harbour address. + Defcon1(Defcon1Payload), } /// Payload for [`BridgeIncomingMsg::UpdateOperatorSet`]. @@ -37,6 +40,10 @@ pub struct UpdateOperatorSetPayload { pub remove_members: Vec, } +/// Empty marker payload for [`BridgeIncomingMsg::Defcon1`]; the signal itself carries no data. +#[derive(Clone, Debug, Eq, PartialEq, Default, Encode, Decode)] +pub struct Defcon1Payload {} + impl InterprotoMsg for BridgeIncomingMsg { fn id(&self) -> SubprotocolId { BRIDGE_V1_SUBPROTOCOL_ID diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index fde11c4f..803da1f7 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -156,6 +156,10 @@ impl Subprotocol for BridgeV1Subproto { ); state.apply_operator_set_update(add_members, remove_members); } + BridgeIncomingMsg::Defcon1(_) => { + info!("Activating safe harbour address on Defcon1 signal from admin subprotocol"); + state.set_safe_harbour_activated(true); + } } } } From 618007bcc3480a465155c2fd914c45b2fab4422d Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Sat, 25 Apr 2026 17:50:03 -0400 Subject: [PATCH 05/13] fix lints --- crates/params/Cargo.toml | 7 ++++++- crates/params/src/subprotocols/bridge.rs | 3 ++- .../subprotocols/bridge-v1/subprotocol/src/subprotocol.rs | 4 +++- tests/harness/bridge.rs | 2 ++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/params/Cargo.toml b/crates/params/Cargo.toml index 5f92833f..6f4f24a4 100644 --- a/crates/params/Cargo.toml +++ b/crates/params/Cargo.toml @@ -26,4 +26,9 @@ proptest.workspace = true serde_json.workspace = true [features] -arbitrary = ["dep:arbitrary", "dep:bitcoin", "bitcoin-bosd/arbitrary", "strata-predicate/arbitrary"] +arbitrary = [ + "dep:arbitrary", + "dep:bitcoin", + "bitcoin-bosd/arbitrary", + "strata-predicate/arbitrary", +] diff --git a/crates/params/src/subprotocols/bridge.rs b/crates/params/src/subprotocols/bridge.rs index e0c9b4b2..93540187 100644 --- a/crates/params/src/subprotocols/bridge.rs +++ b/crates/params/src/subprotocols/bridge.rs @@ -18,7 +18,8 @@ pub struct BridgeV1InitConfig { /// Number of Bitcoin blocks after Deposit Request Transaction that the depositor can reclaim /// funds if operators fail to process the deposit. pub recovery_delay: u16, - /// Predefined safe harbour address. Deactivated at init; the admin multisig toggles activation. + /// Predefined safe harbour address. Deactivated at init; the admin multisig toggles + /// activation. pub safe_harbour_address: Descriptor, } diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index 803da1f7..a6e70145 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -157,7 +157,9 @@ impl Subprotocol for BridgeV1Subproto { state.apply_operator_set_update(add_members, remove_members); } BridgeIncomingMsg::Defcon1(_) => { - info!("Activating safe harbour address on Defcon1 signal from admin subprotocol"); + info!( + "Activating safe harbour address on Defcon1 signal from admin subprotocol" + ); state.set_safe_harbour_activated(true); } } diff --git a/tests/harness/bridge.rs b/tests/harness/bridge.rs index 54ed0b2b..460b4c2d 100644 --- a/tests/harness/bridge.rs +++ b/tests/harness/bridge.rs @@ -508,6 +508,7 @@ pub fn create_test_bridge_setup(num_operators: usize) -> (BridgeV1InitConfig, Br let denomination = BitcoinAmount::from_sat(1_000_000); let recovery_delay = 1008; let operator_fee = BitcoinAmount::from_sat(100_000); + let safe_harbour_address: Descriptor = ArbitraryGenerator::new().generate(); let config = BridgeV1InitConfig { operators: pubkeys.clone(), @@ -515,6 +516,7 @@ pub fn create_test_bridge_setup(num_operators: usize) -> (BridgeV1InitConfig, Br assignment_duration: 144, operator_fee, recovery_delay, + safe_harbour_address, }; // Use a deterministic recovery key for test reproducibility From a5572e032e3ed7ce56268bd757146370b15f8105 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Mon, 27 Apr 2026 15:45:25 -0400 Subject: [PATCH 06/13] doc: update docstrings --- crates/subprotocols/bridge-v1/msgs/src/lib.rs | 3 ++- .../bridge-v1/subprotocol/src/state/bridge.rs | 6 +++--- .../bridge-v1/types/src/safe_harbour.rs | 19 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/subprotocols/bridge-v1/msgs/src/lib.rs b/crates/subprotocols/bridge-v1/msgs/src/lib.rs index 72d0f143..3dfbb658 100644 --- a/crates/subprotocols/bridge-v1/msgs/src/lib.rs +++ b/crates/subprotocols/bridge-v1/msgs/src/lib.rs @@ -27,7 +27,8 @@ pub enum BridgeIncomingMsg { /// Adds new operators by public key and removes existing operators by index. UpdateOperatorSet(UpdateOperatorSetPayload), - /// Emergency signal from the admin subprotocol that activates the safe harbour address. + /// Defcon1 signal raised by the admin subprotocol. The bridge responds by + /// activating the safe harbour. Defcon1(Defcon1Payload), } diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs index 63b2c9bf..26e1cc03 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs @@ -39,7 +39,7 @@ pub struct BridgeV1State { /// funds if operators fail to process the deposit. recovery_delay: u16, - /// Predefined safe harbour address, activated by the admin multisig. + /// Safe harbour safe_harbour: SafeHarbour, } @@ -96,12 +96,12 @@ impl BridgeV1State { self.recovery_delay } - /// Returns the safe harbour address if it is currently activated. + /// Returns a reference to the safe harbour. pub fn safe_harbour(&self) -> &SafeHarbour { &self.safe_harbour } - /// Sets the safe harbour activation flag. Invoked by the admin multisig. + /// Sets the safe harbour activation flag. pub fn set_safe_harbour_activated(&mut self, activated: bool) { self.safe_harbour.set_activated(activated); } diff --git a/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs b/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs index 5d9ea0b6..13745e25 100644 --- a/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs +++ b/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs @@ -1,19 +1,15 @@ -//! Safe harbour address management. +//! Safe harbour address. //! -//! A safe harbour is a predefined Bitcoin output script descriptor that can be -//! activated by the security council (admin multisig) to redirect flows under -//! emergency conditions. The address is fixed at bridge initialization; only the -//! activation flag changes at runtime. +//! A safe harbour is a Bitcoin output script descriptor used to redirect flows +//! under emergency conditions. Activation is restricted to the strata security +//! council; the address itself can be changed by the strata administrator. use arbitrary::Arbitrary; use bitcoin_bosd::Descriptor; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -/// A predefined safe harbour address with an activation flag. -/// -/// The [`Descriptor`] is set once (via the bridge init config) and cannot be -/// changed at runtime. Activation is toggled by the admin multisig. +/// A safe harbour address with an activation flag. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Arbitrary, Encode, Decode)] pub struct SafeHarbour { address: Descriptor, @@ -48,4 +44,9 @@ impl SafeHarbour { pub fn set_activated(&mut self, activated: bool) { self.activated = activated; } + + /// Updates the address + pub fn update_address(&mut self, address: Descriptor) { + self.address = address + } } From 0133b5ee812b54c6210497841fe3b3ea1525c843 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 19 May 2026 06:48:27 +0545 Subject: [PATCH 07/13] feat: add defcon3 support --- crates/subprotocols/bridge-v1/msgs/src/lib.rs | 8 ++++++++ .../bridge-v1/subprotocol/src/state/bridge.rs | 4 +++- .../subprotocols/bridge-v1/subprotocol/src/subprotocol.rs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/subprotocols/bridge-v1/msgs/src/lib.rs b/crates/subprotocols/bridge-v1/msgs/src/lib.rs index 3dfbb658..b5cb6307 100644 --- a/crates/subprotocols/bridge-v1/msgs/src/lib.rs +++ b/crates/subprotocols/bridge-v1/msgs/src/lib.rs @@ -30,6 +30,10 @@ pub enum BridgeIncomingMsg { /// Defcon1 signal raised by the admin subprotocol. The bridge responds by /// activating the safe harbour. Defcon1(Defcon1Payload), + + /// Defcon3 signal raised by the admin subprotocol. The bridge responds by + /// activating the safe harbour. + Defcon3(Defcon3Payload), } /// Payload for [`BridgeIncomingMsg::UpdateOperatorSet`]. @@ -45,6 +49,10 @@ pub struct UpdateOperatorSetPayload { #[derive(Clone, Debug, Eq, PartialEq, Default, Encode, Decode)] pub struct Defcon1Payload {} +/// Empty marker payload for [`BridgeIncomingMsg::Defcon3`]; the signal itself carries no data. +#[derive(Clone, Debug, Eq, PartialEq, Default, Encode, Decode)] +pub struct Defcon3Payload {} + impl InterprotoMsg for BridgeIncomingMsg { fn id(&self) -> SubprotocolId { BRIDGE_V1_SUBPROTOCOL_ID diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs index 26e1cc03..fe1b6063 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs @@ -1,7 +1,9 @@ use ssz_derive::{Decode, Encode}; use strata_asm_params::BridgeV1InitConfig; use strata_asm_proto_bridge_v1_txs::{deposit::DepositInfo, errors::Mismatch}; -use strata_asm_proto_bridge_v1_types::{OperatorIdx, SafeHarbour, WithdrawOutput, WithdrawalCommand}; +use strata_asm_proto_bridge_v1_types::{ + OperatorIdx, SafeHarbour, WithdrawOutput, WithdrawalCommand, +}; use strata_btc_types::BitcoinAmount; use strata_identifiers::L1BlockCommitment; diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index a6e70145..77967e93 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -156,7 +156,7 @@ impl Subprotocol for BridgeV1Subproto { ); state.apply_operator_set_update(add_members, remove_members); } - BridgeIncomingMsg::Defcon1(_) => { + BridgeIncomingMsg::Defcon1(_) | BridgeIncomingMsg::Defcon3(_) => { info!( "Activating safe harbour address on Defcon1 signal from admin subprotocol" ); From d62a0df47f9aa5e47fc4b474a8d58b3b348b8c77 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 19 May 2026 07:04:54 +0545 Subject: [PATCH 08/13] feat: add ability to update the safe harbour address --- Cargo.lock | 1 + crates/subprotocols/bridge-v1/msgs/Cargo.toml | 1 + crates/subprotocols/bridge-v1/msgs/src/lib.rs | 9 +++++++-- crates/subprotocols/bridge-v1/subprotocol/Cargo.toml | 1 + .../bridge-v1/subprotocol/src/state/bridge.rs | 6 ++++++ .../bridge-v1/subprotocol/src/subprotocol.rs | 6 ++++++ guest-builder/sp1/guest-asm/Cargo.lock | 2 ++ 7 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fbf2419..7e2111a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7869,6 +7869,7 @@ dependencies = [ name = "strata-asm-proto-bridge-v1-msgs" version = "0.1.0" dependencies = [ + "bitcoin-bosd 0.10.0", "ssz", "ssz_derive", "strata-asm-common", diff --git a/crates/subprotocols/bridge-v1/msgs/Cargo.toml b/crates/subprotocols/bridge-v1/msgs/Cargo.toml index 8fc9e68e..0de3cd06 100644 --- a/crates/subprotocols/bridge-v1/msgs/Cargo.toml +++ b/crates/subprotocols/bridge-v1/msgs/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" workspace = true [dependencies] +bitcoin-bosd.workspace = true ssz.workspace = true ssz_derive.workspace = true diff --git a/crates/subprotocols/bridge-v1/msgs/src/lib.rs b/crates/subprotocols/bridge-v1/msgs/src/lib.rs index b5cb6307..ca169085 100644 --- a/crates/subprotocols/bridge-v1/msgs/src/lib.rs +++ b/crates/subprotocols/bridge-v1/msgs/src/lib.rs @@ -6,6 +6,7 @@ use std::any::Any; +use bitcoin_bosd::Descriptor; use ssz_derive::{Decode, Encode}; use strata_asm_common::{InterprotoMsg, SubprotocolId}; use strata_asm_proto_bridge_v1_txs::BRIDGE_V1_SUBPROTOCOL_ID; @@ -27,11 +28,15 @@ pub enum BridgeIncomingMsg { /// Adds new operators by public key and removes existing operators by index. UpdateOperatorSet(UpdateOperatorSetPayload), - /// Defcon1 signal raised by the admin subprotocol. The bridge responds by + /// Emitted by the admin subprotocol to update the safe harbour destination + /// descriptor. + UpdateSafeHarbourAddress(Descriptor), + + /// Defcon1 signal raised by the admin subprotocol. The bridge must respond by /// activating the safe harbour. Defcon1(Defcon1Payload), - /// Defcon3 signal raised by the admin subprotocol. The bridge responds by + /// Defcon3 signal raised by the admin subprotocol. The bridge must respond by /// activating the safe harbour. Defcon3(Defcon3Payload), } diff --git a/crates/subprotocols/bridge-v1/subprotocol/Cargo.toml b/crates/subprotocols/bridge-v1/subprotocol/Cargo.toml index 5719d308..22b01389 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/Cargo.toml +++ b/crates/subprotocols/bridge-v1/subprotocol/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] arbitrary.workspace = true bitcoin.workspace = true +bitcoin-bosd.workspace = true rand_chacha.workspace = true serde.workspace = true ssz.workspace = true diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs index fe1b6063..5f569c5b 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/state/bridge.rs @@ -1,3 +1,4 @@ +use bitcoin_bosd::Descriptor; use ssz_derive::{Decode, Encode}; use strata_asm_params::BridgeV1InitConfig; use strata_asm_proto_bridge_v1_txs::{deposit::DepositInfo, errors::Mismatch}; @@ -108,6 +109,11 @@ impl BridgeV1State { self.safe_harbour.set_activated(activated); } + /// Sets the safe harbour activation flag. + pub fn update_safe_harbour_address(&mut self, new_address: Descriptor) { + self.safe_harbour.update_address(new_address); + } + /// Processes a deposit transaction by validating and adding it to the deposits table. /// /// This function takes already parsed deposit transaction information, validates it against the diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index 77967e93..428f41ea 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -156,6 +156,12 @@ impl Subprotocol for BridgeV1Subproto { ); state.apply_operator_set_update(add_members, remove_members); } + + BridgeIncomingMsg::UpdateSafeHarbourAddress(descriptor) => { + info!("Updating the safe harbour address from admin subprotocol"); + state.update_safe_harbour_address(descriptor.clone()); + } + BridgeIncomingMsg::Defcon1(_) | BridgeIncomingMsg::Defcon3(_) => { info!( "Activating safe harbour address on Defcon1 signal from admin subprotocol" diff --git a/guest-builder/sp1/guest-asm/Cargo.lock b/guest-builder/sp1/guest-asm/Cargo.lock index 4b9c5796..842af033 100644 --- a/guest-builder/sp1/guest-asm/Cargo.lock +++ b/guest-builder/sp1/guest-asm/Cargo.lock @@ -2209,6 +2209,7 @@ version = "0.1.0" dependencies = [ "arbitrary", "bitcoin", + "bitcoin-bosd 0.10.0", "rand_chacha 0.9.0", "serde", "ssz", @@ -2231,6 +2232,7 @@ dependencies = [ name = "strata-asm-proto-bridge-v1-msgs" version = "0.1.0" dependencies = [ + "bitcoin-bosd 0.10.0", "ssz", "ssz_derive", "strata-asm-common", From 2697b63fa6afd70fbf109f0655aa348484e5dad2 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 19 May 2026 07:10:08 +0545 Subject: [PATCH 09/13] feat(rpc): expose getSafeHarbour endpoint Lets clients fetch the safe harbour address from the bridge state at a given Bitcoin block hash, completing the read path for the new state field added earlier in this branch. --- Cargo.lock | 2 ++ bin/asm-runner/Cargo.toml | 1 + bin/asm-runner/src/rpc_server.rs | 8 ++++++++ crates/rpc/Cargo.toml | 1 + crates/rpc/src/traits.rs | 5 +++++ 5 files changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 7e2111a3..2f78d7fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8041,6 +8041,7 @@ dependencies = [ "jsonrpsee", "strata-asm-proof-types", "strata-asm-proto-bridge-v1", + "strata-asm-proto-bridge-v1-types", "strata-asm-proto-checkpoint-types", "strata-asm-worker", ] @@ -8080,6 +8081,7 @@ dependencies = [ "strata-asm-proof-types", "strata-asm-proto-bridge-v1", "strata-asm-proto-bridge-v1-txs", + "strata-asm-proto-bridge-v1-types", "strata-asm-proto-checkpoint", "strata-asm-proto-checkpoint-txs", "strata-asm-proto-checkpoint-types", diff --git a/bin/asm-runner/Cargo.toml b/bin/asm-runner/Cargo.toml index cb6c8a3b..5e961ddf 100644 --- a/bin/asm-runner/Cargo.toml +++ b/bin/asm-runner/Cargo.toml @@ -22,6 +22,7 @@ strata-asm-proof-impl.workspace = true strata-asm-proof-types.workspace = true strata-asm-proto-bridge-v1.workspace = true strata-asm-proto-bridge-v1-txs.workspace = true +strata-asm-proto-bridge-v1-types.workspace = true strata-asm-proto-checkpoint.workspace = true strata-asm-proto-checkpoint-txs.workspace = true strata-asm-proto-checkpoint-types.workspace = true diff --git a/bin/asm-runner/src/rpc_server.rs b/bin/asm-runner/src/rpc_server.rs index 58760f12..7081b1b0 100644 --- a/bin/asm-runner/src/rpc_server.rs +++ b/bin/asm-runner/src/rpc_server.rs @@ -17,6 +17,7 @@ use strata_asm_proof_db::{ProofDb, SledMohoStateDb, SledProofDb}; use strata_asm_proof_types::{AsmProof, L1Range, MohoProof}; use strata_asm_proto_bridge_v1::{AssignmentEntry, BridgeV1State, DepositEntry}; use strata_asm_proto_bridge_v1_txs::BRIDGE_V1_SUBPROTOCOL_ID; +use strata_asm_proto_bridge_v1_types::SafeHarbour; use strata_asm_proto_checkpoint::CheckpointState; use strata_asm_proto_checkpoint_txs::CHECKPOINT_SUBPROTOCOL_ID; use strata_asm_proto_checkpoint_types::CheckpointTip; @@ -138,6 +139,13 @@ impl AsmStateApiServer for AsmRpcServer { } } + async fn get_safe_harbour(&self, block_hash: BlockHash) -> RpcResult> { + match self.get_bridge_state(block_hash).await? { + Some(bridge_state) => Ok(Some(bridge_state.safe_harbour().clone())), + None => Ok(None), + } + } + async fn get_checkpoint_tip(&self, block_hash: BlockHash) -> RpcResult> { match self.get_checkpoint_state(block_hash).await? { Some(checkpoint_state) => Ok(Some(*checkpoint_state.verified_tip())), diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 46ef63c7..50152021 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] strata-asm-proof-types.workspace = true strata-asm-proto-bridge-v1.workspace = true +strata-asm-proto-bridge-v1-types.workspace = true strata-asm-proto-checkpoint-types.workspace = true strata-asm-worker.workspace = true diff --git a/crates/rpc/src/traits.rs b/crates/rpc/src/traits.rs index 636b992c..86ee654a 100644 --- a/crates/rpc/src/traits.rs +++ b/crates/rpc/src/traits.rs @@ -4,6 +4,7 @@ use bitcoin::BlockHash; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use strata_asm_proof_types::{AsmProof, MohoProof}; use strata_asm_proto_bridge_v1::{AssignmentEntry, DepositEntry}; +use strata_asm_proto_bridge_v1_types::SafeHarbour; use strata_asm_proto_checkpoint_types::CheckpointTip; use strata_asm_worker::{AsmState, AsmWorkerStatus}; @@ -34,6 +35,10 @@ pub trait AsmStateApi { #[method(name = "getDeposits")] async fn get_deposits(&self, block_hash: BlockHash) -> RpcResult>; + /// Return the safe harbour address for the provided Bitcoin block hash. + #[method(name = "getSafeHarbour")] + async fn get_safe_harbour(&self, block_hash: BlockHash) -> RpcResult>; + /// Return the verified checkpoint tip for the provided Bitcoin block hash. #[method(name = "getCheckpointTip")] async fn get_checkpoint_tip(&self, block_hash: BlockHash) -> RpcResult>; From 421a26ef66634d8ba1752384fe6152ce16b0d078 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Tue, 19 May 2026 07:34:00 +0545 Subject: [PATCH 10/13] test(fn): emit safe_harbour_address in generated ASM params The bridge subprotocol init config now requires a `safe_harbour_address` descriptor. Without it the runner panics on params load and every functional test fails the ASM-ready probe. --- functional-tests/factory/common/asm_params.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/functional-tests/factory/common/asm_params.py b/functional-tests/factory/common/asm_params.py index 44cb9790..3a19147c 100644 --- a/functional-tests/factory/common/asm_params.py +++ b/functional-tests/factory/common/asm_params.py @@ -7,6 +7,10 @@ from constants import ASM_MAGIC_BYTES +# BOSD-encoded P2TR descriptor used as the default safe harbour address in +# tests. Address `bc1ppuxgmd6n4j73wdp688p08a8rte97dkn5n70r2ym6kgsw0v3c5ensrytduf`. +DEFAULT_SAFE_HARBOUR_ADDRESS = "040f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667" + @dataclass class Block: @@ -64,6 +68,7 @@ class BridgeSubprotocol: assignment_duration: int operator_fee: int recovery_delay: int + safe_harbour_address: str @dataclass @@ -110,6 +115,7 @@ def build_subprotocols( assignment_duration: int = 100_000, operator_fee: int = 100_000_000, recovery_delay: int = 1_008, + safe_harbour_address: str = DEFAULT_SAFE_HARBOUR_ADDRESS, ) -> list[dict[str, Any]]: compressed_keys = [f"02{key}" for key in musig2_keys] confirmation_depth = 144 @@ -154,6 +160,7 @@ def build_subprotocols( assignment_duration=assignment_duration, operator_fee=operator_fee, recovery_delay=recovery_delay, + safe_harbour_address=safe_harbour_address, ) ) } @@ -171,6 +178,7 @@ def build_asm_params( assignment_duration: int = 10_000, operator_fee: int = 100_000_000, recovery_delay: int = 1_008, + safe_harbour_address: str = DEFAULT_SAFE_HARBOUR_ADDRESS, ) -> AsmParams: anchor = build_l1_anchor(genesis_height, block_hash, header) subprotocols = build_subprotocols( @@ -180,6 +188,7 @@ def build_asm_params( assignment_duration=assignment_duration, operator_fee=operator_fee, recovery_delay=recovery_delay, + safe_harbour_address=safe_harbour_address, ) return AsmParams( magic=magic, From 6dcf675a9574d759d1bd2193019fde42de0fec8c Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Wed, 20 May 2026 15:37:10 +0545 Subject: [PATCH 11/13] test(bridge-v1-types): cover SafeHarbour activation and serde The safe harbour ships as consensus state and is surfaced by the `getSafeHarbour` RPC, so its activation semantics and JSON shape are load-bearing for both the protocol and clients. Add unit coverage for the initial deactivated state, the activation flag, address updates preserving activation, and SSZ + JSON serde round-trips. --- Cargo.lock | 1 + .../subprotocols/bridge-v1/types/Cargo.toml | 1 + .../bridge-v1/types/src/safe_harbour.rs | 74 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2f78d7fd..71a85ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7908,6 +7908,7 @@ dependencies = [ "bitvec", "borsh", "serde", + "serde_json", "ssz", "ssz_derive", "strata-btc-types", diff --git a/crates/subprotocols/bridge-v1/types/Cargo.toml b/crates/subprotocols/bridge-v1/types/Cargo.toml index bcd2558b..805c2878 100644 --- a/crates/subprotocols/bridge-v1/types/Cargo.toml +++ b/crates/subprotocols/bridge-v1/types/Cargo.toml @@ -20,4 +20,5 @@ serde.workspace = true thiserror.workspace = true [dev-dependencies] +serde_json.workspace = true strata-test-utils-arb.workspace = true diff --git a/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs b/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs index 13745e25..c0c9b249 100644 --- a/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs +++ b/crates/subprotocols/bridge-v1/types/src/safe_harbour.rs @@ -50,3 +50,77 @@ impl SafeHarbour { self.address = address } } + +#[cfg(test)] +mod tests { + use ssz::{Decode, Encode}; + + use super::*; + + fn descriptor_a() -> Descriptor { + Descriptor::new_p2wpkh(&[0xAA; 20]) + } + + fn descriptor_b() -> Descriptor { + Descriptor::new_p2wpkh(&[0xBB; 20]) + } + + #[test] + fn new_is_deactivated() { + let sh = SafeHarbour::new(descriptor_a()); + assert!(!sh.is_activated()); + assert_eq!(sh.address(), &descriptor_a()); + assert_eq!(sh.active_address(), None); + } + + #[test] + fn set_activated_toggles_flag_and_active_address() { + let mut sh = SafeHarbour::new(descriptor_a()); + + sh.set_activated(true); + assert!(sh.is_activated()); + assert_eq!(sh.active_address(), Some(&descriptor_a())); + + sh.set_activated(false); + assert!(!sh.is_activated()); + assert_eq!(sh.active_address(), None); + } + + #[test] + fn update_address_preserves_activation_flag() { + let mut sh = SafeHarbour::new(descriptor_a()); + sh.update_address(descriptor_b()); + assert_eq!(sh.address(), &descriptor_b()); + assert!(!sh.is_activated()); + + sh.set_activated(true); + sh.update_address(descriptor_a()); + assert_eq!(sh.address(), &descriptor_a()); + // Updating the address must not deactivate an already-active safe harbour. + assert!(sh.is_activated()); + assert_eq!(sh.active_address(), Some(&descriptor_a())); + } + + #[test] + fn ssz_roundtrip() { + let mut sh = SafeHarbour::new(descriptor_a()); + sh.set_activated(true); + let bytes = sh.as_ssz_bytes(); + let decoded = SafeHarbour::from_ssz_bytes(&bytes).expect("ssz decode"); + assert_eq!(sh, decoded); + } + + /// The RPC `getSafeHarbour` endpoint returns `SafeHarbour` as JSON, so the + /// serde representation must round-trip and stay in sync with what clients + /// consume. + #[test] + fn json_serde_roundtrip() { + let mut sh = SafeHarbour::new(descriptor_a()); + sh.set_activated(true); + let json = serde_json::to_string(&sh).expect("serialize"); + let decoded: SafeHarbour = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(sh, decoded); + assert!(json.contains("\"activated\":true")); + assert!(json.contains("\"address\"")); + } +} From cefe0a39d3e9eac5dc7e4bf7490b1722633d5ed8 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Wed, 20 May 2026 15:37:25 +0545 Subject: [PATCH 12/13] test(bridge-v1): cover safe harbour handling in process_msgs `process_msgs` is the only entry point through which the admin subprotocol can mutate the safe harbour, so its behaviour for each inter-protocol message variant is consensus-critical. Pin down that `UpdateSafeHarbourAddress` swaps the address without activating, that both Defcon1 and Defcon3 flip the activation flag, and that an admin address update after activation keeps the new address active. --- .../bridge-v1/subprotocol/src/subprotocol.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs index 428f41ea..eb718c99 100644 --- a/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs +++ b/crates/subprotocols/bridge-v1/subprotocol/src/subprotocol.rs @@ -172,3 +172,93 @@ impl Subprotocol for BridgeV1Subproto { } } } + +#[cfg(test)] +mod tests { + use bitcoin_bosd::Descriptor; + use strata_asm_common::Subprotocol; + use strata_asm_proto_bridge_v1_msgs::{BridgeIncomingMsg, Defcon1Payload, Defcon3Payload}; + use strata_identifiers::L1BlockCommitment; + use strata_test_utils_arb::ArbitraryGenerator; + + use super::BridgeV1Subproto; + use crate::test_utils::create_test_state; + + fn descriptor_a() -> Descriptor { + Descriptor::new_p2wpkh(&[0xAA; 20]) + } + + fn descriptor_b() -> Descriptor { + Descriptor::new_p2wpkh(&[0xBB; 20]) + } + + /// The safe harbour must start deactivated so it has no effect until the + /// admin subprotocol explicitly triggers a defcon signal. + #[test] + fn safe_harbour_starts_deactivated() { + let (state, _privkeys) = create_test_state(); + assert!(!state.safe_harbour().is_activated()); + assert_eq!(state.safe_harbour().active_address(), None); + } + + #[test] + fn process_msgs_update_safe_harbour_address() { + let (mut state, _privkeys) = create_test_state(); + let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); + + let new_address = descriptor_a(); + let msgs = vec![BridgeIncomingMsg::UpdateSafeHarbourAddress( + new_address.clone(), + )]; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + + assert_eq!(state.safe_harbour().address(), &new_address); + // Address updates alone must not activate the safe harbour. + assert!(!state.safe_harbour().is_activated()); + } + + #[test] + fn process_msgs_defcon1_activates_safe_harbour() { + let (mut state, _privkeys) = create_test_state(); + let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); + + let msgs = vec![BridgeIncomingMsg::Defcon1(Defcon1Payload::default())]; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + + assert!(state.safe_harbour().is_activated()); + assert_eq!( + state.safe_harbour().active_address(), + Some(state.safe_harbour().address()) + ); + } + + #[test] + fn process_msgs_defcon3_activates_safe_harbour() { + let (mut state, _privkeys) = create_test_state(); + let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); + + let msgs = vec![BridgeIncomingMsg::Defcon3(Defcon3Payload::default())]; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + + assert!(state.safe_harbour().is_activated()); + } + + /// Updating the address after activation must keep the new address active — + /// the strata administrator should be able to redirect an already-activated + /// safe harbour without re-issuing a defcon signal. + #[test] + fn process_msgs_update_after_activation_keeps_activated() { + let (mut state, _privkeys) = create_test_state(); + let l1ref: L1BlockCommitment = ArbitraryGenerator::new().generate(); + + let msgs = vec![ + BridgeIncomingMsg::Defcon1(Defcon1Payload::default()), + BridgeIncomingMsg::UpdateSafeHarbourAddress(descriptor_b()), + ]; + BridgeV1Subproto::process_msgs(&mut state, &msgs, &l1ref); + + assert!(state.safe_harbour().is_activated()); + assert_eq!(state.safe_harbour().address(), &descriptor_b()); + assert_eq!(state.safe_harbour().active_address(), Some(&descriptor_b())); + } +} From c9ab88066818caedfaa8808992ea90b124110d66 Mon Sep 17 00:00:00 2001 From: Prajwol Gyawali Date: Wed, 20 May 2026 15:37:33 +0545 Subject: [PATCH 13/13] test(fn): exercise getSafeHarbour RPC Cover the new `strata_asm_getSafeHarbour` endpoint end-to-end against a live runner: assert that without any admin defcon signal the configured address surfaces verbatim, that the payload is deactivated, and that the response is stable across processed blocks. The RPC is the only client-facing surface of the safe harbour, so its JSON shape is part of the contract clients will rely on. --- .../tests/fn_asm_safe_harbour_test.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 functional-tests/tests/fn_asm_safe_harbour_test.py diff --git a/functional-tests/tests/fn_asm_safe_harbour_test.py b/functional-tests/tests/fn_asm_safe_harbour_test.py new file mode 100644 index 00000000..938dbf55 --- /dev/null +++ b/functional-tests/tests/fn_asm_safe_harbour_test.py @@ -0,0 +1,75 @@ +import logging + +import flexitest + +from factory.common.asm_params import DEFAULT_SAFE_HARBOUR_ADDRESS +from utils.utils import ( + wait_until_asm_reaches_height, + wait_until_asm_ready, + wait_until_bitcoind_ready, +) + + +@flexitest.register +class AsmSafeHarbourTest(flexitest.Test): + """Verify `strata_asm_getSafeHarbour` returns the configured address + in its initial deactivated state. + + The bridge subprotocol is initialised from `asm_params.json` with + `DEFAULT_SAFE_HARBOUR_ADDRESS` and `activated=false`. Without any + admin defcon signal, every processed block must surface that exact + pair via the RPC. + """ + + def __init__(self, ctx: flexitest.InitContext): + ctx.set_env("basic") + + def main(self, ctx: flexitest.RunContext): + bitcoind_service = ctx.get_service("bitcoin") + asm_service = ctx.get_service("asm_rpc") + + bitcoin_rpc = bitcoind_service.create_rpc() + asm_rpc = asm_service.create_rpc() + + wait_until_bitcoind_ready(bitcoin_rpc, timeout=30) + wait_until_asm_ready(asm_rpc) + + initial_btc_height = bitcoin_rpc.proxy.getblockcount() + wallet_addr = bitcoin_rpc.proxy.getnewaddress() + num_blocks = 3 + bitcoin_rpc.proxy.generatetoaddress(num_blocks, wallet_addr) + + target_height = initial_btc_height + num_blocks + asm_height = wait_until_asm_reaches_height(asm_rpc, min_height=target_height) + logging.info("ASM progressed to height %s", asm_height) + + # Tip and an earlier processed block must both return the same payload — + # the safe harbour is consensus state, so it must be consistent across history + # while no admin message has touched it. + heights = (initial_btc_height + 1, target_height) + previous = None + for height in heights: + block_hash = bitcoin_rpc.proxy.getblockhash(height) + result = asm_rpc.strata_asm_getSafeHarbour(block_hash) + assert result is not None, ( + f"strata_asm_getSafeHarbour returned None for processed block at height {height}" + ) + assert set(result.keys()) >= {"address", "activated"}, ( + f"unexpected safe harbour payload at height {height}: {result!r}" + ) + normalized = result["address"].lower().removeprefix("0x") + assert normalized == DEFAULT_SAFE_HARBOUR_ADDRESS, ( + f"expected configured safe harbour address {DEFAULT_SAFE_HARBOUR_ADDRESS}, " + f"got {result['address']}" + ) + assert result["activated"] is False, ( + f"safe harbour should start deactivated, got activated={result['activated']!r}" + ) + if previous is not None: + assert result == previous, ( + "safe harbour should be identical across processed blocks: " + f"{previous} vs {result}" + ) + previous = result + + return True