Skip to content

Commit ac8bc94

Browse files
authored
feat: relay operator set updates from admin to bridge subprotocol (#61)
* refactor: update OperatorSetUpdate payload with stricter types * feat: relay operator set update msg to bridge subprotocol * test: add test that the admin logic propagates to the bridge * refactor: remove custom implementation of BridgeIncomingMsg
1 parent b71a4e3 commit ac8bc94

File tree

15 files changed

+254
-109
lines changed

15 files changed

+254
-109
lines changed

Cargo.lock

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

crates/msgs/bridge/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ ssz_derive.workspace = true
1313
strata-asm-common.workspace = true
1414
strata-asm-txs-bridge-v1.workspace = true
1515
strata-bridge-types.workspace = true
16+
strata-crypto.workspace = true

crates/msgs/bridge/src/lib.rs

Lines changed: 22 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,94 +6,44 @@
66
77
use std::any::Any;
88

9-
use ssz::{Decode as SszDecode, DecodeError, Encode as SszEncode};
109
use ssz_derive::{Decode, Encode};
1110
use strata_asm_common::{InterprotoMsg, SubprotocolId};
1211
use strata_asm_txs_bridge_v1::BRIDGE_V1_SUBPROTOCOL_ID;
13-
use strata_bridge_types::{OperatorSelection, WithdrawOutput};
12+
use strata_bridge_types::{OperatorIdx, OperatorSelection, WithdrawOutput};
13+
use strata_crypto::EvenPublicKey;
1414

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

31-
#[derive(Debug, Encode, Decode)]
32-
struct DispatchWithdrawalPayload {
33-
output: WithdrawOutput,
34-
selected_operator: OperatorSelection,
26+
/// Emitted by the admin subprotocol when the operator set is updated.
27+
/// Adds new operators by public key and removes existing operators by index.
28+
UpdateOperatorSet(UpdateOperatorSetPayload),
3529
}
3630

37-
impl SszEncode for BridgeIncomingMsg {
38-
fn is_ssz_fixed_len() -> bool {
39-
false
40-
}
41-
42-
fn ssz_append(&self, buf: &mut Vec<u8>) {
43-
match self {
44-
Self::DispatchWithdrawal {
45-
output,
46-
selected_operator,
47-
} => {
48-
0_u8.ssz_append(buf);
49-
DispatchWithdrawalPayload {
50-
output: output.clone(),
51-
selected_operator: *selected_operator,
52-
}
53-
.ssz_append(buf);
54-
}
55-
}
56-
}
57-
58-
fn ssz_bytes_len(&self) -> usize {
59-
match self {
60-
Self::DispatchWithdrawal {
61-
output,
62-
selected_operator,
63-
} => {
64-
1 + DispatchWithdrawalPayload {
65-
output: output.clone(),
66-
selected_operator: *selected_operator,
67-
}
68-
.ssz_bytes_len()
69-
}
70-
}
71-
}
31+
/// Payload for [`BridgeIncomingMsg::DispatchWithdrawal`].
32+
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)]
33+
pub struct DispatchWithdrawalPayload {
34+
/// The withdrawal output (destination + amount).
35+
pub output: WithdrawOutput,
36+
/// User's operator selection for withdrawal assignment.
37+
pub selected_operator: OperatorSelection,
7238
}
7339

74-
impl SszDecode for BridgeIncomingMsg {
75-
fn is_ssz_fixed_len() -> bool {
76-
false
77-
}
78-
79-
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
80-
let (tag_bytes, payload_bytes) = bytes.split_first().ok_or_else(|| {
81-
DecodeError::BytesInvalid("missing bridge message variant tag".into())
82-
})?;
83-
84-
match *tag_bytes {
85-
0 => {
86-
let payload = DispatchWithdrawalPayload::from_ssz_bytes(payload_bytes)?;
87-
Ok(Self::DispatchWithdrawal {
88-
output: payload.output,
89-
selected_operator: payload.selected_operator,
90-
})
91-
}
92-
tag => Err(DecodeError::BytesInvalid(format!(
93-
"unknown bridge message variant tag {tag}"
94-
))),
95-
}
96-
}
40+
/// Payload for [`BridgeIncomingMsg::UpdateOperatorSet`].
41+
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)]
42+
pub struct UpdateOperatorSetPayload {
43+
/// Operator public keys to add to the bridge multisig.
44+
pub add_members: Vec<EvenPublicKey>,
45+
/// Operator indices to remove from the bridge multisig.
46+
pub remove_members: Vec<OperatorIdx>,
9747
}
9848

9949
impl InterprotoMsg for BridgeIncomingMsg {

crates/subprotocols/admin/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ edition = "2024"
77
workspace = true
88

99
[dependencies]
10+
strata-asm-bridge-msgs.workspace = true
1011
strata-asm-checkpoint-msgs.workspace = true
1112
strata-asm-common.workspace = true
1213
strata-asm-params.workspace = true

crates/subprotocols/admin/src/handler.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use strata_asm_bridge_msgs::{BridgeIncomingMsg, UpdateOperatorSetPayload};
12
use strata_asm_checkpoint_msgs::CheckpointIncomingMsg;
23
use strata_asm_common::{
34
MsgRelayer,
@@ -67,8 +68,13 @@ pub(crate) fn handle_pending_updates(
6768
}
6869
}
6970
}
70-
UpdateAction::OperatorSet(_update) => {
71-
// TODO(STR-1721): Set an InterProtoMsg to the Bridge Subprotocol
71+
UpdateAction::OperatorSet(update) => {
72+
let (add_members, remove_members) = update.into_inner();
73+
relay_bridge_operator_set_update(relayer, add_members, remove_members);
74+
info!(
75+
update_id = update_id,
76+
"Forwarded operator set update to bridge subprotocol",
77+
);
7278
}
7379
UpdateAction::Sequencer(update) => {
7480
let new_key = update.into_inner();
@@ -175,6 +181,18 @@ fn relay_checkpoint_predicate(relayer: &mut impl MsgRelayer, key: PredicateKey)
175181
relayer.relay_msg(&msg);
176182
}
177183

184+
fn relay_bridge_operator_set_update(
185+
relayer: &mut impl MsgRelayer,
186+
add_members: Vec<strata_crypto::EvenPublicKey>,
187+
remove_members: Vec<u32>,
188+
) {
189+
let msg = BridgeIncomingMsg::UpdateOperatorSet(UpdateOperatorSetPayload {
190+
add_members,
191+
remove_members,
192+
});
193+
relayer.relay_msg(&msg);
194+
}
195+
178196
#[cfg(test)]
179197
mod tests {
180198
use std::{any::Any, num::NonZero};

crates/subprotocols/bridge-v1/src/state/bridge.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ impl BridgeV1State {
214214
self.assignments.remove_assignment(deposit_idx)
215215
}
216216

217+
/// Applies an operator set update by adding new operators and removing existing ones.
218+
pub fn apply_operator_set_update(
219+
&mut self,
220+
add_members: &[strata_crypto::EvenPublicKey],
221+
remove_members: &[OperatorIdx],
222+
) {
223+
self.operators
224+
.apply_membership_changes(add_members, remove_members);
225+
}
226+
217227
/// Removes an operator from the active multisig by deactivating them.
218228
///
219229
/// # Panics

crates/subprotocols/bridge-v1/src/subprotocol.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,27 @@ impl Subprotocol for BridgeV1Subproto {
139139
fn process_msgs(state: &mut Self::State, msgs: &[Self::Msg], l1ref: &L1BlockCommitment) {
140140
for msg in msgs {
141141
match msg {
142-
BridgeIncomingMsg::DispatchWithdrawal {
143-
output,
144-
selected_operator,
145-
} => {
146-
if let Err(e) =
147-
state.create_withdrawal_assignment(output, *selected_operator, l1ref)
148-
{
142+
BridgeIncomingMsg::DispatchWithdrawal(payload) => {
143+
if let Err(e) = state.create_withdrawal_assignment(
144+
&payload.output,
145+
payload.selected_operator,
146+
l1ref,
147+
) {
149148
// PANIC: Withdrawal assignment failure indicates catastrophic system
150149
// compromise.
151150
panic!("Failed to create withdrawal assignment: {e}",);
152151
}
153152
}
153+
BridgeIncomingMsg::UpdateOperatorSet(payload) => {
154+
let add_members = &payload.add_members;
155+
let remove_members = &payload.remove_members;
156+
info!(
157+
added = add_members.len(),
158+
removed = remove_members.len(),
159+
"Applying operator set update from admin subprotocol",
160+
);
161+
state.apply_operator_set_update(add_members, remove_members);
162+
}
154163
}
155164
}
156165
}

crates/subprotocols/checkpoint/src/handler.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use strata_asm_bridge_msgs::BridgeIncomingMsg;
1+
use strata_asm_bridge_msgs::{BridgeIncomingMsg, DispatchWithdrawalPayload};
22
use strata_asm_common::{AsmLogEntry, MsgRelayer, TxInputRef, VerifiedAuxData, logging};
33
use strata_asm_logs::CheckpointTipUpdate;
44
use strata_asm_txs_checkpoint::extract_checkpoint_from_envelope;
@@ -61,10 +61,10 @@ pub(crate) fn handle_checkpoint_tx(
6161
relayer.emit_log(log_entry);
6262

6363
for (output, selected_operator) in withdrawal_intents {
64-
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal {
64+
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal(DispatchWithdrawalPayload {
6565
output,
6666
selected_operator,
67-
};
67+
});
6868
relayer.relay_msg(&bridge_msg);
6969
}
7070
}

crates/subprotocols/debug-v1/src/subprotocol.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! with the Strata Anchor State Machine (ASM).
55
66
use ssz_derive::{Decode, Encode};
7-
use strata_asm_bridge_msgs::BridgeIncomingMsg;
7+
use strata_asm_bridge_msgs::{BridgeIncomingMsg, DispatchWithdrawalPayload};
88
use strata_asm_common::{
99
AsmError, AsmLogEntry, MsgRelayer, NullMsg, Subprotocol, SubprotocolId, TxInputRef,
1010
VerifiedAuxData, logging,
@@ -95,10 +95,10 @@ fn process_parsed_debug_tx(
9595
ParsedDebugTx::MockWithdrawIntent((output, selected_operator)) => {
9696
logging::info!(amount = output.amt.to_sat(), "Processing mock withdrawal");
9797

98-
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal {
98+
let bridge_msg = BridgeIncomingMsg::DispatchWithdrawal(DispatchWithdrawalPayload {
9999
output,
100100
selected_operator,
101-
};
101+
});
102102
relayer.relay_msg(&bridge_msg);
103103

104104
logging::info!("Successfully sent mock withdrawal intent to bridge");

crates/txs/admin/src/actions/updates/operator.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
use arbitrary::Arbitrary;
22
use ssz_derive::{Decode, Encode};
3-
use strata_identifiers::Buf32;
3+
use strata_crypto::EvenPublicKey;
44

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

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

1616
impl OperatorSetUpdate {
1717
/// Creates a new `OperatorSetUpdate`.
18-
pub fn new(add_members: Vec<Buf32>, remove_members: Vec<Buf32>) -> Self {
18+
pub fn new(add_members: Vec<EvenPublicKey>, remove_members: Vec<u32>) -> Self {
1919
Self {
2020
add_members,
2121
remove_members,
2222
}
2323
}
2424

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

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

3535
/// Consume and return the inner vectors `(add_members, remove_members)`.
36-
pub fn into_inner(self) -> (Vec<Buf32>, Vec<Buf32>) {
36+
pub fn into_inner(self) -> (Vec<EvenPublicKey>, Vec<u32>) {
3737
(self.add_members, self.remove_members)
3838
}
3939
}
@@ -44,18 +44,18 @@ impl Sighash for OperatorSetUpdate {
4444
}
4545

4646
/// Returns `len(add) ‖ add[0] ‖ … ‖ add[n] ‖ len(rem) ‖ rem[0] ‖ … ‖ rem[m]`
47-
/// where lengths are encoded as big-endian `u32`.
47+
/// where lengths are encoded as big-endian `u32`, add members as 32-byte x-only keys,
48+
/// and remove members as 4-byte big-endian operator indices.
4849
fn sighash_payload(&self) -> Vec<u8> {
49-
let mut buf = Vec::with_capacity(
50-
4 + self.add_members.len() * 32 + 4 + self.remove_members.len() * 32,
51-
);
50+
let mut buf =
51+
Vec::with_capacity(4 + self.add_members.len() * 32 + 4 + self.remove_members.len() * 4);
5252
buf.extend_from_slice(&(self.add_members.len() as u32).to_be_bytes());
5353
for member in &self.add_members {
54-
buf.extend_from_slice(&member.0);
54+
buf.extend_from_slice(&member.x_only_public_key().0.serialize());
5555
}
5656
buf.extend_from_slice(&(self.remove_members.len() as u32).to_be_bytes());
5757
for member in &self.remove_members {
58-
buf.extend_from_slice(&member.0);
58+
buf.extend_from_slice(&member.to_be_bytes());
5959
}
6060
buf
6161
}

0 commit comments

Comments
 (0)