diff --git a/src/app.rs b/src/app.rs index a627b71e9..1e0c15485 100644 --- a/src/app.rs +++ b/src/app.rs @@ -184,8 +184,8 @@ impl InnerApp { let fee = coins.take(fee)?; self.bitcoin.give_rewards(fee)?; - let building = &mut self.bitcoin.checkpoints.building_mut()?; let dest = Dest::Ibc(dest); + let mut building = self.bitcoin.checkpoints.building_mut()?; building.insert_pending(dest, coins)?; Ok(()) @@ -245,20 +245,26 @@ impl InnerApp { sigset_index: u32, dest: Dest, ) -> Result<()> { - if let Dest::Ibc(dest) = dest.clone() { - dest.source_port()?; - dest.source_channel()?; - dest.sender_address()?; - } + #[cfg(target_arch = "wasm32")] + unimplemented!(); + + #[cfg(not(target_arch = "wasm32"))] + { + if let Dest::Ibc(dest) = dest.clone() { + dest.source_port()?; + dest.source_channel()?; + dest.sender_address()?; + } - Ok(self.bitcoin.relay_deposit( - btc_tx, - btc_height, - btc_proof, - btc_vout, - sigset_index, - dest, - )?) + Ok(self.bitcoin.relay_deposit( + btc_tx, + btc_height, + btc_proof, + btc_vout, + sigset_index, + dest, + )?) + } } #[call] @@ -508,12 +514,9 @@ mod abci { self.credit_transfer(dest, coins)?; } - let external_outputs = if self.bitcoin.should_push_checkpoint()? { - self.cosmos - .build_outputs(&self.ibc, self.bitcoin.checkpoints.index)? - } else { - vec![] - }; + let external_outputs = self + .cosmos + .build_outputs(&self.ibc, self.bitcoin.checkpoints.index)?; let offline_signers = self .bitcoin .begin_block_step(external_outputs.into_iter().map(Ok), ctx.hash.clone())?; diff --git a/src/bitcoin/checkpoint.rs b/src/bitcoin/checkpoint.rs index fd1dd0fef..2bc901449 100644 --- a/src/bitcoin/checkpoint.rs +++ b/src/bitcoin/checkpoint.rs @@ -22,7 +22,7 @@ use orga::context::GetContext; use orga::plugins::Time; use orga::{ call::Call, - collections::{map::ReadOnly, ChildMut, Deque, Map, Ref}, + collections::{ChildMut, Deque, Map, Ref}, encoding::{Decode, Encode, LengthVec}, migrate::{Migrate, MigrateFrom}, orga, @@ -891,7 +891,7 @@ impl Checkpoint { } /// Configuration parameters used in processing checkpoints. -#[orga(skip(Default), version = 3)] +#[orga(skip(Default), version = 4)] #[derive(Clone)] pub struct Config { /// The minimum amount of time between the creation of checkpoints, in @@ -919,7 +919,7 @@ pub struct Config { /// If a checkpoint has more inputs than this when advancing from `Building` /// to `Signing`, the excess inputs will be moved to the suceeding, /// newly-created `Building` checkpoint. - pub max_inputs: u64, + pub max_inputs: u32, /// The maximum number of outputs allowed in a checkpoint transaction. /// @@ -929,7 +929,7 @@ pub struct Config { /// If a checkpoint has more outputs than this when advancing from `Building` /// to `Signing`, the excess outputs will be moved to the suceeding, /// newly-created `Building` checkpoint.∑ - pub max_outputs: u64, + pub max_outputs: u32, /// The default fee rate to use when creating the first checkpoint of the /// network, in satoshis per virtual byte. @@ -950,17 +950,17 @@ pub struct Config { /// will be adjusted up if the checkpoint transaction is not confirmed /// within the target number of blocks, and will be adjusted down if the /// checkpoint transaction faster than the target. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub target_checkpoint_inclusion: u32, /// The lower bound to use when adjusting the fee rate of the checkpoint /// transaction, in satoshis per virtual byte. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub min_fee_rate: u64, /// The upper bound to use when adjusting the fee rate of the checkpoint /// transaction, in satoshis per virtual byte. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub max_fee_rate: u64, /// The value (in basis points) to multiply by when calculating the miner @@ -970,24 +970,24 @@ pub struct Config { /// The difference in the fee deducted and the fee paid in the checkpoint /// transaction is added to the fee pool, to help the network pay for /// its own miner fees. - #[orga(version(V3))] + #[orga(version(V3, V4))] pub user_fee_factor: u64, /// The threshold of signatures required to spend reserve scripts, as a /// ratio represented by a tuple, `(numerator, denominator)`. /// /// For example, `(9, 10)` means the threshold is 90% of the signatory set. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub sigset_threshold: (u64, u64), /// The minimum amount of nBTC an account must hold to be eligible for an /// output in the emergency disbursal. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub emergency_disbursal_min_tx_amt: u64, /// The amount of time between the creation of a checkpoint and when the /// associated emergency disbursal transactions can be spent, in seconds. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub emergency_disbursal_lock_time_interval: u32, /// The maximum size of a final emergency disbursal transaction, in virtual @@ -995,7 +995,7 @@ pub struct Config { /// /// The outputs to be included in final emergency disbursal transactions /// will be distributed across multiple transactions around this size. - #[orga(version(V1, V2, V3))] + #[orga(version(V1, V2, V3, V4))] pub emergency_disbursal_max_tx_size: u64, /// The maximum number of unconfirmed checkpoints before the network will @@ -1011,8 +1011,11 @@ pub struct Config { /// This will also stop the fee rate from being adjusted too high if the /// issue is simply with relayers failing to report the confirmation of the /// checkpoint transactions. - #[orga(version(V2, V3))] + #[orga(version(V2, V3, V4))] pub max_unconfirmed_checkpoints: u32, + + #[orga(version(V4))] + pub min_completed_checkpoints: u32, } impl MigrateFrom for ConfigV1 { @@ -1023,14 +1026,14 @@ impl MigrateFrom for ConfigV1 { max_inputs: value.max_inputs, max_outputs: value.max_outputs, max_age: value.max_age, - target_checkpoint_inclusion: ConfigV3::default().target_checkpoint_inclusion, - min_fee_rate: ConfigV3::default().min_fee_rate, - max_fee_rate: ConfigV3::default().max_fee_rate, - sigset_threshold: ConfigV3::default().sigset_threshold, - emergency_disbursal_min_tx_amt: ConfigV3::default().emergency_disbursal_min_tx_amt, - emergency_disbursal_lock_time_interval: ConfigV3::default() + target_checkpoint_inclusion: ConfigV4::default().target_checkpoint_inclusion, + min_fee_rate: ConfigV4::default().min_fee_rate, + max_fee_rate: ConfigV4::default().max_fee_rate, + sigset_threshold: ConfigV4::default().sigset_threshold, + emergency_disbursal_min_tx_amt: ConfigV4::default().emergency_disbursal_min_tx_amt, + emergency_disbursal_lock_time_interval: ConfigV4::default() .emergency_disbursal_lock_time_interval, - emergency_disbursal_max_tx_size: ConfigV3::default().emergency_disbursal_max_tx_size, + emergency_disbursal_max_tx_size: ConfigV4::default().emergency_disbursal_max_tx_size, }) } } @@ -1050,7 +1053,7 @@ impl MigrateFrom for ConfigV2 { emergency_disbursal_min_tx_amt: value.emergency_disbursal_min_tx_amt, emergency_disbursal_lock_time_interval: value.emergency_disbursal_lock_time_interval, emergency_disbursal_max_tx_size: value.emergency_disbursal_max_tx_size, - max_unconfirmed_checkpoints: ConfigV3::default().max_unconfirmed_checkpoints, + max_unconfirmed_checkpoints: ConfigV4::default().max_unconfirmed_checkpoints, }) } } @@ -1070,7 +1073,30 @@ impl MigrateFrom for ConfigV3 { emergency_disbursal_min_tx_amt: value.emergency_disbursal_min_tx_amt, emergency_disbursal_lock_time_interval: value.emergency_disbursal_lock_time_interval, emergency_disbursal_max_tx_size: value.emergency_disbursal_max_tx_size, - ..Default::default() + max_unconfirmed_checkpoints: value.max_unconfirmed_checkpoints, + user_fee_factor: ConfigV4::default().user_fee_factor, + }) + } +} + +impl MigrateFrom for ConfigV4 { + fn migrate_from(value: ConfigV3) -> OrgaResult { + Ok(Self { + min_checkpoint_interval: value.min_checkpoint_interval, + max_checkpoint_interval: value.max_checkpoint_interval, + max_inputs: value.max_inputs, + max_outputs: value.max_outputs, + max_age: value.max_age, + target_checkpoint_inclusion: value.target_checkpoint_inclusion, + min_fee_rate: value.min_fee_rate, + max_fee_rate: value.max_fee_rate, + sigset_threshold: value.sigset_threshold, + emergency_disbursal_min_tx_amt: value.emergency_disbursal_min_tx_amt, + emergency_disbursal_lock_time_interval: value.emergency_disbursal_lock_time_interval, + emergency_disbursal_max_tx_size: value.emergency_disbursal_max_tx_size, + max_unconfirmed_checkpoints: value.max_unconfirmed_checkpoints, + user_fee_factor: value.user_fee_factor, + min_completed_checkpoints: ConfigV4::default().min_completed_checkpoints, }) } } @@ -1106,6 +1132,7 @@ impl Config { emergency_disbursal_lock_time_interval: 60 * 60 * 24 * 7 * 2, // two weeks emergency_disbursal_max_tx_size: 50_000, max_unconfirmed_checkpoints: 15, + min_completed_checkpoints: 10, } } } @@ -1137,7 +1164,7 @@ impl Default for Config { /// broadcast to the Bitcoin network. The queue also maintains a counter /// (`confirmed_index`) to track which of these completed checkpoints have been /// confirmed in a Bitcoin block. -#[orga(version = 2)] +#[orga(version = 3)] pub struct CheckpointQueue { /// The checkpoints in the queue, in order from oldest to newest. The last /// checkpoint is the checkpoint currently being built, and has the index @@ -1151,11 +1178,17 @@ pub struct CheckpointQueue { /// block. Since checkpoints are a sequential cahin, each spending an output /// from the previous, all checkpoints with an index lower than this must /// have also been confirmed. - #[orga(version(V2))] + #[orga(version(V2, V3))] pub confirmed_index: Option, /// Configuration parameters used in processing checkpoints. pub config: Config, + + #[orga(version(V3))] + pub building_deposits: u32, + + #[orga(version(V3))] + pub building_withdrawals: u32, } impl MigrateFrom for CheckpointQueueV1 { @@ -1175,6 +1208,20 @@ impl MigrateFrom for CheckpointQueueV2 { } } +impl MigrateFrom for CheckpointQueueV3 { + fn migrate_from(value: CheckpointQueueV2) -> OrgaResult { + Ok(Self { + queue: value.queue, + index: value.index, + confirmed_index: value.confirmed_index, + config: value.config, + //TODO: Iterate over queue to populate these values + building_deposits: 0, + building_withdrawals: 0, + }) + } +} + /// A wrapper around an immutable reference to a `Checkpoint` which adds type /// information guaranteeing that the checkpoint is in the `Complete` state. #[derive(Deref)] @@ -1236,11 +1283,11 @@ pub struct BuildingCheckpointMut<'a>(ChildMut<'a, u64, Checkpoint>); /// The data returned by the `advance()` method of `BuildingCheckpointMut`. type BuildingAdvanceRes = ( - bitcoin::OutPoint, // reserve outpoint - u64, // reserve size (sats) - u64, // fees paid (sats) - Vec>, // excess inputs - Vec>, // excess outputs + bitcoin::OutPoint, // reserve outpoint + u64, // reserve size (sats) + u64, // fees paid (sats) + u32, // building deposits + u32, // building withdrawals ); impl<'a> BuildingCheckpointMut<'a> { @@ -1579,6 +1626,7 @@ impl<'a> BuildingCheckpointMut<'a> { /// transactions contained within have a known transaction id which will not /// change. #[allow(unused_variables)] + #[allow(clippy::too_many_arguments)] pub fn advance( mut self, nbtc_accounts: &Accounts, @@ -1587,6 +1635,7 @@ impl<'a> BuildingCheckpointMut<'a> { timestamping_commitment: Vec, additional_fees: u64, config: &Config, + first: bool, ) -> Result { self.0.status = CheckpointStatus::Signing; @@ -1595,23 +1644,18 @@ impl<'a> BuildingCheckpointMut<'a> { let mut checkpoint_batch = self.batches.get_mut(BatchType::Checkpoint as u64)?.unwrap(); let mut checkpoint_tx = checkpoint_batch.get_mut(0)?.unwrap(); + + let num_deposits = if first { + checkpoint_tx.input.len() + } else { + checkpoint_tx.input.len() - 1 + }; + let num_withdrawals = checkpoint_tx.output.len(); + for out in outs.iter().rev() { checkpoint_tx.output.push_front(Adapter::new(out.clone()))?; } - // Remove excess inputs and outputs from the checkpoint tx, to be pushed - // onto the suceeding checkpoint while in its `Building` state. - let mut excess_inputs = vec![]; - while checkpoint_tx.input.len() > config.max_inputs { - let removed_input = checkpoint_tx.input.pop_back()?.unwrap(); - excess_inputs.push(removed_input); - } - let mut excess_outputs = vec![]; - while checkpoint_tx.output.len() > config.max_outputs { - let removed_output = checkpoint_tx.output.pop_back()?.unwrap(); - excess_outputs.push(removed_output); - } - // Sum the total input and output amounts. // TODO: Input/Output sum functions let mut in_amount = 0; @@ -1669,8 +1713,8 @@ impl<'a> BuildingCheckpointMut<'a> { reserve_outpoint, reserve_value, fee, - excess_inputs, - excess_outputs, + num_deposits as u32, + num_withdrawals as u32, )) } @@ -1807,14 +1851,16 @@ impl CheckpointQueue { return Ok(out); } - let skip = if self.signing()?.is_some() { 2 } else { 1 }; + let skip = self.signing()?.is_some() as u32 + self.num_building()?; let end = self.index.saturating_sub(skip - 1); let start = end - limit.min(length - skip); for i in start..end { let checkpoint = self.get(i)?; - out.push(CompletedCheckpoint(checkpoint)); + if matches!(checkpoint.status, CheckpointStatus::Complete) { + out.push(CompletedCheckpoint(checkpoint)); + } } Ok(out) @@ -1831,6 +1877,16 @@ impl CheckpointQueue { .ok_or_else(|| Error::Orga(OrgaError::App("No completed checkpoints yet".to_string()))) } + pub fn has_completed_checkpoint(&self) -> Result { + match self.last_completed_index() { + Ok(_) => Ok(true), + Err(Error::Orga(OrgaError::App(err))) if err == "No completed checkpoints yet" => { + Ok(false) + } + Err(err) => Err(err), + } + } + #[query] pub fn first_index(&self) -> Result { Ok(self.index + 1 - self.len()?) @@ -1878,16 +1934,16 @@ impl CheckpointQueue { /// A reference to the checkpoint in the `Signing` state, if there is one. #[query] pub fn signing(&self) -> Result>> { - if self.queue.len() < 2 { - return Ok(None); - } - - let second = self.get(self.index - 1)?; - if !matches!(second.status, CheckpointStatus::Signing) { - return Ok(None); + for c in self.queue.iter()? { + let c = c?; + match c.status { + CheckpointStatus::Signing => return Ok(Some(SigningCheckpoint(c))), + CheckpointStatus::Building => return Ok(None), + _ => continue, + } } - Ok(Some(SigningCheckpoint(second))) + Ok(None) } /// A mutable reference to the checkpoint in the `Signing` state, if there @@ -1911,11 +1967,49 @@ impl CheckpointQueue { /// being signed. Other than at the start of the network, before the first /// deposit has been received, there will always be a checkpoint in this /// state. - pub fn building(&self) -> Result { + pub fn current_building(&self) -> Result { let last = self.get(self.index)?; Ok(BuildingCheckpoint(last)) } + fn next_building_index(&self) -> Result { + let num_building = self.num_building()?; + if self.index == 0 { + Ok(0) + } else { + Ok(self.index + 1 - num_building) + } + } + + pub fn next_building(&self) -> Result { + let index = self.next_building_index()?; + let last = self.get(index)?; + + Ok(BuildingCheckpoint(last)) + } + + pub fn next_building_mut(&mut self) -> Result> { + let index = self.next_building_index()?; + self.get_mut(index) + } + + pub fn all_building(&self) -> Result> { + //TODO: Use reverse iteration + let mut all_building = vec![]; + for c in self.queue.iter()? { + let c = c?; + if matches!(c.status, CheckpointStatus::Building) { + all_building.push(BuildingCheckpoint(c)); + } + } + + Ok(all_building) + } + + pub fn num_building(&self) -> Result { + Ok(self.all_building()?.len() as u32) + } + /// A mutable reference to the checkpoint in the `Building` state. /// /// This is the checkpoint which is currently being built, and is not yet @@ -1927,13 +2021,47 @@ impl CheckpointQueue { Ok(BuildingCheckpointMut(last)) } + #[cfg(not(target_arch = "wasm32"))] + pub fn building_mut_or_insert( + &mut self, + deposit: bool, + sig_keys: &Map, + deposits_enabled: bool, + ) -> Result { + let num_deposit_full = self.building_deposits / self.config.max_inputs; + let num_withdrawal_full = self.building_withdrawals / self.config.max_outputs; + + let num_building = self.num_building()?; + + let num_action_full = if deposit { + num_deposit_full + } else { + num_withdrawal_full + }; + + let building = if num_building != 0 && num_action_full as u64 >= num_building as u64 { + self.maybe_push(sig_keys, deposits_enabled)? + .ok_or_else(|| Error::Checkpoint("Maybe push failed".to_string()))? + } else { + let first_available_building = self.index - num_action_full; + BuildingCheckpointMut(self.get_mut(first_available_building)?) + }; + + Ok(building) + } + /// Prunes old checkpoints from the queue. pub fn prune(&mut self) -> Result<()> { - let latest = self.building()?.create_time(); + if self.is_empty()? { + return Ok(()); + } + + let latest = self.next_building()?.create_time(); + let mut num_completed = + self.len()? - self.num_building()? - self.signing()?.is_some() as u32; while let Some(oldest) = self.queue.front()? { - // TODO: move to min_checkpoints field in config - if self.queue.len() <= 10 { + if num_completed <= self.config.min_completed_checkpoints { break; } @@ -1942,11 +2070,34 @@ impl CheckpointQueue { } self.queue.pop_front()?; + num_completed -= 1; } Ok(()) } + #[cfg(not(target_arch = "wasm32"))] + pub fn insert_pending_deposit( + &mut self, + dest: Dest, + coins: Coin, + sig_keys: &Map, + deposits_enabled: bool, + ) -> Result<()> { + let mut building = self.building_mut_or_insert(true, sig_keys, deposits_enabled)?; + + let mut amount = building + .pending + .remove(dest.clone())? + .map_or(0.into(), |c| c.amount); + amount = (amount + coins.amount).result()?; + + building.pending.insert(dest, Coin::mint(amount))?; + self.building_deposits += 1; + + Ok(()) + } + /// Advances the checkpoint queue state machine. /// /// This method is called once per sidechain block, and will handle adding @@ -1994,74 +2145,49 @@ impl CheckpointQueue { fee_pool: &mut i64, parent_config: &super::Config, ) -> Result { - if !self.should_push(sig_keys, ×tamping_commitment, btc_height)? { - return Ok(false); - } + let mut pushed = false; - if self.maybe_push(sig_keys, should_allow_deposits)?.is_none() { - return Ok(false); + if self.is_empty()? { + pushed |= self.maybe_push(sig_keys, should_allow_deposits)?.is_some(); } self.prune()?; - if self.index > 0 { - let prev = self.get(self.index - 1)?; - let additional_fees = self.fee_adjustment(prev.fee_rate, &self.config)?; + if !self.is_empty()? && self.num_building()? > 0 { + if !self.should_advance(sig_keys, ×tamping_commitment, btc_height)? { + return Ok(pushed); + } + let current_index = self.index; + + let oldest_building = self.next_building()?; + let additional_fees = self.fee_adjustment(oldest_building.fee_rate, &self.config)?; let config = self.config(); - let prev = self.get_mut(self.index - 1)?; - let sigset = prev.sigset.clone(); - let prev_fee_rate = prev.fee_rate; - - let (reserve_outpoint, reserve_value, fees_paid, excess_inputs, excess_outputs) = - BuildingCheckpointMut(prev).advance( + let oldest_building = self.next_building_mut()?; + let sigset = oldest_building.sigset.clone(); + let prev_fee_rate = oldest_building.fee_rate; + let (reserve_outpoint, reserve_value, fees_paid, num_deposits, num_withdrawals) = + BuildingCheckpointMut(oldest_building).advance( nbtc_accounts, recovery_scripts, external_outputs, timestamping_commitment, additional_fees, &config, + current_index == 0, )?; + self.building_deposits -= num_deposits; + self.building_withdrawals -= num_withdrawals; + + if self.num_building()? == 0 { + pushed |= self.maybe_push(sig_keys, should_allow_deposits)?.is_some(); + } + *fee_pool -= (fees_paid * parent_config.units_per_sat) as i64; - // Adjust the fee rate for the next checkpoint based on whether past - // checkpoints have been confirmed in greater or less than the - // target number of Bitcoin blocks. - let fee_rate = if let Some(first_unconf_index) = self.first_unconfirmed_index()? { - // There are unconfirmed checkpoints. - - let first_unconf = self.get(first_unconf_index)?; - let btc_blocks_since_first = - btc_height - first_unconf.signed_at_btc_height.unwrap_or(0); - let miners_excluded_cps = - btc_blocks_since_first >= config.target_checkpoint_inclusion; - - let last_unconf_index = self.last_completed_index()?; - let last_unconf = self.get(last_unconf_index)?; - let btc_blocks_since_last = - btc_height - last_unconf.signed_at_btc_height.unwrap_or(0); - let block_was_mined = btc_blocks_since_last > 0; - - if miners_excluded_cps && block_was_mined { - // Blocks were mined since a signed checkpoint, but it was - // not included. - adjust_fee_rate(prev_fee_rate, true, &config) - } else { - prev_fee_rate - } - } else { - let has_completed = self.last_completed_index().is_ok(); - if has_completed { - // No unconfirmed checkpoints. - adjust_fee_rate(prev_fee_rate, false, &config) - } else { - // This case only happens at start of chain - having no - // unconfs doesn't mean anything. - prev_fee_rate - } - }; + let fee_rate = self.get_adjusted_fee_rate(prev_fee_rate, btc_height, &config)?; - let mut building = self.building_mut()?; + let mut building = self.next_building_mut()?; building.fee_rate = fee_rate; let mut building_checkpoint_batch = building .batches @@ -2079,22 +2205,9 @@ impl CheckpointQueue { config.sigset_threshold, )?; checkpoint_tx.input.push_back(input)?; - - // Add any excess inputs and outputs from the previous checkpoint to - // the new checkpoint. - for input in excess_inputs { - let shares = input.signatures.shares()?; - let mut data = input.into_inner(); - data.signatures = ThresholdSig::from_shares(shares)?; - checkpoint_tx.input.push_back(data)?; - } - for output in excess_outputs { - let data = output.into_inner(); - checkpoint_tx.output.push_back(data)?; - } } - Ok(true) + Ok(pushed) } /// Returns `true` if a new checkpoint will be pushed to the queue in the @@ -2103,13 +2216,13 @@ impl CheckpointQueue { /// Note that a new checkpoint being pushed also necessarily means that the /// `Building` checkpoint will be advanced to `Signing`. #[cfg(feature = "full")] - pub fn should_push( + pub fn should_advance( &mut self, sig_keys: &Map, timestamping_commitment: &[u8], btc_height: u32, ) -> Result { - // Do not push if there is a checkpoint in the `Signing` state. There + // Do not advance if there is a checkpoint in the `Signing` state. There // should only ever be at most one checkpoint in this state. if self.signing()?.is_some() { return Ok(false); @@ -2120,15 +2233,15 @@ impl CheckpointQueue { .context::