Repository: chainwayxyz/clementine
Pull request #730: Security council
Author: login: ekrembal
Created: 2025-05-02T01:23:11Z | Merged: 2025-05-05T20:24:12Z



Diff:
diff --git a/core/src/actor.rs b/core/src/actor.rs
index 8519930c13..a0fbfabbd3 100644
--- a/core/src/actor.rs
+++ b/core/src/actor.rs
@@ -440,7 +440,8 @@ impl Actor {
                         | Kind::Other(_)
                         | Kind::BaseDepositScript(_)
                         | Kind::ReplacementDepositScript(_)
-                        | Kind::TimelockScript(_) => return Ok(None),
+                        | Kind::TimelockScript(_)
+                        | Kind::ManualSpend(_) => return Ok(None),
                     };
 
                     if signed_preimage {
@@ -520,7 +521,8 @@ impl Actor {
                         | Kind::Other(_)
                         | Kind::BaseDepositScript(_)
                         | Kind::ReplacementDepositScript(_)
-                        | Kind::TimelockScript(_) => return Ok(None),
+                        | Kind::TimelockScript(_)
+                        | Kind::ManualSpend(_) => return Ok(None),
                     };
 
                     if signed_winternitz {
@@ -636,7 +638,8 @@ impl Actor {
                         },
                         Kind::WinternitzCommit(_)
                         | Kind::PreimageRevealScript(_)
-                        | Kind::Other(_) => return Ok(None),
+                        | Kind::Other(_)
+                        | Kind::ManualSpend(_) => return Ok(None),
                     };
 
                     // Add P2TR elements (control block and script) to the witness
diff --git a/core/src/bin/cli.rs b/core/src/bin/cli.rs
index a75dde46af..a3082bfa08 100644
--- a/core/src/bin/cli.rs
+++ b/core/src/bin/cli.rs
@@ -3,10 +3,11 @@
 use std::str::FromStr;
 
 use alloy::primitives::{Bytes, FixedBytes, Uint};
-use bitcoin::{consensus::Encodable, hashes::Hash, Amount, Block, Txid};
+use bitcoin::{consensus::Encodable, hashes::Hash, Block, Txid};
 use bitcoincore_rpc::RpcApi;
 use clap::{Parser, Subcommand};
 use clementine_core::{
+    builder::transaction::SecurityCouncil,
     citrea::Bridge::TransactionParams,
     errors::BridgeError,
     extended_rpc,
@@ -133,8 +134,6 @@ enum AggregatorCommands {
         #[arg(long)]
         network: Option<String>,
         #[arg(long)]
-        bridge_amount: Option<u64>,
-        #[arg(long)]
         user_takes_after: Option<u64>,
     },
     /// Get transaction parameters of a move transaction
@@ -154,7 +153,7 @@ enum AggregatorCommands {
         #[arg(long)]
         network: Option<String>,
         #[arg(long)]
-        bridge_amount: Option<u64>,
+        security_council: Option<SecurityCouncil>,
     },
     /// Process a new withdrawal
     NewWithdrawal {
@@ -355,6 +354,10 @@ async fn handle_operator_call(url: String, command: OperatorCommands) {
                 deposit_outpoint_txid, deposit_outpoint_vout
             );
             let params = clementine_core::rpc::clementine::DepositParams {
+                security_council: Some(clementine::SecurityCouncil {
+                    pks: vec![],
+                    threshold: 0,
+                }),
                 deposit: Some(Deposit {
                     deposit_outpoint: Some(Outpoint {
                         txid: deposit_outpoint_txid.into(),
@@ -528,7 +531,6 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
             evm_address,
             recovery_taproot_address,
             network,
-            bridge_amount,
             user_takes_after,
         } => {
             let response = aggregator
@@ -564,11 +566,6 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
                 None => bitcoin::Network::Regtest,
             };
 
-            let bridge_amount = match bridge_amount {
-                Some(amount) => Amount::from_sat(amount),
-                None => Amount::from_sat(1_000_000_000),
-            };
-
             let user_takes_after = match user_takes_after {
                 Some(amount) => amount as u16,
                 None => 200,
@@ -578,7 +575,6 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
                 xonly_pk,
                 &recovery_taproot_address,
                 evm_address,
-                bridge_amount,
                 network,
                 user_takes_after,
             )
@@ -719,7 +715,7 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
         AggregatorCommands::GetReplacementDepositAddress {
             move_txid,
             network,
-            bridge_amount,
+            security_council,
         } => {
             let mut move_txid = hex::decode(move_txid).expect("Failed to decode txid");
             move_txid.reverse();
@@ -745,17 +741,12 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
                 None => bitcoin::Network::Regtest,
             };
 
-            let bridge_amount = match bridge_amount {
-                Some(amount) => Amount::from_sat(amount),
-                None => Amount::from_sat(1_000_000_000),
-            };
-
             let (replacement_deposit_address, _) =
                 clementine_core::builder::address::generate_replacement_deposit_address(
                     move_txid,
                     nofn_xonly_pk,
-                    bridge_amount,
                     network,
+                    security_council.expect("Security council is required"),
                 )
                 .expect("Failed to generate replacement deposit address");
 
diff --git a/core/src/builder/address.rs b/core/src/builder/address.rs
index 923ee9cc4f..1f31d9160f 100644
--- a/core/src/builder/address.rs
+++ b/core/src/builder/address.rs
@@ -4,13 +4,14 @@
 //! addresses.
 
 use super::script::{
-    BaseDepositScript, CheckSig, ReplacementDepositScript, SpendableScript, TimelockScript,
+    BaseDepositScript, CheckSig, Multisig, ReplacementDepositScript, SpendableScript,
+    TimelockScript,
 };
+use super::transaction::SecurityCouncil;
 use crate::bitvm_client::SECP;
 use crate::errors::BridgeError;
 use crate::{bitvm_client, EVMAddress};
 use bitcoin::address::NetworkUnchecked;
-use bitcoin::Amount;
 use bitcoin::{
     secp256k1::XOnlyPublicKey,
     taproot::{TaprootBuilder, TaprootSpendInfo},
@@ -124,12 +125,10 @@ pub fn generate_deposit_address(
     nofn_xonly_pk: XOnlyPublicKey,
     recovery_taproot_address: &Address<NetworkUnchecked>,
     user_evm_address: EVMAddress,
-    amount: Amount,
     network: bitcoin::Network,
     user_takes_after: u16,
 ) -> Result<(Address, TaprootSpendInfo), BridgeError> {
-    let deposit_script =
-        BaseDepositScript::new(nofn_xonly_pk, user_evm_address, amount).to_script_buf();
+    let deposit_script = BaseDepositScript::new(nofn_xonly_pk, user_evm_address).to_script_buf();
 
     let recovery_script_pubkey = recovery_taproot_address
         .clone()
@@ -150,13 +149,16 @@ pub fn generate_deposit_address(
 pub fn generate_replacement_deposit_address(
     old_move_txid: bitcoin::Txid,
     nofn_xonly_pk: XOnlyPublicKey,
-    amount: Amount,
     network: bitcoin::Network,
+    security_council: SecurityCouncil,
 ) -> Result<(Address, TaprootSpendInfo), BridgeError> {
     let deposit_script =
-        ReplacementDepositScript::new(nofn_xonly_pk, old_move_txid, amount).to_script_buf();
+        ReplacementDepositScript::new(nofn_xonly_pk, old_move_txid).to_script_buf();
+
+    let security_council_script = Multisig::from_security_council(security_council).to_script_buf();
 
-    let (addr, spend) = create_taproot_address(&[deposit_script], None, network);
+    let (addr, spend) =
+        create_taproot_address(&[deposit_script, security_council_script], None, network);
     Ok((addr, spend))
 }
 
@@ -186,7 +188,7 @@ mod tests {
     use bitcoin::{
         key::{Keypair, TapTweak},
         secp256k1::{PublicKey, SecretKey},
-        Address, AddressType, Amount, ScriptBuf, XOnlyPublicKey,
+        Address, AddressType, ScriptBuf, XOnlyPublicKey,
     };
     use secp256k1::rand;
     use std::str::FromStr;
@@ -293,7 +295,6 @@ mod tests {
             nofn_xonly_pk,
             recovery_taproot_address.as_unchecked(),
             crate::EVMAddress(evm_address),
-            Amount::from_sat(100_000_000),
             bitcoin::Network::Regtest,
             200,
         )
diff --git a/core/src/builder/script.rs b/core/src/builder/script.rs
index 3e6a5483e9..52f91428d7 100644
--- a/core/src/builder/script.rs
+++ b/core/src/builder/script.rs
@@ -15,12 +15,14 @@ use bitcoin::{
     script::Builder,
     ScriptBuf, XOnlyPublicKey,
 };
-use bitcoin::{taproot, Amount, Txid, Witness};
+use bitcoin::{taproot, Txid, Witness};
 use bitvm::signatures::winternitz::{Parameters, PublicKey, SecretKey};
 use eyre::{Context, Result};
 use std::any::Any;
 use std::fmt::Debug;
 
+use super::transaction::SecurityCouncil;
+
 #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
 pub enum SpendPath {
     ScriptSpend(usize),
@@ -179,6 +181,48 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
+    pubkeys: Vec<XOnlyPublicKey>,
+    threshold: u32,
+}
+
+impl SpendableScript for Multisig {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn kind(&self) -> ScriptKind {
+        ScriptKind::ManualSpend(self)
+    }
+
+    fn to_script_buf(&self) -> ScriptBuf {
+        let mut script_builder = Builder::new()
+            .push_x_only_key(&self.pubkeys[0])
+            .push_opcode(OP_CHECKSIG);
+        for pubkey in self.pubkeys.iter().skip(1) {
+            script_builder = script_builder.push_x_only_key(pubkey);
+            script_builder = script_builder.push_opcode(OP_CHECKSIGADD);
+        }
+        script_builder = script_builder.push_int(self.threshold as i64);
+        script_builder = script_builder.push_opcode(OP_NUMEQUAL);
+        script_builder.into_script()
+    }
+}
+
+impl Multisig {
+    pub fn new(pubkeys: Vec<XOnlyPublicKey>, threshold: u32) -> Self {
+        Self { pubkeys, threshold }
+    }
+
+    pub fn from_security_council(security_council: SecurityCouncil) -> Self {
+        Self {
+            pubkeys: security_council.pks,
+            threshold: security_council.threshold,
+        }
+    }
+}
+
 /// Struct for scripts that commit to a message using Winternitz keys
 /// Contains the Winternitz PK, CheckSig PK, message length respectively
 /// can contain multiple different Winternitz public keys for different messages
@@ -363,7 +407,7 @@ impl PreimageRevealScript {
 
 /// Struct for deposit script that commits Citrea address to be deposited into onchain.
 #[derive(Debug, Clone)]
-pub struct BaseDepositScript(pub(crate) XOnlyPublicKey, EVMAddress, Amount);
+pub struct BaseDepositScript(pub(crate) XOnlyPublicKey, EVMAddress);
 
 impl SpendableScript for BaseDepositScript {
     fn as_any(&self) -> &dyn Any {
@@ -384,7 +428,6 @@ impl SpendableScript for BaseDepositScript {
             .push_opcode(OP_IF)
             .push_slice(citrea)
             .push_slice(self.1 .0)
-            .push_slice(self.2.to_sat().to_be_bytes())
             .push_opcode(OP_ENDIF)
             .into_script()
     }
@@ -395,15 +438,15 @@ impl BaseDepositScript {
         Witness::from_slice(&[signature.serialize()])
     }
 
-    pub fn new(nofn_xonly_pk: XOnlyPublicKey, evm_address: EVMAddress, amount: Amount) -> Self {
-        Self(nofn_xonly_pk, evm_address, amount)
+    pub fn new(nofn_xonly_pk: XOnlyPublicKey, evm_address: EVMAddress) -> Self {
+        Self(nofn_xonly_pk, evm_address)
     }
 }
 
 /// Struct for deposit script that replaces an old move tx with a replacement deposit (to update bridge design on chain)
 /// It commits to the old move txid inside the script.
 #[derive(Debug, Clone)]
-pub struct ReplacementDepositScript(pub(crate) XOnlyPublicKey, Txid, Amount);
+pub struct ReplacementDepositScript(pub(crate) XOnlyPublicKey, Txid);
 
 impl SpendableScript for ReplacementDepositScript {
     fn as_any(&self) -> &dyn Any {
@@ -434,8 +477,8 @@ impl ReplacementDepositScript {
         Witness::from_slice(&[signature.serialize()])
     }
 
-    pub fn new(nofn_xonly_pk: XOnlyPublicKey, old_move_txid: Txid, amount: Amount) -> Self {
-        Self(nofn_xonly_pk, old_move_txid, amount)
+    pub fn new(nofn_xonly_pk: XOnlyPublicKey, old_move_txid: Txid) -> Self {
+        Self(nofn_xonly_pk, old_move_txid)
     }
 }
 
@@ -448,6 +491,7 @@ pub enum ScriptKind<'a> {
     BaseDepositScript(&'a BaseDepositScript),
     ReplacementDepositScript(&'a ReplacementDepositScript),
     Other(&'a OtherSpendable),
+    ManualSpend(&'a Multisig),
 }
 
 #[cfg(test)]
@@ -461,7 +505,8 @@ fn get_script_from_arr<T: SpendableScript>(
 #[cfg(test)]
 mod tests {
     use crate::actor::{Actor, WinternitzDerivationPath};
-    use crate::bitvm_client;
+    use crate::bitvm_client::{self, UNSPENDABLE_XONLY_PUBKEY};
+    use crate::builder::address::create_taproot_address;
     use crate::config::protocol::ProtocolParamsetName;
     use crate::extended_rpc::ExtendedRpc;
     use std::sync::Arc;
@@ -469,9 +514,9 @@ mod tests {
     use super::*;
 
     use bitcoin::hashes::Hash;
-    use bitcoin::secp256k1::PublicKey;
+    use bitcoin::secp256k1::{PublicKey, SecretKey};
     use bitcoincore_rpc::RpcApi;
-    use secp256k1::rand;
+    use secp256k1::rand::{self, Rng};
     // Create some dummy values for testing.
     // Note: These values are not cryptographically secure and are only used for tests.
     fn dummy_xonly() -> XOnlyPublicKey {
@@ -509,11 +554,7 @@ mod tests {
             )),
             Box::new(TimelockScript::new(Some(dummy_xonly()), 10)),
             Box::new(PreimageRevealScript::new(dummy_xonly(), [0; 20])),
-            Box::new(BaseDepositScript::new(
-                dummy_xonly(),
-                dummy_evm_address(),
-                Amount::from_sat(100),
-            )),
+            Box::new(BaseDepositScript::new(dummy_xonly(), dummy_evm_address())),
         ];
 
         // helper closures that return Option<(usize, &T)> using get_script_from_arr.
@@ -582,18 +623,13 @@ mod tests {
             ),
             (
                 "BaseDepositScript",
-                Arc::new(BaseDepositScript::new(
-                    dummy_xonly(),
-                    dummy_evm_address(),
-                    Amount::from_sat(50),
-                )),
+                Arc::new(BaseDepositScript::new(dummy_xonly(), dummy_evm_address())),
             ),
             (
                 "ReplacementDepositScript",
                 Arc::new(ReplacementDepositScript::new(
                     dummy_xonly(),
                     Txid::all_zeros(),
-                    Amount::from_sat(50),
                 )),
             ),
             ("Other", Arc::new(OtherSpendable::new(dummy_scriptbuf()))),
@@ -901,11 +937,8 @@ mod tests {
         let kp = bitcoin::secp256k1::Keypair::new(&SECP, &mut rand::thread_rng());
         let xonly_pk = kp.public_key().x_only_public_key().0;
 
-        let script: Arc<dyn SpendableScript> = Arc::new(BaseDepositScript::new(
-            xonly_pk,
-            EVMAddress([2; 20]),
-            Amount::from_sat(50),
-        ));
+        let script: Arc<dyn SpendableScript> =
+            Arc::new(BaseDepositScript::new(xonly_pk, EVMAddress([2; 20])));
         let scripts = vec![script];
         let (builder, _) = create_taproot_test_tx(
             &rpc,
@@ -941,11 +974,8 @@ mod tests {
         let kp = bitcoin::secp256k1::Keypair::new(&SECP, &mut rand::thread_rng());
         let xonly_pk = kp.public_key().x_only_public_key().0;
 
-        let script: Arc<dyn SpendableScript> = Arc::new(ReplacementDepositScript::new(
-            xonly_pk,
-            Txid::all_zeros(),
-            Amount::from_sat(50),
-        ));
+        let script: Arc<dyn SpendableScript> =
+            Arc::new(ReplacementDepositScript::new(xonly_pk, Txid::all_zeros()));
         let scripts = vec![script];
         let (builder, _) = create_taproot_test_tx(
             &rpc,
@@ -1040,4 +1070,60 @@ mod tests {
         assert_eq!(extracted[0], kickoff_blockhash);
         assert_eq!(extracted[1], assert_commit_data);
     }
+
+    #[tokio::test]
+    async fn test_multisig_matches_descriptor() {
+        let mut config = create_test_config_with_thread_name().await;
+        let regtest = create_regtest_rpc(&mut config).await;
+        let rpc = regtest.rpc().clone();
+
+        // select a random number of public keys
+        let num_pks = rand::thread_rng().gen_range(1..=10);
+        let threshold = rand::thread_rng().gen_range(1..=num_pks);
+
+        let mut pks = Vec::new();
+        for _ in 0..num_pks {
+            let secret_key = SecretKey::new(&mut rand::thread_rng());
+            let kp = bitcoin::secp256k1::Keypair::from_secret_key(&*SECP, &secret_key);
+            pks.push(kp.public_key().x_only_public_key().0);
+        }
+
+        let unspendable_xonly_pk_str = (*UNSPENDABLE_XONLY_PUBKEY).to_string();
+        let descriptor = format!(
+            "tr({},multi_a({},{}))",
+            unspendable_xonly_pk_str,
+            threshold,
+            pks.iter()
+                .map(|pk| pk.to_string())
+                .collect::<Vec<String>>()
+                .join(",")
+        );
+
+        let descriptor_info = rpc.client.get_descriptor_info(&descriptor).await.expect("");
+
+        let descriptor = descriptor_info.descriptor;
+
+        let addresses = rpc
+            .client
+            .derive_addresses(&descriptor, None)
+            .await
+            .expect("");
+
+        tracing::info!("{:?}", addresses);
+
+        let multisig_address = addresses[0].clone().assume_checked();
+
+        let multisig = Multisig::new(pks, threshold);
+
+        let (addr, _) = create_taproot_address(
+            &[multisig.to_script_buf()],
+            None,
+            config.protocol_paramset().network,
+        );
+
+        // println!("addr: {:?}", addr);
+        // println!("multisig_address: {:?}", multisig_address);
+
+        assert_eq!(addr, multisig_address);
+    }
 }
diff --git a/core/src/builder/transaction/mod.rs b/core/src/builder/transaction/mod.rs
index be3e1907e3..9e5c393cc3 100644
--- a/core/src/builder/transaction/mod.rs
+++ b/core/src/builder/transaction/mod.rs
@@ -3,7 +3,7 @@
 //! Transaction builder provides useful functions for building typical Bitcoin
 //! transactions.
 
-use super::script::{BaseDepositScript, CheckSig, SpendableScript, TimelockScript};
+use super::script::{BaseDepositScript, CheckSig, Multisig, SpendableScript, TimelockScript};
 use super::script::{ReplacementDepositScript, SpendPath};
 use crate::builder::transaction::challenge::*;
 use crate::builder::transaction::input::SpendableTxIn;
@@ -29,6 +29,7 @@ use bitcoin::secp256k1::PublicKey;
 use bitcoin::transaction::Version;
 use bitcoin::{Address, Amount, OutPoint, ScriptBuf, TxOut, Txid, XOnlyPublicKey};
 use eyre::Context;
+use hex;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use thiserror::Error;
@@ -111,6 +112,7 @@ pub struct DepositData {
     pub nofn_xonly_pk: Option<XOnlyPublicKey>,
     pub deposit: DepositInfo,
     pub actors: Actors,
+    pub security_council: SecurityCouncil,
 }
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
@@ -201,6 +203,90 @@ pub struct Actors {
     pub operators: Vec<XOnlyPublicKey>,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SecurityCouncil {
+    pub pks: Vec<XOnlyPublicKey>,
+    pub threshold: u32,
+}
+
+impl std::str::FromStr for SecurityCouncil {
+    type Err = eyre::Report;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let mut parts = s.split(':');
+        let threshold_str = parts
+            .next()
+            .ok_or_else(|| eyre::eyre!("Missing threshold"))?;
+        let pks_str = parts
+            .next()
+            .ok_or_else(|| eyre::eyre!("Missing public keys"))?;
+
+        if parts.next().is_some() {
+            return Err(eyre::eyre!("Too many parts in security council string"));
+        }
+
+        let threshold = threshold_str
+            .parse::<u32>()
+            .map_err(|e| eyre::eyre!("Invalid threshold: {}", e))?;
+
+        let pks: Result<Vec<XOnlyPublicKey>, _> = pks_str
+            .split(',')
+            .map(|pk_str| {
+                let bytes = hex::decode(pk_str)
+                    .map_err(|e| eyre::eyre!("Invalid hex in public key: {}", e))?;
+                XOnlyPublicKey::from_slice(&bytes)
+                    .map_err(|e| eyre::eyre!("Invalid public key: {}", e))
+            })
+            .collect();
+
+        let pks = pks?;
+
+        if pks.is_empty() {
+            return Err(eyre::eyre!("No public keys provided"));
+        }
+
+        if threshold > pks.len() as u32 {
+            return Err(eyre::eyre!(
+                "Threshold cannot be greater than number of public keys"
+            ));
+        }
+
+        Ok(SecurityCouncil { pks, threshold })
+    }
+}
+
+impl serde::Serialize for SecurityCouncil {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
+impl<'de> serde::Deserialize<'de> for SecurityCouncil {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        s.parse().map_err(serde::de::Error::custom)
+    }
+}
+
+impl std::fmt::Display for SecurityCouncil {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}:", self.threshold)?;
+        let pks_str = self
+            .pks
+            .iter()
+            .map(|pk| hex::encode(pk.serialize()))
+            .collect::<Vec<_>>()
+            .join(",");
+        write!(f, "{}", pks_str)
+    }
+}
+
 /// Type to uniquely identify a deposit.
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 pub struct BaseDepositData {
@@ -478,13 +564,15 @@ pub fn create_move_to_vault_txhandler(
     let nofn_xonly_pk = deposit_data.get_nofn_xonly_pk()?;
     let deposit_outpoint = deposit_data.get_deposit_outpoint();
     let nofn_script = Arc::new(CheckSig::new(nofn_xonly_pk));
+    let security_council_script = Arc::new(Multisig::from_security_council(
+        deposit_data.security_council.clone(),
+    ));
 
     let builder = match &mut deposit_data.deposit.deposit_type {
         DepositType::BaseDeposit(original_deposit_data) => {
             let deposit_script = Arc::new(BaseDepositScript::new(
                 nofn_xonly_pk,
                 original_deposit_data.evm_address,
-                paramset.bridge_amount,
             ));
 
             let recovery_script_pubkey = original_deposit_data
@@ -521,7 +609,6 @@ pub fn create_move_to_vault_txhandler(
             let deposit_script = Arc::new(ReplacementDepositScript::new(
                 nofn_xonly_pk,
                 replacement_deposit_data.old_move_txid,
-                paramset.bridge_amount,
             ));
 
             TxHandlerBuilder::new(TransactionType::MoveToVault)
@@ -531,7 +618,7 @@ pub fn create_move_to_vault_txhandler(
                     SpendableTxIn::from_scripts(
                         deposit_outpoint,
                         paramset.bridge_amount,
-                        vec![deposit_script],
+                        vec![deposit_script, security_council_script.clone()],
                         None,
                         paramset.network,
                     ),
@@ -544,7 +631,7 @@ pub fn create_move_to_vault_txhandler(
     Ok(builder
         .add_output(UnspentTxOut::from_scripts(
             paramset.bridge_amount - ANCHOR_AMOUNT,
-            vec![nofn_script],
+            vec![nofn_script, security_council_script],
             None,
             paramset.network,
         ))
@@ -559,6 +646,7 @@ pub fn create_replacement_deposit_txhandler(
     old_move_txid: Txid,
     nofn_xonly_pk: XOnlyPublicKey,
     paramset: &'static ProtocolParamset,
+    security_council: SecurityCouncil,
 ) -> Result<TxHandlerBuilder, BridgeError> {
     Ok(TxHandlerBuilder::new(TransactionType::ReplacementDeposit)
         .with_version(Version::non_standard(3))
@@ -570,7 +658,10 @@ pub fn create_replacement_deposit_txhandler(
                     vout: 0,
                 },
                 paramset.bridge_amount - ANCHOR_AMOUNT,
-                vec![Arc::new(CheckSig::new(nofn_xonly_pk))],
+                vec![
+                    Arc::new(CheckSig::new(nofn_xonly_pk)),
+                    Arc::new(Multisig::from_security_council(security_council.clone())),
+                ],
                 None,
                 paramset.network,
             ),
@@ -579,11 +670,10 @@ pub fn create_replacement_deposit_txhandler(
         )
         .add_output(UnspentTxOut::from_scripts(
             paramset.bridge_amount,
-            vec![Arc::new(ReplacementDepositScript::new(
-                nofn_xonly_pk,
-                old_move_txid,
-                paramset.bridge_amount,
-            ))],
+            vec![
+                Arc::new(ReplacementDepositScript::new(nofn_xonly_pk, old_move_txid)),
+                Arc::new(Multisig::from_security_council(security_council)),
+            ],
             None,
             paramset.network,
         ))
@@ -627,59 +717,70 @@ pub fn create_taproot_output_with_hidden_node(
 
 #[cfg(test)]
 mod tests {
-    // #[test]
-    // fn create_watchtower_challenge_page_txhandler() {
-    //     let network = bitcoin::Network::Regtest;
-    //     let secret_key = SecretKey::new(&mut rand::thread_rng());
-    //     let nofn_xonly_pk =
-    //         XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(&SECP, &secret_key)).0;
-    //     let (nofn_musig2_address, _) =
-    //         builder::address::create_musig2_address(nofn_xonly_pk, network);
-
-    //     let kickoff_outpoint = OutPoint {
-    //         txid: Txid::all_zeros(),
-    //         vout: 0x45,
-    //     };
-    //     let kickoff_utxo = UTXO {
-    //         outpoint: kickoff_outpoint,
-    //         txout: TxOut {
-    //             value: Amount::from_int_btc(2),
-    //             script_pubkey: nofn_musig2_address.script_pubkey(),
-    //         },
-    //     };
-
-    //     let bridge_amount_sats = Amount::from_sat(0x1F45);
-    //     let num_watchtowers = 3;
-
-    //     let wcp_txhandler = super::create_watchtower_challenge_page_txhandler(
-    //         &kickoff_utxo,
-    //         nofn_xonly_pk,
-    //         bridge_amount_sats,
-    //         num_watchtowers,
-    //         network,
-    //     );
-    //     assert_eq!(wcp_txhandler.tx.output.len(), num_watchtowers as usize);
-    // }
-
-    // #[test]
-    // fn create_challenge_tx() {
-    //     let operator_secret_key = SecretKey::new(&mut rand::thread_rng());
-    //     let operator_xonly_pk =
-    //         XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(&SECP, &operator_secret_key)).0;
-
-    //     let kickoff_outpoint = OutPoint {
-    //         txid: Txid::all_zeros(),
-    //         vout: 0x45,
-    //     };
-
-    //     let challenge_tx = super::create_challenge_tx(kickoff_outpoint, operator_xonly_pk);
-    //     assert_eq!(
-    //         challenge_tx.tx_out(0).unwrap().value,
-    //         Amount::from_int_btc(2)
-    //     );
-    //     assert_eq!(
-    //         challenge_tx.tx_out(0).unwrap().script_pubkey,
-    //         ScriptBuf::new_p2tr(&SECP, operator_xonly_pk, None)
-    //     )
-    // }
+    use super::*;
+    use bitcoin::secp256k1::XOnlyPublicKey;
+    use std::str::FromStr;
+
+    #[test]
+    fn test_security_council_from_str() {
+        // Create some test public keys
+        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
+        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
+
+        // Test valid input
+        let input = format!(
+            "2:{},{}",
+            hex::encode(pk1.serialize()),
+            hex::encode(pk2.serialize())
+        );
+        let council = SecurityCouncil::from_str(&input).unwrap();
+        assert_eq!(council.threshold, 2);
+        assert_eq!(council.pks.len(), 2);
+        assert_eq!(council.pks[0], pk1);
+        assert_eq!(council.pks[1], pk2);
+
+        // Test invalid threshold
+        let input = format!(
+            "3:{},{}",
+            hex::encode(pk1.serialize()),
+            hex::encode(pk2.serialize())
+        );
+        assert!(SecurityCouncil::from_str(&input).is_err());
+
+        // Test invalid hex
+        let input = "2:invalid,pk2";
+        assert!(SecurityCouncil::from_str(input).is_err());
+
+        // Test missing parts
+        assert!(SecurityCouncil::from_str("2").is_err());
+        assert!(SecurityCouncil::from_str(":").is_err());
+
+        // Test too many parts
+        let input = format!(
+            "2:{},{}:extra",
+            hex::encode(pk1.serialize()),
+            hex::encode(pk2.serialize())
+        );
+        assert!(SecurityCouncil::from_str(&input).is_err());
+
+        // Test empty public keys
+        assert!(SecurityCouncil::from_str("2:").is_err());
+    }
+
+    #[test]
+    fn test_security_council_round_trip() {
+        // Create some test public keys
+        let pk1 = XOnlyPublicKey::from_slice(&[1; 32]).unwrap();
+        let pk2 = XOnlyPublicKey::from_slice(&[2; 32]).unwrap();
+
+        let original = SecurityCouncil {
+            pks: vec![pk1, pk2],
+            threshold: 2,
+        };
+
+        let string = original.to_string();
+        let parsed = SecurityCouncil::from_str(&string).unwrap();
+
+        assert_eq!(original, parsed);
+    }
 }
diff --git a/core/src/cli.rs b/core/src/cli.rs
index 3f75c1f643..5552a4ecae 100644
--- a/core/src/cli.rs
+++ b/core/src/cli.rs
@@ -378,6 +378,10 @@ mod tests {
             "BRIDGE_CONTRACT_ADDRESS",
             "3100000000000000000000000000000000000002",
         );
+        env::set_var(
+            "SECURITY_COUNCIL",
+            "1:50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
+        );
     }
 
     // Helper to set up all environment variables needed for protocol paramset
@@ -425,6 +429,7 @@ mod tests {
         env::remove_var("CITREA_RPC_URL");
         env::remove_var("CITREA_LIGHT_CLIENT_PROVER_URL");
         env::remove_var("BRIDGE_CONTRACT_ADDRESS");
+        env::remove_var("SECURITY_COUNCIL");
     }
 
     // Helper to clean up all protocol paramset environment variables
diff --git a/core/src/config/env.rs b/core/src/config/env.rs
index b17bde8867..b9bf777800 100644
--- a/core/src/config/env.rs
+++ b/core/src/config/env.rs
@@ -1,7 +1,7 @@
 //! # Environment Variable Support For [`BridgeConfig`]
 
 use super::BridgeConfig;
-use crate::errors::BridgeError;
+use crate::{builder::transaction::SecurityCouncil, errors::BridgeError};
 use bitcoin::{secp256k1::SecretKey, Amount};
 use std::{path::PathBuf, str::FromStr};
 
@@ -105,6 +105,10 @@ impl BridgeConfig {
                 None
             };
 
+        let security_council_string = read_string_from_env("SECURITY_COUNCIL")?;
+
+        let security_council = SecurityCouncil::from_str(&security_council_string)?;
+
         let config = BridgeConfig {
             // Protocol paramset's source is independently defined
             protocol_paramset: Default::default(),
@@ -129,6 +133,7 @@ impl BridgeConfig {
             operator_endpoints,
             all_verifiers_secret_keys,
             all_operators_secret_keys,
+            security_council,
 
             #[cfg(test)]
             test_params: super::TestParams::default(),
@@ -187,6 +192,12 @@ mod tests {
             "BRIDGE_CONTRACT_ADDRESS",
             &default_config.bridge_contract_address,
         );
+
+        std::env::set_var(
+            "SECURITY_COUNCIL",
+            default_config.security_council.to_string(),
+        );
+
         if let Some(ref header_chain_proof_path) = default_config.header_chain_proof_path {
             std::env::set_var("HEADER_CHAIN_PROOF_PATH", header_chain_proof_path);
         }
diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs
index fc4e1e3f77..9c5c9c0dc8 100644
--- a/core/src/config/mod.rs
+++ b/core/src/config/mod.rs
@@ -10,6 +10,8 @@
 //! Configuration options can be read from a TOML file. File contents are
 //! described in `BridgeConfig` struct.
 
+use crate::bitvm_client::UNSPENDABLE_XONLY_PUBKEY;
+use crate::builder::transaction::SecurityCouncil;
 use crate::errors::BridgeError;
 use bitcoin::secp256k1::SecretKey;
 use bitcoin::Amount;
@@ -79,6 +81,9 @@ pub struct BridgeConfig {
     // Initial header chain proof receipt's file path.
     pub header_chain_proof_path: Option<PathBuf>,
 
+    /// Security council.
+    pub security_council: SecurityCouncil,
+
     /// Verifier endpoints. For the aggregator only
     pub verifier_endpoints: Option<Vec<String>>,
     /// Operator endpoint. For the aggregator only
@@ -169,6 +174,11 @@ impl Default for BridgeConfig {
                 PathBuf::from_str("../core/tests/data/first_1.bin").expect("known valid input"),
             ),
 
+            security_council: SecurityCouncil {
+                pks: vec![*UNSPENDABLE_XONLY_PUBKEY],
+                threshold: 1,
+            },
+
             all_verifiers_secret_keys: Some(vec![
                 SecretKey::from_str(
                     "1111111111111111111111111111111111111111111111111111111111111111",
diff --git a/core/src/extended_rpc.rs b/core/src/extended_rpc.rs
index fa05dda108..f781b6122d 100644
--- a/core/src/extended_rpc.rs
+++ b/core/src/extended_rpc.rs
@@ -5,10 +5,6 @@
 use std::str::FromStr;
 use std::sync::Arc;
 
-use crate::builder;
-use crate::errors::ResultExt;
-use crate::EVMAddress;
-use bitcoin::address::NetworkUnchecked;
 use bitcoin::Address;
 use bitcoin::Amount;
 use bitcoin::BlockHash;
@@ -17,7 +13,6 @@ use bitcoin::OutPoint;
 use bitcoin::ScriptBuf;
 use bitcoin::TxOut;
 use bitcoin::Txid;
-use bitcoin::XOnlyPublicKey;
 use bitcoincore_rpc::Auth;
 use bitcoincore_rpc::Client;
 use bitcoincore_rpc::RpcApi;
@@ -243,64 +238,6 @@ impl ExtendedRpc {
         Ok(txout)
     }
 
-    /// Verifies that a deposit UTXO meets the required criteria.
-    ///
-    /// # Arguments
-    /// * `nofn_xonly_pk` - The x-only public key of the node
-    /// * `deposit_outpoint` - The outpoint of the deposit transaction
-    /// * `recovery_taproot_address` - The recovery taproot address
-    /// * `evm_address` - The Ethereum address
-    /// * `amount_sats` - The expected amount in satoshis
-    /// * `confirmation_block_count` - The required number of confirmations
-    /// * `network` - The Bitcoin network
-    /// * `user_takes_after` - The number of blocks after which the user can take control
-    ///
-    /// # Returns
-    /// `Ok(())` if the deposit UTXO is valid, or an error if any check fails.
-    #[tracing::instrument(skip(self), err(level = tracing::Level::ERROR), ret(level = tracing::Level::TRACE))]
-    pub async fn check_deposit_utxo(
-        &self,
-        nofn_xonly_pk: XOnlyPublicKey,
-        deposit_outpoint: &OutPoint,
-        recovery_taproot_address: &Address<NetworkUnchecked>,
-        evm_address: EVMAddress,
-        amount_sats: Amount,
-        confirmation_block_count: u32,
-        network: bitcoin::Network,
-        user_takes_after: u16,
-    ) -> Result<()> {
-        if self.confirmation_blocks(&deposit_outpoint.txid).await? < confirmation_block_count {
-            return Err(eyre::eyre!("Deposit not finalized").into());
-        }
-
-        let (deposit_address, _) = builder::address::generate_deposit_address(
-            nofn_xonly_pk,
-            recovery_taproot_address,
-            evm_address,
-            amount_sats,
-            network,
-            user_takes_after,
-        )
-        .map_to_eyre()?;
-
-        if !self
-            .check_utxo_address_and_amount(
-                deposit_outpoint,
-                &deposit_address.script_pubkey(),
-                amount_sats,
-            )
-            .await?
-        {
-            return Err(eyre::eyre!("Invalid deposit UTXO").into());
-        }
-
-        if self.is_utxo_spent(deposit_outpoint).await? {
-            return Err(eyre::eyre!("Deposit UTXO has already spent").into());
-        }
-
-        Ok(())
-    }
-
     /// Bumps the fee of a transaction to meet or exceed a target fee rate. Does
     /// nothing if the transaction is already confirmed. Returns the original
     /// txid if no bump was needed.
diff --git a/core/src/rpc/aggregator.rs b/core/src/rpc/aggregator.rs
index 6cbca3b299..55062c745e 100644
--- a/core/src/rpc/aggregator.rs
+++ b/core/src/rpc/aggregator.rs
@@ -809,6 +809,7 @@ impl ClementineAggregator for Aggregator {
                 watchtowers: vec![],
                 operators: self.get_operator_keys(),
             },
+            security_council: self.config.security_council.clone(),
         };
 
         let deposit_params = deposit_data.clone().into();
@@ -1146,7 +1147,6 @@ mod tests {
             nofn_xonly_pk,
             signer.address.as_unchecked(),
             evm_address,
-            config.protocol_paramset().bridge_amount,
             config.protocol_paramset().network,
             config.protocol_paramset().user_takes_after,
         )
@@ -1235,7 +1235,6 @@ mod tests {
             nofn_xonly_pk,
             signer.address.as_unchecked(),
             evm_address,
-            config.protocol_paramset().bridge_amount,
             config.protocol_paramset().network,
             config.protocol_paramset().user_takes_after,
         )
diff --git a/core/src/rpc/clementine.proto b/core/src/rpc/clementine.proto
index 9e09db6b65..138a13f586 100644
--- a/core/src/rpc/clementine.proto
+++ b/core/src/rpc/clementine.proto
@@ -99,6 +99,12 @@ message WinternitzPubkey {
 message DepositParams {
   Deposit deposit = 1;
   Actors actors = 2;
+  SecurityCouncil security_council = 3;
+}
+
+message SecurityCouncil {
+  repeated bytes pks = 1;
+  uint32 threshold = 2;
 }
 
 message Deposit {
diff --git a/core/src/rpc/clementine.rs b/core/src/rpc/clementine.rs
index 1d95a59962..48d40c2b05 100644
--- a/core/src/rpc/clementine.rs
+++ b/core/src/rpc/clementine.rs
@@ -78,6 +78,15 @@ pub struct DepositParams {
     pub deposit: ::core::option::Option<Deposit>,
     #[prost(message, optional, tag = "2")]
     pub actors: ::core::option::Option<Actors>,
+    #[prost(message, optional, tag = "3")]
+    pub security_council: ::core::option::Option<SecurityCouncil>,
+}
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct SecurityCouncil {
+    #[prost(bytes = "vec", repeated, tag = "1")]
+    pub pks: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
+    #[prost(uint32, tag = "2")]
+    pub threshold: u32,
 }
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct Deposit {
diff --git a/core/src/rpc/parser/mod.rs b/core/src/rpc/parser/mod.rs
index 497c7a1cc1..3c1f523658 100644
--- a/core/src/rpc/parser/mod.rs
+++ b/core/src/rpc/parser/mod.rs
@@ -6,6 +6,7 @@ use super::error;
 use crate::builder::transaction::sign::TransactionRequestData;
 use crate::builder::transaction::{
     Actors, BaseDepositData, DepositData, DepositInfo, DepositType, ReplacementDepositData,
+    SecurityCouncil,
 };
 use crate::errors::BridgeError;
 use crate::tx_sender::FeePayingType;
@@ -221,12 +222,13 @@ impl TryFrom<clementine::Deposit> for DepositInfo {
 impl From<DepositData> for DepositParams {
     fn from(value: DepositData) -> Self {
         let actors: clementine::Actors = value.actors.into();
-
+        let security_council: clementine::SecurityCouncil = value.security_council.into();
         let deposit: clementine::Deposit = value.deposit.into();
 
         DepositParams {
             deposit: Some(deposit),
             actors: Some(actors),
+            security_council: Some(security_council),
         }
     }
 }
@@ -244,10 +246,16 @@ impl TryFrom<DepositParams> for DepositData {
             .ok_or(Status::invalid_argument("No actors received"))?
             .try_into()?;
 
+        let security_council: SecurityCouncil = value
+            .security_council
+            .ok_or(Status::invalid_argument("No security council received"))?
+            .try_into()?;
+
         Ok(DepositData {
             nofn_xonly_pk: None,
             deposit,
             actors,
+            security_council,
         })
     }
 }
@@ -370,6 +378,40 @@ impl From<Actors> for clementine::Actors {
     }
 }
 
+impl From<SecurityCouncil> for clementine::SecurityCouncil {
+    fn from(value: SecurityCouncil) -> Self {
+        clementine::SecurityCouncil {
+            pks: value
+                .pks
+                .into_iter()
+                .map(|pk| pk.serialize().to_vec())
+                .collect(),
+            threshold: value.threshold,
+        }
+    }
+}
+
+impl TryFrom<clementine::SecurityCouncil> for SecurityCouncil {
+    type Error = Status;
+
+    fn try_from(value: clementine::SecurityCouncil) -> Result<Self, Self::Error> {
+        let pks = value
+            .pks
+            .into_iter()
+            .map(|pk| {
+                XOnlyPublicKey::from_slice(&pk).map_err(|e| {
+                    Status::invalid_argument(format!("Failed to parse xonly public key: {}", e))
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?;
+
+        Ok(SecurityCouncil {
+            pks,
+            threshold: value.threshold,
+        })
+    }
+}
+
 impl TryFrom<RawSignedTx> for bitcoin::Transaction {
     type Error = Status;
 
diff --git a/core/src/test/common/citrea/mod.rs b/core/src/test/common/citrea/mod.rs
index ef4fc1f526..bc2f6ee46e 100644
--- a/core/src/test/common/citrea/mod.rs
+++ b/core/src/test/common/citrea/mod.rs
@@ -34,7 +34,13 @@ pub fn get_bridge_params() -> String {
             .unwrap()
             .to_string();
 
-    format!("000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000008ac7230489e80000000000000000000000000000000000000000000000000000000000000000002d4a20{}ac0063066369747265611400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a08000000003b9aca006800000000000000000000000000000000000000000000", nofn_xonly_pk)
+    let bridge_params = format!(
+        "000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000008ac7230489e80000000000000000000000000000000000000000000000000000000000000000002d4120{}ac006306636974726561140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016800000000000000000000000000000000000000000000000000000000000000", nofn_xonly_pk
+    );
+
+    tracing::info!("Bridge params: {}", bridge_params);
+
+    bridge_params
 }
 
 /// Citrea e2e hardcoded EVM secret keys.
diff --git a/core/src/test/common/mod.rs b/core/src/test/common/mod.rs
index 3295a1e541..df4cd8f38d 100644
--- a/core/src/test/common/mod.rs
+++ b/core/src/test/common/mod.rs
@@ -4,11 +4,11 @@ use std::time::Duration;
 use crate::actor::Actor;
 use crate::bitvm_client::SECP;
 use crate::builder::address::create_taproot_address;
-use crate::builder::script::{CheckSig, SpendPath, SpendableScript};
+use crate::builder::script::{CheckSig, Multisig, SpendPath, SpendableScript};
 use crate::builder::transaction::input::SpendableTxIn;
 use crate::builder::transaction::{
     create_replacement_deposit_txhandler, BaseDepositData, DepositInfo, DepositType,
-    ReplacementDepositData, TxHandler, DEFAULT_SEQUENCE,
+    ReplacementDepositData, SecurityCouncil, TxHandler, DEFAULT_SEQUENCE,
 };
 use crate::citrea::mock::MockCitreaClient;
 use crate::citrea::CitreaClientT;
@@ -433,6 +433,7 @@ fn sign_nofn_deposit_tx(
     deposit_tx: &TxHandler,
     config: &BridgeConfig,
     verifiers_public_keys: Vec<PublicKey>,
+    security_council: SecurityCouncil,
 ) -> Transaction {
     let nofn_xonly_pk =
         bitcoin::XOnlyPublicKey::from_musig2_pks(verifiers_public_keys.clone(), None).unwrap();
@@ -501,8 +502,9 @@ fn sign_nofn_deposit_tx(
     let mut witness = Witness::from_slice(&[final_taproot_sig.serialize()]);
     // get script of movetx
     let script_buf = CheckSig::new(nofn_xonly_pk).to_script_buf();
+    let multisig_script_buf = Multisig::from_security_council(security_council).to_script_buf();
     let (_, spend_info) = create_taproot_address(
-        &[script_buf.clone()],
+        &[script_buf.clone(), multisig_script_buf.clone()],
         None,
         config.protocol_paramset().network,
     );
@@ -549,9 +551,30 @@ pub async fn run_replacement_deposit(
 
     tracing::info!("First deposit move txid: {}", move_txid);
 
+    let (addr, _) = create_taproot_address(
+        &[
+            CheckSig::new(nofn_xonly_pk).to_script_buf(),
+            Multisig::from_security_council(config.security_council.clone()).to_script_buf(),
+        ],
+        None,
+        config.protocol_paramset().network,
+    );
+
+    let move_tx = rpc
+        .client
+        .get_raw_transaction(&move_txid, None)
+        .await
+        .expect("Failed to get move tx");
+
+    assert_eq!(move_tx.output[0].script_pubkey, addr.script_pubkey());
+
     // generate replacement deposit tx
-    let new_deposit_tx =
-        create_replacement_deposit_txhandler(move_txid, nofn_xonly_pk, config.protocol_paramset())?;
+    let new_deposit_tx = create_replacement_deposit_txhandler(
+        move_txid,
+        nofn_xonly_pk,
+        config.protocol_paramset(),
+        config.security_council.clone(),
+    )?;
     let some_funding_utxo = rpc
         .send_to_address(
             &create_taproot_address(
@@ -594,8 +617,12 @@ pub async fn run_replacement_deposit(
         )
         .expect("Failed to sign replacement deposit tx");
 
-    let replacement_deposit_tx =
-        sign_nofn_deposit_tx(&new_deposit_tx, config, verifiers_public_keys.clone());
+    let replacement_deposit_tx = sign_nofn_deposit_tx(
+        &new_deposit_tx,
+        config,
+        verifiers_public_keys.clone(),
+        config.security_council.clone(),
+    );
 
     aggregator
         .internal_send_tx(SendTxRequest {
diff --git a/core/src/test/common/setup_utils.rs b/core/src/test/common/setup_utils.rs
index c471c6096d..e226719c62 100644
--- a/core/src/test/common/setup_utils.rs
+++ b/core/src/test/common/setup_utils.rs
@@ -492,7 +492,6 @@ pub fn get_deposit_address(
         nofn_xonly_pk,
         signer.address.as_unchecked(),
         evm_address,
-        config.protocol_paramset().bridge_amount,
         config.protocol_paramset().network,
         config.protocol_paramset().user_takes_after,
     )
diff --git a/core/src/tx_sender/rbf.rs b/core/src/tx_sender/rbf.rs
index eec698b0d4..0f3095187a 100644
--- a/core/src/tx_sender/rbf.rs
+++ b/core/src/tx_sender/rbf.rs
@@ -836,6 +836,7 @@ mod tests {
     use bitcoin::hashes::Hash;
     use bitcoin::transaction::Version;
     use bitcoin::TxOut;
+    use bitcoincore_rpc::json::GetRawTransactionResult;
     use std::result::Result;
     use std::time::Duration;
 
@@ -1344,7 +1345,10 @@ mod tests {
                     .get_raw_transaction_info(&tx.compute_txid(), None)
                     .await;
 
-                Ok(tx_result.is_ok() && tx_result.unwrap().confirmations.unwrap() > 0)
+                Ok(matches!(tx_result, Ok(GetRawTransactionResult {
+                    confirmations: Some(confirmations),
+                    ..
+                }) if confirmations > 0))
             },
             Some(Duration::from_secs(30)),
             Some(Duration::from_millis(100)),
diff --git a/core/tests/data/test_config.toml b/core/tests/data/test_config.toml
index b1fd2a090b..a286b922df 100644
--- a/core/tests/data/test_config.toml
+++ b/core/tests/data/test_config.toml
@@ -47,6 +47,10 @@ bridge_contract_address = "3100000000000000000000000000000000000002"
 # Header chain prover's assumption to start with.
 header_chain_proof_path = "../core/tests/data/first_1.bin"
 
+security_council = "1:50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
+
+
+
 verifier_endpoints = ["http://127.0.0.1:17001", "http://127.0.0.1:17002", "http://127.0.0.1:17003", "http://127.0.0.1:17004"]
 
 operator_endpoints = ["http://127.0.0.1:17005", "http://127.0.0.1:17006"]
diff --git a/scripts/docker/docker_config.toml b/scripts/docker/docker_config.toml
index 4b8b0e72e1..fc8dd028a8 100644
--- a/scripts/docker/docker_config.toml
+++ b/scripts/docker/docker_config.toml
@@ -66,6 +66,8 @@ bridge_contract_address = "3100000000000000000000000000000000000002"
 # Header chain prover's assumption to start with.
 header_chain_proof_path = "../core/tests/data/first_1.bin"
 
+security_council = "1:50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
+
 # Testing only fields.
 all_verifiers_secret_keys = [
     "1111111111111111111111111111111111111111111111111111111111111111",


Reviews:
user: login: mmtftr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:05:58Z
user: login: ekrembal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T16:43:58Z
user: login: mmtftr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T08:12:14Z
user: login: ceyhunsen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T08:25:36Z
user: login: ekrembal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T14:48:05Z
user: login: ekrembal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T14:50:54Z
user: login: ekrembal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:09:56Z
user: login: ekrembal
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:10:04Z
user: login: mmtftr
body: looks good to me, thanks
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:32:38Z
user: login: aoengin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T19:03:22Z

Review comments:
diff_hunk: @@ -180,6 +182,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
+    pubkeys: Vec<XOnlyPublicKey>,
+    threshold: u32,
+}
+
+impl SpendableScript for Multisig {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn kind(&self) -> ScriptKind {
+        unimplemented!()
path: core/src/builder/script.rs
user: login: mmtftr
body: better to define this even if you're not signing it.

we might want to have ScriptKind::ManualSpend for scripts we're not going to automatically sign.
created_at: 2025-05-02T08:05:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,6 +182,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
+    pubkeys: Vec<XOnlyPublicKey>,
+    threshold: u32,
+}
+
+impl SpendableScript for Multisig {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn kind(&self) -> ScriptKind {
+        unimplemented!()
path: core/src/builder/script.rs
user: login: ekrembal
body: Makes sense
created_at: 2025-05-02T16:43:58Z
author_association: MEMBER
diff_hunk: @@ -154,7 +153,9 @@ enum AggregatorCommands {
         #[arg(long)]
         network: Option<String>,
         #[arg(long)]
-        bridge_amount: Option<u64>,
+        security_council_pks: Option<String>,
path: core/src/bin/cli.rs
user: login: mmtftr
body: add docs: 

comma separated list of xonly pks.
created_at: 2025-05-05T07:47:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +181,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
path: core/src/builder/script.rs
user: login: mmtftr
body: isn't MofN a better name here?
created_at: 2025-05-05T07:48:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +181,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
+    pubkeys: Vec<XOnlyPublicKey>,
+    threshold: u32,
+}
+
+impl SpendableScript for Multisig {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn kind(&self) -> ScriptKind {
+        unimplemented!()
+    }
+
+    fn to_script_buf(&self) -> ScriptBuf {
+        let mut script_builder = Builder::new()
+            .push_x_only_key(&self.pubkeys[0])
+            .push_opcode(OP_CHECKSIG);
+        for pubkey in self.pubkeys.iter().skip(1) {
+            script_builder = script_builder.push_x_only_key(pubkey);
+            script_builder = script_builder.push_opcode(OP_CHECKSIGADD);
+        }
+        script_builder = script_builder.push_int(self.threshold as i64);
+        script_builder = script_builder.push_opcode(OP_NUMEQUAL);
+        script_builder.into_script()
+    }
+}
+
+impl Multisig {
+    pub fn new(security_council: SecurityCouncil) -> Self {
path: core/src/builder/script.rs
user: login: mmtftr
body: would rather use `from_security_council` for this function. Otherwise, it looks like you're using an unrelated type for the creator.

`new` should be a simple constructor that takes `keys: Vec<XOnlyPublicKey>, threshold: int`
created_at: 2025-05-05T07:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -745,17 +744,23 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
                 None => bitcoin::Network::Regtest,
             };
 
-            let bridge_amount = match bridge_amount {
-                Some(amount) => Amount::from_sat(amount),
-                None => Amount::from_sat(1_000_000_000),
-            };
-
             let (replacement_deposit_address, _) =
                 clementine_core::builder::address::generate_replacement_deposit_address(
                     move_txid,
                     nofn_xonly_pk,
-                    bridge_amount,
                     network,
+                    SecurityCouncil {
+                        pks: security_council_pks
+                            .unwrap_or_default()
+                            .split(',')
path: core/src/bin/cli.rs
user: login: mmtftr
body: this code is duplicated below in env parsing. Instead, can we implement FromStr for SecurityCouncil and use that? We'd have to encode the entire thing in a single string but wouldn't an encoding like

`threshold:pk1,pk2` be appropriate here?

eg. `1:abcdbcb123,abcbdb321` 

Another benefit in the parser is that we'd be able to verify that the threshold is less than or equal to the # of keys we're provided
created_at: 2025-05-05T08:00:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -745,17 +744,23 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
                 None => bitcoin::Network::Regtest,
             };
 
-            let bridge_amount = match bridge_amount {
-                Some(amount) => Amount::from_sat(amount),
-                None => Amount::from_sat(1_000_000_000),
-            };
-
             let (replacement_deposit_address, _) =
                 clementine_core::builder::address::generate_replacement_deposit_address(
                     move_txid,
                     nofn_xonly_pk,
-                    bridge_amount,
                     network,
+                    SecurityCouncil {
+                        pks: security_council_pks
+                            .unwrap_or_default()
+                            .split(',')
path: core/src/bin/cli.rs
user: login: mmtftr
body: also let's not use expects in the actual implementation of FromStr
created_at: 2025-05-05T08:00:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -201,6 +202,12 @@ pub struct Actors {
     pub operators: Vec<XOnlyPublicKey>,
 }
 
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
+pub struct SecurityCouncil {
path: core/src/builder/transaction/mod.rs
user: login: mmtftr
body: Would it be better to name this MofN and move it to script? This doesn't have to be specific to a `SecurityCouncil`. We can type-alias SecurityCouncil to MofN with `pub type SecurityCouncil = MofN`
created_at: 2025-05-05T08:01:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +181,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
path: core/src/builder/script.rs
user: login: ceyhunsen
body: Multisig is the common name i believe
created_at: 2025-05-05T08:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,6 +182,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
+    pubkeys: Vec<XOnlyPublicKey>,
+    threshold: u32,
+}
+
+impl SpendableScript for Multisig {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn kind(&self) -> ScriptKind {
+        unimplemented!()
path: core/src/builder/script.rs
user: login: ekrembal
body: 63b2ce9
created_at: 2025-05-05T14:48:05Z
author_association: MEMBER
diff_hunk: @@ -179,6 +181,44 @@ impl CheckSig {
     }
 }
 
+#[derive(Clone)]
+pub struct Multisig {
+    pubkeys: Vec<XOnlyPublicKey>,
+    threshold: u32,
+}
+
+impl SpendableScript for Multisig {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn kind(&self) -> ScriptKind {
+        unimplemented!()
+    }
+
+    fn to_script_buf(&self) -> ScriptBuf {
+        let mut script_builder = Builder::new()
+            .push_x_only_key(&self.pubkeys[0])
+            .push_opcode(OP_CHECKSIG);
+        for pubkey in self.pubkeys.iter().skip(1) {
+            script_builder = script_builder.push_x_only_key(pubkey);
+            script_builder = script_builder.push_opcode(OP_CHECKSIGADD);
+        }
+        script_builder = script_builder.push_int(self.threshold as i64);
+        script_builder = script_builder.push_opcode(OP_NUMEQUAL);
+        script_builder.into_script()
+    }
+}
+
+impl Multisig {
+    pub fn new(security_council: SecurityCouncil) -> Self {
path: core/src/builder/script.rs
user: login: ekrembal
body: 3cc57e1
created_at: 2025-05-05T14:50:53Z
author_association: MEMBER
diff_hunk: @@ -201,6 +202,12 @@ pub struct Actors {
     pub operators: Vec<XOnlyPublicKey>,
 }
 
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
+pub struct SecurityCouncil {
path: core/src/builder/transaction/mod.rs
user: login: ekrembal
body: I think this way is better
created_at: 2025-05-05T15:09:56Z
author_association: MEMBER
diff_hunk: @@ -745,17 +744,23 @@ async fn handle_aggregator_call(url: String, command: AggregatorCommands) {
                 None => bitcoin::Network::Regtest,
             };
 
-            let bridge_amount = match bridge_amount {
-                Some(amount) => Amount::from_sat(amount),
-                None => Amount::from_sat(1_000_000_000),
-            };
-
             let (replacement_deposit_address, _) =
                 clementine_core::builder::address::generate_replacement_deposit_address(
                     move_txid,
                     nofn_xonly_pk,
-                    bridge_amount,
                     network,
+                    SecurityCouncil {
+                        pks: security_council_pks
+                            .unwrap_or_default()
+                            .split(',')
path: core/src/bin/cli.rs
user: login: ekrembal
body: done
created_at: 2025-05-05T15:10:03Z
author_association: MEMBER

PR comments:
user: login: mmtftr
created_at: 2025-05-02T08:06:29Z
body: What is the security council? Is it the trusted set that can replace deposits?
author_association: CONTRIBUTOR
user: login: ekrembal
created_at: 2025-05-02T16:43:37Z
body: @mmtftr Yes, for upgradability
author_association: MEMBER