Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/msgs/bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ ssz_derive.workspace = true
strata-asm-common.workspace = true
strata-asm-txs-bridge-v1.workspace = true
strata-bridge-types.workspace = true
strata-crypto.workspace = true
94 changes: 22 additions & 72 deletions crates/msgs/bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,94 +6,44 @@

use std::any::Any;

use ssz::{Decode as SszDecode, DecodeError, Encode as SszEncode};
use ssz_derive::{Decode, Encode};
use strata_asm_common::{InterprotoMsg, SubprotocolId};
use strata_asm_txs_bridge_v1::BRIDGE_V1_SUBPROTOCOL_ID;
use strata_bridge_types::{OperatorSelection, WithdrawOutput};
use strata_bridge_types::{OperatorIdx, OperatorSelection, WithdrawOutput};
use strata_crypto::EvenPublicKey;

/// Incoming message types received from other subprotocols.
///
/// This enum represents all possible message types that the bridge subprotocol can
/// receive from other subprotocols in the ASM.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)]
#[ssz(enum_behaviour = "union")]
pub enum BridgeIncomingMsg {
/// Emitted after a checkpoint proof has been validated. Contains the withdrawal command
/// specifying the destination descriptor and amount to be withdrawn.
DispatchWithdrawal {
/// The withdrawal output (destination + amount).
output: WithdrawOutput,
/// User's operator selection for withdrawal assignment.
selected_operator: OperatorSelection,
},
}
DispatchWithdrawal(DispatchWithdrawalPayload),

#[derive(Debug, Encode, Decode)]
struct DispatchWithdrawalPayload {
output: WithdrawOutput,
selected_operator: OperatorSelection,
/// 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),
}

impl SszEncode for BridgeIncomingMsg {
fn is_ssz_fixed_len() -> bool {
false
}

fn ssz_append(&self, buf: &mut Vec<u8>) {
match self {
Self::DispatchWithdrawal {
output,
selected_operator,
} => {
0_u8.ssz_append(buf);
DispatchWithdrawalPayload {
output: output.clone(),
selected_operator: *selected_operator,
}
.ssz_append(buf);
}
}
}

fn ssz_bytes_len(&self) -> usize {
match self {
Self::DispatchWithdrawal {
output,
selected_operator,
} => {
1 + DispatchWithdrawalPayload {
output: output.clone(),
selected_operator: *selected_operator,
}
.ssz_bytes_len()
}
}
}
/// Payload for [`BridgeIncomingMsg::DispatchWithdrawal`].
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)]
pub struct DispatchWithdrawalPayload {
/// The withdrawal output (destination + amount).
pub output: WithdrawOutput,
/// User's operator selection for withdrawal assignment.
pub selected_operator: OperatorSelection,
}

impl SszDecode for BridgeIncomingMsg {
fn is_ssz_fixed_len() -> bool {
false
}

fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
let (tag_bytes, payload_bytes) = bytes.split_first().ok_or_else(|| {
DecodeError::BytesInvalid("missing bridge message variant tag".into())
})?;

match *tag_bytes {
0 => {
let payload = DispatchWithdrawalPayload::from_ssz_bytes(payload_bytes)?;
Ok(Self::DispatchWithdrawal {
output: payload.output,
selected_operator: payload.selected_operator,
})
}
tag => Err(DecodeError::BytesInvalid(format!(
"unknown bridge message variant tag {tag}"
))),
}
}
/// Payload for [`BridgeIncomingMsg::UpdateOperatorSet`].
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)]
pub struct UpdateOperatorSetPayload {
/// Operator public keys to add to the bridge multisig.
pub add_members: Vec<EvenPublicKey>,
/// Operator indices to remove from the bridge multisig.
pub remove_members: Vec<OperatorIdx>,
}

impl InterprotoMsg for BridgeIncomingMsg {
Expand Down
1 change: 1 addition & 0 deletions crates/subprotocols/admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2024"
workspace = true

[dependencies]
strata-asm-bridge-msgs.workspace = true
strata-asm-checkpoint-msgs.workspace = true
strata-asm-common.workspace = true
strata-asm-params.workspace = true
Expand Down
22 changes: 20 additions & 2 deletions crates/subprotocols/admin/src/handler.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use strata_asm_bridge_msgs::{BridgeIncomingMsg, UpdateOperatorSetPayload};
use strata_asm_checkpoint_msgs::CheckpointIncomingMsg;
use strata_asm_common::{
MsgRelayer,
Expand Down Expand Up @@ -67,8 +68,13 @@ pub(crate) fn handle_pending_updates(
}
}
}
UpdateAction::OperatorSet(_update) => {
// TODO(STR-1721): Set an InterProtoMsg to the Bridge Subprotocol
UpdateAction::OperatorSet(update) => {
let (add_members, remove_members) = update.into_inner();
relay_bridge_operator_set_update(relayer, add_members, remove_members);
info!(
update_id = update_id,
"Forwarded operator set update to bridge subprotocol",
);
}
UpdateAction::Sequencer(update) => {
let new_key = update.into_inner();
Expand Down Expand Up @@ -175,6 +181,18 @@ fn relay_checkpoint_predicate(relayer: &mut impl MsgRelayer, key: PredicateKey)
relayer.relay_msg(&msg);
}

fn relay_bridge_operator_set_update(
relayer: &mut impl MsgRelayer,
add_members: Vec<strata_crypto::EvenPublicKey>,
remove_members: Vec<u32>,
) {
let msg = BridgeIncomingMsg::UpdateOperatorSet(UpdateOperatorSetPayload {
add_members,
remove_members,
});
relayer.relay_msg(&msg);
}

#[cfg(test)]
mod tests {
use std::{any::Any, num::NonZero};
Expand Down
10 changes: 10 additions & 0 deletions crates/subprotocols/bridge-v1/src/state/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ impl BridgeV1State {
self.assignments.remove_assignment(deposit_idx)
}

/// Applies an operator set update by adding new operators and removing existing ones.
pub fn apply_operator_set_update(
&mut self,
add_members: &[strata_crypto::EvenPublicKey],
remove_members: &[OperatorIdx],
) {
self.operators
.apply_membership_changes(add_members, remove_members);
}

/// Removes an operator from the active multisig by deactivating them.
///
/// # Panics
Expand Down
23 changes: 16 additions & 7 deletions crates/subprotocols/bridge-v1/src/subprotocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,27 @@ impl Subprotocol for BridgeV1Subproto {
fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], l1ref: &L1BlockCommitment) {
for msg in msgs {
match msg {
BridgeIncomingMsg::DispatchWithdrawal {
output,
selected_operator,
} => {
if let Err(e) =
state.create_withdrawal_assignment(output, *selected_operator, l1ref)
{
BridgeIncomingMsg::DispatchWithdrawal(payload) => {
if let Err(e) = state.create_withdrawal_assignment(
&payload.output,
payload.selected_operator,
l1ref,
) {
// PANIC: Withdrawal assignment failure indicates catastrophic system
// compromise.
panic!("Failed to create withdrawal assignment: {e}",);
}
}
BridgeIncomingMsg::UpdateOperatorSet(payload) => {
let add_members = &payload.add_members;
let remove_members = &payload.remove_members;
info!(
added = add_members.len(),
removed = remove_members.len(),
"Applying operator set update from admin subprotocol",
);
state.apply_operator_set_update(add_members, remove_members);
}
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions crates/subprotocols/checkpoint/src/handler.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use strata_asm_bridge_msgs::BridgeIncomingMsg;
use strata_asm_bridge_msgs::{BridgeIncomingMsg, DispatchWithdrawalPayload};
use strata_asm_common::{AsmLogEntry, MsgRelayer, TxInputRef, VerifiedAuxData, logging};
use strata_asm_logs::CheckpointTipUpdate;
use strata_asm_txs_checkpoint::extract_checkpoint_from_envelope;
Expand Down Expand Up @@ -61,10 +61,10 @@ pub(crate) fn handle_checkpoint_tx(
relayer.emit_log(log_entry);

for (output, selected_operator) in withdrawal_intents {
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal {
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal(DispatchWithdrawalPayload {
output,
selected_operator,
};
});
relayer.relay_msg(&bridge_msg);
}
}
Expand Down
6 changes: 3 additions & 3 deletions crates/subprotocols/debug-v1/src/subprotocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! with the Strata Anchor State Machine (ASM).

use ssz_derive::{Decode, Encode};
use strata_asm_bridge_msgs::BridgeIncomingMsg;
use strata_asm_bridge_msgs::{BridgeIncomingMsg, DispatchWithdrawalPayload};
use strata_asm_common::{
AsmError, AsmLogEntry, MsgRelayer, NullMsg, Subprotocol, SubprotocolId, TxInputRef,
VerifiedAuxData, logging,
Expand Down Expand Up @@ -95,10 +95,10 @@ fn process_parsed_debug_tx(
ParsedDebugTx::MockWithdrawIntent((output, selected_operator)) => {
logging::info!(amount = output.amt.to_sat(), "Processing mock withdrawal");

let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal {
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal(DispatchWithdrawalPayload {
output,
selected_operator,
};
});
relayer.relay_msg(&bridge_msg);

logging::info!("Successfully sent mock withdrawal intent to bridge");
Expand Down
32 changes: 16 additions & 16 deletions crates/txs/admin/src/actions/updates/operator.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
use arbitrary::Arbitrary;
use ssz_derive::{Decode, Encode};
use strata_identifiers::Buf32;
use strata_crypto::EvenPublicKey;

use crate::{actions::Sighash, constants::AdminTxType};

/// An update to the Bridge Operator Set:
/// - removes the specified `remove_members`
/// - adds the specified `add_members`
/// - removes the specified `remove_members` (by operator index)
/// - adds the specified `add_members` (by public key)
#[derive(Clone, Debug, Eq, PartialEq, Arbitrary, Encode, Decode)]
pub struct OperatorSetUpdate {
add_members: Vec<Buf32>,
remove_members: Vec<Buf32>,
add_members: Vec<EvenPublicKey>,
remove_members: Vec<u32>,
}

impl OperatorSetUpdate {
/// Creates a new `OperatorSetUpdate`.
pub fn new(add_members: Vec<Buf32>, remove_members: Vec<Buf32>) -> Self {
pub fn new(add_members: Vec<EvenPublicKey>, remove_members: Vec<u32>) -> Self {
Self {
add_members,
remove_members,
}
}

/// Borrow the list of operator public keys to add.
pub fn add_members(&self) -> &[Buf32] {
pub fn add_members(&self) -> &[EvenPublicKey] {
&self.add_members
}

/// Borrow the list of operator public keys to remove.
pub fn remove_members(&self) -> &[Buf32] {
/// Borrow the list of operator indices to remove.
pub fn remove_members(&self) -> &[u32] {
&self.remove_members
}

/// Consume and return the inner vectors `(add_members, remove_members)`.
pub fn into_inner(self) -> (Vec<Buf32>, Vec<Buf32>) {
pub fn into_inner(self) -> (Vec<EvenPublicKey>, Vec<u32>) {
(self.add_members, self.remove_members)
}
}
Expand All @@ -44,18 +44,18 @@ impl Sighash for OperatorSetUpdate {
}

/// Returns `len(add) ‖ add[0] ‖ … ‖ add[n] ‖ len(rem) ‖ rem[0] ‖ … ‖ rem[m]`
/// where lengths are encoded as big-endian `u32`.
/// where lengths are encoded as big-endian `u32`, add members as 32-byte x-only keys,
/// and remove members as 4-byte big-endian operator indices.
fn sighash_payload(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(
4 + self.add_members.len() * 32 + 4 + self.remove_members.len() * 32,
);
let mut buf =
Vec::with_capacity(4 + self.add_members.len() * 32 + 4 + self.remove_members.len() * 4);
buf.extend_from_slice(&(self.add_members.len() as u32).to_be_bytes());
for member in &self.add_members {
buf.extend_from_slice(&member.0);
buf.extend_from_slice(&member.x_only_public_key().0.serialize());
}
buf.extend_from_slice(&(self.remove_members.len() as u32).to_be_bytes());
for member in &self.remove_members {
buf.extend_from_slice(&member.0);
buf.extend_from_slice(&member.to_be_bytes());
}
buf
}
Expand Down
2 changes: 2 additions & 0 deletions guest-builder/sp1/guest-asm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ path = "asm/admin_to_checkpoint.rs"
name = "asm_checkpoint"
path = "asm/checkpoint.rs"

[[test]]
name = "asm_admin_to_bridge"
path = "asm/admin_to_bridge.rs"

[[test]]
name = "asm_bridge_to_checkpoint"
path = "asm/bridge_to_checkpoint.rs"
Loading
Loading