From 1c7d50af6e88c34958cc7ff752441282656b3284 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Thu, 1 Feb 2024 14:43:03 -0600 Subject: [PATCH 01/13] Add SignatorySet::from_script --- src/bitcoin/signatory.rs | 131 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/bitcoin/signatory.rs b/src/bitcoin/signatory.rs index 83b268a9..3c9c3123 100644 --- a/src/bitcoin/signatory.rs +++ b/src/bitcoin/signatory.rs @@ -1,9 +1,15 @@ #![allow(clippy::redundant_closure_call)] // TODO: fix bitcoin-script then remove this #![allow(unused_imports)] // TODO +use crate::bitcoin::threshold_sig::Pubkey; #[cfg(feature = "full")] use crate::error::Error; use crate::error::Result; +use bitcoin::blockdata::opcodes::all::{ + OP_ADD, OP_CHECKSIG, OP_DROP, OP_ELSE, OP_ENDIF, OP_GREATERTHAN, OP_IF, OP_SWAP, +}; +use bitcoin::blockdata::opcodes::{self, OP_FALSE}; +use bitcoin::blockdata::script::{read_scriptint, Instruction, Instructions}; use bitcoin::secp256k1::Context as SecpContext; use bitcoin::secp256k1::PublicKey; use bitcoin::secp256k1::Secp256k1; @@ -153,6 +159,131 @@ impl SignatorySet { Ok(sigset) } + pub fn from_script(script: &bitcoin::Script) -> Result<(Self, Vec)> { + fn take_instruction<'a>(ins: &mut Instructions<'a>) -> Result> { + ins.next() + .ok_or_else(|| orga::Error::App("Unexpected end of script".to_string()))? + .map_err(|_| orga::Error::App("Failed to read script".to_string()).into()) + } + + fn take_bytes<'a>(ins: &mut Instructions<'a>) -> Result<&'a [u8]> { + let instruction = take_instruction(ins)?; + + let Instruction::PushBytes(bytes) = instruction else { + return Err(Error::Orga(orga::Error::App("Expected OP_PUSHBYTES".to_string()))); + }; + + Ok(bytes) + } + + fn take_key(ins: &mut Instructions) -> Result { + let bytes = take_bytes(ins)?; + + if bytes.len() != 33 { + return Err(Error::Orga(orga::Error::App( + "Expected 33 bytes".to_string(), + ))); + } + + Ok(Pubkey::try_from_slice(&bytes)?) + } + + fn take_number(ins: &mut Instructions) -> Result { + let bytes = take_bytes(ins)?; + read_scriptint(&bytes) + .map_err(|_| orga::Error::App("Failed to read scriptint".to_string()).into()) + } + + fn take_op(ins: &mut Instructions, op: opcodes::All) -> Result { + let instruction = take_instruction(ins)?; + + let op = match instruction { + Instruction::Op(op) => op, + Instruction::PushBytes(&[]) => OP_FALSE, + _ => { + return Err(Error::Orga(orga::Error::App(format!( + "Expected {}", + format!("{:?}", op) + )))) + } + }; + + if op != op { + return Err(Error::Orga(orga::Error::App(format!( + "Expected {}", + format!("{:?}", op), + )))); + } + + Ok(op) + } + + fn take_first_signatory(ins: &mut Instructions) -> Result { + let pubkey = take_key(ins)?; + take_op(ins, OP_CHECKSIG)?; + take_op(ins, OP_IF)?; + let voting_power = take_number(ins)?; + take_op(ins, OP_ELSE)?; + take_op(ins, OP_FALSE)?; + take_op(ins, OP_ENDIF)?; + + Ok::<_, Error>(Signatory { + pubkey: pubkey.into(), + voting_power: voting_power as u64, + }) + } + + fn take_nth_signatory(ins: &mut Instructions) -> Result { + take_op(ins, OP_SWAP)?; + let pubkey = take_key(ins)?; + take_op(ins, OP_CHECKSIG)?; + take_op(ins, OP_IF)?; + let voting_power = take_number(ins)?; + take_op(ins, OP_ADD)?; + take_op(ins, OP_ENDIF)?; + + Ok::<_, Error>(Signatory { + pubkey: pubkey.into(), + voting_power: voting_power as u64, + }) + } + + fn take_threshold(ins: &mut Instructions) -> Result { + let threshold = take_number(ins)?; + take_op(ins, OP_GREATERTHAN)?; + Ok(threshold as u64) + } + + fn take_commitment<'a>(ins: &mut Instructions<'a>) -> Result<&'a [u8]> { + let bytes = take_bytes(ins)?; + take_op(ins, OP_DROP)?; + Ok(bytes) + } + + let mut ins = script.instructions(); + let mut sigs = vec![take_first_signatory(&mut ins)?]; + // TODO: support variable number of signatories + for _ in 1..MAX_SIGNATORIES { + sigs.push(take_nth_signatory(&mut ins)?); + } + + take_threshold(&mut ins)?; + let commitment = take_commitment(&mut ins)?; + + assert!(ins.next().is_none()); + + let total_vp: u64 = sigs.iter().map(|s| s.voting_power).sum(); + let sigset = Self { + signatories: sigs, + present_vp: total_vp, + possible_vp: total_vp, + create_time: 0, + index: 0, + }; + + Ok((sigset, commitment.to_vec())) + } + /// Inserts a signatory into the set. This may cause the signatory set to be /// unsorted. #[cfg(feature = "full")] From 77d0ae5b5b31ae2826dd1f77cd1c45b4f1eb07b2 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Thu, 1 Feb 2024 14:45:02 -0600 Subject: [PATCH 02/13] Add test for SignatorySet::from_script --- src/bitcoin/signatory.rs | 186 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/src/bitcoin/signatory.rs b/src/bitcoin/signatory.rs index 3c9c3123..69ea205d 100644 --- a/src/bitcoin/signatory.rs +++ b/src/bitcoin/signatory.rs @@ -570,4 +570,190 @@ mod tests { // signatories.set(mock_signatory(1, 100_000_000)); // assert_eq!(get_truncation(&signatories, 23), 4); // } + + use bitcoin::hashes::hex::FromHex; + + use crate::bitcoin::{signatory::Signatory, threshold_sig::Pubkey}; + + use super::SignatorySet; + + #[test] + fn from_script() { + let script = bitcoin::Script::from_hex("21028891f36b691a40036f2b3ecb17c13780a932503ef2c39f3faed9b95bf71ea27fac630339e0116700687c2102f6fee7ad7dc87d0a636ae1584273c849bf540f4c1780434a0430888b0c5b151cac63033c910e93687c2102d207371a1e9a588e447d91dc12a8f3479f1f9ff8da748aae04bb5d07f0737790ac630371730893687c2103713e9bb6025fa9dc3c26507762cffd2a9524ff48f1d84c6753caa581347e5e10ac63031def0793687c2103d8fc0412a866bfb14d3fbc9e1b714ca31141d0f7e211d0fa634d53dda9789ecaac6303d1f00693687c2102c7961e04206af92f4b4cf3f19b43722f301e4915a49f5ca2908d9af5ce343830ac6303496f0693687c2103205472bb87799cb9140b5d471cc045b65821a4e75591026a8411ee3ac3e27027ac6303fe500693687c2102c923df10e8141072504b1f9513ee6796dc4d748d774ce9396942b63d42d3d575ac6303ed1f0593687c21031e8124547a5f28e04652d61fab1053ba8af41b682ccecdf5fa58595add7c7d9eac6303d4a00493687c21038060738940b9b3513851aa45df9f8b9d8e3304ef5abc5f8c1928bf4f1c8601adac630347210493687c21022e1efe78c688bceb7a36bf8af0e905da65e1942b84afe31716a356a91c0d9c05ac6303c5620393687c21020598956ed409e190b763bed8ed1ec3a18138c582c761eb8a4cf60861bfb44f13ac6303b3550393687c2102c8b2e54cafced96b1438e9ee6ebddc27c4aca68f14b2199eb8b8da111b584c2cac63036c330393687c2102d8a4c0accefa93b6a8d390a81dbffa4d05cd0a844371b2bed0ba1b1b65e14300ac6303521d0393687c2102460ccc0db97b1027e4fe2ab178f015a786b6b8f016b580f495dde3230f34984cac630304060393687c2102def64dfc155e17988ea6dee5a5659e2ec0a19fce54af90ca84dcd4df53b1a222ac630341d20293687c21030c9057c92c19f749c891037379766c0642d03bd1c50e3b262fc7d954c232f4d8ac630356c30293687c21027e1ebe3dd4fbbf250a8161a8a7af19815d5c07363e220f28f81c535c3950c7cbac6303d3ab0293687c210235e1d72961cb475971e2bc437ac21f9be13c83f1aa039e64f406aae87e2b4816ac6303bdaa0293687c210295d565c8ae94d46d439b4591dcd146742f918893292c23c49d000c4023bad4ffac630308aa029368030fb34aa0010075").unwrap(); + + let (sigset, commitment) = SignatorySet::from_script(&script).unwrap(); + + let pk = |bytes| Pubkey::new(bytes).unwrap().into(); + assert_eq!( + sigset, + SignatorySet { + create_time: 0, + present_vp: 7343244, + possible_vp: 7343244, + index: 0, + signatories: vec![ + Signatory { + voting_power: 1171513, + pubkey: pk([ + 2, 136, 145, 243, 107, 105, 26, 64, 3, 111, 43, 62, 203, 23, 193, 55, + 128, 169, 50, 80, 62, 242, 195, 159, 63, 174, 217, 185, 91, 247, 30, + 162, 127 + ]) + }, + Signatory { + voting_power: 954684, + pubkey: pk([ + 2, 246, 254, 231, 173, 125, 200, 125, 10, 99, 106, 225, 88, 66, 115, + 200, 73, 191, 84, 15, 76, 23, 128, 67, 74, 4, 48, 136, 139, 12, 91, 21, + 28 + ]) + }, + Signatory { + voting_power: 553841, + pubkey: pk([ + 2, 210, 7, 55, 26, 30, 154, 88, 142, 68, 125, 145, 220, 18, 168, 243, + 71, 159, 31, 159, 248, 218, 116, 138, 174, 4, 187, 93, 7, 240, 115, + 119, 144 + ]) + }, + Signatory { + voting_power: 519965, + pubkey: pk([ + 3, 113, 62, 155, 182, 2, 95, 169, 220, 60, 38, 80, 119, 98, 207, 253, + 42, 149, 36, 255, 72, 241, 216, 76, 103, 83, 202, 165, 129, 52, 126, + 94, 16 + ]) + }, + Signatory { + voting_power: 454865, + pubkey: pk([ + 3, 216, 252, 4, 18, 168, 102, 191, 177, 77, 63, 188, 158, 27, 113, 76, + 163, 17, 65, 208, 247, 226, 17, 208, 250, 99, 77, 83, 221, 169, 120, + 158, 202 + ]) + }, + Signatory { + voting_power: 421705, + pubkey: pk([ + 2, 199, 150, 30, 4, 32, 106, 249, 47, 75, 76, 243, 241, 155, 67, 114, + 47, 48, 30, 73, 21, 164, 159, 92, 162, 144, 141, 154, 245, 206, 52, 56, + 48 + ]) + }, + Signatory { + voting_power: 413950, + pubkey: pk([ + 3, 32, 84, 114, 187, 135, 121, 156, 185, 20, 11, 93, 71, 28, 192, 69, + 182, 88, 33, 164, 231, 85, 145, 2, 106, 132, 17, 238, 58, 195, 226, + 112, 39 + ]) + }, + Signatory { + voting_power: 335853, + pubkey: pk([ + 2, 201, 35, 223, 16, 232, 20, 16, 114, 80, 75, 31, 149, 19, 238, 103, + 150, 220, 77, 116, 141, 119, 76, 233, 57, 105, 66, 182, 61, 66, 211, + 213, 117 + ]) + }, + Signatory { + voting_power: 303316, + pubkey: pk([ + 3, 30, 129, 36, 84, 122, 95, 40, 224, 70, 82, 214, 31, 171, 16, 83, + 186, 138, 244, 27, 104, 44, 206, 205, 245, 250, 88, 89, 90, 221, 124, + 125, 158 + ]) + }, + Signatory { + voting_power: 270663, + pubkey: pk([ + 3, 128, 96, 115, 137, 64, 185, 179, 81, 56, 81, 170, 69, 223, 159, 139, + 157, 142, 51, 4, 239, 90, 188, 95, 140, 25, 40, 191, 79, 28, 134, 1, + 173 + ]) + }, + Signatory { + voting_power: 221893, + pubkey: pk([ + 2, 46, 30, 254, 120, 198, 136, 188, 235, 122, 54, 191, 138, 240, 233, + 5, 218, 101, 225, 148, 43, 132, 175, 227, 23, 22, 163, 86, 169, 28, 13, + 156, 5 + ]) + }, + Signatory { + voting_power: 218547, + pubkey: pk([ + 2, 5, 152, 149, 110, 212, 9, 225, 144, 183, 99, 190, 216, 237, 30, 195, + 161, 129, 56, 197, 130, 199, 97, 235, 138, 76, 246, 8, 97, 191, 180, + 79, 19 + ]) + }, + Signatory { + voting_power: 209772, + pubkey: pk([ + 2, 200, 178, 229, 76, 175, 206, 217, 107, 20, 56, 233, 238, 110, 189, + 220, 39, 196, 172, 166, 143, 20, 178, 25, 158, 184, 184, 218, 17, 27, + 88, 76, 44 + ]) + }, + Signatory { + voting_power: 204114, + pubkey: pk([ + 2, 216, 164, 192, 172, 206, 250, 147, 182, 168, 211, 144, 168, 29, 191, + 250, 77, 5, 205, 10, 132, 67, 113, 178, 190, 208, 186, 27, 27, 101, + 225, 67, 0 + ]) + }, + Signatory { + voting_power: 198148, + pubkey: pk([ + 2, 70, 12, 204, 13, 185, 123, 16, 39, 228, 254, 42, 177, 120, 240, 21, + 167, 134, 182, 184, 240, 22, 181, 128, 244, 149, 221, 227, 35, 15, 52, + 152, 76 + ]) + }, + Signatory { + voting_power: 184897, + pubkey: pk([ + 2, 222, 246, 77, 252, 21, 94, 23, 152, 142, 166, 222, 229, 165, 101, + 158, 46, 192, 161, 159, 206, 84, 175, 144, 202, 132, 220, 212, 223, 83, + 177, 162, 34 + ]) + }, + Signatory { + voting_power: 181078, + pubkey: pk([ + 3, 12, 144, 87, 201, 44, 25, 247, 73, 200, 145, 3, 115, 121, 118, 108, + 6, 66, 208, 59, 209, 197, 14, 59, 38, 47, 199, 217, 84, 194, 50, 244, + 216 + ]) + }, + Signatory { + voting_power: 175059, + pubkey: pk([ + 2, 126, 30, 190, 61, 212, 251, 191, 37, 10, 129, 97, 168, 167, 175, 25, + 129, 93, 92, 7, 54, 62, 34, 15, 40, 248, 28, 83, 92, 57, 80, 199, 203 + ]) + }, + Signatory { + voting_power: 174781, + pubkey: pk([ + 2, 53, 225, 215, 41, 97, 203, 71, 89, 113, 226, 188, 67, 122, 194, 31, + 155, 225, 60, 131, 241, 170, 3, 158, 100, 244, 6, 170, 232, 126, 43, + 72, 22 + ]) + }, + Signatory { + voting_power: 174600, + pubkey: pk([ + 2, 149, 213, 101, 200, 174, 148, 212, 109, 67, 155, 69, 145, 220, 209, + 70, 116, 47, 145, 136, 147, 41, 44, 35, 196, 157, 0, 12, 64, 35, 186, + 212, 255 + ]) + } + ] + } + ); + assert_eq!(commitment, vec![0]); + } } From 08b7256fa660344c460838ded1abdd54365f4785 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 13:32:36 -0600 Subject: [PATCH 03/13] Add binary which retrieves reserve script spends from Bitcoin block data --- src/bin/get-reserve-scripts.rs | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/bin/get-reserve-scripts.rs diff --git a/src/bin/get-reserve-scripts.rs b/src/bin/get-reserve-scripts.rs new file mode 100644 index 00000000..9df057d1 --- /dev/null +++ b/src/bin/get-reserve-scripts.rs @@ -0,0 +1,82 @@ +use bitcoincore_rpc_async::{Auth, Client, RpcApi}; +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Opts { + #[clap(default_value_t = 30)] + lookback_days: u64, + + #[clap(short = 'p', long, default_value_t = 8332)] + rpc_port: u16, + + #[clap(short = 'u', long)] + rpc_user: Option, + + #[clap(short = 'P', long)] + rpc_pass: Option, +} + +#[tokio::main] +pub async fn main() { + let opts = Opts::parse(); + + let rpc_url = format!("http://localhost:{}", opts.rpc_port); + let auth = match (opts.rpc_user, opts.rpc_pass) { + (Some(user), Some(pass)) => Auth::UserPass(user, pass), + _ => Auth::None, + }; + let btc_client = Client::new(rpc_url, auth).await.unwrap(); + + let nomic_client = nomic::app_client("http://localhost:26657"); + + let (last_conf_index, last_conf_cp) = nomic_client + .query(|app| { + let conf_index = app.bitcoin.checkpoints.confirmed_index.unwrap(); + let conf_cp = app.bitcoin.checkpoints.get(conf_index)?; + Ok((conf_index, conf_cp.checkpoint_tx()?.txid())) + }) + .await + .unwrap(); + + let mut index = last_conf_index - 1; // subtract 1 because the witness data + // represents the sigset for the + // previous checkpoint + let mut prev_txid = last_conf_cp; + let mut block_hash = btc_client.get_best_block_hash().await.unwrap(); + let mut scripts = vec![]; + + let target_time = now() - 60 * 60 * 24 * opts.lookback_days; + + loop { + let block = btc_client.get_block_info(&block_hash).await.unwrap(); + let has_tx = block.tx.iter().find(|txid| **txid == prev_txid).is_some(); + if !has_tx { + block_hash = block.previousblockhash.unwrap(); + continue; + } + + let tx = btc_client + .get_raw_transaction(&prev_txid, Some(&block_hash)) + .await + .unwrap(); + prev_txid = tx.input[0].previous_output.txid; + index -= 1; + + scripts.push((index, tx.input[0].witness.last().unwrap().to_vec())); + + if (block.time as u64) < target_time { + break; + } + } + + for (index, script) in scripts.iter() { + println!("{},{}", index, hex::encode(script)); + } +} + +fn now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() +} From 7a5efc480dd5eba6b6b410c09bdcf4c0943e4d2f Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 14:51:08 -0600 Subject: [PATCH 04/13] Add CheckpointQueue::backfill --- src/bitcoin/checkpoint.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index 4f5f996f..0d2e207f 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -11,8 +11,8 @@ use crate::{ app::Dest, bitcoin::{signatory::derive_pubkey, Nbtc}, }; -use bitcoin::hashes::Hash; use bitcoin::{blockdata::transaction::EcdsaSighashType, Sequence, Transaction, TxIn, TxOut}; +use bitcoin::{hashes::Hash, Script}; use derive_more::{Deref, DerefMut}; use log::info; use orga::coins::{Accounts, Coin}; @@ -1701,7 +1701,7 @@ impl CheckpointQueue { Ok(()) } - /// Gets a refernce to the checkpoint at the given index. + /// Gets a reference to the checkpoint at the given index. /// /// If the index is out of bounds or was pruned, an error is returned. #[query] @@ -1822,6 +1822,11 @@ impl CheckpointQueue { .ok_or_else(|| Error::Orga(OrgaError::App("No completed checkpoints yet".to_string()))) } + #[query] + pub fn first_index(&self) -> Result { + Ok(self.index + 1 - self.len()?) + } + /// A reference to the last completed checkpoint. #[query] pub fn last_completed(&self) -> Result> { @@ -2362,6 +2367,30 @@ impl CheckpointQueue { let unconf_vbytes = self.unconfirmed_vbytes(config)?; Ok((unconf_vbytes * fee_rate).saturating_sub(unconf_fees_paid)) } + + pub fn backfill( + &mut self, + first_index: u32, + redeem_scripts: impl Iterator, + ) -> Result<()> { + let mut index = first_index; + + for script in redeem_scripts { + if index >= self.first_index()? { + index -= 1; + continue; + } + + let (mut sigset, _) = SignatorySet::from_script(&script)?; + sigset.index = index; + let cp = Checkpoint::new(sigset)?; + + self.queue.push_front(cp)?; + index -= 1; + } + + Ok(()) + } } /// Takes a previous fee rate and returns a new fee rate, adjusted up or down by From eda35f0070e579189e0f4b49cdc02935e55e4bb4 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 14:51:20 -0600 Subject: [PATCH 05/13] Add backfill test --- src/bitcoin/checkpoint.rs | 83 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index 0d2e207f..fa23d30d 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -2408,6 +2408,7 @@ pub fn adjust_fee_rate(prev_fee_rate: u64, up: bool, config: &Config) -> u64 { #[cfg(test)] mod test { + use crate::bitcoin::threshold_sig::Pubkey; #[cfg(feature = "full")] use crate::utils::set_time; @@ -2419,7 +2420,11 @@ mod test { util::bip32::{ChildNumber, ExtendedPrivKey, ExtendedPubKey}, OutPoint, PubkeyHash, Script, Txid, }; - use orga::{collections::EntryMap, context::Context}; + use orga::{ + collections::EntryMap, + context::Context, + secp256k1::{PublicKey, SecretKey}, + }; #[cfg(all(feature = "full"))] use rand::Rng; @@ -2973,4 +2978,80 @@ mod test { assert_eq!(queue.borrow().len().unwrap(), 5); } + + #[test] + fn backfill() { + fn sigset(n: u32) -> SignatorySet { + let mut sigset = SignatorySet::default(); + sigset.index = n; + sigset.create_time = n as u64; + + let secret = bitcoin::secp256k1::SecretKey::from_slice(&[n as u8; 32]).unwrap(); + let pubkey: Pubkey = bitcoin::secp256k1::PublicKey::from_secret_key( + &bitcoin::secp256k1::Secp256k1::new(), + &secret, + ) + .into(); + + sigset.signatories.push(Signatory { + pubkey: pubkey.into(), + voting_power: 100, + }); + + sigset.possible_vp = 100; + sigset.present_vp = 100; + + sigset + } + + let mut queue = CheckpointQueue::default(); + queue.index = 10; + queue + .queue + .push_back(Checkpoint::new(sigset(7)).unwrap()) + .unwrap(); + queue + .queue + .push_back(Checkpoint::new(sigset(8)).unwrap()) + .unwrap(); + queue + .queue + .push_back(Checkpoint::new(sigset(9)).unwrap()) + .unwrap(); + queue + .queue + .push_back(Checkpoint::new(sigset(10)).unwrap()) + .unwrap(); + + let backfill_data = vec![ + sigset(8).redeem_script(&[0], (2, 3)).unwrap(), + sigset(7).redeem_script(&[0], (2, 3)).unwrap(), + sigset(6).redeem_script(&[0], (2, 3)).unwrap(), + sigset(5).redeem_script(&[0], (2, 3)).unwrap(), + sigset(4).redeem_script(&[0], (2, 3)).unwrap(), + sigset(3).redeem_script(&[0], (2, 3)).unwrap(), + ]; + queue.backfill(8, backfill_data.into_iter()).unwrap(); + + assert_eq!(queue.len().unwrap(), 8); + assert_eq!(queue.index, 10); + assert_eq!( + queue + .get(3) + .unwrap() + .sigset + .redeem_script(&[0], (2, 3)) + .unwrap(), + sigset(3).redeem_script(&[0], (2, 3)).unwrap(), + ); + assert_eq!( + queue + .get(10) + .unwrap() + .sigset + .redeem_script(&[0], (2, 3)) + .unwrap(), + sigset(10).redeem_script(&[0], (2, 3)).unwrap(), + ); + } } From 820d2efe6b7c1b87ee7c4914bacae48c99809ea7 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 14:51:47 -0600 Subject: [PATCH 06/13] Support variable number of signatories in SignatorySet::from_script --- src/bitcoin/signatory.rs | 43 ++++++++++++++++++++++++++++------------ src/lib.rs | 1 + 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/bitcoin/signatory.rs b/src/bitcoin/signatory.rs index 69ea205d..7034fb61 100644 --- a/src/bitcoin/signatory.rs +++ b/src/bitcoin/signatory.rs @@ -160,13 +160,17 @@ impl SignatorySet { } pub fn from_script(script: &bitcoin::Script) -> Result<(Self, Vec)> { - fn take_instruction<'a>(ins: &mut Instructions<'a>) -> Result> { + trait Iter<'a> = Iterator< + Item = std::result::Result, bitcoin::blockdata::script::Error>, + >; + + fn take_instruction<'a>(ins: &mut impl Iter<'a>) -> Result> { ins.next() .ok_or_else(|| orga::Error::App("Unexpected end of script".to_string()))? .map_err(|_| orga::Error::App("Failed to read script".to_string()).into()) } - fn take_bytes<'a>(ins: &mut Instructions<'a>) -> Result<&'a [u8]> { + fn take_bytes<'a>(ins: &mut impl Iter<'a>) -> Result<&'a [u8]> { let instruction = take_instruction(ins)?; let Instruction::PushBytes(bytes) = instruction else { @@ -176,7 +180,7 @@ impl SignatorySet { Ok(bytes) } - fn take_key(ins: &mut Instructions) -> Result { + fn take_key<'a>(ins: &mut impl Iter<'a>) -> Result { let bytes = take_bytes(ins)?; if bytes.len() != 33 { @@ -188,13 +192,13 @@ impl SignatorySet { Ok(Pubkey::try_from_slice(&bytes)?) } - fn take_number(ins: &mut Instructions) -> Result { + fn take_number<'a>(ins: &mut impl Iter<'a>) -> Result { let bytes = take_bytes(ins)?; read_scriptint(&bytes) .map_err(|_| orga::Error::App("Failed to read scriptint".to_string()).into()) } - fn take_op(ins: &mut Instructions, op: opcodes::All) -> Result { + fn take_op<'a>(ins: &mut impl Iter<'a>, op: opcodes::All) -> Result { let instruction = take_instruction(ins)?; let op = match instruction { @@ -218,7 +222,7 @@ impl SignatorySet { Ok(op) } - fn take_first_signatory(ins: &mut Instructions) -> Result { + fn take_first_signatory<'a>(ins: &mut impl Iter<'a>) -> Result { let pubkey = take_key(ins)?; take_op(ins, OP_CHECKSIG)?; take_op(ins, OP_IF)?; @@ -233,7 +237,7 @@ impl SignatorySet { }) } - fn take_nth_signatory(ins: &mut Instructions) -> Result { + fn take_nth_signatory<'a>(ins: &mut impl Iter<'a>) -> Result { take_op(ins, OP_SWAP)?; let pubkey = take_key(ins)?; take_op(ins, OP_CHECKSIG)?; @@ -248,23 +252,36 @@ impl SignatorySet { }) } - fn take_threshold(ins: &mut Instructions) -> Result { + fn take_threshold<'a>(ins: &mut impl Iter<'a>) -> Result { let threshold = take_number(ins)?; take_op(ins, OP_GREATERTHAN)?; Ok(threshold as u64) } - fn take_commitment<'a>(ins: &mut Instructions<'a>) -> Result<&'a [u8]> { + fn take_commitment<'a>(ins: &mut impl Iter<'a>) -> Result<&'a [u8]> { let bytes = take_bytes(ins)?; take_op(ins, OP_DROP)?; Ok(bytes) } - let mut ins = script.instructions(); + let mut ins = script.instructions().peekable(); let mut sigs = vec![take_first_signatory(&mut ins)?]; - // TODO: support variable number of signatories - for _ in 1..MAX_SIGNATORIES { - sigs.push(take_nth_signatory(&mut ins)?); + loop { + let next = ins + .peek() + .ok_or_else(|| { + Error::Orga(orga::Error::App("Unexpected end of script".to_string())) + })? + .clone() + .map_err(|_| { + Error::Orga(orga::Error::App("Failed to read script".to_string()).into()) + })?; + + if let Instruction::Op(opcodes::all::OP_SWAP) = next { + sigs.push(take_nth_signatory(&mut ins)?); + } else { + break; + } } take_threshold(&mut ins)?; diff --git a/src/lib.rs b/src/lib.rs index ca4a04ab..5a9a02b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ #![feature(type_alias_impl_trait)] #![feature(async_closure)] #![feature(string_leak)] +#![feature(trait_alias)] #[cfg(feature = "full")] use orga::{ From dfeb3616b4481e800a001dc518694f41a1a04c59 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 14:54:38 -0600 Subject: [PATCH 07/13] Fix clippy warnings --- src/bin/get-reserve-scripts.rs | 2 +- src/bitcoin/signatory.rs | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/bin/get-reserve-scripts.rs b/src/bin/get-reserve-scripts.rs index 9df057d1..66b2bf92 100644 --- a/src/bin/get-reserve-scripts.rs +++ b/src/bin/get-reserve-scripts.rs @@ -49,7 +49,7 @@ pub async fn main() { loop { let block = btc_client.get_block_info(&block_hash).await.unwrap(); - let has_tx = block.tx.iter().find(|txid| **txid == prev_txid).is_some(); + let has_tx = block.tx.iter().any(|txid| *txid == prev_txid); if !has_tx { block_hash = block.previousblockhash.unwrap(); continue; diff --git a/src/bitcoin/signatory.rs b/src/bitcoin/signatory.rs index 7034fb61..36d2f46d 100644 --- a/src/bitcoin/signatory.rs +++ b/src/bitcoin/signatory.rs @@ -189,16 +189,16 @@ impl SignatorySet { ))); } - Ok(Pubkey::try_from_slice(&bytes)?) + Ok(Pubkey::try_from_slice(bytes)?) } fn take_number<'a>(ins: &mut impl Iter<'a>) -> Result { let bytes = take_bytes(ins)?; - read_scriptint(&bytes) + read_scriptint(bytes) .map_err(|_| orga::Error::App("Failed to read scriptint".to_string()).into()) } - fn take_op<'a>(ins: &mut impl Iter<'a>, op: opcodes::All) -> Result { + fn take_op<'a>(ins: &mut impl Iter<'a>, expected_op: opcodes::All) -> Result { let instruction = take_instruction(ins)?; let op = match instruction { @@ -207,15 +207,15 @@ impl SignatorySet { _ => { return Err(Error::Orga(orga::Error::App(format!( "Expected {}", - format!("{:?}", op) + format!("{:?}", expected_op) )))) } }; - if op != op { + if op != expected_op { return Err(Error::Orga(orga::Error::App(format!( "Expected {}", - format!("{:?}", op), + format!("{:?}", expected_op), )))); } @@ -273,9 +273,7 @@ impl SignatorySet { Error::Orga(orga::Error::App("Unexpected end of script".to_string())) })? .clone() - .map_err(|_| { - Error::Orga(orga::Error::App("Failed to read script".to_string()).into()) - })?; + .map_err(|_| Error::Orga(orga::Error::App("Failed to read script".to_string())))?; if let Instruction::Op(opcodes::all::OP_SWAP) = next { sigs.push(take_nth_signatory(&mut ins)?); From 4bbe0c77690b3cdfd92583527bbb89dd2ecac776 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 15:04:30 -0600 Subject: [PATCH 08/13] Set backfill sigset create_time to prevent instant checkpoint pruning --- src/bitcoin/checkpoint.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index fa23d30d..dcad3f1e 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -2375,6 +2375,8 @@ impl CheckpointQueue { ) -> Result<()> { let mut index = first_index; + let create_time = self.queue.get(0)?.unwrap().create_time(); + for script in redeem_scripts { if index >= self.first_index()? { index -= 1; @@ -2383,6 +2385,7 @@ impl CheckpointQueue { let (mut sigset, _) = SignatorySet::from_script(&script)?; sigset.index = index; + sigset.create_time = create_time; let cp = Checkpoint::new(sigset)?; self.queue.push_front(cp)?; From cb0960c0779e813024e037f9c2f0e4b10d7c03e1 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 5 Feb 2024 15:14:14 -0600 Subject: [PATCH 09/13] Fix wasm build --- src/bitcoin/signatory.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bitcoin/signatory.rs b/src/bitcoin/signatory.rs index 36d2f46d..e90a44e4 100644 --- a/src/bitcoin/signatory.rs +++ b/src/bitcoin/signatory.rs @@ -2,7 +2,6 @@ #![allow(unused_imports)] // TODO use crate::bitcoin::threshold_sig::Pubkey; -#[cfg(feature = "full")] use crate::error::Error; use crate::error::Result; use bitcoin::blockdata::opcodes::all::{ From 79b0415163c79d254360b487f4fb5f2559fccfa6 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 6 Feb 2024 16:40:11 -0600 Subject: [PATCH 10/13] Fix get-reserve-scripts binary indexes --- src/bin/get-reserve-scripts.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bin/get-reserve-scripts.rs b/src/bin/get-reserve-scripts.rs index 66b2bf92..0cc175fd 100644 --- a/src/bin/get-reserve-scripts.rs +++ b/src/bin/get-reserve-scripts.rs @@ -38,9 +38,7 @@ pub async fn main() { .await .unwrap(); - let mut index = last_conf_index - 1; // subtract 1 because the witness data - // represents the sigset for the - // previous checkpoint + let mut index = last_conf_index; let mut prev_txid = last_conf_cp; let mut block_hash = btc_client.get_best_block_hash().await.unwrap(); let mut scripts = vec![]; From cbb00a8ce300d0f5d0c037b3de02592f7f847fec Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 6 Feb 2024 16:40:45 -0600 Subject: [PATCH 11/13] Prevent overflow when backfilling to checkpoint index 0 --- src/bitcoin/checkpoint.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index dcad3f1e..4c944f20 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -2373,23 +2373,24 @@ impl CheckpointQueue { first_index: u32, redeem_scripts: impl Iterator, ) -> Result<()> { - let mut index = first_index; + let mut index = first_index + 1; let create_time = self.queue.get(0)?.unwrap().create_time(); for script in redeem_scripts { + index -= 1; + if index >= self.first_index()? { - index -= 1; continue; } let (mut sigset, _) = SignatorySet::from_script(&script)?; sigset.index = index; sigset.create_time = create_time; - let cp = Checkpoint::new(sigset)?; + let mut cp = Checkpoint::new(sigset)?; + cp.status = CheckpointStatus::Complete; self.queue.push_front(cp)?; - index -= 1; } Ok(()) From 82eb7688e22d65748fe2f89979ccb15ebc2d4985 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 6 Feb 2024 16:41:00 -0600 Subject: [PATCH 12/13] Add test case for backfill to index 0 --- src/bitcoin/checkpoint.rs | 76 ++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index 4c944f20..86a894a1 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -2983,31 +2983,31 @@ mod test { assert_eq!(queue.borrow().len().unwrap(), 5); } - #[test] - fn backfill() { - fn sigset(n: u32) -> SignatorySet { - let mut sigset = SignatorySet::default(); - sigset.index = n; - sigset.create_time = n as u64; - - let secret = bitcoin::secp256k1::SecretKey::from_slice(&[n as u8; 32]).unwrap(); - let pubkey: Pubkey = bitcoin::secp256k1::PublicKey::from_secret_key( - &bitcoin::secp256k1::Secp256k1::new(), - &secret, - ) - .into(); + fn sigset(n: u32) -> SignatorySet { + let mut sigset = SignatorySet::default(); + sigset.index = n; + sigset.create_time = n as u64; - sigset.signatories.push(Signatory { - pubkey: pubkey.into(), - voting_power: 100, - }); + let secret = bitcoin::secp256k1::SecretKey::from_slice(&[(n + 1) as u8; 32]).unwrap(); + let pubkey: Pubkey = bitcoin::secp256k1::PublicKey::from_secret_key( + &bitcoin::secp256k1::Secp256k1::new(), + &secret, + ) + .into(); - sigset.possible_vp = 100; - sigset.present_vp = 100; + sigset.signatories.push(Signatory { + pubkey: pubkey.into(), + voting_power: 100, + }); - sigset - } + sigset.possible_vp = 100; + sigset.present_vp = 100; + + sigset + } + #[test] + fn backfill_basic() { let mut queue = CheckpointQueue::default(); queue.index = 10; queue @@ -3058,4 +3058,38 @@ mod test { sigset(10).redeem_script(&[0], (2, 3)).unwrap(), ); } + + #[test] + fn backfill_with_zeroth() { + let mut queue = CheckpointQueue::default(); + queue.index = 1; + queue + .queue + .push_back(Checkpoint::new(sigset(1)).unwrap()) + .unwrap(); + + let backfill_data = vec![sigset(0).redeem_script(&[0], (2, 3)).unwrap()]; + queue.backfill(0, backfill_data.into_iter()).unwrap(); + + assert_eq!(queue.len().unwrap(), 2); + assert_eq!(queue.index, 1); + assert_eq!( + queue + .get(0) + .unwrap() + .sigset + .redeem_script(&[0], (2, 3)) + .unwrap(), + sigset(0).redeem_script(&[0], (2, 3)).unwrap(), + ); + assert_eq!( + queue + .get(1) + .unwrap() + .sigset + .redeem_script(&[0], (2, 3)) + .unwrap(), + sigset(1).redeem_script(&[0], (2, 3)).unwrap(), + ); + } } From 93f44ce9b6b49e0af1aba0a53bcc8d5cef12f605 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 6 Feb 2024 16:41:27 -0600 Subject: [PATCH 13/13] Skip relay of backfilled checkpoints --- src/bitcoin/relayer.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bitcoin/relayer.rs b/src/bitcoin/relayer.rs index 3cf556e0..9e4ce227 100644 --- a/src/bitcoin/relayer.rs +++ b/src/bitcoin/relayer.rs @@ -501,6 +501,10 @@ impl Relayer { if relayed.contains(&tx.txid()) { continue; } + // skip checkpoints that came from backfill + if tx.input.is_empty() { + continue; + } let mut tx_bytes = vec![]; tx.consensus_encode(&mut tx_bytes)?;