Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
4 changes: 4 additions & 0 deletions 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 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
858 changes: 809 additions & 49 deletions bin/citrea/tests/bitcoin/light_client_test.rs

Large diffs are not rendered by default.

108 changes: 53 additions & 55 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,49 @@ 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 3 valid signatures from the first 3 signers for the given prehash
pub(crate) fn create_valid_signatures(
/// Creates valid signatures from the first 3 signers for the given payload
pub(crate) fn create_valid_signatures<T: SolStruct>(
signers: &[PrivateKeySigner],
prehash: &B256,
) -> [([u8; SECURITY_COUNCIL_SIGNATURE_SIZE], u8); SECURITY_COUNCIL_SIGNATURE_THRESHOLD] {
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();

for (i, signer) in signers.iter().enumerate().take(3) {
let sig = signer.sign_hash_sync(prehash).unwrap();
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));
}
Expand Down Expand Up @@ -434,7 +433,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 @@ -485,21 +484,20 @@ pub async fn generate_mock_txs(
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 msg = method_id_body.serialize();
let prehash = eip191_hash_message(msg.as_slice());
let (_initial_addresses, signers) =
generate_initial_addresses_with_signers_from_pks(&pk_bytes_arr);
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.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 @@ -605,21 +603,21 @@ pub async fn generate_mock_txs(
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",
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
26 changes: 13 additions & 13 deletions crates/bitcoin-da/src/helpers/parsers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub enum ParsedTransaction {
/// Kind 2
Chunk(ParsedChunk),
/// Kind 3
BatchProofMethodId(ParsedBatchProofMethodId),
SecurityCouncilTx(ParsedSecurityCouncilTx),
/// Kind 4
SequencerCommitment(ParsedSequencerCommitment),
// /// Kind ?
Expand Down Expand Up @@ -60,17 +60,17 @@ pub struct ParsedSequencerCommitment {
pub(crate) public_key: Vec<u8>,
}

/// ParsedBatchProofMethodId is a transaction that contains the batch proof method ID
/// and the security council signatures and pubkeys.
/// ParsedSecurityCouncilTx is a transaction that contains a security council transaction
/// (method ID update, member management, etc.) and the security council signatures.
#[derive(Debug, Clone)]
pub struct ParsedBatchProofMethodId {
/// Contains borsh(BatchProofMethodId)
/// So it has public keys and signatures of security council and the body
/// which is BatchProofMethodIdBody{activation_l2_height, method_id}
pub struct ParsedSecurityCouncilTx {
/// Contains borsh(SecurityCouncilTx)
/// So it has signatures of security council and the body
/// which is one of the SecurityCouncilTxType variants
pub(crate) body: Vec<u8>,
}

impl ParsedBatchProofMethodId {
impl ParsedSecurityCouncilTx {
/// Hash of the body
pub fn hash(&self) -> [u8; 32] {
let hash = sha2::Sha256::new_with_prefix(&self.body);
Expand Down Expand Up @@ -217,8 +217,8 @@ fn parse_transaction(
TransactionKind::Chunks => {
body_parsers::parse_type_2_body(instructions).map(ParsedTransaction::Chunk)
}
TransactionKind::BatchProofMethodId => {
body_parsers::parse_type_3_body(instructions).map(ParsedTransaction::BatchProofMethodId)
TransactionKind::SecurityCouncilTx => {
body_parsers::parse_type_3_body(instructions).map(ParsedTransaction::SecurityCouncilTx)
}
TransactionKind::SequencerCommitment => body_parsers::parse_type_4_body(instructions)
.map(ParsedTransaction::SequencerCommitment),
Expand Down Expand Up @@ -268,7 +268,7 @@ mod body_parsers {
read_instr, read_opcode, read_push_bytes, ParsedAggregate, ParsedChunk, ParsedComplete,
ParsedSequencerCommitment, ParserError,
};
use crate::helpers::parsers::ParsedBatchProofMethodId;
use crate::helpers::parsers::ParsedSecurityCouncilTx;

/// Parse transaction body of Type0 Complete proof
pub(super) fn parse_type_0_body(
Expand Down Expand Up @@ -440,7 +440,7 @@ mod body_parsers {
/// Parse transaction body of Type3 Batch Proof MethodId upgrade tx
pub(super) fn parse_type_3_body(
instructions: &mut dyn Iterator<Item = Result<Instruction<'_>, ParserError>>,
) -> Result<ParsedBatchProofMethodId, ParserError> {
) -> Result<ParsedSecurityCouncilTx, ParserError> {
let op_false = read_push_bytes(instructions)?;
if !op_false.is_empty() {
// OP_FALSE = OP_PUSHBYTES_0
Expand All @@ -467,7 +467,7 @@ mod body_parsers {
return Err(ParserError::UnexpectedOpcode);
}

Ok(ParsedBatchProofMethodId { body })
Ok(ParsedSecurityCouncilTx { body })
}

/// Parse transaction body of Type4 Sequencer Commitment
Expand Down
Loading
Loading