diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index b380147630..7f096c475a 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -47,6 +47,7 @@ jobs: - tests::neon_integrations::bitcoind_integration_test - tests::neon_integrations::liquid_ustx_integration - tests::neon_integrations::stx_transfer_btc_integration_test + - tests::neon_integrations::stx_delegate_btc_integration_test - tests::neon_integrations::bitcoind_forking_test - tests::neon_integrations::should_fix_2771 - tests::neon_integrations::pox_integration_test diff --git a/src/chainstate/stacks/boot/pox_2_tests.rs b/src/chainstate/stacks/boot/pox_2_tests.rs index 959db16ce3..ab9cf9ad42 100644 --- a/src/chainstate/stacks/boot/pox_2_tests.rs +++ b/src/chainstate/stacks/boot/pox_2_tests.rs @@ -1339,6 +1339,7 @@ fn delegate_stack_increase() { let tip = get_tip(peer.sortdb.as_ref()); // submit delegation tx + let success_alice_delegation = alice_nonce; let alice_delegation_1 = make_pox_2_contract_call( &alice, alice_nonce, @@ -1554,6 +1555,37 @@ fn delegate_stack_increase() { "(err 18)" ); + let delegate_stx_tx = &alice_txs + .get(&success_alice_delegation) + .unwrap() + .clone() + .events[0]; + let delegate_stx_op_data = HashMap::from([ + ("pox-addr", Value::none()), + ("amount-ustx", Value::UInt(10230000000000)), + ("unlock-burn-height", Value::none()), + ( + "delegate-to", + Value::Principal( + StacksAddress::from_string("ST1GCB6NH3XR67VT4R5PKVJ2PYXNVQ4AYQATXNP4P") + .unwrap() + .to_account_principal(), + ), + ), + ]); + let common_data = PoxPrintFields { + op_name: "delegate-stx".to_string(), + stacker: Value::Principal( + StacksAddress::from_string("ST2Q1B4S2DY2Y96KYNZTVCCZZD1V9AGWCS5MFXM4C") + .unwrap() + .to_account_principal(), + ), + balance: Value::UInt(10240000000000), + locked: Value::UInt(0), + burnchain_unlock_height: Value::UInt(0), + }; + check_pox_print_event(delegate_stx_tx, common_data, delegate_stx_op_data); + // Check that the call to `delegate-stack-increase` has a well-formed print event. let delegate_stack_increase_tx = &bob_txs.get(&4).unwrap().clone().events[0]; let pox_addr_val = generate_pox_clarity_value("60c59ab11f7063ef44c16d3dc856f76bbb915eba"); diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 7b0dfbd96a..7cb5354b2b 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -5867,9 +5867,9 @@ impl StacksChainState { /// in the block, and a `PreCommitClarityBlock` struct. /// /// The `StacksEpochReceipts` contains the list of transaction - /// receipts for both the preceeding microblock stream that the - /// block confirms, as well as the transaction receipts for the - /// anchored block's transactions. Finally, it returns the + /// receipts for the preceeding microblock stream that the + /// block confirms, the anchored block's transactions, and the + /// btc wire transactions. Finally, it returns the /// execution costs for the microblock stream and for the anchored /// block (separately). /// @@ -12132,7 +12132,10 @@ pub mod test { .collect(); init_balances.push((addr.to_account_principal(), initial_balance)); peer_config.initial_balances = init_balances; - peer_config.epochs = Some(StacksEpoch::unit_test_2_1(0)); + let mut epochs = StacksEpoch::unit_test_2_1(0); + let num_epochs = epochs.len(); + epochs[num_epochs - 1].block_limit.runtime = 10_000_000; + peer_config.epochs = Some(epochs); peer_config.burnchain.pox_constants.v1_unlock_height = 26; let mut peer = TestPeer::new(peer_config); diff --git a/src/clarity_vm/special.rs b/src/clarity_vm/special.rs index 17a99626de..15eb778421 100644 --- a/src/clarity_vm/special.rs +++ b/src/clarity_vm/special.rs @@ -253,16 +253,19 @@ fn handle_pox_v1_api_contract_call( } /// Determine who the stacker is for a given function. -/// - for non-delegate functions, it's tx-sender -/// - for delegate functions, it's the first argument +/// - for non-delegate stacking functions, it's tx-sender +/// - for delegate stacking functions, it's the first argument fn get_stacker(sender: &PrincipalData, function_name: &str, args: &[Value]) -> Value { match function_name { - "stack-stx" | "stack-increase" | "stack-extend" => Value::Principal(sender.clone()), + "stack-stx" | "stack-increase" | "stack-extend" | "delegate-stx" => { + Value::Principal(sender.clone()) + } _ => args[0].clone(), } } -/// Craft the code snippet to evaluate an event-info for a stack-* or a delegate-stack-* function +/// Craft the code snippet to evaluate an event-info for a stack-* function, +/// a delegate-stack-* function, or for delegate-stx fn create_event_info_stack_or_delegate_code( sender: &PrincipalData, function_name: &str, @@ -532,6 +535,32 @@ fn create_event_info_data_code(function_name: &str, args: &[Value]) -> String { reward_cycle = &args[1] ) } + "delegate-stx" => { + format!( + r#" + {{ + data: {{ + ;; amount of ustx to delegate. + ;; equal to args[0] + amount-ustx: {amount_ustx}, + ;; address of delegatee. + ;; equal to args[1] + delegate-to: '{delegate_to}, + ;; optional burnchain height when the delegation finishes. + ;; derived from args[2] + unlock-burn-height: {until_burn_height}, + ;; optional PoX address tuple. + ;; equal to args[3]. + pox-addr: {pox_addr} + }} + }} + "#, + amount_ustx = &args[0], + delegate_to = &args[1], + until_burn_height = &args[2], + pox_addr = &args[3], + ) + } _ => format!("{{ data: {{ unimplemented: true }} }}"), } } @@ -558,7 +587,8 @@ fn synthesize_pox_2_event_info( | "stack-extend" | "delegate-stack-extend" | "stack-increase" - | "delegate-stack-increase" => Some(create_event_info_stack_or_delegate_code( + | "delegate-stack-increase" + | "delegate-stx" => Some(create_event_info_stack_or_delegate_code( sender, function_name, args, diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 294d7bb2b8..06fb637a5f 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -39,8 +39,8 @@ use stacks::burnchains::{ use stacks::burnchains::{Burnchain, BurnchainParameters}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, TransferStxOp, - UserBurnSupportOp, + BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, + TransferStxOp, UserBurnSupportOp, }; use stacks::chainstate::coordinator::comm::CoordinatorChannels; #[cfg(test)] @@ -834,6 +834,17 @@ impl BitcoinRegtestController { unimplemented!() } + #[cfg(not(test))] + fn build_delegate_stacks_tx( + &mut self, + _epoch_id: StacksEpochId, + _payload: DelegateStxOp, + _signer: &mut BurnchainOpSigner, + _utxo: Option, + ) -> Option { + unimplemented!() + } + #[cfg(test)] pub fn submit_manual( &mut self, @@ -950,6 +961,89 @@ impl BitcoinRegtestController { Some(tx) } + #[cfg(test)] + /// Build a delegate stacks tx. + /// this *only* works if the only existant UTXO is from a PreStx Op + /// this is okay for testing, but obviously not okay for actual use. + /// The reason for this constraint is that the bitcoin_regtest_controller's UTXO + /// and signing logic are fairly intertwined, and untangling the two seems excessive + /// for a functionality that won't be implemented for production via this controller. + fn build_delegate_stacks_tx( + &mut self, + epoch_id: StacksEpochId, + payload: DelegateStxOp, + signer: &mut BurnchainOpSigner, + utxo_to_use: Option, + ) -> Option { + let public_key = signer.get_public_key(); + let max_tx_size = 230; + + let (mut tx, mut utxos) = if let Some(utxo) = utxo_to_use { + ( + Transaction { + input: vec![], + output: vec![], + version: 1, + lock_time: 0, + }, + UTXOSet { + bhh: BurnchainHeaderHash::zero(), + utxos: vec![utxo], + }, + ) + } else { + self.prepare_tx( + epoch_id, + &public_key, + DUST_UTXO_LIMIT + max_tx_size * self.config.burnchain.satoshis_per_byte, + None, + None, + 0, + )? + }; + + // Serialize the payload + let op_bytes = { + let mut bytes = self.config.burnchain.magic_bytes.as_bytes().to_vec(); + payload.consensus_serialize(&mut bytes).ok()?; + bytes + }; + + let consensus_output = TxOut { + value: 0, + script_pubkey: Builder::new() + .push_opcode(opcodes::All::OP_RETURN) + .push_slice(&op_bytes) + .into_script(), + }; + + tx.output = vec![consensus_output]; + tx.output.push( + PoxAddress::Standard(payload.delegate_to.clone(), None) + .to_bitcoin_tx_out(DUST_UTXO_LIMIT), + ); + + self.finalize_tx( + epoch_id, + &mut tx, + DUST_UTXO_LIMIT, + 0, + max_tx_size, + self.config.burnchain.satoshis_per_byte, + &mut utxos, + signer, + )?; + + increment_btc_ops_sent_counter(); + + info!( + "Miner node: submitting stacks delegate op - {}", + public_key.to_hex() + ); + + Some(tx) + } + #[cfg(not(test))] fn build_pre_stacks_tx( &mut self, @@ -1687,7 +1781,9 @@ impl BitcoinRegtestController { self.build_transfer_stacks_tx(epoch_id, payload, op_signer, None) } BlockstackOperationType::StackStx(_payload) => unimplemented!(), - BlockstackOperationType::DelegateStx(_payload) => unimplemented!(), + BlockstackOperationType::DelegateStx(payload) => { + self.build_delegate_stacks_tx(epoch_id, payload, op_signer, None) + } }; transaction.map(|tx| SerializedTx::new(tx)) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 5ff49faae4..cd8161e5cd 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -16,7 +16,9 @@ use rusqlite::types::ToSql; use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType}; use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::Txid; -use stacks::chainstate::burn::operations::{BlockstackOperationType, PreStxOp, TransferStxOp}; +use stacks::chainstate::burn::operations::{ + BlockstackOperationType, DelegateStxOp, PreStxOp, TransferStxOp, +}; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::clarity_cli::vm_execute as execute; use stacks::codec::StacksMessageCodec; @@ -956,6 +958,7 @@ pub fn get_balance(http_origin: &str, account: &F) -> u128 #[derive(Debug)] pub struct Account { pub balance: u128, + pub locked: u128, pub nonce: u64, } @@ -971,6 +974,7 @@ pub fn get_account(http_origin: &str, account: &F) -> Acco info!("Account response: {:#?}", res); Account { balance: u128::from_str_radix(&res.balance[2..], 16).unwrap(), + locked: u128::from_str_radix(&res.locked[2..], 16).unwrap(), nonce: res.nonce, } } @@ -1685,6 +1689,266 @@ fn stx_transfer_btc_integration_test() { channel.stop_chains_coordinator(); } +#[test] +#[ignore] +fn stx_delegate_btc_integration_test() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let spender_sk = StacksPrivateKey::from_hex(SK_1).unwrap(); + let spender_stx_addr: StacksAddress = to_addr(&spender_sk); + let spender_addr: PrincipalData = spender_stx_addr.clone().into(); + + let recipient_sk = StacksPrivateKey::new(); + let recipient_addr = to_addr(&recipient_sk); + let pox_pubkey = Secp256k1PublicKey::from_hex( + "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", + ) + .unwrap(); + let pox_pubkey_hash = bytes_to_hex( + &Hash160::from_node_public_key(&pox_pubkey) + .to_bytes() + .to_vec(), + ); + + let (mut conf, _miner_account) = neon_integration_test_conf(); + + conf.initial_balances.push(InitialBalance { + address: spender_addr.clone(), + amount: 100300, + }); + conf.initial_balances.push(InitialBalance { + address: recipient_addr.clone().into(), + amount: 300, + }); + + // update epoch info so that Epoch 2.1 takes effect + conf.burnchain.epochs = Some(vec![ + StacksEpoch { + epoch_id: StacksEpochId::Epoch20, + start_height: 0, + end_height: 1, + block_limit: BLOCK_LIMIT_MAINNET_20.clone(), + network_epoch: PEER_VERSION_EPOCH_2_0, + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch2_05, + start_height: 1, + end_height: 2, + block_limit: BLOCK_LIMIT_MAINNET_205.clone(), + network_epoch: PEER_VERSION_EPOCH_2_05, + }, + StacksEpoch { + epoch_id: StacksEpochId::Epoch21, + start_height: 2, + end_height: 9223372036854775807, + block_limit: BLOCK_LIMIT_MAINNET_21.clone(), + network_epoch: PEER_VERSION_EPOCH_2_1, + }, + ]); + conf.burnchain.pox_2_activation = Some(3); + + test_observer::spawn(); + conf.events_observers.push(EventObserverConfig { + endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT), + events_keys: vec![EventKeyType::AnyEvent], + }); + + let mut btcd_controller = BitcoinCoreController::new(conf.clone()); + btcd_controller + .start_bitcoind() + .map_err(|_e| ()) + .expect("Failed starting bitcoind"); + + let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); + + // reward cycle length = 5, so 3 reward cycle slots + 2 prepare-phase burns + let reward_cycle_len = 5; + let prepare_phase_len = 2; + let pox_constants = PoxConstants::new( + reward_cycle_len, + prepare_phase_len, + 2, + 5, + 15, + (16 * reward_cycle_len - 1).into(), + (17 * reward_cycle_len).into(), + u32::MAX, + ); + burnchain_config.pox_constants = pox_constants.clone(); + + let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( + conf.clone(), + None, + Some(burnchain_config.clone()), + None, + ); + let http_origin = format!("http://{}", &conf.node.rpc_bind); + + btc_regtest_controller.bootstrap_chain(201); + + eprintln!("Chain bootstrapped..."); + + let mut run_loop = neon::RunLoop::new(conf.clone()); + let blocks_processed = run_loop.get_blocks_processed_arc(); + + let channel = run_loop.get_coordinator_channel().unwrap(); + + thread::spawn(move || run_loop.start(None, 0)); + + // give the run loop some time to start up! + wait_for_runloop(&blocks_processed); + + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + test_observer::clear(); + + // Mine a few more blocks so that Epoch 2.1 (and thus pox-2) can take effect. + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + // okay, let's send a pre-stx op. + let pre_stx_op = PreStxOp { + output: spender_stx_addr.clone(), + // to be filled in + txid: Txid([0u8; 32]), + vtxindex: 0, + block_height: 0, + burn_header_hash: BurnchainHeaderHash([0u8; 32]), + }; + + let mut miner_signer = Keychain::default(conf.node.seed.clone()).generate_op_signer(); + + assert!( + btc_regtest_controller + .submit_operation( + StacksEpochId::Epoch21, + BlockstackOperationType::PreStx(pre_stx_op), + &mut miner_signer, + 1 + ) + .is_some(), + "Pre-stx operation should submit successfully" + ); + + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + // let's fire off our delegate op. + let del_stx_op = DelegateStxOp { + sender: spender_stx_addr.clone(), + delegate_to: recipient_addr.clone(), + reward_addr: None, + delegated_ustx: 100_000, + // to be filled in + txid: Txid([0u8; 32]), + vtxindex: 0, + block_height: 0, + burn_header_hash: BurnchainHeaderHash([0u8; 32]), + until_burn_height: None, + }; + + let mut spender_signer = BurnchainOpSigner::new(spender_sk.clone(), false); + assert!( + btc_regtest_controller + .submit_operation( + StacksEpochId::Epoch21, + BlockstackOperationType::DelegateStx(del_stx_op), + &mut spender_signer, + 1 + ) + .is_some(), + "Delegate operation should submit successfully" + ); + + // the second block should process the delegation, after which the balaces should be unchanged + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + assert_eq!(get_balance(&http_origin, &spender_addr), 100300); + assert_eq!(get_balance(&http_origin, &recipient_addr), 300); + + // send a delegate-stack-stx transaction + let sort_height = channel.get_sortitions_processed(); + let tx = make_contract_call( + &recipient_sk, + 0, + 293, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox-2", + "delegate-stack-stx", + &[ + Value::Principal(spender_addr.clone()), + Value::UInt(100_000), + execute( + &format!("{{ hashbytes: 0x{}, version: 0x00 }}", pox_pubkey_hash), + ClarityVersion::Clarity2, + ) + .unwrap() + .unwrap(), + Value::UInt(sort_height as u128), + Value::UInt(6), + ], + ); + + // push the stacking transaction + submit_tx(&http_origin, &tx); + + // let's mine until the next reward cycle starts ... + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + // check the locked amount for the spender account + let account = get_account(&http_origin, &spender_stx_addr); + assert_eq!(account.locked, 100_000); + + let mut delegate_stack_stx_found = false; + let mut delegate_stx_found = false; + let blocks = test_observer::get_blocks(); + for block in blocks.iter() { + let events = block.get("events").unwrap().as_array().unwrap(); + for event in events.iter() { + let event_type = event.get("type").unwrap().as_str().unwrap(); + if event_type == "contract_event" { + let contract_event = event.get("contract_event").unwrap().as_object().unwrap(); + + // Check that it is a print event + let sub_type = contract_event.get("topic").unwrap().as_str().unwrap(); + assert_eq!(sub_type, "print"); + + // Ensure that the function name is as expected + // This verifies that there were print events for delegate-stack-stx and delegate-stx + let name_field = + &contract_event["value"]["Response"]["data"]["Tuple"]["data_map"]["name"]; + let name_data = name_field["Sequence"]["String"]["ASCII"]["data"] + .as_array() + .unwrap(); + let ascii_vec = name_data + .iter() + .map(|num| num.as_u64().unwrap() as u8) + .collect(); + let name = String::from_utf8(ascii_vec).unwrap(); + if name == "delegate-stack-stx" { + delegate_stack_stx_found = true; + } else if name == "delegate-stx" { + delegate_stx_found = true; + } + } + } + } + assert!(delegate_stx_found); + assert!(delegate_stack_stx_found); + + test_observer::clear(); + channel.stop_chains_coordinator(); +} + #[test] #[ignore] fn bitcoind_resubmission_test() { @@ -5536,7 +5800,7 @@ fn pox_integration_test() { 15, (16 * reward_cycle_len - 1).into(), (17 * reward_cycle_len).into(), - u32::max_value(), + u32::MAX, ); burnchain_config.pox_constants = pox_constants.clone();