From 3c68635d4f725ae98893799060c6dcd39a991715 Mon Sep 17 00:00:00 2001 From: Roberto De Ioris Date: Mon, 7 Jul 2025 16:09:20 +0200 Subject: [PATCH 1/3] sip-031 boot contract setup --- stackslib/src/chainstate/stacks/boot/mod.rs | 2 + .../src/chainstate/stacks/boot/sip-031.clar | 11 +++ stackslib/src/clarity_vm/clarity.rs | 89 ++++++++++++++++++- .../src/tests/nakamoto_integrations.rs | 42 +++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 stackslib/src/chainstate/stacks/boot/sip-031.clar diff --git a/stackslib/src/chainstate/stacks/boot/mod.rs b/stackslib/src/chainstate/stacks/boot/mod.rs index 27740f75af..6c0bb3c569 100644 --- a/stackslib/src/chainstate/stacks/boot/mod.rs +++ b/stackslib/src/chainstate/stacks/boot/mod.rs @@ -63,6 +63,7 @@ pub const POX_4_NAME: &str = "pox-4"; pub const SIGNERS_NAME: &str = "signers"; pub const SIGNERS_VOTING_NAME: &str = "signers-voting"; pub const SIGNERS_VOTING_FUNCTION_NAME: &str = "vote-for-aggregate-public-key"; +pub const SIP_031_NAME: &str = "sip-031"; /// This is the name of a variable in the `.signers` contract which tracks the most recently updated /// reward cycle number. pub const SIGNERS_UPDATE_STATE: &str = "last-set-cycle"; @@ -76,6 +77,7 @@ pub const SIGNERS_BODY: &str = std::include_str!("signers.clar"); pub const SIGNERS_DB_0_BODY: &str = std::include_str!("signers-0-xxx.clar"); pub const SIGNERS_DB_1_BODY: &str = std::include_str!("signers-1-xxx.clar"); pub const SIGNERS_VOTING_BODY: &str = std::include_str!("signers-voting.clar"); +pub const SIP_031_BODY: &str = std::include_str!("sip-031.clar"); pub const COSTS_1_NAME: &str = "costs"; pub const COSTS_2_NAME: &str = "costs-2"; diff --git a/stackslib/src/chainstate/stacks/boot/sip-031.clar b/stackslib/src/chainstate/stacks/boot/sip-031.clar new file mode 100644 index 0000000000..d276b8c3f0 --- /dev/null +++ b/stackslib/src/chainstate/stacks/boot/sip-031.clar @@ -0,0 +1,11 @@ +(define-data-var recipient principal 'ST000000000000000000002AMW42H) + +(define-public (update-recipient (new-recipient principal)) (begin + (asserts! (is-eq tx-sender (var-get recipient)) (err u101)) + (var-set recipient new-recipient) + (ok true) +)) ;; Returns (response bool uint) + +(define-read-only (get-recipient) (ok (var-get recipient))) ;; Returns (response principal uint) + +(define-public (claim) (ok true) ) ;; Returns (response uint uint) \ No newline at end of file diff --git a/stackslib/src/clarity_vm/clarity.rs b/stackslib/src/clarity_vm/clarity.rs index 5c465b1a9c..b2ee3e1770 100644 --- a/stackslib/src/clarity_vm/clarity.rs +++ b/stackslib/src/clarity_vm/clarity.rs @@ -29,6 +29,7 @@ use clarity::vm::database::{ RollbackWrapperPersistedLog, STXBalance, NULL_BURN_STATE_DB, NULL_HEADER_DB, }; use clarity::vm::errors::Error as InterpreterError; +use clarity::vm::events::{STXEventType, STXMintEventData}; use clarity::vm::representations::SymbolicExpression; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, Value}; use clarity::vm::{ClarityVersion, ContractName}; @@ -42,7 +43,8 @@ use crate::chainstate::stacks::boot::{ BOOT_CODE_COST_VOTING_TESTNET as BOOT_CODE_COST_VOTING, BOOT_CODE_POX_TESTNET, COSTS_2_NAME, COSTS_3_NAME, POX_2_MAINNET_CODE, POX_2_NAME, POX_2_TESTNET_CODE, POX_3_MAINNET_CODE, POX_3_NAME, POX_3_TESTNET_CODE, POX_4_CODE, POX_4_NAME, SIGNERS_BODY, SIGNERS_DB_0_BODY, - SIGNERS_DB_1_BODY, SIGNERS_NAME, SIGNERS_VOTING_BODY, SIGNERS_VOTING_NAME, + SIGNERS_DB_1_BODY, SIGNERS_NAME, SIGNERS_VOTING_BODY, SIGNERS_VOTING_NAME, SIP_031_BODY, + SIP_031_NAME, }; use crate::chainstate::stacks::db::{StacksAccount, StacksChainState}; use crate::chainstate::stacks::events::{StacksTransactionEvent, StacksTransactionReceipt}; @@ -57,6 +59,8 @@ use crate::util_lib::boot::{boot_code_acc, boot_code_addr, boot_code_id, boot_co use crate::util_lib::db::Error as DatabaseError; use crate::util_lib::strings::StacksString; +pub const SIP_031_INITIAL_MINT: u128 = 200_000_000_000_000; + /// /// A high-level interface for interacting with the Clarity VM. /// @@ -1617,10 +1621,89 @@ impl<'a> ClarityBlockConnection<'a, '_> { tx_conn.epoch = StacksEpochId::Epoch32; }); - // TODO: SIP-031 setup (minting and transfer to the boot contract) + let mut receipts = vec![]; + + let boot_code_account = self + .get_boot_code_account() + .expect("FATAL: did not get boot account"); + + let mainnet = self.mainnet; + let tx_version = if mainnet { + TransactionVersion::Mainnet + } else { + TransactionVersion::Testnet + }; + + let boot_code_address = boot_code_addr(mainnet); + let boot_code_auth = boot_code_tx_auth(boot_code_address.clone()); + + // SIP-031 setup (deploy of the boot contract, minting and transfer to the boot contract) + let sip_031_contract_id = boot_code_id(SIP_031_NAME, mainnet); + let payload = TransactionPayload::SmartContract( + TransactionSmartContract { + name: ContractName::try_from(SIP_031_NAME) + .expect("FATAL: invalid boot-code contract name"), + code_body: StacksString::from_str(SIP_031_BODY) + .expect("FATAL: invalid boot code body"), + }, + Some(ClarityVersion::Clarity3), + ); + + let sip_031_contract_tx = + StacksTransaction::new(tx_version.clone(), boot_code_auth, payload); + + let mut sip_031_initialization_receipt = self.as_transaction(|tx_conn| { + // initialize with a synthetic transaction + debug!("Instantiate {} contract", &sip_031_contract_id); + let receipt = StacksChainState::process_transaction_payload( + tx_conn, + &sip_031_contract_tx, + &boot_code_account, + ASTRules::PrecheckSize, + None, + ) + .expect("FATAL: Failed to process .sip031 contract initialization"); + receipt + }); + + if sip_031_initialization_receipt.result != Value::okay_true() + || sip_031_initialization_receipt.post_condition_aborted + { + panic!( + "FATAL: Failure processing sip031 contract initialization: {:#?}", + &sip_031_initialization_receipt + ); + } + + let recipient = PrincipalData::Contract(boot_code_id(SIP_031_NAME, mainnet)); + + self.as_transaction(|tx_conn| { + tx_conn + .with_clarity_db(|db| { + db.increment_ustx_liquid_supply(SIP_031_INITIAL_MINT) + .map_err(|e| e.into()) + }) + .expect("FATAL: `SIP-031 initial mint` overflowed"); + StacksChainState::account_credit( + tx_conn, + &recipient, + u64::try_from(SIP_031_INITIAL_MINT) + .expect("FATAL: transferred more STX than exist"), + ); + }); + + let event = STXEventType::STXMintEvent(STXMintEventData { + recipient, + amount: SIP_031_INITIAL_MINT, + }); + sip_031_initialization_receipt + .events + .push(StacksTransactionEvent::STXEvent(event)); + + receipts.push(sip_031_initialization_receipt); debug!("Epoch 3.2 initialized"); - (old_cost_tracker, Ok(vec![])) + (old_cost_tracker, Ok(receipts)) }) } diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 068a75c983..eb05b6c025 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -128,6 +128,10 @@ use crate::{tests, BitcoinRegtestController, BurnchainController, Config, Config pub static POX_4_DEFAULT_STACKER_BALANCE: u64 = 100_000_000_000_000; pub static POX_4_DEFAULT_STACKER_STX_AMT: u128 = 99_000_000_000_000; +use clarity::vm::database::STXBalance; +use stacks::chainstate::stacks::boot::SIP_031_NAME; +use stacks::clarity_vm::clarity::SIP_031_INITIAL_MINT; + use crate::clarity::vm::clarity::ClarityConnection; lazy_static! { @@ -12802,6 +12806,44 @@ fn test_sip_031_activation() { assert_eq!(epoch_version, Some(StacksEpochId::Epoch32)); + // check if sip-031 boot contract has been deployed + let sip_031_boot_contract_exists = chainstate.with_read_only_clarity_tx( + &sortdb + .index_handle_at_block(&chainstate, &latest_stacks_block_id) + .unwrap(), + &latest_stacks_block_id, + |conn| { + conn.with_clarity_db_readonly(|db| { + db.has_contract(&boot_code_id(SIP_031_NAME, naka_conf.is_mainnet())) + }) + }, + ); + + assert_eq!(sip_031_boot_contract_exists, Some(true)); + + // check if sip-031 boot contract has a balance of 200_000_000 STX + let sip_031_boot_contract_balance = chainstate.with_read_only_clarity_tx( + &sortdb + .index_handle_at_block(&chainstate, &latest_stacks_block_id) + .unwrap(), + &latest_stacks_block_id, + |conn| { + conn.with_clarity_db_readonly(|db| { + db.get_account_stx_balance(&PrincipalData::Contract(boot_code_id( + SIP_031_NAME, + naka_conf.is_mainnet(), + ))) + }) + }, + ); + + assert_eq!( + sip_031_boot_contract_balance, + Some(Ok(STXBalance::Unlocked { + amount: SIP_031_INITIAL_MINT + })) + ); + coord_channel .lock() .expect("Mutex poisoned") From 9211bcdb2293a84daa74c26d19f5d1f0de88b545 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Tue, 15 Jul 2025 21:23:46 -0700 Subject: [PATCH 2/3] fix: remove TODO from integration test --- stacks-node/src/tests/nakamoto_integrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index a24f5f0803..60cffc8c65 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -12639,7 +12639,7 @@ fn write_signer_update( /// Test SIP-031 activation /// /// - check epoch 3.2 is active -/// - TODO: check sip031 boot contract has a balance of 200_000_000 STX (TODO) +/// - check sip031 boot contract has a balance of 200_000_000 STX (TODO) #[test] #[ignore] fn test_sip_031_activation() { From 642780519a26ebee29d96541472352cc9a36c061 Mon Sep 17 00:00:00 2001 From: Roberto De Ioris Date: Wed, 16 Jul 2025 07:56:28 +0200 Subject: [PATCH 3/3] added mitn receipt check in integration test --- .../src/tests/nakamoto_integrations.rs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 60cffc8c65..e60bd8a99b 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -12798,6 +12798,83 @@ fn test_sip_031_activation() { })) ); + // check if the coinbase activation block receipt has the mint event + let mut mint_event_found: Option = None; + let mut coinbase_txid: Option = None; + for block in test_observer::get_blocks().iter().rev() { + let burn_block_height = block.get("burn_block_height").unwrap().as_u64().unwrap(); + if burn_block_height + == naka_conf.burnchain.epochs.clone().unwrap()[StacksEpochId::Epoch32].start_height + { + // the first transaction is the coinbase + coinbase_txid = Some( + block + .get("transactions") + .unwrap() + .as_array() + .unwrap() + .first() + .unwrap() + .get("txid") + .unwrap() + .as_str() + .unwrap() + .into(), + ); + let events = block.get("events").unwrap().as_array().unwrap(); + for event in events { + if let Some(_) = event.get("stx_mint_event") { + mint_event_found = Some(event.clone()); + break; + } + } + break; + } + } + + assert!(coinbase_txid.is_some()); + assert!(mint_event_found.is_some()); + + // check the amount + assert_eq!( + mint_event_found + .clone() + .unwrap() + .get("stx_mint_event") + .unwrap() + .get("amount") + .unwrap() + .as_str() + .unwrap(), + "200000000000000" + ); + + // check the recipient + assert_eq!( + mint_event_found + .clone() + .unwrap() + .get("stx_mint_event") + .unwrap() + .get("recipient") + .unwrap() + .as_str() + .unwrap(), + boot_code_id(SIP_031_NAME, naka_conf.is_mainnet()).to_string() + ); + + // check the txid + assert_eq!( + mint_event_found + .clone() + .unwrap() + .get("txid") + .unwrap() + .as_str() + .unwrap(), + coinbase_txid.unwrap() + ); + coord_channel .lock() .expect("Mutex poisoned")