Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e7a0d20
Save build script just in case
ercecan Jan 7, 2026
89ac22a
WIP use addresses instead of pubkeys for security council
ercecan Jan 7, 2026
7388f1c
Fix method id verifier unit tests
ercecan Jan 7, 2026
db81c88
Fix possible underflow in pubkey recovery
ercecan Jan 7, 2026
fb0fe35
Custom error type for pubkey recovery
ercecan Jan 7, 2026
1d8a18e
Lints
ercecan Jan 7, 2026
6e9ab32
Function and parameter rename
ercecan Jan 7, 2026
4210506
Fix comments
ercecan Jan 7, 2026
87978b1
Merge branch 'nightly' into erce/use-address-instead-of-pubkeys-for-s…
ercecan Jan 7, 2026
6ce73f8
Run the guest code ci
ercecan Jan 7, 2026
7aa4783
Fix nits
ercecan Jan 7, 2026
cd734e6
Fix comment
ercecan Jan 7, 2026
47e9389
Merge branch 'needs-audit' into erce/use-address-instead-of-pubkeys-f…
ercecan Jan 20, 2026
89d5ad9
Implement eip712 typed message structure for batch proof method id up…
ercecan Feb 1, 2026
f049740
Lints
ercecan Feb 1, 2026
648f8c7
Merge branch 'needs-audit' into erce/use-address-instead-of-pubkeys-f…
ercecan Feb 13, 2026
0eb4639
Update e2e tests
ercecan Feb 27, 2026
17e4f07
Merge branch 'erce/use-address-instead-of-pubkeys-for-security-counci…
ercecan Feb 27, 2026
3c87048
Lint fix
ercecan Feb 27, 2026
b97b54d
Merge branch 'needs-audit' of https://github.com/chainwayxyz/citrea i…
ercecan Mar 2, 2026
71ce119
Lints
ercecan Mar 2, 2026
a407b6c
Remove spaces in domain name to be safe
ercecan Mar 2, 2026
0996845
feat: lcp security council member management messages (#3174)
ercecan Mar 3, 2026
912051c
feat: update da pubkey messages (#3177)
ercecan Mar 5, 2026
4850d5a
feat: lcp upgrade support (#3179)
ercecan Mar 10, 2026
9397eca
feat: add nonce to security council messages (#3182)
ercecan Mar 10, 2026
746b1ad
feat: Remove batch proof method id message (#3183)
ercecan Mar 11, 2026
b61217f
refactor: Remove chain id from message bodies (#3184)
ercecan Mar 11, 2026
34cb50c
fix: Handle nonce overflow (#3186)
ercecan Mar 11, 2026
ab328b0
test: Add some missing sc messages tests (#3187)
ercecan Mar 11, 2026
f17a5e0
feat: Security council set lcp state message (#3188)
ercecan Mar 16, 2026
000c546
Move sec council messages
ercecan Mar 16, 2026
394bb9d
Add prev allowed lcp method ids for networks
ercecan Mar 16, 2026
ea7dba7
Merge branch 'needs-audit' of https://github.com/chainwayxyz/citrea i…
ercecan Mar 16, 2026
2c0bfab
fix: consume nonce on failed council messages (#3194)
ercecan Mar 26, 2026
ab544a1
refactor: sort sc messages by nonce (#3196)
ercecan Mar 30, 2026
e73c487
refactor: Rename method id upgrade authority to security council (#3189)
ercecan Apr 6, 2026
242e11e
Rename security council tx kind backup name
ercecan Apr 6, 2026
005a5aa
Central registry of prefixes
ercecan Apr 6, 2026
a91b1b7
Move import
ercecan Apr 6, 2026
e2540aa
Increment nonce only after signature validation
ercecan Apr 7, 2026
5c4cd0d
Fix lint
ercecan Apr 7, 2026
afe75e3
Assert threshold
ercecan Apr 7, 2026
acfc492
Remove comment
ercecan Apr 7, 2026
63ce59c
Move under impl
ercecan Apr 7, 2026
e6330ef
Fix test
ercecan Apr 7, 2026
2ff6059
Assert removed addresses
ercecan Apr 7, 2026
b1f9f29
Test unsorted nonce messages also work
ercecan Apr 7, 2026
364eea4
Calculate what we assert in terms of constants in tests
ercecan Apr 7, 2026
dd80544
Validate initial lcp
ercecan Apr 7, 2026
83e1727
Lint fmt
ercecan Apr 7, 2026
e46f435
Fix set lcp state test
ercecan Apr 8, 2026
4e85afd
Alphabetical order of prefixes
ercecan Apr 8, 2026
02343a8
Move l2 height validation after signature verification and nonce incr…
ercecan Apr 8, 2026
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
6 changes: 5 additions & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ alloy-network = { version = "0.13", default-features = false }
alloy-signer = { version = "0.13", default-features = false }
alloy-signer-local = { version = "0.13.0", default-features = false }

citrea-e2e = { git = "https://github.com/chainwayxyz/citrea-e2e", rev = "143d76086591006d2424bda027512bd03565229a" }
citrea-e2e = { git = "https://github.com/chainwayxyz/citrea-e2e", rev = "f0ddc2b" }

[patch.crates-io]
bitcoincore-rpc = { version = "0.18.0", git = "https://github.com/chainwayxyz/rust-bitcoincore-rpc.git", rev = "a6e011a" }
Expand Down
2 changes: 1 addition & 1 deletion bin/citrea/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ alloy-rlp = { workspace = true }
alloy-rpc-types = { workspace = true }
alloy-rpc-types-trace = { workspace = true }
alloy-rpc-types-txpool = { workspace = true }
alloy-signer = { workspace = true }
alloy-signer = { workspace = true, features = ["eip712"] }
alloy-signer-local = { workspace = true }
base64 = { workspace = true }
bincode = { workspace = true }
Expand Down
2,483 changes: 2,421 additions & 62 deletions bin/citrea/tests/bitcoin/light_client_test.rs

Large diffs are not rendered by default.

136 changes: 79 additions & 57 deletions bin/citrea/tests/bitcoin/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, Instant};

use alloy_primitives::{eip191_hash_message, B256, U64};
use alloy_primitives::{keccak256, Address, U64};
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::{eip712_domain, SolStruct};
use anyhow::bail;
use bitcoin_da::fee::FeeService;
use bitcoin_da::monitoring::{MonitoringConfig, MonitoringService};
Expand All @@ -20,16 +21,14 @@ use citrea_e2e::bitcoin::BitcoinNode;
use citrea_e2e::config::BitcoinConfig;
use citrea_e2e::node::{BatchProver, FullNode, NodeKind};
use citrea_e2e::traits::NodeT;
use citrea_light_client_prover::circuit::{
citrea_network_to_chain_id, SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE,
SECURITY_COUNCIL_MEMBER_COUNT,
};
use citrea_light_client_prover::circuit::initial_values::bitcoinda;
use citrea_light_client_prover::circuit::{citrea_network_to_chain_id, BatchProofMethodIdUpdate};
use citrea_primitives::{MAX_TX_BODY_SIZE, REVEAL_TX_PREFIX};
use reth_tasks::TaskExecutor;
use sov_ledger_rpc::LedgerRpcClient;
use sov_rollup_interface::da::{
BatchProofMethodId, BatchProofMethodIdBody, DaTxRequest, SequencerCommitment,
SECURITY_COUNCIL_SIGNATURE_SIZE, SECURITY_COUNCIL_SIGNATURE_THRESHOLD,
BatchProofMethodIdBody, DaTxRequest, SecurityCouncilTx, SecurityCouncilTxType,
SequencerCommitment, SECURITY_COUNCIL_SIGNATURE_SIZE,
};
use sov_rollup_interface::rpc::{JobRpcResponse, VerifiedBatchProofResponse};
use sov_rollup_interface::services::da::DaService;
Expand Down Expand Up @@ -358,49 +357,73 @@ async fn create_and_fund_wallet(wallet: String, da_node: &BitcoinNode) {
da_node.fund_wallet(wallet, 5).await.unwrap();
}

/// Converts a vector of signatures in Vec<u8> format to an array of signatures in [u8; 64] format
fn from_vec_to_sigs(
vec: Vec<(Vec<u8>, u8)>,
) -> [([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8); SECURITY_COUNCIL_SIGNATURE_THRESHOLD] {
let mut sigs = Vec::new();
for (v, i) in vec.into_iter() {
sigs.push((v.try_into().unwrap(), i));
}
sigs.try_into().unwrap()
/// Converts a vector of signatures in Vec<u8> format to a vector of signatures in [u8; 65] format
fn from_vec_to_sigs(vec: Vec<(Vec<u8>, u8)>) -> Vec<([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8)> {
vec.into_iter()
.map(|(v, i)| (v.try_into().unwrap(), i))
.collect()
}

/// Generates 5 valid keypairs and returns the public keys and signers from the given private keys
pub(crate) fn generate_initial_pub_keys_with_signers_from_pks(
private_keys: [[u8; 32]; 5],
) -> (
[[u8; SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE]; SECURITY_COUNCIL_MEMBER_COUNT],
Vec<PrivateKeySigner>,
) {
let mut initial_da_pubkeys =
[[0u8; SECURITY_COUNCIL_COMPRESSED_PUBKEY_SIZE]; SECURITY_COUNCIL_MEMBER_COUNT];
/// Generates valid keypairs and returns the addresses and signers from the given private keys
pub(crate) fn generate_initial_addresses_with_signers_from_pks(
private_keys: &[[u8; 32]],
) -> (Vec<Address>, Vec<PrivateKeySigner>) {
let mut initial_da_addresses = Vec::new();
let mut signers = Vec::new();

// Generate 5 valid keypairs and signatures
for (i, secret_key) in private_keys.iter().enumerate() {
let signer = PrivateKeySigner::from_bytes(&secret_key.into()).unwrap();
for private_key in private_keys {
let signer = PrivateKeySigner::from_bytes(&(*private_key).into()).unwrap();
let verifying_key = signer.credential().verifying_key();
let pubkey = verifying_key.to_sec1_bytes();
initial_da_pubkeys[i] = pubkey.to_vec().try_into().unwrap();
let ep = verifying_key.to_encoded_point(false); // uncompressed: 0x04 + X(32) + Y(32)
let bytes = ep.as_bytes();
initial_da_addresses.push(Address::from_slice(&keccak256(&bytes[1..])[12..]));
signers.push(signer);
}

(initial_da_pubkeys, signers)
(initial_da_addresses, signers)
}

/// Creates valid signatures from the first 3 signers for the given payload
pub(crate) fn create_valid_signatures<T: SolStruct>(
signers: &[PrivateKeySigner],
payload: &T,
// Pass threshold here to determine the number of signatures needed
threshold: usize,
) -> Vec<([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8)> {
let mut signatures_in_inscription = Vec::new();

let domain = eip712_domain! {
name: bitcoinda::NIGHTLY_EIP712_SECURITY_COUNCIL_MESSAGE_DOMAIN_NAME,
version: "1",
chain_id: citrea_network_to_chain_id(Network::Nightly),
};

for (i, signer) in signers.iter().enumerate().take(threshold) {
let sig = signer.sign_typed_data_sync(payload, &domain).unwrap();
let signature = sig.as_bytes()[0..SECURITY_COUNCIL_SIGNATURE_SIZE].to_vec();
signatures_in_inscription.push((signature, i as u8));
}

from_vec_to_sigs(signatures_in_inscription)
}

/// Creates 3 valid signatures from the first 3 signers for the given prehash
pub(crate) fn create_valid_signatures(
/// Creates signatures using a wrong domain (wrong chain_id) to test rejection of wrong-network messages
pub(crate) fn create_valid_signatures_with_wrong_domain<T: SolStruct>(
signers: &[PrivateKeySigner],
prehash: &B256,
) -> [([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8); SECURITY_COUNCIL_SIGNATURE_THRESHOLD] {
payload: &T,
threshold: usize,
) -> Vec<([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8)> {
let mut signatures_in_inscription = Vec::new();

for (i, signer) in signers.iter().enumerate().take(3) {
let sig = signer.sign_hash_sync(prehash).unwrap();
// Use a wrong chain_id (9999) to create signatures that won't verify against the correct domain
let domain = eip712_domain! {
name: bitcoinda::NIGHTLY_EIP712_SECURITY_COUNCIL_MESSAGE_DOMAIN_NAME,
version: "1",
chain_id: 9999u64,
};

for (i, signer) in signers.iter().enumerate().take(threshold) {
let sig = signer.sign_typed_data_sync(payload, &domain).unwrap();
let signature = sig.as_bytes()[0..SECURITY_COUNCIL_SIGNATURE_SIZE].to_vec();
signatures_in_inscription.push((signature, i as u8));
}
Expand Down Expand Up @@ -434,7 +457,7 @@ pub async fn generate_mock_txs(
BitcoinBlock,
Vec<SequencerCommitment>,
Vec<Vec<u8>>,
Vec<BatchProofMethodId>,
Vec<SecurityCouncilTx>,
) {
// Funding wallet requires block generation, hence we do funding at the beginning
// to be able to write all transactions into the same block.
Expand Down Expand Up @@ -479,27 +502,26 @@ pub async fn generate_mock_txs(
let method_id_body = BatchProofMethodIdBody {
method_id: [0; 8],
activation_l2_height: 0,
chain_id: citrea_network_to_chain_id(Network::Nightly),
nonce: 1,
};

let pk_bytes_arr: [[u8; 32]; 5] = BATCH_PROOF_METHOD_ID_UPDATE_AUTHORITY_TEST_PRIVATE_KEYS
.map(|s| hex::decode(s).unwrap().try_into().unwrap());

let (_initial_pubkeys, signers) = generate_initial_pub_keys_with_signers_from_pks(pk_bytes_arr);
let (_initial_addresses, signers) =
generate_initial_addresses_with_signers_from_pks(&pk_bytes_arr);
let payload = BatchProofMethodIdUpdate::from(method_id_body.clone());

let msg = method_id_body.serialize();
let prehash = eip191_hash_message(msg.as_slice());

let signatures_with_index = create_valid_signatures(&signers, &prehash);
let signatures_with_index = create_valid_signatures(&signers, &payload, 3);

// Send method id update tx
let method_id = BatchProofMethodId {
body: method_id_body.clone(),
let sc_tx = SecurityCouncilTx {
tx_type: SecurityCouncilTxType::BatchProofMethodIdUpdateV1(method_id_body.clone()),
signatures_with_index,
};
valid_method_ids.push(method_id.clone());
valid_method_ids.push(sc_tx.clone());
da_service
.send_transaction(DaTxRequest::BatchProofMethodId(method_id))
.send_transaction(DaTxRequest::SecurityCouncilTx(sc_tx))
.await
.expect("Failed to send transaction");

Expand Down Expand Up @@ -599,27 +621,27 @@ pub async fn generate_mock_txs(
let method_id_body = BatchProofMethodIdBody {
method_id: [1; 8],
activation_l2_height: 100,
chain_id: citrea_network_to_chain_id(Network::Nightly),
nonce: 2,
};

let pk_bytes_arr: [[u8; 32]; 5] = BATCH_PROOF_METHOD_ID_UPDATE_AUTHORITY_TEST_PRIVATE_KEYS
.map(|s| hex::decode(s).unwrap().try_into().unwrap());

let (_initial_pubkeys, signers) = generate_initial_pub_keys_with_signers_from_pks(pk_bytes_arr);
let (_initial_addresses, signers) =
generate_initial_addresses_with_signers_from_pks(&pk_bytes_arr);

let msg = method_id_body.serialize();
let prehash = eip191_hash_message(msg.as_slice());
let payload = BatchProofMethodIdUpdate::from(method_id_body.clone());

let signatures_with_index = create_valid_signatures(&signers, &prehash);
let signatures_with_index = create_valid_signatures(&signers, &payload, 3);

// Send method id update tx
let method_id = BatchProofMethodId {
body: method_id_body,
let sc_tx = SecurityCouncilTx {
tx_type: SecurityCouncilTxType::BatchProofMethodIdUpdateV1(method_id_body),
signatures_with_index,
};
valid_method_ids.push(method_id.clone());
valid_method_ids.push(sc_tx.clone());
da_service
.send_transaction(DaTxRequest::BatchProofMethodId(method_id))
.send_transaction(DaTxRequest::SecurityCouncilTx(sc_tx))
.await
.expect("Failed to send transaction");

Expand Down
4 changes: 2 additions & 2 deletions crates/bitcoin-da/src/helpers/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn transaction_kind_to_backup_name(kind: &TransactionKind) -> &str {
match kind {
TransactionKind::Complete => "complete_zk_proof",
TransactionKind::SequencerCommitment => "sequencer_commitment",
TransactionKind::BatchProofMethodId => "method_id_update",
TransactionKind::SecurityCouncilTx => "method_id_update",
Comment thread
ercecan marked this conversation as resolved.
Outdated
TransactionKind::Chunks => "chunks",
TransactionKind::Aggregate => "aggregate",
TransactionKind::Unknown(_) => "unknown",
Expand All @@ -29,7 +29,7 @@ pub(crate) fn backup_txs_to_file(
if let Some(tx) = txs.first() {
match &tx.kind {
TransactionKind::Complete
| TransactionKind::BatchProofMethodId
| TransactionKind::SecurityCouncilTx
| TransactionKind::SequencerCommitment => {
if txs.len() != 1 {
return Err(BitcoinServiceError::TransactionBackupError(format!(
Expand Down
18 changes: 9 additions & 9 deletions crates/bitcoin-da/src/helpers/builders/body_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub(crate) enum RawTxData {
/// let chunks = compressed.chunks(MAX_TX_BODY_SIZE)
/// [borsh(DataOnDa::Chunk(chunk)) for chunk in chunks]
Chunks(Vec<Vec<u8>>),
/// borsh(DataOnDa::BatchProofMethodId(MethodId))
BatchProofMethodId(Vec<u8>),
/// borsh(DataOnDa::SecurityCouncilTx(SecurityCouncilTx))
SecurityCouncilTx(Vec<u8>),
/// borsh(DataOnDa::SequencerCommitment(SequencerCommitment))
SequencerCommitment(Vec<u8>),
}
Expand All @@ -62,8 +62,8 @@ pub enum DaTxs {
/// Signed
reveal: TxWithId,
},
/// BatchProof method id.
BatchProofMethodId {
/// Security council transaction.
SecurityCouncilTx {
/// Unsigned
commit: Transaction,
/// Signed
Expand Down Expand Up @@ -115,7 +115,7 @@ pub fn create_inscription_transactions(
network,
&reveal_tx_prefix,
),
RawTxData::BatchProofMethodId(body) => create_inscription_type_3(
RawTxData::SecurityCouncilTx(body) => create_inscription_type_3(
body,
&da_private_key,
utxo_context,
Expand Down Expand Up @@ -671,7 +671,7 @@ pub fn create_inscription_type_1(
}
}

/// Creates the inscription transactions Type 3 - BatchProofMethodId
/// Creates the inscription transactions Type 3 - SecurityCouncilTx
#[allow(clippy::too_many_arguments)]
#[instrument(level = "trace", skip_all, err)]
pub fn create_inscription_type_3(
Expand All @@ -693,7 +693,7 @@ pub fn create_inscription_type_3(
let key_pair = UntweakedKeypair::from_secret_key(SECP256K1, da_private_key);
let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair);

let kind = TransactionKind::BatchProofMethodId;
let kind = TransactionKind::SecurityCouncilTx;
let kind_bytes = kind.to_bytes();

let start = Instant::now();
Expand Down Expand Up @@ -821,11 +821,11 @@ pub fn create_inscription_type_3(

if let Some(root) = merkle_root {
info!(
"Taproot merkle root for inscription - BatchProofMethodId: {}",
"Taproot merkle root for inscription - SecurityCouncilTx: {}",
root
);
}
return Ok(DaTxs::BatchProofMethodId {
return Ok(DaTxs::SecurityCouncilTx {
commit: unsigned_commit_tx,
reveal: TxWithId {
id: reveal_tx.compute_txid(),
Expand Down
8 changes: 4 additions & 4 deletions crates/bitcoin-da/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ pub(crate) enum TransactionKind {
Aggregate = 1,
/// This type of transaction includes chunk parts of body (>= 400kb)
Chunks = 2,
/// This type of transaction includes a new batch proof method_id
BatchProofMethodId = 3,
/// This type of transaction includes a security council transaction
SecurityCouncilTx = 3,
/// SequencerCommitment
SequencerCommitment = 4,
// /// ForcedTransaction
Expand All @@ -43,7 +43,7 @@ impl TransactionKind {
TransactionKind::Complete => 0u16.to_le_bytes(),
TransactionKind::Aggregate => 1u16.to_le_bytes(),
TransactionKind::Chunks => 2u16.to_le_bytes(),
TransactionKind::BatchProofMethodId => 3u16.to_le_bytes(),
TransactionKind::SecurityCouncilTx => 3u16.to_le_bytes(),
TransactionKind::SequencerCommitment => 4u16.to_le_bytes(),
TransactionKind::Unknown(n) => n.get().to_le_bytes(),
}
Expand All @@ -59,7 +59,7 @@ impl TransactionKind {
0 => Some(TransactionKind::Complete),
1 => Some(TransactionKind::Aggregate),
2 => Some(TransactionKind::Chunks),
3 => Some(TransactionKind::BatchProofMethodId),
3 => Some(TransactionKind::SecurityCouncilTx),
4 => Some(TransactionKind::SequencerCommitment),
n => Some(TransactionKind::Unknown(
NonZero::new(n).expect("Is not zero"),
Expand Down
Loading
Loading