From b05d453bc5f908bac48a804bffd69300a52e8e08 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Tue, 30 Jan 2024 12:31:54 -0500 Subject: [PATCH] feat: improved stacking orders (#1331) * add some pox constants * validate epoch 3 block * implement stack-extend for stacking orders * validate wallet names for stacking orders * fix build and ui pox info * add todo's * refactor: rework devnet stack-extend logic * refactor: remove dead code * refactor: remove comment * refactor: format * refactor: devnet handle stacking auto-extend config * refactor: remove dead code * chore: remove useless mut * chore: force update * refactor: improve stacking_order condition * fix: updater default stacking_orders config --------- Co-authored-by: Hugo Caillard <911307+hugocaillard@users.noreply.github.com> --- .../clarinet-cli/src/generate/project.rs | 4 +- components/clarinet-files/src/lib.rs | 2 +- .../clarinet-files/src/network_manifest.rs | 57 +++++ components/stacks-devnet-js/src/lib.rs | 1 + .../stacks-network/src/chains_coordinator.rs | 202 ++++++++++-------- components/stacks-network/src/ui/ui.rs | 2 +- .../stacks-rpc-client/src/rpc_client.rs | 2 +- 7 files changed, 177 insertions(+), 93 deletions(-) diff --git a/components/clarinet-cli/src/generate/project.rs b/components/clarinet-cli/src/generate/project.rs index cbf94f83b..b6c51fb7a 100644 --- a/components/clarinet-cli/src/generate/project.rs +++ b/components/clarinet-cli/src/generate/project.rs @@ -415,14 +415,14 @@ disable_stacks_api = false # Send some stacking orders [[devnet.pox_stacking_orders]] -start_at_cycle = 0 +start_at_cycle = 2 duration = 12 wallet = "wallet_1" slots = 2 btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" [[devnet.pox_stacking_orders]] -start_at_cycle = 1 +start_at_cycle = 2 duration = 12 wallet = "wallet_2" slots = 1 diff --git a/components/clarinet-files/src/lib.rs b/components/clarinet-files/src/lib.rs index 34608a617..629089239 100644 --- a/components/clarinet-files/src/lib.rs +++ b/components/clarinet-files/src/lib.rs @@ -23,7 +23,7 @@ pub use network_manifest::{ DEFAULT_BITCOIN_NODE_IMAGE, DEFAULT_DERIVATION_PATH, DEFAULT_DOCKER_PLATFORM, DEFAULT_EPOCH_2_0, DEFAULT_EPOCH_2_05, DEFAULT_EPOCH_2_1, DEFAULT_EPOCH_2_2, DEFAULT_EPOCH_2_3, DEFAULT_EPOCH_2_4, DEFAULT_EPOCH_2_5, DEFAULT_EPOCH_3_0, DEFAULT_FAUCET_MNEMONIC, - DEFAULT_POSTGRES_IMAGE, DEFAULT_STACKS_API_IMAGE, DEFAULT_STACKS_API_IMAGE_NAKA, + DEFAULT_FIRST_BURN_HEADER_HEIGHT, DEFAULT_POSTGRES_IMAGE, DEFAULT_STACKS_API_IMAGE, DEFAULT_STACKS_EXPLORER_IMAGE, DEFAULT_STACKS_MINER_MNEMONIC, DEFAULT_STACKS_NODE_IMAGE, DEFAULT_STACKS_NODE_IMAGE_NAKA, DEFAULT_SUBNET_API_IMAGE, DEFAULT_SUBNET_CONTRACT_ID, DEFAULT_SUBNET_MNEMONIC, DEFAULT_SUBNET_NODE_IMAGE, diff --git a/components/clarinet-files/src/network_manifest.rs b/components/clarinet-files/src/network_manifest.rs index 83b8faac8..ff70efcf9 100644 --- a/components/clarinet-files/src/network_manifest.rs +++ b/components/clarinet-files/src/network_manifest.rs @@ -48,6 +48,13 @@ pub const DEFAULT_EPOCH_2_4: u64 = 104; pub const DEFAULT_EPOCH_2_5: u64 = 105; pub const DEFAULT_EPOCH_3_0: u64 = 121; +// Currently, the pox-4 contract has these values hardcoded: +// https://github.com/stacks-network/stacks-core/blob/e09ab931e2f15ff70f3bb5c2f4d7afb[…]42bd7bec6/stackslib/src/chainstate/stacks/boot/pox-testnet.clar +// but they may be configurable in the future. +pub const DEFAULT_POX_PREPARE_LENGTH: u64 = 4; +pub const DEFAULT_POX_REWARD_LENGTH: u64 = 10; +pub const DEFAULT_FIRST_BURN_HEADER_HEIGHT: u64 = 100; + #[derive(Serialize, Deserialize, Debug)] pub struct NetworkManifestFile { network: NetworkConfigFile, @@ -316,6 +323,7 @@ pub struct PoxStackingOrder { pub wallet: String, pub slots: u64, pub btc_address: String, + pub auto_extend: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -770,6 +778,34 @@ impl NetworkManifest { let remapped_subnet_contract_id = format!("{}.{}", default_deployer.stx_address, contract_id.name); + // validate that epoch 3.0 is started in a reward phase + let epoch_3_0 = devnet_config.epoch_3_0.unwrap_or(DEFAULT_EPOCH_3_0); + if !is_in_reward_phase( + DEFAULT_FIRST_BURN_HEADER_HEIGHT, + DEFAULT_POX_REWARD_LENGTH, + DEFAULT_POX_PREPARE_LENGTH, + &epoch_3_0, + ) { + return Err(format!( + "Epoch 3.0 must start *during* a reward phase, not a prepare phase. Epoch 3.0 start set to: {}. Reward Cycle Length: {}. Prepare Phase Length: {}", + epoch_3_0, DEFAULT_POX_REWARD_LENGTH, DEFAULT_POX_PREPARE_LENGTH + )); + } + + // for stacking orders, we validate that wallet names match one of the provided accounts + if let Some(ref val) = devnet_config.pox_stacking_orders { + for (i, stacking_order) in val.iter().enumerate() { + let wallet_name = &stacking_order.wallet; + let wallet_is_in_accounts = accounts + .iter() + .any(|(account_name, _)| wallet_name == account_name); + if !wallet_is_in_accounts { + return Err(format!("Account data was not provided for the wallet ({}) listed in stacking order {}.", wallet_name, i + 1)); + }; + } + + devnet_config.pox_stacking_orders = Some(val.clone()); + } let config = DevnetConfig { name: devnet_config.name.take().unwrap_or("devnet".into()), network_id: devnet_config.network_id, @@ -1008,6 +1044,27 @@ fn compute_btc_address(public_key: &PublicKey, network: &BitcoinNetwork) -> Stri btc_address.to_string() } +// This logic was taken from stacks-core: +// https://github.com/stacks-network/stacks-core/blob/524b0e1ae9ad3c8d2d2ac37e72be4aee2c045ef8/src/burnchains/mod.rs#L513C30-L530 +pub fn is_in_reward_phase( + first_block_height: u64, + reward_cycle_length: u64, + prepare_length: u64, + block_height: &u64, +) -> bool { + if block_height <= &first_block_height { + // not a reward cycle start if we're the first block after genesis. + false + } else { + let effective_height = block_height - first_block_height; + let reward_index = effective_height % reward_cycle_length; + + // NOTE: first block in reward cycle is mod 1, so mod 0 is the last block in the + // prepare phase. + !(reward_index == 0 || reward_index > (reward_cycle_length - prepare_length)) + } +} + #[cfg(feature = "wasm")] fn compute_btc_address(_public_key: &PublicKey, _network: &BitcoinNetwork) -> String { "__not_implemented__".to_string() diff --git a/components/stacks-devnet-js/src/lib.rs b/components/stacks-devnet-js/src/lib.rs index 9eda9c385..8a891326c 100644 --- a/components/stacks-devnet-js/src/lib.rs +++ b/components/stacks-devnet-js/src/lib.rs @@ -746,6 +746,7 @@ impl StacksDevnet { wallet, slots, btc_address, + auto_extend: Some(false), }); } overrides.pox_stacking_orders = Some(stacking_orders); diff --git a/components/stacks-network/src/chains_coordinator.rs b/components/stacks-network/src/chains_coordinator.rs index 8892a57e3..ddbb8856f 100644 --- a/components/stacks-network/src/chains_coordinator.rs +++ b/components/stacks-network/src/chains_coordinator.rs @@ -14,11 +14,9 @@ use chainhook_sdk::observer::{ use chainhook_sdk::types::BitcoinBlockSignaling; use chainhook_sdk::types::BitcoinChainEvent; use chainhook_sdk::types::BitcoinNetwork; -use chainhook_sdk::types::StacksBlockData; use chainhook_sdk::types::StacksChainEvent; use chainhook_sdk::types::StacksNetwork; use chainhook_sdk::types::StacksNodeConfig; -use chainhook_sdk::types::StacksTransactionKind; use chainhook_sdk::utils::Context; use clarinet_deployments::onchain::TransactionStatus; use clarinet_deployments::onchain::{ @@ -26,6 +24,7 @@ use clarinet_deployments::onchain::{ }; use clarinet_deployments::types::DeploymentSpecification; use clarinet_files::PoxStackingOrder; +use clarinet_files::DEFAULT_FIRST_BURN_HEADER_HEIGHT; use clarinet_files::{self, AccountConfig, DevnetConfig, NetworkManifest, ProjectManifest}; use clarity_repl::clarity::address::AddressHashMode; use clarity_repl::clarity::util::hash::{hex_bytes, Hash160}; @@ -285,9 +284,30 @@ pub async fn start_chains_coordinator( let (log, status) = match &chain_update { BitcoinChainEvent::ChainUpdatedWithBlocks(event) => { let tip = event.new_blocks.last().unwrap(); - let log = format!("Bitcoin block #{} received", tip.block_identifier.index); + let bitcoin_block_height = tip.block_identifier.index; + let log = format!("Bitcoin block #{} received", bitcoin_block_height); let status = - format!("mining blocks (chaintip = #{})", tip.block_identifier.index); + format!("mining blocks (chaintip = #{})", bitcoin_block_height); + + // Stacking orders can't be published until devnet is ready + if bitcoin_block_height >= DEFAULT_FIRST_BURN_HEADER_HEIGHT + 10 { + let res = publish_stacking_orders( + &config.devnet_config, + &devnet_event_tx, + &config.accounts, + &config.services_map_hosts, + config.deployment_fee_rate, + bitcoin_block_height as u32, + ) + .await; + if let Some(tx_count) = res { + let _ = devnet_event_tx.send(DevnetEvent::success(format!( + "Broadcasted {} stacking orders", + tx_count + ))); + } + } + (log, status) } BitcoinChainEvent::ChainUpdatedWithReorg(events) => { @@ -382,42 +402,6 @@ pub async fn start_chains_coordinator( ) }; let _ = devnet_event_tx.send(DevnetEvent::info(message)); - - // only publish stacking order txs in tenure-change blocks - let has_coinbase_tx = known_tip - .block - .transactions - .iter() - .any(|tx| tx.metadata.kind == StacksTransactionKind::Coinbase); - if has_coinbase_tx { - let bitcoin_block_height = known_tip - .block - .metadata - .bitcoin_anchor_block_identifier - .index; - - // stacking early in the cycle to make sure that - // the transactions are included in the next cycle - let should_submit_pox_orders = known_tip.block.metadata.pox_cycle_position == 1; - if should_submit_pox_orders { - let res = publish_stacking_orders( - &known_tip.block, - &config.devnet_config, - &devnet_event_tx, - &config.accounts, - &config.services_map_hosts, - config.deployment_fee_rate, - bitcoin_block_height as u32, - ) - .await; - if let Some(tx_count) = res { - let _ = devnet_event_tx.send(DevnetEvent::success(format!( - "Broadcasted {} stacking orders", - tx_count - ))); - } - } - } } ObserverEvent::NotifyBitcoinTransactionProxied => { if !boot_completed.load(Ordering::SeqCst) { @@ -542,7 +526,6 @@ pub fn relay_devnet_protocol_deployment( } pub async fn publish_stacking_orders( - block: &StacksBlockData, devnet_config: &DevnetConfig, devnet_event_tx: &Sender, accounts: &[AccountConfig], @@ -550,30 +533,58 @@ pub async fn publish_stacking_orders( fee_rate: u64, bitcoin_block_height: u32, ) -> Option { - let orders_to_broadcast: Vec<&PoxStackingOrder> = devnet_config - .pox_stacking_orders - .iter() - .filter(|pox_stacking_order| { - pox_stacking_order.start_at_cycle - 1 == block.metadata.pox_cycle_index - }) - .collect(); - - if orders_to_broadcast.is_empty() { - return None; - } - let stacks_node_rpc_url = format!("http://{}", &services_map_hosts.stacks_node_host); + let pox_info: PoxInfo = match reqwest::get(format!("{}/v2/pox", stacks_node_rpc_url)).await { + Ok(result) => match result.json().await { + Ok(pox_info) => Some(pox_info), + Err(e) => { + let _ = devnet_event_tx.send(DevnetEvent::warning(format!( + "Unable to parse pox info: {}", + e + ))); - let mut transactions = 0; + None + } + }, + Err(e) => { + let _ = devnet_event_tx.send(DevnetEvent::warning(format!( + "unable to retrieve pox info: {}", + e + ))); + None + } + }?; - let pox_info: PoxInfo = reqwest::get(format!("{}/v2/pox", stacks_node_rpc_url)) - .await - .expect("Unable to retrieve pox info") - .json() - .await - .expect("Unable to parse contract"); + let effective_height = u64::saturating_sub( + bitcoin_block_height.into(), + pox_info.first_burnchain_block_height, + ); + let pox_cycle_length: u64 = + (pox_info.prepare_phase_block_length + pox_info.reward_phase_block_length).into(); + let reward_cycle_id = effective_height / pox_cycle_length; + + let pox_cycle_position = (effective_height % pox_cycle_length) as u32; - for (i, pox_stacking_order) in orders_to_broadcast.iter().enumerate() { + let should_submit_pox_orders = pox_cycle_position == 1; + if !should_submit_pox_orders { + return None; + } + + let mut transactions = 0; + for (i, pox_stacking_order) in devnet_config.pox_stacking_orders.iter().enumerate() { + let PoxStackingOrder { + duration, + start_at_cycle, + .. + } = pox_stacking_order; + + if ((reward_cycle_id as u32) % duration) != (start_at_cycle - 1) { + continue; + } + let extend_stacking = reward_cycle_id as u32 != start_at_cycle - 1; + if extend_stacking && !pox_stacking_order.auto_extend.unwrap_or_default() { + continue; + } let account = accounts .iter() .find(|e| e.label == pox_stacking_order.wallet); @@ -590,7 +601,6 @@ pub async fn publish_stacking_orders( .btc_address .from_base58() .expect("Unable to get bytes from btc address"); - let duration = pox_stacking_order.duration.into(); let node_url = stacks_node_rpc_url.clone(); let pox_contract_id = pox_info.contract_id.clone(); let pox_version = pox_contract_id @@ -599,6 +609,7 @@ pub async fn publish_stacking_orders( .and_then(|version| version.parse::().ok()) .unwrap(); + let duration = *duration; let stacking_result = hiro_system_kit::thread_named("Stacking orders handler").spawn(move || { let default_fee = fee_rate * 1000; @@ -611,43 +622,58 @@ pub async fn publish_stacking_orders( &StacksNetwork::Devnet.get_networks(), ); - let addr_bytes = Hash160::from_bytes(&addr_bytes[1..21]).unwrap(); - let addr_version = AddressHashMode::SerializeP2PKH; - - let mut arguments = vec![ - ClarityValue::UInt(stx_amount.into()), - ClarityValue::Tuple( - TupleData::from_data(vec![ - ( - ClarityName::try_from("version".to_owned()).unwrap(), - ClarityValue::buff_from_byte(addr_version as u8), - ), - ( - ClarityName::try_from("hashbytes".to_owned()).unwrap(), - ClarityValue::Sequence(SequenceData::Buffer(BuffData { - data: addr_bytes.as_bytes().to_vec(), - })), - ), - ]) - .unwrap(), + let pox_addr_arg = ClarityValue::Tuple( + TupleData::from_data(vec![ + ( + ClarityName::try_from("version".to_owned()).unwrap(), + ClarityValue::buff_from_byte(AddressHashMode::SerializeP2PKH as u8), + ), + ( + ClarityName::try_from("hashbytes".to_owned()).unwrap(), + ClarityValue::Sequence(SequenceData::Buffer(BuffData { + data: Hash160::from_bytes(&addr_bytes[1..21]) + .unwrap() + .as_bytes() + .to_vec(), + })), + ), + ]) + .unwrap(), + ); + + let (method, mut arguments) = match extend_stacking { + false => ( + "stack-stx", + vec![ + ClarityValue::UInt(stx_amount.into()), + pox_addr_arg, + ClarityValue::UInt((bitcoin_block_height - 1).into()), + ClarityValue::UInt(duration.into()), + ], ), - ClarityValue::UInt((bitcoin_block_height - 1).into()), - ClarityValue::UInt(duration), - ]; + true => ( + "stack-extend", + vec![ClarityValue::UInt(duration.into()), pox_addr_arg], + ), + }; + if pox_version >= 4 { - let signer_key = vec![i as u8; 33]; + let mut signer_key = vec![0; 33]; + signer_key[0] = i as u8; + signer_key[1] = nonce as u8; arguments.push(ClarityValue::buff_from(signer_key).unwrap()); }; - let stack_stx_tx = codec::build_contrat_call_transaction( + let tx = codec::build_contrat_call_transaction( pox_contract_id, - "stack-stx".into(), + method.into(), arguments, nonce, default_fee, &hex_bytes(&account_secret_key).unwrap(), ); - stacks_rpc.post_transaction(&stack_stx_tx) + + stacks_rpc.post_transaction(&tx) }); match stacking_result { diff --git a/components/stacks-network/src/ui/ui.rs b/components/stacks-network/src/ui/ui.rs index c49de6d51..ccbe1fb53 100644 --- a/components/stacks-network/src/ui/ui.rs +++ b/components/stacks-network/src/ui/ui.rs @@ -227,7 +227,7 @@ fn draw_block_details(f: &mut Frame, area: Rect, block: &StacksBlockData) { Paragraph::new("PoX informations").style(Style::default().add_modifier(Modifier::BOLD)); f.render_widget(title, labels[7]); - let label = format!("PoX Cycle: {}", block.metadata.pox_cycle_index); + let label = format!("PoX Cycle: {}", block.metadata.pox_cycle_index + 1); let paragraph = Paragraph::new(label); f.render_widget(paragraph, labels[8]); diff --git a/components/stacks-rpc-client/src/rpc_client.rs b/components/stacks-rpc-client/src/rpc_client.rs index a6696a7c8..f002753ea 100644 --- a/components/stacks-rpc-client/src/rpc_client.rs +++ b/components/stacks-rpc-client/src/rpc_client.rs @@ -112,7 +112,7 @@ impl Default for PoxInfo { current_burnchain_block_height: 100, first_burnchain_block_height: 100, prepare_phase_block_length: 4, - reward_phase_block_length: 6, + reward_phase_block_length: 10, reward_slots: 10, total_liquid_supply_ustx: 1000000000000000, reward_cycle_id: 0,