Skip to content

Commit de00401

Browse files
authored
set up partner sig validation on Solana with new interface (#76)
1 parent 50ea9a9 commit de00401

File tree

4 files changed

+146
-39
lines changed

4 files changed

+146
-39
lines changed

solana/programs/bridge/src/base_to_solana/constants.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ pub const OUTPUT_ROOT_SEED: &[u8] = b"output_root";
77
#[constant]
88
pub const BRIDGE_CPI_AUTHORITY_SEED: &[u8] = b"bridge_cpi_authority";
99
#[constant]
10-
pub const PARTNER_SIGNERS_ACCOUNT_SEED: &[u8] = b"config";
10+
pub const PARTNER_SIGNERS_ACCOUNT_SEED: &[u8] = b"signers";
1111
#[constant]
1212
pub const PARTNER_PROGRAM_ID: Pubkey = pubkey!("offqSMQWgQud6WJz694LRzkeN5kMYpCHTpXQr3Rkcjm"); // TODO: placeholder

solana/programs/bridge/src/base_to_solana/instructions/register_output_root.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,13 @@ mod tests {
148148

149149
use crate::{
150150
accounts,
151+
base_to_solana::state::partner_config::{PartnerConfig, PartnerSigner},
151152
base_to_solana::{
152153
constants::{OUTPUT_ROOT_SEED, PARTNER_SIGNERS_ACCOUNT_SEED},
153154
internal::compute_output_root_message_hash,
154155
},
155156
common::{bridge::Bridge, MAX_SIGNER_COUNT},
156157
instruction::RegisterOutputRoot as RegisterOutputRootIx,
157-
partner_config::PartnerConfig,
158158
test_utils::setup_bridge_and_svm,
159159
ID,
160160
};
@@ -172,13 +172,12 @@ mod tests {
172172

173173
fn write_partner_config_account(svm: &mut LiteSVM, signers: &[[u8; 20]]) -> Pubkey {
174174
let pda = partner_config_pda();
175-
// Build fixed-size array of up to `MAX_SIGNER_COUNT` signers
176-
let mut fixed: [[u8; 20]; MAX_SIGNER_COUNT] = [[0u8; 20]; MAX_SIGNER_COUNT];
177-
let count = core::cmp::min(signers.len(), MAX_SIGNER_COUNT);
178-
fixed[..count].copy_from_slice(&signers[..count]);
175+
// Build PartnerConfig with provided EVM addresses; new_evm_address defaults to None
179176
let cfg = PartnerConfig {
180-
signer_count: count as u8,
181-
signers: fixed,
177+
signers: signers
178+
.iter()
179+
.map(|addr| PartnerSigner::from_evm_address(*addr))
180+
.collect(),
182181
};
183182
let mut data = Vec::new();
184183
cfg.try_serialize(&mut data).unwrap();

solana/programs/bridge/src/base_to_solana/state/partner_config.rs

Lines changed: 129 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,28 @@
2222
/// - Up to `MAX_SIGNER_COUNT` signers are supported to keep the account small and rent-cheap.
2323
use anchor_lang::prelude::*;
2424

25-
use crate::common::MAX_SIGNER_COUNT;
26-
2725
#[account]
28-
#[derive(Debug)]
26+
#[derive(InitSpace)]
2927
pub struct PartnerConfig {
30-
/// Number of valid entries at the start of `signers` to consider.
31-
pub signer_count: u8,
32-
/// Fixed-capacity array of authorized EVM addresses (20-byte) for partner approvals.
33-
/// Only the first `signer_count` elements should be treated as initialized.
34-
pub signers: [[u8; 20]; MAX_SIGNER_COUNT],
28+
// Static list of partner signers, max_len 20 to facilitate max of 4 concurrent validator rotations
29+
// at regular operating capacity of 16 validators, while capping heap usage to 800b
30+
#[max_len(20)]
31+
pub signers: Vec<PartnerSigner>,
3532
}
3633

37-
#[derive(Default)]
38-
/// Internal helper that materializes the configured signer set in a structure
39-
/// with fast membership checks.
40-
struct PartnerSet {
41-
signers: std::collections::BTreeSet<[u8; 20]>,
34+
#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, InitSpace)]
35+
pub struct PartnerSigner {
36+
// Regular active EVM address of the signer
37+
pub evm_address: [u8; 20],
38+
// New candidate address that each signer will sign with in a blue/green key rotation setting
39+
// When this value is not empty, the signer will start exclusively signing with this key offchain
40+
// However, from an onchain perspective, signature could arrive with either the old or new address for a while
41+
//
42+
// Since Base enforces that nonce ranges never skip, when a signature with the new address arrives,
43+
// it should be safe to assume that because it's the latest nonce range, all previous nonce ranges that could've been
44+
// signed with the old address must have already been consumed, at that point it'd be safe for partner Owner to
45+
// move the new_address to address field
46+
pub new_evm_address: Option<[u8; 20]>,
4247
}
4348

4449
impl PartnerConfig {
@@ -49,28 +54,121 @@ impl PartnerConfig {
4954
/// double counting.
5055
/// - Returns the number of addresses present in this config's allowlist.
5156
pub fn count_approvals(&self, signers: &[[u8; 20]]) -> u32 {
52-
let mut partner_set = PartnerSet::default();
53-
let n = self.signer_count as usize;
54-
let max = core::cmp::min(n, MAX_SIGNER_COUNT);
55-
for i in 0..max {
56-
partner_set.signers.insert(self.signers[i]);
57+
let mut count: u32 = 0;
58+
// Track which indices of self.signers have already been matched so that
59+
// the same configured signer is not counted more than once (e.g. if
60+
// both its old and new addresses appear in `signers`).
61+
let mut matched_indices = vec![false; self.signers.len()];
62+
63+
'outer: for provided in signers.iter() {
64+
for (idx, configured) in self.signers.iter().enumerate() {
65+
if matched_indices[idx] {
66+
continue;
67+
}
68+
69+
let is_match_old = configured.evm_address == *provided;
70+
let is_match_new = configured
71+
.new_evm_address
72+
.as_ref()
73+
.is_some_and(|new_addr| new_addr == provided);
74+
75+
if is_match_old || is_match_new {
76+
matched_indices[idx] = true;
77+
count += 1;
78+
// Move to next provided signer. This ensures a single
79+
// configured signer index cannot be matched more than once.
80+
continue 'outer;
81+
}
82+
}
5783
}
58-
partner_set.count_approvals(signers)
84+
85+
count
5986
}
6087
}
6188

62-
impl PartnerSet {
63-
/// Returns how many of the provided addresses exist in the configured set.
64-
///
65-
/// Caller should pass a deduplicated list; duplicates would be counted more
66-
/// than once by this function.
67-
pub fn count_approvals(&self, signers: &[[u8; 20]]) -> u32 {
68-
let mut count: u32 = 0;
69-
for signer in signers.iter() {
70-
if self.signers.contains(signer) {
71-
count += 1;
72-
}
89+
#[cfg(test)]
90+
mod tests {
91+
92+
use super::*;
93+
94+
fn addr(byte: u8) -> [u8; 20] {
95+
[byte; 20]
96+
}
97+
98+
fn signer(old: u8, new: Option<u8>) -> PartnerSigner {
99+
PartnerSigner {
100+
evm_address: addr(old),
101+
new_evm_address: new.map(addr),
73102
}
74-
count
103+
}
104+
105+
#[test]
106+
fn returns_zero_when_no_configured_signers() {
107+
let cfg = PartnerConfig { signers: vec![] };
108+
let provided = [addr(1), addr(2)];
109+
assert_eq!(cfg.count_approvals(&provided), 0);
110+
}
111+
112+
#[test]
113+
fn returns_zero_when_no_provided_addresses() {
114+
let cfg = PartnerConfig {
115+
signers: vec![signer(1, None)],
116+
};
117+
let provided: [[u8; 20]; 0] = [];
118+
assert_eq!(cfg.count_approvals(&provided), 0);
119+
}
120+
121+
#[test]
122+
fn matches_old_address_counts_one() {
123+
let cfg = PartnerConfig {
124+
signers: vec![signer(1, None)],
125+
};
126+
let provided = [addr(1)];
127+
assert_eq!(cfg.count_approvals(&provided), 1);
128+
}
129+
130+
#[test]
131+
fn matches_new_address_counts_one() {
132+
let cfg = PartnerConfig {
133+
signers: vec![signer(1, Some(2))],
134+
};
135+
let provided = [addr(2)];
136+
assert_eq!(cfg.count_approvals(&provided), 1);
137+
}
138+
139+
#[test]
140+
fn old_and_new_for_same_signer_counts_once() {
141+
let cfg = PartnerConfig {
142+
signers: vec![signer(1, Some(2))],
143+
};
144+
let provided = [addr(1), addr(2)];
145+
assert_eq!(cfg.count_approvals(&provided), 1);
146+
}
147+
148+
#[test]
149+
fn multiple_distinct_matches_count_correctly() {
150+
let cfg = PartnerConfig {
151+
signers: vec![signer(1, None), signer(2, None), signer(3, Some(4))],
152+
};
153+
let provided = [addr(1), addr(4)];
154+
assert_eq!(cfg.count_approvals(&provided), 2);
155+
}
156+
157+
#[test]
158+
fn non_matching_addresses_count_zero() {
159+
let cfg = PartnerConfig {
160+
signers: vec![signer(1, None)],
161+
};
162+
let provided = [addr(9)];
163+
assert_eq!(cfg.count_approvals(&provided), 0);
164+
}
165+
166+
#[test]
167+
fn duplicate_provided_addresses_do_not_increase_count() {
168+
let cfg = PartnerConfig {
169+
signers: vec![signer(1, None)],
170+
};
171+
let provided = [addr(1), addr(1)];
172+
assert_eq!(cfg.count_approvals(&provided), 1);
75173
}
76174
}

solana/programs/bridge/src/test_utils/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use solana_transaction::Transaction;
2828

2929
use crate::{
3030
accounts,
31+
base_to_solana::partner_config::PartnerSigner,
3132
common::{
3233
bridge::{BufferConfig, Eip1559Config, GasConfig, PartnerOracleConfig, ProtocolConfig},
3334
BaseOracleConfig, Config, PartialTokenMetadata, BRIDGE_SEED, MAX_SIGNER_COUNT,
@@ -89,6 +90,15 @@ impl BaseOracleConfig {
8990
}
9091
}
9192

93+
impl PartnerSigner {
94+
pub fn from_evm_address(evm_address: [u8; 20]) -> Self {
95+
Self {
96+
evm_address,
97+
new_evm_address: None,
98+
}
99+
}
100+
}
101+
92102
pub fn setup_bridge_and_svm() -> (LiteSVM, solana_keypair::Keypair, Pubkey) {
93103
let mut svm = LiteSVM::new();
94104
svm.add_program_from_file(ID, "../../target/deploy/bridge.so")

0 commit comments

Comments
 (0)