MultiSig API
Multi-Signature (MultiSig) is implemented in src/multisig.rs and supports Bitcoin’s M-of-N signature scheme: out of N participants, at least M valid ECDSA signatures are required to authorize fund movement.
Core Concepts
M-of-N signatures: N participants each hold a key pair; any M of them can authorize a transaction. Common combinations:
| Type | Meaning | Typical Use Case |
|---|---|---|
| 2-of-2 | Both must agree | Joint accounts, partnerships |
| 2-of-3 | Any two of three agree | Corporate fund management, escrow services |
| 3-of-5 | Any three of five agree | Large institutions, board decisions |
Address format: Multisig addresses start with "3", corresponding to Bitcoin’s P2SH (Pay-to-Script-Hash) format.
MultiSigAddress Struct
Represents an M-of-N multisig address and its configuration.
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiSigAddress {
pub address: String, // Multisig address ("3"-prefixed, P2SH format)
pub required_sigs: usize, // Required number of signatures M
pub total_keys: usize, // Total number of keys N
pub public_keys: Vec<String>, // Public key list for all participants
pub script: String, // Locking script (simplified Script code)
}
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
address | String | Multisig address starting with "3", generated by taking 42 characters of the SHA256 hash of the locking script. |
required_sigs | usize | M value: minimum number of valid signatures required to spend funds. |
total_keys | usize | N value: total number of participants (equals public_keys.len()). |
public_keys | Vec<String> | Public key list of all participants (hex-encoded). |
script | String | Locking script in the format OP_{M}{pubkeys...}OP_CHECKMULTISIG. |
MultiSigAddress Methods
MultiSigAddress::new
Creates an M-of-N multisig address. Validates parameter legality, then generates the locking script and the corresponding P2SH address.
#![allow(unused)]
fn main() {
pub fn new(
required_sigs: usize,
public_keys: Vec<String>,
) -> Result<Self, String>
}
Parameters:
required_sigs— Required number of signatures M; must satisfy1 <= M <= N.public_keys— List of all participants’ public keys; length equals N, maximum 15.
Return value:
Ok(MultiSigAddress)— Created successfully.Err(String)— Invalid parameters; error reasons include:"Invalid signature requirement"—required_sigs == 0orrequired_sigs > total_keys."Maximum 15 keys supported"—public_keys.len() > 15(Bitcoin protocol limit).
#![allow(unused)]
fn main() {
use simplebtc::multisig::MultiSigAddress;
use simplebtc::wallet::Wallet;
// Create three participant wallets
let wallet1 = Wallet::new();
let wallet2 = Wallet::new();
let wallet3 = Wallet::new();
let public_keys = vec![
wallet1.public_key.clone(),
wallet2.public_key.clone(),
wallet3.public_key.clone(),
];
// Create 2-of-3 multisig address
let multisig = MultiSigAddress::new(2, public_keys).unwrap();
assert!(multisig.address.starts_with('3'));
assert_eq!(multisig.required_sigs, 2);
assert_eq!(multisig.total_keys, 3);
println!("Multisig address: {}", multisig.address);
println!("Locking script: {}", multisig.script);
// Parameter validation error examples
let result = MultiSigAddress::new(0, vec!["key1".to_string()]);
assert!(result.is_err()); // required_sigs cannot be 0
let result = MultiSigAddress::new(3, vec!["key1".to_string(), "key2".to_string()]);
assert!(result.is_err()); // required_sigs(3) > total_keys(2)
}
MultiSigAddress::verify_signatures
Quickly checks whether the number of signatures meets the requirement (does not verify signature content, only validates count).
#![allow(unused)]
fn main() {
pub fn verify_signatures(&self, signatures: &[String]) -> bool
}
Parameters:
signatures— List of signatures.
Return value: signatures.len() >= self.required_sigs.
#![allow(unused)]
fn main() {
let sigs = vec!["sig1".to_string(), "sig2".to_string()];
let count_ok = multisig.verify_signatures(&sigs);
println!("Signature count meets requirement: {}", count_ok); // true (2 >= 2)
}
MultiSigAddress::verify_signatures_with_data
Full signature verification: not only checks count, but also uses ECDSA to verify whether each signature was produced by one of the keys in public_keys.
#![allow(unused)]
fn main() {
pub fn verify_signatures_with_data(
&self,
signatures: &[String],
data: &str,
) -> bool
}
Parameters:
signatures— List of ECDSA signatures in hex DER encoding.data— The original data string that was signed (typically a transaction ID or transaction digest).
Return value:
true— Number of valid signatures >=required_sigs. Each signature matches at most one public key (preventing the same signature from being counted twice).false— Insufficient valid signatures, or signatures do not correspond to any key inpublic_keys.
#![allow(unused)]
fn main() {
// Sign transaction data with wallet1 and wallet2
let data = "Transaction digest: Alice transfers 0.01 BTC to Bob";
let sig1 = wallet1.sign(data);
let sig2 = wallet2.sign(data);
let valid = multisig.verify_signatures_with_data(
&[sig1, sig2],
data,
);
println!("ECDSA verification passed: {}", valid);
}
MultiSigTxBuilder Struct
The multisig transaction builder is responsible for collecting signatures and confirming whether the M-of-N requirement is met. Built-in duplicate signature detection.
#![allow(unused)]
fn main() {
pub struct MultiSigTxBuilder {
pub multisig_address: MultiSigAddress,
pub signatures: Vec<String>,
// signed_keys: HashMap<String, bool> // Private field, prevents same public key from signing twice
}
}
| Field | Type | Description |
|---|---|---|
multisig_address | MultiSigAddress | Associated multisig address configuration (includes M, N, public key list). |
signatures | Vec<String> | List of collected valid ECDSA signatures. |
MultiSigTxBuilder Methods
MultiSigTxBuilder::new
Creates a multisig transaction builder associated with the specified MultiSigAddress.
#![allow(unused)]
fn main() {
pub fn new(multisig_address: MultiSigAddress) -> Self
}
Parameters:
multisig_address— An already-createdMultiSigAddressinstance.
#![allow(unused)]
fn main() {
use simplebtc::multisig::MultiSigTxBuilder;
let builder = MultiSigTxBuilder::new(multisig);
assert_eq!(builder.signatures.len(), 0);
assert!(!builder.is_complete());
}
MultiSigTxBuilder::add_signature
Adds an ECDSA signature from a participant. Internally validates automatically:
- The wallet’s public key must be in
multisig_address.public_keys. - The wallet cannot sign twice (prevents the same person from signing twice to fake M signatures).
#![allow(unused)]
fn main() {
pub fn add_signature(
&mut self,
wallet: &Wallet,
data: &str,
) -> Result<(), String>
}
Parameters:
wallet— The participant’sWalletinstance (used to callwallet.sign(data)to generate a signature).data— Data to sign (typically a transaction digest or transaction ID).
Return value:
Ok(())— Signature added successfully.Err(String)— Error reasons:"This wallet is not in the multisig address"— The wallet’s public key is not in thepublic_keyslist."This wallet has already signed"— The wallet has previously signed.
#![allow(unused)]
fn main() {
let mut builder = MultiSigTxBuilder::new(multisig.clone());
let data = "transfer_tx_hash_abc123";
// wallet1 signs successfully
builder.add_signature(&wallet1, data).unwrap();
assert_eq!(builder.signatures.len(), 1);
// wallet1 cannot sign again
let err = builder.add_signature(&wallet1, data);
assert!(err.is_err());
println!("Duplicate signature error: {}", err.unwrap_err()); // "This wallet has already signed"
// A wallet not in the multisig address cannot sign
let outsider = Wallet::new();
let err = builder.add_signature(&outsider, data);
assert!(err.is_err());
println!("External wallet error: {}", err.unwrap_err()); // "This wallet is not in the multisig address"
}
MultiSigTxBuilder::is_complete
Checks whether enough signatures have been collected (signatures.len() >= required_sigs).
#![allow(unused)]
fn main() {
pub fn is_complete(&self) -> bool
}
Return value: true means the M-of-N requirement is satisfied and the transaction can be broadcast.
#![allow(unused)]
fn main() {
let mut builder = MultiSigTxBuilder::new(multisig);
assert!(!builder.is_complete()); // 0 signatures
builder.add_signature(&wallet1, data).unwrap();
assert!(!builder.is_complete()); // 1 signature, not enough yet (needs 2)
builder.add_signature(&wallet2, data).unwrap();
assert!(builder.is_complete()); // 2 signatures, satisfies 2-of-3
println!("Multisig complete, transaction can be broadcast");
}
MultiSigTxBuilder::get_signatures
Gets a copy of all collected signatures.
#![allow(unused)]
fn main() {
pub fn get_signatures(&self) -> Vec<String>
}
Return value: Vec<String> — Clone of the signature list (does not affect the builder’s internal state).
#![allow(unused)]
fn main() {
let sigs = builder.get_signatures();
println!("Collected {} signatures", sigs.len());
for (i, sig) in sigs.iter().enumerate() {
println!(" Signature {}: {}...", i + 1, &sig[..16]);
}
}
MultiSigType Enum (Convenience API)
Provides quick creation for common multisig types.
#![allow(unused)]
fn main() {
pub enum MultiSigType {
TwoOfTwo, // 2-of-2
TwoOfThree, // 2-of-3 (most common)
ThreeOfFive, // 3-of-5
}
impl MultiSigType {
pub fn create_address(&self, wallets: &[Wallet]) -> Result<MultiSigAddress, String>
}
}
#![allow(unused)]
fn main() {
use simplebtc::multisig::MultiSigType;
use simplebtc::wallet::Wallet;
let wallets: Vec<Wallet> = (0..3).map(|_| Wallet::new()).collect();
let ms_addr = MultiSigType::TwoOfThree.create_address(&wallets).unwrap();
println!("2-of-3 address: {}", ms_addr.address);
// Returns an error if the number of wallets does not match
let err = MultiSigType::ThreeOfFive.create_address(&wallets); // Requires 5 wallets
assert!(err.is_err());
}
Complete Usage Examples
Scenario 1: Corporate Fund Management (2-of-3)
#![allow(unused)]
fn main() {
use simplebtc::multisig::{MultiSigAddress, MultiSigTxBuilder};
use simplebtc::wallet::Wallet;
fn corporate_treasury() {
// Three company executives each hold one key
let ceo = Wallet::new();
let cfo = Wallet::new();
let cto = Wallet::new();
// Create 2-of-3 corporate multisig address
let pub_keys = vec![
ceo.public_key.clone(),
cfo.public_key.clone(),
cto.public_key.clone(),
];
let treasury = MultiSigAddress::new(2, pub_keys).unwrap();
println!("Corporate treasury address: {}", treasury.address);
// Initiate a payment (requires CEO + CFO signatures)
let tx_data = "Pay 10 BTC to supplier ABC";
let mut builder = MultiSigTxBuilder::new(treasury);
builder.add_signature(&ceo, tx_data).unwrap();
println!("CEO has signed, waiting for second authorization...");
builder.add_signature(&cfo, tx_data).unwrap();
println!("CFO has signed");
if builder.is_complete() {
let signatures = builder.get_signatures();
println!("Transaction authorized, signature count: {}", signatures.len());
// Here, attach signatures to the transaction and broadcast to the network
}
}
}
Scenario 2: Third-Party Escrow (Buyer-Seller-Arbitrator)
#![allow(unused)]
fn main() {
fn escrow_service() {
let buyer = Wallet::new();
let seller = Wallet::new();
let arbiter = Wallet::new();
let pub_keys = vec![
buyer.public_key.clone(),
seller.public_key.clone(),
arbiter.public_key.clone(),
];
// 2-of-3: normally buyer+seller; in a dispute, either party + arbitrator
let escrow = MultiSigAddress::new(2, pub_keys).unwrap();
println!("Escrow address: {}", escrow.address);
let release_tx = "Release escrow funds to seller address";
// Normal flow: buyer confirms receipt; buyer+seller sign to release funds
let mut builder = MultiSigTxBuilder::new(escrow.clone());
builder.add_signature(&buyer, release_tx).unwrap();
builder.add_signature(&seller, release_tx).unwrap();
assert!(builder.is_complete());
println!("Funds released normally to seller");
// Dispute flow: arbitrator intervenes; seller+arbitrator sign to release funds
let dispute_tx = "Dispute ruling: refund to buyer";
let mut dispute_builder = MultiSigTxBuilder::new(escrow);
dispute_builder.add_signature(&buyer, dispute_tx).unwrap();
dispute_builder.add_signature(&arbiter, dispute_tx).unwrap();
assert!(dispute_builder.is_complete());
println!("Arbitration complete, funds returned to buyer");
}
}
Scenario 3: Full Signature Verification Flow
#![allow(unused)]
fn main() {
fn full_verification() {
let w1 = Wallet::new();
let w2 = Wallet::new();
let pub_keys = vec![w1.public_key.clone(), w2.public_key.clone()];
let ms = MultiSigAddress::new(2, pub_keys).unwrap(); // 2-of-2
let tx_data = "Transfer 1 BTC";
// Collect signatures
let sig1 = w1.sign(tx_data);
let sig2 = w2.sign(tx_data);
let sigs = vec![sig1, sig2];
// Full ECDSA verification (verifies signatures were produced by keys in public_keys)
let valid = ms.verify_signatures_with_data(&sigs, tx_data);
println!("ECDSA multisig verification: {}", valid);
// Quick count check (does not verify content)
let count_ok = ms.verify_signatures(&sigs);
println!("Signature count satisfied: {}", count_ok);
}
}
Limitations and Notes
- Maximum keys: Due to Bitcoin script limits, N is at most 15 (returns an error if exceeded).
- Duplicate signature prevention:
MultiSigTxBuilderinternally maintains a set of already-signed public keys; callingadd_signaturetwice with the same wallet returns an error. - Signature order: Verification does not require signatures to be in the same order as the public keys; any M valid signatures will do.
- Zero-confirmation risk: A multisig transaction is still unconfirmed before being mined. For important transactions, wait for at least 1 block confirmation.
Related Modules
Wallet— Providessign()andverify_signature()methods, the foundation of multisig ECDSA operations.AdvancedTxBuilder— Can be combined with timelocks to implement advanced scenarios such as “reduced multisig requirement after time expiry.”- Advanced Modules Overview — Understand multisig’s place in SimpleBTC’s overall architecture.