From 18f4555b601fb5ebc455cfc6a39cfbd22c2bf5de Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 13 Jul 2023 15:26:03 -0500 Subject: [PATCH 1/2] refactor: pox lock handling into separate workspace member --- Cargo.lock | 10 + Cargo.toml | 4 +- pox-locking/Cargo.toml | 27 + pox-locking/src/events.rs | 440 +++++++++ pox-locking/src/lib.rs | 108 +++ pox-locking/src/pox_1.rs | 198 ++++ pox-locking/src/pox_2.rs | 532 +++++++++++ pox-locking/src/pox_3.rs | 415 +++++++++ src/chainstate/stacks/db/accounts.rs | 281 ------ src/clarity_vm/special.rs | 1268 +------------------------- 10 files changed, 1747 insertions(+), 1536 deletions(-) create mode 100644 pox-locking/Cargo.toml create mode 100644 pox-locking/src/events.rs create mode 100644 pox-locking/src/lib.rs create mode 100644 pox-locking/src/pox_1.rs create mode 100644 pox-locking/src/pox_2.rs create mode 100644 pox-locking/src/pox_3.rs diff --git a/Cargo.lock b/Cargo.lock index 19108e3b9e..a6e5bb085d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,6 +393,7 @@ dependencies = [ "mio 0.6.23", "nix", "percent-encoding", + "pox-locking", "prometheus", "rand 0.7.3", "rand_chacha 0.2.2", @@ -1870,6 +1871,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "pox-locking" +version = "2.4.0" +dependencies = [ + "clarity", + "slog", + "stacks-common", +] + [[package]] name = "ppv-lite86" version = "0.2.17" diff --git a/Cargo.toml b/Cargo.toml index 0b7ce13203..333fb02667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ chrono = "0.4.19" libc = "0.2.82" clarity = { package = "clarity", path = "./clarity/." } stacks_common = { package = "stacks-common", path = "./stacks-common/." } +pox_locking = { package = "pox-locking", path = "./pox-locking/." } siphasher = "0.3.7" [target.'cfg(unix)'.dependencies] @@ -106,7 +107,7 @@ profile-sqlite = [] disable-costs = [] developer-mode = [] monitoring_prom = ["prometheus"] -slog_json = ["slog-json", "stacks_common/slog_json", "clarity/slog_json"] +slog_json = ["slog-json", "stacks_common/slog_json", "clarity/slog_json", "pox_locking/slog_json"] testing = [] # Use a bit more than default optimization for @@ -129,6 +130,7 @@ sha2 = { version = "0.10" } [workspace] members = [ ".", + "pox-locking", "clarity", "stx-genesis", "testnet/stacks-node"] diff --git a/pox-locking/Cargo.toml b/pox-locking/Cargo.toml new file mode 100644 index 0000000000..2193bfb0ec --- /dev/null +++ b/pox-locking/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pox-locking" +version = "2.4.0" +authors = [ "Jude Nelson ", + "Aaron Blankstein ", + "Ludo Galabru " ] +license = "GPLv3" +homepage = "https://github.com/blockstack/stacks-blockchain" +repository = "https://github.com/blockstack/stacks-blockchain" +description = "Contract call result handler for applying PoX lock operations" +keywords = [ "stacks", "stx", "bitcoin", "crypto", "blockstack", "decentralized", "dapps", "blockchain" ] +readme = "README.md" +resolver = "2" +edition = "2021" +rust-version = "1.61" + +[lib] +name = "pox_locking" +path = "src/lib.rs" + +[dependencies] +clarity = { package = "clarity", path = "../clarity/." } +stacks_common = { package = "stacks-common", path = "../stacks-common/." } +slog = { version = "2.5.2", features = [ "max_level_trace" ] } + +[features] +slog_json = ["stacks_common/slog_json", "clarity/slog_json"] diff --git a/pox-locking/src/events.rs b/pox-locking/src/events.rs new file mode 100644 index 0000000000..f37fdc68e4 --- /dev/null +++ b/pox-locking/src/events.rs @@ -0,0 +1,440 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::vm::ast::ASTRules; +use clarity::vm::contexts::GlobalContext; +use clarity::vm::errors::Error as ClarityError; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, TupleData}; +use clarity::vm::Value; +#[cfg(test)] +use slog::slog_debug; +use slog::slog_error; +#[cfg(test)] +use stacks_common::debug; +use stacks_common::{error, test_debug}; + +/// Determine who the stacker is for a given function. +/// - 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" | "delegate-stx" => { + Value::Principal(sender.clone()) + } + _ => args[0].clone(), + } +} + +/// 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, + args: &[Value], +) -> String { + format!( + r#" + (let ( + (stacker '{stacker}) + (func-name "{func_name}") + (stacker-info (stx-account stacker)) + (total-balance (stx-get-balance stacker)) + ) + {{ + ;; Function name + name: func-name, + ;; The principal of the stacker + stacker: stacker, + ;; The current available balance + balance: total-balance, + ;; The amount of locked STX + locked: (get locked stacker-info), + ;; The burnchain block height of when the tokens unlock. Zero if no tokens are locked. + burnchain-unlock-height: (get unlock-height stacker-info), + }} + ) + "#, + stacker = get_stacker(sender, function_name, args), + func_name = function_name + ) +} + +/// Craft the code snippet to evaluate a stack-aggregation-* function +fn create_event_info_aggregation_code(function_name: &str) -> String { + format!( + r#" + (let ( + (stacker-info (stx-account tx-sender)) + ) + {{ + ;; Function name + name: "{func_name}", + ;; who called this + ;; NOTE: these fields are required by downstream clients. + ;; Even though tx-sender is *not* a stacker, the field is + ;; called "stacker" and these clients know to treat it as + ;; the delegator. + stacker: tx-sender, + balance: (stx-get-balance tx-sender), + locked: (get locked stacker-info), + burnchain-unlock-height: (get unlock-height stacker-info), + + }} + ) + "#, + func_name = function_name + ) +} + +/// Craft the code snippet to generate the method-specific `data` payload +fn create_event_info_data_code(function_name: &str, args: &[Value]) -> String { + match function_name { + "stack-stx" => { + format!( + r#" + {{ + data: {{ + ;; amount of ustx to lock. + ;; equal to args[0] + lock-amount: {lock_amount}, + ;; burnchain height when the unlock finishes. + ;; derived from args[3] + unlock-burn-height: (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 {lock_period})), + ;; PoX address tuple. + ;; equal to args[1]. + pox-addr: {pox_addr}, + ;; start of lock-up. + ;; equal to args[2] + start-burn-height: {start_burn_height}, + ;; how long to lock, in burn blocks + ;; equal to args[3] + lock-period: {lock_period} + }} + }} + "#, + lock_amount = &args[0], + lock_period = &args[3], + pox_addr = &args[1], + start_burn_height = &args[2], + ) + } + "delegate-stack-stx" => { + format!( + r#" + {{ + data: {{ + ;; amount of ustx to lock. + ;; equal to args[1] + lock-amount: {lock_amount}, + ;; burnchain height when the unlock finishes. + ;; derived from args[4] + unlock-burn-height: (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 {lock_period})), + ;; PoX address tuple. + ;; equal to args[2] + pox-addr: {pox_addr}, + ;; start of lock-up + ;; equal to args[3] + start-burn-height: {start_burn_height}, + ;; how long to lock, in burn blocks + ;; equal to args[3] + lock-period: {lock_period}, + ;; delegator + delegator: tx-sender, + ;; stacker + ;; equal to args[0] + stacker: '{stacker} + }} + }} + "#, + stacker = &args[0], + lock_amount = &args[1], + pox_addr = &args[2], + start_burn_height = &args[3], + lock_period = &args[4], + ) + } + "stack-increase" => { + format!( + r#" + {{ + data: {{ + ;; amount to increase by + ;; equal to args[0] + increase-by: {increase_by}, + ;; new amount locked + ;; NOTE: the lock has not yet been applied! + ;; derived from args[0] + total-locked: (+ {increase_by} (get locked (stx-account tx-sender))), + ;; pox addr increased + pox-addr: (get pox-addr (unwrap-panic (map-get? stacking-state {{ stacker: tx-sender }}))) + }} + }} + "#, + increase_by = &args[0] + ) + } + "delegate-stack-increase" => { + format!( + r#" + {{ + data: {{ + ;; pox addr + ;; equal to args[1] + pox-addr: {pox_addr}, + ;; amount to increase by + ;; equal to args[2] + increase-by: {increase_by}, + ;; total amount locked now + ;; NOTE: the lock itself has not yet been applied! + ;; this is for the stacker, so args[0] + total-locked: (+ {increase_by} (get locked (stx-account '{stacker}))), + ;; delegator + delegator: tx-sender, + ;; stacker + ;; equal to args[0] + stacker: '{stacker} + }} + }} + "#, + stacker = &args[0], + pox_addr = &args[1], + increase_by = &args[2], + ) + } + "stack-extend" => { + format!( + r#" + (let ( + ;; variable declarations derived from pox-2 + (cur-cycle (current-pox-reward-cycle)) + (unlock-height (get unlock-height (stx-account tx-sender))) + (unlock-in-cycle (burn-height-to-reward-cycle unlock-height)) + (first-extend-cycle + (if (> (+ cur-cycle u1) unlock-in-cycle) + (+ cur-cycle u1) + unlock-in-cycle)) + (last-extend-cycle (- (+ first-extend-cycle {extend_count}) u1)) + (new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle))) + ) + {{ + data: {{ + ;; pox addr extended + ;; equal to args[1] + pox-addr: {pox_addr}, + ;; number of cycles extended + ;; equal to args[0] + extend-count: {extend_count}, + ;; new unlock burnchain block height + unlock-burn-height: new-unlock-ht + }} + }}) + "#, + extend_count = &args[0], + pox_addr = &args[1], + ) + } + "delegate-stack-extend" => { + format!( + r#" + (let ( + (unlock-height (get unlock-height (stx-account '{stacker}))) + (unlock-in-cycle (burn-height-to-reward-cycle unlock-height)) + (cur-cycle (current-pox-reward-cycle)) + (first-extend-cycle + (if (> (+ cur-cycle u1) unlock-in-cycle) + (+ cur-cycle u1) + unlock-in-cycle)) + (last-extend-cycle (- (+ first-extend-cycle {extend_count}) u1)) + (new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle))) + ) + {{ + data: {{ + ;; pox addr extended + ;; equal to args[1] + pox-addr: {pox_addr}, + ;; number of cycles extended + ;; equal to args[2] + extend-count: {extend_count}, + ;; new unlock burnchain block height + unlock-burn-height: new-unlock-ht, + ;; delegator locking this up + delegator: tx-sender, + ;; stacker + ;; equal to args[0] + stacker: '{stacker} + }} + }}) + "#, + stacker = &args[0], + pox_addr = &args[1], + extend_count = &args[2] + ) + } + "stack-aggregation-commit" + | "stack-aggregation-commit-indexed" + | "stack-aggregation-increase" => { + format!( + r#" + {{ + data: {{ + ;; pox addr locked up + ;; equal to args[0] in all methods + pox-addr: {pox_addr}, + ;; reward cycle locked up + ;; equal to args[1] in all methods + reward-cycle: {reward_cycle}, + ;; amount locked behind this PoX address by this method + amount-ustx: (get stacked-amount + (unwrap-panic (map-get? logged-partial-stacked-by-cycle + {{ pox-addr: {pox_addr}, sender: tx-sender, reward-cycle: {reward_cycle} }}))), + ;; delegator (this is the caller) + delegator: tx-sender + }} + }} + "#, + pox_addr = &args[0], + 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 }} }}"), + } +} + +/// Synthesize an events data tuple to return on the successful execution of a pox-2 or pox-3 stacking +/// function. It runs a series of Clarity queries against the PoX contract's data space (including +/// calling PoX functions). +pub fn synthesize_pox_2_or_3_event_info( + global_context: &mut GlobalContext, + contract_id: &QualifiedContractIdentifier, + sender_opt: Option<&PrincipalData>, + function_name: &str, + args: &[Value], +) -> Result, ClarityError> { + let sender = match sender_opt { + Some(sender) => sender, + None => { + return Ok(None); + } + }; + let code_snippet_template_opt = match function_name { + "stack-stx" + | "delegate-stack-stx" + | "stack-extend" + | "delegate-stack-extend" + | "stack-increase" + | "delegate-stack-increase" + | "delegate-stx" => Some(create_event_info_stack_or_delegate_code( + sender, + function_name, + args, + )), + "stack-aggregation-commit" + | "stack-aggregation-commit-indexed" + | "stack-aggregation-increase" => Some(create_event_info_aggregation_code(function_name)), + _ => None, + }; + let code_snippet = match code_snippet_template_opt { + Some(x) => x, + None => return Ok(None), + }; + + let data_snippet = create_event_info_data_code(function_name, args); + + test_debug!("Evaluate snippet:\n{}", &code_snippet); + test_debug!("Evaluate data code:\n{}", &data_snippet); + + let pox_2_contract = global_context + .database + .get_contract(contract_id) + .expect("FATAL: could not load PoX contract metadata"); + + let event_info = global_context + .special_cc_handler_execute_read_only( + sender.clone(), + None, + pox_2_contract.contract_context, + |env| { + let base_event_info = env + .eval_read_only_with_rules(contract_id, &code_snippet, ASTRules::PrecheckSize) + .map_err(|e| { + error!( + "Failed to run event-info code snippet for '{}': {:?}", + function_name, &e + ); + e + })?; + + let data_event_info = env + .eval_read_only_with_rules(contract_id, &data_snippet, ASTRules::PrecheckSize) + .map_err(|e| { + error!( + "Failed to run data-info code snippet for '{}': {:?}", + function_name, &e + ); + e + })?; + + // merge them + let base_event_tuple = base_event_info.expect_tuple(); + let data_tuple = data_event_info.expect_tuple(); + let event_tuple = + TupleData::shallow_merge(base_event_tuple, data_tuple).map_err(|e| { + error!("Failed to merge data-info and event-info: {:?}", &e); + e + })?; + + Ok(Value::Tuple(event_tuple)) + }, + ) + .map_err(|e: ClarityError| { + error!("Failed to synthesize PoX event: {:?}", &e); + e + })?; + + test_debug!( + "Synthesized PoX event info for '{}''s call to '{}': {:?}", + sender, + function_name, + &event_info + ); + Ok(Some(event_info)) +} diff --git a/pox-locking/src/lib.rs b/pox-locking/src/lib.rs new file mode 100644 index 0000000000..17d921fa38 --- /dev/null +++ b/pox-locking/src/lib.rs @@ -0,0 +1,108 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::boot_util::boot_code_id; +use clarity::vm::contexts::GlobalContext; +use clarity::vm::errors::{Error as ClarityError, RuntimeErrorType}; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use clarity::vm::Value; +use slog::slog_warn; +use stacks_common::types::StacksEpochId; +use stacks_common::warn; + +mod events; +mod pox_1; +mod pox_2; +mod pox_3; + +#[derive(Debug)] +pub enum LockingError { + DefunctPoxContract, + PoxAlreadyLocked, + PoxInsufficientBalance, + PoxExtendNotLocked, + PoxIncreaseOnV1, + PoxInvalidIncrease, +} + +pub const POX_1_NAME: &'static str = "pox"; +pub const POX_2_NAME: &'static str = "pox-2"; +pub const POX_3_NAME: &'static str = "pox-3"; + +/// Handle special cases of contract-calls -- namely, those into PoX that should lock up STX +pub fn handle_contract_call_special_cases( + global_context: &mut GlobalContext, + sender: Option<&PrincipalData>, + _sponsor: Option<&PrincipalData>, + contract_id: &QualifiedContractIdentifier, + function_name: &str, + args: &[Value], + result: &Value, +) -> Result<(), ClarityError> { + if *contract_id == boot_code_id(POX_1_NAME, global_context.mainnet) { + if !pox_1::is_read_only(function_name) + && global_context.database.get_v1_unlock_height() + <= global_context.database.get_current_burnchain_block_height() + { + // NOTE: get-pox-info is read-only, so it can call old pox v1 stuff + warn!("PoX-1 function call attempted on an account after v1 unlock height"; + "v1_unlock_ht" => global_context.database.get_v1_unlock_height(), + "current_burn_ht" => global_context.database.get_current_burnchain_block_height(), + "function_name" => function_name, + "contract_id" => %contract_id + ); + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )); + } + return pox_1::handle_contract_call(global_context, sender, function_name, result); + } else if *contract_id == boot_code_id(POX_2_NAME, global_context.mainnet) { + if !pox_2::is_read_only(function_name) && global_context.epoch_id >= StacksEpochId::Epoch22 + { + warn!("PoX-2 function call attempted on an account after Epoch 2.2"; + "v2_unlock_ht" => global_context.database.get_v2_unlock_height(), + "current_burn_ht" => global_context.database.get_current_burnchain_block_height(), + "function_name" => function_name, + "contract_id" => %contract_id + ); + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )); + } + + return pox_2::handle_contract_call( + global_context, + sender, + contract_id, + function_name, + args, + result, + ); + } else if *contract_id == boot_code_id(POX_3_NAME, global_context.mainnet) { + return pox_3::handle_contract_call( + global_context, + sender, + contract_id, + function_name, + args, + result, + ); + } + + Ok(()) +} diff --git a/pox-locking/src/pox_1.rs b/pox-locking/src/pox_1.rs new file mode 100644 index 0000000000..2f9a1010d9 --- /dev/null +++ b/pox-locking/src/pox_1.rs @@ -0,0 +1,198 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::boot_util::boot_code_id; +use clarity::vm::contexts::GlobalContext; +use clarity::vm::costs::cost_functions::ClarityCostFunction; +use clarity::vm::costs::runtime_cost; +use clarity::vm::database::ClarityDatabase; +use clarity::vm::errors::Error as ClarityError; +use clarity::vm::errors::RuntimeErrorType; +use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; +use clarity::vm::types::PrincipalData; +use clarity::vm::Value; +use slog::slog_debug; +use stacks_common::debug; + +use crate::LockingError; + +/// Parse the returned value from PoX `stack-stx` and `delegate-stack-stx` functions +/// from pox.clar into a format more readily digestible in rust. +/// Panics if the supplied value doesn't match the expected tuple structure +fn parse_pox_stacking_result_v1( + result: &Value, +) -> std::result::Result<(PrincipalData, u128, u64), i128> { + match result.clone().expect_result() { + Ok(res) => { + // should have gotten back (ok (tuple (stacker principal) (lock-amount uint) (unlock-burn-height uint))) + let tuple_data = res.expect_tuple(); + let stacker = tuple_data + .get("stacker") + .expect(&format!("FATAL: no 'stacker'")) + .to_owned() + .expect_principal(); + + let lock_amount = tuple_data + .get("lock-amount") + .expect(&format!("FATAL: no 'lock-amount'")) + .to_owned() + .expect_u128(); + + let unlock_burn_height = tuple_data + .get("unlock-burn-height") + .expect(&format!("FATAL: no 'unlock-burn-height'")) + .to_owned() + .expect_u128() + .try_into() + .expect("FATAL: 'unlock-burn-height' overflow"); + + Ok((stacker, lock_amount, unlock_burn_height)) + } + Err(e) => Err(e.expect_i128()), + } +} + +/// Is a PoX-1 function read-only? +/// i.e. can we call it without incurring an error? +pub fn is_read_only(func_name: &str) -> bool { + func_name == "get-pox-rejection" + || func_name == "is-pox-active" + || func_name == "get-stacker-info" + || func_name == "get-reward-set-size" + || func_name == "get-total-ustx-stacked" + || func_name == "get-reward-set-pox-address" + || func_name == "get-stacking-minimum" + || func_name == "can-stack-stx" + || func_name == "minimal-can-stack-stx" + || func_name == "get-pox-info" +} + +/////////////////////// PoX (first version) ///////////////////////////////// + +/// Lock up STX for PoX for a time. Does NOT touch the account nonce. +pub fn pox_lock_v1( + db: &mut ClarityDatabase, + principal: &PrincipalData, + lock_amount: u128, + unlock_burn_height: u64, +) -> Result<(), LockingError> { + assert!(unlock_burn_height > 0); + assert!(lock_amount > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if snapshot.balance().was_locked_by_v2() { + debug!("PoX Lock attempted on an account locked by v2"); + return Err(LockingError::DefunctPoxContract); + } + + if snapshot.has_locked_tokens() { + return Err(LockingError::PoxAlreadyLocked); + } + if !snapshot.can_transfer(lock_amount) { + return Err(LockingError::PoxInsufficientBalance); + } + snapshot.lock_tokens_v1(lock_amount, unlock_burn_height); + + debug!( + "PoX v1 lock applied"; + "pox_locked_ustx" => snapshot.balance().amount_locked(), + "available_ustx" => snapshot.balance().amount_unlocked(), + "unlock_burn_height" => unlock_burn_height, + "account" => %principal, + ); + + snapshot.save(); + Ok(()) +} + +/// Handle special cases when calling into the PoX v1 contract +pub fn handle_contract_call( + global_context: &mut GlobalContext, + _sender_opt: Option<&PrincipalData>, + function_name: &str, + value: &Value, +) -> Result<(), ClarityError> { + if !(function_name == "stack-stx" || function_name == "delegate-stack-stx") { + // only have work to do if the function is `stack-stx` or `delegate-stack-stx` + return Ok(()); + } + + debug!( + "Handle special-case contract-call to {:?} {} (which returned {:?})", + "pox-1", function_name, value + ); + + // applying a pox lock at this point is equivalent to evaluating a transfer + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, locked_amount, unlock_height) = match parse_pox_stacking_result_v1(value) { + Ok(x) => x, + Err(_) => { + // the pox method failed: do not apply a lock. + return Ok(()); + } + }; + + // in most cases, if this fails, then there's a bug in the contract (since it already does + // the necessary checks), but with v2 introduction, that's no longer true -- if someone + // locks on PoX v2, and then tries to lock again in PoX v1, that's not captured by the v1 + // contract. + match pox_lock_v1( + &mut global_context.database, + &stacker, + locked_amount, + unlock_height as u64, + ) { + Ok(_) => { + if let Some(batch) = global_context.event_batches.last_mut() { + batch.events.push(StacksTransactionEvent::STXEvent( + STXEventType::STXLockEvent(STXLockEventData { + locked_amount, + unlock_height, + locked_address: stacker, + contract_identifier: boot_code_id("pox", global_context.mainnet), + }), + )); + } + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )) + } + Err(LockingError::PoxAlreadyLocked) => { + // the caller tried to lock tokens into both pox-1 and pox-2 + return Err(ClarityError::Runtime( + RuntimeErrorType::PoxAlreadyLocked, + None, + )); + } + Err(e) => { + panic!( + "FATAL: failed to lock {} from {} until {}: '{:?}'", + locked_amount, stacker, unlock_height, &e + ); + } + } + + Ok(()) +} diff --git a/pox-locking/src/pox_2.rs b/pox-locking/src/pox_2.rs new file mode 100644 index 0000000000..e49b99257d --- /dev/null +++ b/pox-locking/src/pox_2.rs @@ -0,0 +1,532 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::boot_util::boot_code_id; +use clarity::vm::contexts::GlobalContext; +use clarity::vm::costs::cost_functions::ClarityCostFunction; +use clarity::vm::costs::runtime_cost; +use clarity::vm::database::{ClarityDatabase, STXBalance}; +use clarity::vm::errors::Error as ClarityError; +use clarity::vm::errors::RuntimeErrorType; +use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use clarity::vm::{Environment, Value}; +use slog::slog_debug; +use slog::slog_error; +use stacks_common::{debug, error}; + +use crate::events::synthesize_pox_2_or_3_event_info; +use crate::LockingError; + +/// is a PoX-2 function call read only? +pub fn is_read_only(func_name: &str) -> bool { + "get-pox-rejection" == func_name + || "is-pox-active" == func_name + || "burn-height-to-reward-cycle" == func_name + || "reward-cycle-to-burn-height" == func_name + || "current-pox-reward-cycle" == func_name + || "get-stacker-info" == func_name + || "get-check-delegation" == func_name + || "get-reward-set-size" == func_name + || "next-cycle-rejection-votes" == func_name + || "get-total-ustx-stacked" == func_name + || "get-reward-set-pox-address" == func_name + || "get-stacking-minimum" == func_name + || "check-pox-addr-version" == func_name + || "check-pox-addr-hashbytes" == func_name + || "check-pox-lock-period" == func_name + || "can-stack-stx" == func_name + || "minimal-can-stack-stx" == func_name + || "get-pox-info" == func_name + || "get-delegation-info" == func_name + || "get-allowance-contract-callers" == func_name + || "get-num-reward-set-pox-addresses" == func_name + || "get-partial-stacked-by-cycle" == func_name + || "get-total-pox-rejection" == func_name +} + +/// Parse the returned value from PoX `stack-stx` and `delegate-stack-stx` functions +/// from pox-2.clar or pox-3.clar into a format more readily digestible in rust. +/// Panics if the supplied value doesn't match the expected tuple structure +pub fn parse_pox_stacking_result( + result: &Value, +) -> std::result::Result<(PrincipalData, u128, u64), i128> { + match result.clone().expect_result() { + Ok(res) => { + // should have gotten back (ok { stacker: principal, lock-amount: uint, unlock-burn-height: uint .. } .. }))) + let tuple_data = res.expect_tuple(); + let stacker = tuple_data + .get("stacker") + .expect(&format!("FATAL: no 'stacker'")) + .to_owned() + .expect_principal(); + + let lock_amount = tuple_data + .get("lock-amount") + .expect(&format!("FATAL: no 'lock-amount'")) + .to_owned() + .expect_u128(); + + let unlock_burn_height = tuple_data + .get("unlock-burn-height") + .expect(&format!("FATAL: no 'unlock-burn-height'")) + .to_owned() + .expect_u128() + .try_into() + .expect("FATAL: 'unlock-burn-height' overflow"); + + Ok((stacker, lock_amount, unlock_burn_height)) + } + Err(e) => Err(e.expect_i128()), + } +} + +/// Parse the returned value from PoX2 or PoX3 `stack-extend` and `delegate-stack-extend` functions +/// into a format more readily digestible in rust. +/// Panics if the supplied value doesn't match the expected tuple structure +pub fn parse_pox_extend_result(result: &Value) -> std::result::Result<(PrincipalData, u64), i128> { + match result.clone().expect_result() { + Ok(res) => { + // should have gotten back (ok { stacker: principal, unlock-burn-height: uint .. } .. }) + let tuple_data = res.expect_tuple(); + let stacker = tuple_data + .get("stacker") + .expect(&format!("FATAL: no 'stacker'")) + .to_owned() + .expect_principal(); + + let unlock_burn_height = tuple_data + .get("unlock-burn-height") + .expect(&format!("FATAL: no 'unlock-burn-height'")) + .to_owned() + .expect_u128() + .try_into() + .expect("FATAL: 'unlock-burn-height' overflow"); + + Ok((stacker, unlock_burn_height)) + } + // in the error case, the function should have returned `int` error code + Err(e) => Err(e.expect_i128()), + } +} + +/// Parse the returned value from PoX2 or PoX3 `stack-increase` function +/// into a format more readily digestible in rust. +/// Panics if the supplied value doesn't match the expected tuple structure +pub fn parse_pox_increase(result: &Value) -> std::result::Result<(PrincipalData, u128), i128> { + match result.clone().expect_result() { + Ok(res) => { + // should have gotten back (ok { stacker: principal, total-locked: uint .. } .. }) + let tuple_data = res.expect_tuple(); + let stacker = tuple_data + .get("stacker") + .expect(&format!("FATAL: no 'stacker'")) + .to_owned() + .expect_principal(); + + let total_locked = tuple_data + .get("total-locked") + .expect(&format!("FATAL: no 'total-locked'")) + .to_owned() + .expect_u128(); + + Ok((stacker, total_locked)) + } + // in the error case, the function should have returned `int` error code + Err(e) => Err(e.expect_i128()), + } +} + +/////////////////////// PoX-2 ///////////////////////////////// + +/// Increase a STX lock up for PoX. Does NOT touch the account nonce. +/// Returns Ok( account snapshot ) when successful +/// +/// # Errors +/// - Returns Error::PoxExtendNotLocked if this function was called on an account +/// which isn't locked. This *should* have been checked by the PoX v2 contract, +/// so this should surface in a panic. +pub fn pox_lock_increase_v2( + db: &mut ClarityDatabase, + principal: &PrincipalData, + new_total_locked: u128, +) -> Result { + assert!(new_total_locked > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if !snapshot.has_locked_tokens() { + return Err(LockingError::PoxExtendNotLocked); + } + + if !snapshot.is_v2_locked() { + return Err(LockingError::PoxIncreaseOnV1); + } + + let bal = snapshot.canonical_balance_repr(); + let total_amount = bal + .amount_unlocked() + .checked_add(bal.amount_locked()) + .expect("STX balance overflowed u128"); + if total_amount < new_total_locked { + return Err(LockingError::PoxInsufficientBalance); + } + + if bal.amount_locked() > new_total_locked { + return Err(LockingError::PoxInvalidIncrease); + } + + snapshot.increase_lock_v2(new_total_locked); + + let out_balance = snapshot.canonical_balance_repr(); + + debug!( + "PoX v2 lock increased"; + "pox_locked_ustx" => out_balance.amount_locked(), + "available_ustx" => out_balance.amount_unlocked(), + "unlock_burn_height" => out_balance.unlock_height(), + "account" => %principal, + ); + + snapshot.save(); + Ok(out_balance) +} + +/// Extend a STX lock up for PoX for a time. Does NOT touch the account nonce. +/// Returns Ok(lock_amount) when successful +/// +/// # Errors +/// - Returns Error::PoxExtendNotLocked if this function was called on an account +/// which isn't locked. This *should* have been checked by the PoX v2 contract, +/// so this should surface in a panic. +pub fn pox_lock_extend_v2( + db: &mut ClarityDatabase, + principal: &PrincipalData, + unlock_burn_height: u64, +) -> Result { + assert!(unlock_burn_height > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if !snapshot.has_locked_tokens() { + return Err(LockingError::PoxExtendNotLocked); + } + + snapshot.extend_lock_v2(unlock_burn_height); + + let amount_locked = snapshot.balance().amount_locked(); + + debug!( + "PoX v2 lock applied"; + "pox_locked_ustx" => amount_locked, + "available_ustx" => snapshot.balance().amount_unlocked(), + "unlock_burn_height" => unlock_burn_height, + "account" => %principal, + ); + + snapshot.save(); + Ok(amount_locked) +} + +/// Lock up STX for PoX for a time. Does NOT touch the account nonce. +fn pox_lock_v2( + db: &mut ClarityDatabase, + principal: &PrincipalData, + lock_amount: u128, + unlock_burn_height: u64, +) -> Result<(), LockingError> { + assert!(unlock_burn_height > 0); + assert!(lock_amount > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if snapshot.has_locked_tokens() { + return Err(LockingError::PoxAlreadyLocked); + } + if !snapshot.can_transfer(lock_amount) { + return Err(LockingError::PoxInsufficientBalance); + } + snapshot.lock_tokens_v2(lock_amount, unlock_burn_height); + + debug!( + "PoX v2 lock applied"; + "pox_locked_ustx" => snapshot.balance().amount_locked(), + "available_ustx" => snapshot.balance().amount_unlocked(), + "unlock_burn_height" => unlock_burn_height, + "account" => %principal, + ); + + snapshot.save(); + Ok(()) +} + +/// Handle responses from stack-stx and delegate-stack-stx -- functions that *lock up* STX +fn handle_stack_lockup_pox_v2( + global_context: &mut GlobalContext, + function_name: &str, + value: &Value, +) -> Result, ClarityError> { + debug!( + "Handle special-case contract-call to {:?} {} (which returned {:?})", + "PoX-2 contract", function_name, value + ); + // applying a pox lock at this point is equivalent to evaluating a transfer + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, locked_amount, unlock_height) = match parse_pox_stacking_result(value) { + Ok(x) => x, + Err(_) => { + // nothing to do -- the function failed + return Ok(None); + } + }; + + match pox_lock_v2( + &mut global_context.database, + &stacker, + locked_amount, + unlock_height as u64, + ) { + Ok(_) => { + let event = + StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { + locked_amount, + unlock_height, + locked_address: stacker, + contract_identifier: boot_code_id("pox-2", global_context.mainnet), + })); + return Ok(Some(event)); + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )); + } + Err(LockingError::PoxAlreadyLocked) => { + // the caller tried to lock tokens into both pox-1 and pox-2 + return Err(ClarityError::Runtime( + RuntimeErrorType::PoxAlreadyLocked, + None, + )); + } + Err(e) => { + panic!( + "FATAL: failed to lock {} from {} until {}: '{:?}'", + locked_amount, stacker, unlock_height, &e + ); + } + } +} + +/// Handle responses from stack-extend and delegate-stack-extend -- functions that *extend +/// already-locked* STX. +fn handle_stack_lockup_extension_pox_v2( + global_context: &mut GlobalContext, + function_name: &str, + value: &Value, +) -> Result, ClarityError> { + // in this branch case, the PoX-2 contract has stored the extension information + // and performed the extension checks. Now, the VM needs to update the account locks + // (because the locks cannot be applied directly from the Clarity code itself) + // applying a pox lock at this point is equivalent to evaluating a transfer + debug!( + "Handle special-case contract-call to {:?} {} (which returned {:?})", + boot_code_id("pox-2", global_context.mainnet), + function_name, + value + ); + + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, unlock_height) = match parse_pox_extend_result(value) { + Ok(x) => x, + Err(_) => { + // The stack-extend function returned an error: we do not need to apply a lock + // in this case, and can just return and let the normal VM codepath surface the + // error response type. + return Ok(None); + } + }; + + match pox_lock_extend_v2(&mut global_context.database, &stacker, unlock_height as u64) { + Ok(locked_amount) => { + let event = + StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { + locked_amount, + unlock_height, + locked_address: stacker, + contract_identifier: boot_code_id("pox-2", global_context.mainnet), + })); + return Ok(Some(event)); + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )) + } + Err(e) => { + // Error results *other* than a DefunctPoxContract panic, because + // those errors should have been caught by the PoX contract before + // getting to this code path. + panic!( + "FATAL: failed to extend lock from {} until {}: '{:?}'", + stacker, unlock_height, &e + ); + } + } +} + +/// Handle responses from stack-increase and delegate-stack-increase -- functions that *increase +/// already-locked* STX amounts. +fn handle_stack_lockup_increase_pox_v2( + global_context: &mut GlobalContext, + function_name: &str, + value: &Value, +) -> Result, ClarityError> { + // in this branch case, the PoX-2 contract has stored the increase information + // and performed the increase checks. Now, the VM needs to update the account locks + // (because the locks cannot be applied directly from the Clarity code itself) + // applying a pox lock at this point is equivalent to evaluating a transfer + debug!( + "Handle special-case contract-call"; + "contract" => ?boot_code_id("pox-2", global_context.mainnet), + "function" => function_name, + "return-value" => %value, + ); + + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, total_locked) = match parse_pox_increase(value) { + Ok(x) => x, + Err(_) => { + // Function failed, do nothing. + return Ok(None); + } + }; + + match pox_lock_increase_v2(&mut global_context.database, &stacker, total_locked) { + Ok(new_balance) => { + let event = + StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { + locked_amount: new_balance.amount_locked(), + unlock_height: new_balance.unlock_height(), + locked_address: stacker, + contract_identifier: boot_code_id("pox-2", global_context.mainnet), + })); + + return Ok(Some(event)); + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )) + } + Err(e) => { + // Error results *other* than a DefunctPoxContract panic, because + // those errors should have been caught by the PoX contract before + // getting to this code path. + panic!( + "FATAL: failed to increase lock from {}: '{:?}'", + stacker, &e + ); + } + } +} + +/// Handle special cases when calling into the PoX API contract +pub fn handle_contract_call( + global_context: &mut GlobalContext, + sender_opt: Option<&PrincipalData>, + contract_id: &QualifiedContractIdentifier, + function_name: &str, + args: &[Value], + value: &Value, +) -> Result<(), ClarityError> { + // Generate a synthetic print event for all functions that alter stacking state + let print_event_opt = if let Value::Response(response) = value { + if response.committed { + // method succeeded. Synthesize event info, but default to no event report if we fail + // for some reason. + // Failure to synthesize an event due to a bug is *NOT* an excuse to crash the whole + // network! Event capture is not consensus-critical. + let event_info_opt = match synthesize_pox_2_or_3_event_info( + global_context, + contract_id, + sender_opt, + function_name, + args, + ) { + Ok(Some(event_info)) => Some(event_info), + Ok(None) => None, + Err(e) => { + error!("Failed to synthesize PoX-3 event info: {:?}", &e); + None + } + }; + if let Some(event_info) = event_info_opt { + let event_response = + Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); + let tx_event = + Environment::construct_print_transaction_event(contract_id, &event_response); + Some(tx_event) + } else { + None + } + } else { + None + } + } else { + None + }; + + // Execute function specific logic to complete the lock-up + let lock_event_opt = if function_name == "stack-stx" || function_name == "delegate-stack-stx" { + handle_stack_lockup_pox_v2(global_context, function_name, value)? + } else if function_name == "stack-extend" || function_name == "delegate-stack-extend" { + handle_stack_lockup_extension_pox_v2(global_context, function_name, value)? + } else if function_name == "stack-increase" || function_name == "delegate-stack-increase" { + handle_stack_lockup_increase_pox_v2(global_context, function_name, value)? + } else { + None + }; + + // append the lockup event, so it looks as if the print event happened before the lock-up + if let Some(batch) = global_context.event_batches.last_mut() { + if let Some(print_event) = print_event_opt { + batch.events.push(print_event); + } + if let Some(lock_event) = lock_event_opt { + batch.events.push(lock_event); + } + } + + Ok(()) +} diff --git a/pox-locking/src/pox_3.rs b/pox-locking/src/pox_3.rs new file mode 100644 index 0000000000..7e045a9429 --- /dev/null +++ b/pox-locking/src/pox_3.rs @@ -0,0 +1,415 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::boot_util::boot_code_id; +use clarity::vm::contexts::GlobalContext; +use clarity::vm::costs::cost_functions::ClarityCostFunction; +use clarity::vm::costs::runtime_cost; +use clarity::vm::database::{ClarityDatabase, STXBalance}; +use clarity::vm::errors::Error as ClarityError; +use clarity::vm::errors::RuntimeErrorType; +use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use clarity::vm::{Environment, Value}; +use slog::slog_debug; +use slog::slog_error; +use stacks_common::{debug, error}; + +use crate::events::synthesize_pox_2_or_3_event_info; +use crate::LockingError; +use crate::POX_3_NAME; +// Note: PoX-3 uses the same contract-call result parsing routines as PoX-2 +use crate::pox_2::{parse_pox_extend_result, parse_pox_increase, parse_pox_stacking_result}; + +/////////////////////// PoX-3 ///////////////////////////////// + +/// Lock up STX for PoX for a time. Does NOT touch the account nonce. +pub fn pox_lock_v3( + db: &mut ClarityDatabase, + principal: &PrincipalData, + lock_amount: u128, + unlock_burn_height: u64, +) -> Result<(), LockingError> { + assert!(unlock_burn_height > 0); + assert!(lock_amount > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if snapshot.has_locked_tokens() { + return Err(LockingError::PoxAlreadyLocked); + } + if !snapshot.can_transfer(lock_amount) { + return Err(LockingError::PoxInsufficientBalance); + } + snapshot.lock_tokens_v3(lock_amount, unlock_burn_height); + + debug!( + "PoX v3 lock applied"; + "pox_locked_ustx" => snapshot.balance().amount_locked(), + "available_ustx" => snapshot.balance().amount_unlocked(), + "unlock_burn_height" => unlock_burn_height, + "account" => %principal, + ); + + snapshot.save(); + Ok(()) +} + +/// Extend a STX lock up for PoX for a time. Does NOT touch the account nonce. +/// Returns Ok(lock_amount) when successful +/// +/// # Errors +/// - Returns Error::PoxExtendNotLocked if this function was called on an account +/// which isn't locked. This *should* have been checked by the PoX v3 contract, +/// so this should surface in a panic. +pub fn pox_lock_extend_v3( + db: &mut ClarityDatabase, + principal: &PrincipalData, + unlock_burn_height: u64, +) -> Result { + assert!(unlock_burn_height > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if !snapshot.has_locked_tokens() { + return Err(LockingError::PoxExtendNotLocked); + } + + snapshot.extend_lock_v3(unlock_burn_height); + + let amount_locked = snapshot.balance().amount_locked(); + + debug!( + "PoX v3 lock applied"; + "pox_locked_ustx" => amount_locked, + "available_ustx" => snapshot.balance().amount_unlocked(), + "unlock_burn_height" => unlock_burn_height, + "account" => %principal, + ); + + snapshot.save(); + Ok(amount_locked) +} + +/// Increase a STX lock up for PoX-3. Does NOT touch the account nonce. +/// Returns Ok( account snapshot ) when successful +/// +/// # Errors +/// - Returns Error::PoxExtendNotLocked if this function was called on an account +/// which isn't locked. This *should* have been checked by the PoX v3 contract, +/// so this should surface in a panic. +pub fn pox_lock_increase_v3( + db: &mut ClarityDatabase, + principal: &PrincipalData, + new_total_locked: u128, +) -> Result { + assert!(new_total_locked > 0); + + let mut snapshot = db.get_stx_balance_snapshot(principal); + + if !snapshot.has_locked_tokens() { + return Err(LockingError::PoxExtendNotLocked); + } + + let bal = snapshot.canonical_balance_repr(); + let total_amount = bal + .amount_unlocked() + .checked_add(bal.amount_locked()) + .expect("STX balance overflowed u128"); + if total_amount < new_total_locked { + return Err(LockingError::PoxInsufficientBalance); + } + + if bal.amount_locked() > new_total_locked { + return Err(LockingError::PoxInvalidIncrease); + } + + snapshot.increase_lock_v3(new_total_locked); + + let out_balance = snapshot.canonical_balance_repr(); + + debug!( + "PoX v3 lock increased"; + "pox_locked_ustx" => out_balance.amount_locked(), + "available_ustx" => out_balance.amount_unlocked(), + "unlock_burn_height" => out_balance.unlock_height(), + "account" => %principal, + ); + + snapshot.save(); + Ok(out_balance) +} + +/////////////// PoX-3 ////////////////////////////////////////// + +/// Handle responses from stack-stx and delegate-stack-stx in pox-3 -- functions that *lock up* STX +fn handle_stack_lockup_pox_v3( + global_context: &mut GlobalContext, + function_name: &str, + value: &Value, +) -> Result, ClarityError> { + debug!( + "Handle special-case contract-call to {:?} {} (which returned {:?})", + boot_code_id(POX_3_NAME, global_context.mainnet), + function_name, + value + ); + // applying a pox lock at this point is equivalent to evaluating a transfer + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, locked_amount, unlock_height) = match parse_pox_stacking_result(value) { + Ok(x) => x, + Err(_) => { + // nothing to do -- the function failed + return Ok(None); + } + }; + + match pox_lock_v3( + &mut global_context.database, + &stacker, + locked_amount, + unlock_height as u64, + ) { + Ok(_) => { + let event = + StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { + locked_amount, + unlock_height, + locked_address: stacker, + contract_identifier: boot_code_id(POX_3_NAME, global_context.mainnet), + })); + return Ok(Some(event)); + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )); + } + Err(LockingError::PoxAlreadyLocked) => { + // the caller tried to lock tokens into multiple pox contracts + return Err(ClarityError::Runtime( + RuntimeErrorType::PoxAlreadyLocked, + None, + )); + } + Err(e) => { + panic!( + "FATAL: failed to lock {} from {} until {}: '{:?}'", + locked_amount, stacker, unlock_height, &e + ); + } + } +} + +/// Handle responses from stack-extend and delegate-stack-extend in pox-3 -- functions that *extend +/// already-locked* STX. +fn handle_stack_lockup_extension_pox_v3( + global_context: &mut GlobalContext, + function_name: &str, + value: &Value, +) -> Result, ClarityError> { + // in this branch case, the PoX-3 contract has stored the extension information + // and performed the extension checks. Now, the VM needs to update the account locks + // (because the locks cannot be applied directly from the Clarity code itself) + // applying a pox lock at this point is equivalent to evaluating a transfer + debug!( + "Handle special-case contract-call to {:?} {} (which returned {:?})", + boot_code_id("pox-3", global_context.mainnet), + function_name, + value + ); + + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, unlock_height) = match parse_pox_extend_result(value) { + Ok(x) => x, + Err(_) => { + // The stack-extend function returned an error: we do not need to apply a lock + // in this case, and can just return and let the normal VM codepath surface the + // error response type. + return Ok(None); + } + }; + + match pox_lock_extend_v3(&mut global_context.database, &stacker, unlock_height as u64) { + Ok(locked_amount) => { + let event = + StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { + locked_amount, + unlock_height, + locked_address: stacker, + contract_identifier: boot_code_id(POX_3_NAME, global_context.mainnet), + })); + return Ok(Some(event)); + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )) + } + Err(e) => { + // Error results *other* than a DefunctPoxContract panic, because + // those errors should have been caught by the PoX contract before + // getting to this code path. + panic!( + "FATAL: failed to extend lock from {} until {}: '{:?}'", + stacker, unlock_height, &e + ); + } + } +} + +/// Handle responses from stack-increase and delegate-stack-increase in PoX-3 -- functions +/// that *increase already-locked* STX amounts. +fn handle_stack_lockup_increase_pox_v3( + global_context: &mut GlobalContext, + function_name: &str, + value: &Value, +) -> Result, ClarityError> { + // in this branch case, the PoX-3 contract has stored the increase information + // and performed the increase checks. Now, the VM needs to update the account locks + // (because the locks cannot be applied directly from the Clarity code itself) + // applying a pox lock at this point is equivalent to evaluating a transfer + debug!( + "Handle special-case contract-call"; + "contract" => ?boot_code_id("pox-3", global_context.mainnet), + "function" => function_name, + "return-value" => %value, + ); + + runtime_cost( + ClarityCostFunction::StxTransfer, + &mut global_context.cost_track, + 1, + )?; + + let (stacker, total_locked) = match parse_pox_increase(value) { + Ok(x) => x, + Err(_) => { + // nothing to do -- function failed + return Ok(None); + } + }; + match pox_lock_increase_v3(&mut global_context.database, &stacker, total_locked) { + Ok(new_balance) => { + let event = + StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { + locked_amount: new_balance.amount_locked(), + unlock_height: new_balance.unlock_height(), + locked_address: stacker, + contract_identifier: boot_code_id(POX_3_NAME, global_context.mainnet), + })); + + return Ok(Some(event)); + } + Err(LockingError::DefunctPoxContract) => { + return Err(ClarityError::Runtime( + RuntimeErrorType::DefunctPoxContract, + None, + )) + } + Err(e) => { + // Error results *other* than a DefunctPoxContract panic, because + // those errors should have been caught by the PoX contract before + // getting to this code path. + panic!( + "FATAL: failed to increase lock from {}: '{:?}'", + stacker, &e + ); + } + } +} + +/// Handle special cases when calling into the PoX-3 API contract +pub fn handle_contract_call( + global_context: &mut GlobalContext, + sender_opt: Option<&PrincipalData>, + contract_id: &QualifiedContractIdentifier, + function_name: &str, + args: &[Value], + value: &Value, +) -> Result<(), ClarityError> { + // Generate a synthetic print event for all functions that alter stacking state + let print_event_opt = if let Value::Response(response) = value { + if response.committed { + // method succeeded. Synthesize event info, but default to no event report if we fail + // for some reason. + // Failure to synthesize an event due to a bug is *NOT* an excuse to crash the whole + // network! Event capture is not consensus-critical. + let event_info_opt = match synthesize_pox_2_or_3_event_info( + global_context, + contract_id, + sender_opt, + function_name, + args, + ) { + Ok(Some(event_info)) => Some(event_info), + Ok(None) => None, + Err(e) => { + error!("Failed to synthesize PoX-3 event info: {:?}", &e); + None + } + }; + if let Some(event_info) = event_info_opt { + let event_response = + Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); + let tx_event = + Environment::construct_print_transaction_event(contract_id, &event_response); + Some(tx_event) + } else { + None + } + } else { + None + } + } else { + None + }; + + // Execute function specific logic to complete the lock-up + let lock_event_opt = if function_name == "stack-stx" || function_name == "delegate-stack-stx" { + handle_stack_lockup_pox_v3(global_context, function_name, value)? + } else if function_name == "stack-extend" || function_name == "delegate-stack-extend" { + handle_stack_lockup_extension_pox_v3(global_context, function_name, value)? + } else if function_name == "stack-increase" || function_name == "delegate-stack-increase" { + handle_stack_lockup_increase_pox_v3(global_context, function_name, value)? + } else { + None + }; + + // append the lockup event, so it looks as if the print event happened before the lock-up + if let Some(batch) = global_context.event_batches.last_mut() { + if let Some(print_event) = print_event_opt { + batch.events.push(print_event); + } + if let Some(lock_event) = lock_event_opt { + batch.events.push(lock_event); + } + } + + Ok(()) +} diff --git a/src/chainstate/stacks/db/accounts.rs b/src/chainstate/stacks/db/accounts.rs index cf65dcf705..9c1fd0a708 100644 --- a/src/chainstate/stacks/db/accounts.rs +++ b/src/chainstate/stacks/db/accounts.rs @@ -392,287 +392,6 @@ impl StacksChainState { .expect("FATAL: failed to set account nonce") } - /////////////////////// PoX-3 ///////////////////////////////// - - /// Lock up STX for PoX for a time. Does NOT touch the account nonce. - pub fn pox_lock_v3( - db: &mut ClarityDatabase, - principal: &PrincipalData, - lock_amount: u128, - unlock_burn_height: u64, - ) -> Result<(), Error> { - assert!(unlock_burn_height > 0); - assert!(lock_amount > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if snapshot.has_locked_tokens() { - return Err(Error::PoxAlreadyLocked); - } - if !snapshot.can_transfer(lock_amount) { - return Err(Error::PoxInsufficientBalance); - } - snapshot.lock_tokens_v3(lock_amount, unlock_burn_height); - - debug!( - "PoX v3 lock applied"; - "pox_locked_ustx" => snapshot.balance().amount_locked(), - "available_ustx" => snapshot.balance().amount_unlocked(), - "unlock_burn_height" => unlock_burn_height, - "account" => %principal, - ); - - snapshot.save(); - Ok(()) - } - - /// Extend a STX lock up for PoX for a time. Does NOT touch the account nonce. - /// Returns Ok(lock_amount) when successful - /// - /// # Errors - /// - Returns Error::PoxExtendNotLocked if this function was called on an account - /// which isn't locked. This *should* have been checked by the PoX v3 contract, - /// so this should surface in a panic. - pub fn pox_lock_extend_v3( - db: &mut ClarityDatabase, - principal: &PrincipalData, - unlock_burn_height: u64, - ) -> Result { - assert!(unlock_burn_height > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if !snapshot.has_locked_tokens() { - return Err(Error::PoxExtendNotLocked); - } - - snapshot.extend_lock_v3(unlock_burn_height); - - let amount_locked = snapshot.balance().amount_locked(); - - debug!( - "PoX v3 lock applied"; - "pox_locked_ustx" => amount_locked, - "available_ustx" => snapshot.balance().amount_unlocked(), - "unlock_burn_height" => unlock_burn_height, - "account" => %principal, - ); - - snapshot.save(); - Ok(amount_locked) - } - - /// Increase a STX lock up for PoX-3. Does NOT touch the account nonce. - /// Returns Ok( account snapshot ) when successful - /// - /// # Errors - /// - Returns Error::PoxExtendNotLocked if this function was called on an account - /// which isn't locked. This *should* have been checked by the PoX v3 contract, - /// so this should surface in a panic. - pub fn pox_lock_increase_v3( - db: &mut ClarityDatabase, - principal: &PrincipalData, - new_total_locked: u128, - ) -> Result { - assert!(new_total_locked > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if !snapshot.has_locked_tokens() { - return Err(Error::PoxExtendNotLocked); - } - - let bal = snapshot.canonical_balance_repr(); - let total_amount = bal - .amount_unlocked() - .checked_add(bal.amount_locked()) - .expect("STX balance overflowed u128"); - if total_amount < new_total_locked { - return Err(Error::PoxInsufficientBalance); - } - - if bal.amount_locked() > new_total_locked { - return Err(Error::PoxInvalidIncrease); - } - - snapshot.increase_lock_v3(new_total_locked); - - let out_balance = snapshot.canonical_balance_repr(); - - debug!( - "PoX v3 lock increased"; - "pox_locked_ustx" => out_balance.amount_locked(), - "available_ustx" => out_balance.amount_unlocked(), - "unlock_burn_height" => out_balance.unlock_height(), - "account" => %principal, - ); - - snapshot.save(); - Ok(out_balance) - } - - /////////////////////// PoX-2 ///////////////////////////////// - - /// Increase a STX lock up for PoX. Does NOT touch the account nonce. - /// Returns Ok( account snapshot ) when successful - /// - /// # Errors - /// - Returns Error::PoxExtendNotLocked if this function was called on an account - /// which isn't locked. This *should* have been checked by the PoX v2 contract, - /// so this should surface in a panic. - pub fn pox_lock_increase_v2( - db: &mut ClarityDatabase, - principal: &PrincipalData, - new_total_locked: u128, - ) -> Result { - assert!(new_total_locked > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if !snapshot.has_locked_tokens() { - return Err(Error::PoxExtendNotLocked); - } - - if !snapshot.is_v2_locked() { - return Err(Error::PoxIncreaseOnV1); - } - - let bal = snapshot.canonical_balance_repr(); - let total_amount = bal - .amount_unlocked() - .checked_add(bal.amount_locked()) - .expect("STX balance overflowed u128"); - if total_amount < new_total_locked { - return Err(Error::PoxInsufficientBalance); - } - - if bal.amount_locked() > new_total_locked { - return Err(Error::PoxInvalidIncrease); - } - - snapshot.increase_lock_v2(new_total_locked); - - let out_balance = snapshot.canonical_balance_repr(); - - debug!( - "PoX v2 lock increased"; - "pox_locked_ustx" => out_balance.amount_locked(), - "available_ustx" => out_balance.amount_unlocked(), - "unlock_burn_height" => out_balance.unlock_height(), - "account" => %principal, - ); - - snapshot.save(); - Ok(out_balance) - } - - /// Extend a STX lock up for PoX for a time. Does NOT touch the account nonce. - /// Returns Ok(lock_amount) when successful - /// - /// # Errors - /// - Returns Error::PoxExtendNotLocked if this function was called on an account - /// which isn't locked. This *should* have been checked by the PoX v2 contract, - /// so this should surface in a panic. - pub fn pox_lock_extend_v2( - db: &mut ClarityDatabase, - principal: &PrincipalData, - unlock_burn_height: u64, - ) -> Result { - assert!(unlock_burn_height > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if !snapshot.has_locked_tokens() { - return Err(Error::PoxExtendNotLocked); - } - - snapshot.extend_lock_v2(unlock_burn_height); - - let amount_locked = snapshot.balance().amount_locked(); - - debug!( - "PoX v2 lock applied"; - "pox_locked_ustx" => amount_locked, - "available_ustx" => snapshot.balance().amount_unlocked(), - "unlock_burn_height" => unlock_burn_height, - "account" => %principal, - ); - - snapshot.save(); - Ok(amount_locked) - } - - /// Lock up STX for PoX for a time. Does NOT touch the account nonce. - pub fn pox_lock_v2( - db: &mut ClarityDatabase, - principal: &PrincipalData, - lock_amount: u128, - unlock_burn_height: u64, - ) -> Result<(), Error> { - assert!(unlock_burn_height > 0); - assert!(lock_amount > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if snapshot.has_locked_tokens() { - return Err(Error::PoxAlreadyLocked); - } - if !snapshot.can_transfer(lock_amount) { - return Err(Error::PoxInsufficientBalance); - } - snapshot.lock_tokens_v2(lock_amount, unlock_burn_height); - - debug!( - "PoX v2 lock applied"; - "pox_locked_ustx" => snapshot.balance().amount_locked(), - "available_ustx" => snapshot.balance().amount_unlocked(), - "unlock_burn_height" => unlock_burn_height, - "account" => %principal, - ); - - snapshot.save(); - Ok(()) - } - - /////////////////////// PoX (first version) ///////////////////////////////// - - /// Lock up STX for PoX for a time. Does NOT touch the account nonce. - pub fn pox_lock_v1( - db: &mut ClarityDatabase, - principal: &PrincipalData, - lock_amount: u128, - unlock_burn_height: u64, - ) -> Result<(), Error> { - assert!(unlock_burn_height > 0); - assert!(lock_amount > 0); - - let mut snapshot = db.get_stx_balance_snapshot(principal); - - if snapshot.balance().was_locked_by_v2() { - debug!("PoX Lock attempted on an account locked by v2"); - return Err(Error::DefunctPoxContract); - } - - if snapshot.has_locked_tokens() { - return Err(Error::PoxAlreadyLocked); - } - if !snapshot.can_transfer(lock_amount) { - return Err(Error::PoxInsufficientBalance); - } - snapshot.lock_tokens_v1(lock_amount, unlock_burn_height); - - debug!( - "PoX v1 lock applied"; - "pox_locked_ustx" => snapshot.balance().amount_locked(), - "available_ustx" => snapshot.balance().amount_unlocked(), - "unlock_burn_height" => unlock_burn_height, - "account" => %principal, - ); - - snapshot.save(); - Ok(()) - } - /// Schedule a miner payment in the future. /// Schedules payments out to both miners and users that support them. pub fn insert_miner_payment_schedule<'a>( diff --git a/src/clarity_vm/special.rs b/src/clarity_vm/special.rs index e738c3b7df..64e4188128 100644 --- a/src/clarity_vm/special.rs +++ b/src/clarity_vm/special.rs @@ -14,1267 +14,27 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use clarity::vm::ast::ASTRules; -use clarity::vm::costs::cost_functions::ClarityCostFunction; -use clarity::vm::costs::{CostTracker, MemoryConsumer}; -use clarity::vm::{ast, eval_all}; -use std::cmp; -use std::convert::{TryFrom, TryInto}; - -use crate::chainstate::stacks::boot::{POX_1_NAME, POX_2_NAME, POX_3_NAME}; -use crate::chainstate::stacks::db::StacksChainState; -use crate::chainstate::stacks::Error as ChainstateError; -use crate::chainstate::stacks::StacksMicroblockHeader; -use crate::util_lib::boot::boot_code_id; -use clarity::vm::contexts::{Environment, GlobalContext}; -use clarity::vm::errors::Error; -use clarity::vm::errors::{ - CheckErrors, InterpreterError, InterpreterResult as Result, RuntimeErrorType, -}; -use clarity::vm::representations::{ClarityName, SymbolicExpression, SymbolicExpressionType}; -use clarity::vm::types::{ - BuffData, OptionalData, PrincipalData, QualifiedContractIdentifier, ResponseData, SequenceData, - TupleData, TypeSignature, Value, -}; - -use clarity::vm::clarity::Error as clarity_interpreter_error; -use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; -use clarity::vm::ClarityVersion; - -use crate::chainstate::stacks::address::PoxAddress; -use crate::core::StacksEpochId; -use stacks_common::util::hash::Hash160; - -use crate::vm::costs::runtime_cost; - -/// Parse the returned value from PoX `stack-stx` and `delegate-stack-stx` functions -/// from pox-2.clar or pox-3.clar into a format more readily digestible in rust. -/// Panics if the supplied value doesn't match the expected tuple structure -fn parse_pox_stacking_result( - result: &Value, -) -> std::result::Result<(PrincipalData, u128, u64), i128> { - match result.clone().expect_result() { - Ok(res) => { - // should have gotten back (ok { stacker: principal, lock-amount: uint, unlock-burn-height: uint .. } .. }))) - let tuple_data = res.expect_tuple(); - let stacker = tuple_data - .get("stacker") - .expect(&format!("FATAL: no 'stacker'")) - .to_owned() - .expect_principal(); - - let lock_amount = tuple_data - .get("lock-amount") - .expect(&format!("FATAL: no 'lock-amount'")) - .to_owned() - .expect_u128(); - - let unlock_burn_height = tuple_data - .get("unlock-burn-height") - .expect(&format!("FATAL: no 'unlock-burn-height'")) - .to_owned() - .expect_u128() - .try_into() - .expect("FATAL: 'unlock-burn-height' overflow"); - - Ok((stacker, lock_amount, unlock_burn_height)) - } - Err(e) => Err(e.expect_i128()), - } -} - -/// Parse the returned value from PoX `stack-stx` and `delegate-stack-stx` functions -/// from pox.clar into a format more readily digestible in rust. -/// Panics if the supplied value doesn't match the expected tuple structure -fn parse_pox_stacking_result_v1( - result: &Value, -) -> std::result::Result<(PrincipalData, u128, u64), i128> { - match result.clone().expect_result() { - Ok(res) => { - // should have gotten back (ok (tuple (stacker principal) (lock-amount uint) (unlock-burn-height uint))) - let tuple_data = res.expect_tuple(); - let stacker = tuple_data - .get("stacker") - .expect(&format!("FATAL: no 'stacker'")) - .to_owned() - .expect_principal(); - - let lock_amount = tuple_data - .get("lock-amount") - .expect(&format!("FATAL: no 'lock-amount'")) - .to_owned() - .expect_u128(); - - let unlock_burn_height = tuple_data - .get("unlock-burn-height") - .expect(&format!("FATAL: no 'unlock-burn-height'")) - .to_owned() - .expect_u128() - .try_into() - .expect("FATAL: 'unlock-burn-height' overflow"); - - Ok((stacker, lock_amount, unlock_burn_height)) - } - Err(e) => Err(e.expect_i128()), - } -} - -/// Parse the returned value from PoX2 or PoX3 `stack-extend` and `delegate-stack-extend` functions -/// into a format more readily digestible in rust. -/// Panics if the supplied value doesn't match the expected tuple structure -fn parse_pox_extend_result(result: &Value) -> std::result::Result<(PrincipalData, u64), i128> { - match result.clone().expect_result() { - Ok(res) => { - // should have gotten back (ok { stacker: principal, unlock-burn-height: uint .. } .. }) - let tuple_data = res.expect_tuple(); - let stacker = tuple_data - .get("stacker") - .expect(&format!("FATAL: no 'stacker'")) - .to_owned() - .expect_principal(); - - let unlock_burn_height = tuple_data - .get("unlock-burn-height") - .expect(&format!("FATAL: no 'unlock-burn-height'")) - .to_owned() - .expect_u128() - .try_into() - .expect("FATAL: 'unlock-burn-height' overflow"); - - Ok((stacker, unlock_burn_height)) - } - // in the error case, the function should have returned `int` error code - Err(e) => Err(e.expect_i128()), - } -} - -/// Parse the returned value from PoX2 or PoX3 `stack-increase` function -/// into a format more readily digestible in rust. -/// Panics if the supplied value doesn't match the expected tuple structure -fn parse_pox_increase(result: &Value) -> std::result::Result<(PrincipalData, u128), i128> { - match result.clone().expect_result() { - Ok(res) => { - // should have gotten back (ok { stacker: principal, total-locked: uint .. } .. }) - let tuple_data = res.expect_tuple(); - let stacker = tuple_data - .get("stacker") - .expect(&format!("FATAL: no 'stacker'")) - .to_owned() - .expect_principal(); - - let total_locked = tuple_data - .get("total-locked") - .expect(&format!("FATAL: no 'total-locked'")) - .to_owned() - .expect_u128(); - - Ok((stacker, total_locked)) - } - // in the error case, the function should have returned `int` error code - Err(e) => Err(e.expect_i128()), - } -} - -/// Handle special cases when calling into the PoX API contract -fn handle_pox_v1_api_contract_call( - global_context: &mut GlobalContext, - _sender_opt: Option<&PrincipalData>, - function_name: &str, - value: &Value, -) -> Result<()> { - if function_name == "stack-stx" || function_name == "delegate-stack-stx" { - debug!( - "Handle special-case contract-call to {:?} {} (which returned {:?})", - boot_code_id(POX_1_NAME, global_context.mainnet), - function_name, - value - ); - - // applying a pox lock at this point is equivalent to evaluating a transfer - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - match parse_pox_stacking_result_v1(value) { - Ok((stacker, locked_amount, unlock_height)) => { - // in most cases, if this fails, then there's a bug in the contract (since it already does - // the necessary checks), but with v2 introduction, that's no longer true -- if someone - // locks on PoX v2, and then tries to lock again in PoX v1, that's not captured by the v1 - // contract. - match StacksChainState::pox_lock_v1( - &mut global_context.database, - &stacker, - locked_amount, - unlock_height as u64, - ) { - Ok(_) => { - if let Some(batch) = global_context.event_batches.last_mut() { - batch.events.push(StacksTransactionEvent::STXEvent( - STXEventType::STXLockEvent(STXLockEventData { - locked_amount, - unlock_height, - locked_address: stacker, - contract_identifier: boot_code_id( - "pox", - global_context.mainnet, - ), - }), - )); - } - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)) - } - Err(ChainstateError::PoxAlreadyLocked) => { - // the caller tried to lock tokens into both pox-1 and pox-2 - return Err(Error::Runtime(RuntimeErrorType::PoxAlreadyLocked, None)); - } - Err(e) => { - panic!( - "FATAL: failed to lock {} from {} until {}: '{:?}'", - locked_amount, stacker, unlock_height, &e - ); - } - } - - return Ok(()); - } - Err(_) => { - // nothing to do -- the function failed - return Ok(()); - } - } - } - // nothing to do - Ok(()) -} - -/// Determine who the stacker is for a given function. -/// - 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" | "delegate-stx" => { - Value::Principal(sender.clone()) - } - _ => args[0].clone(), - } -} - -/// 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, - args: &[Value], -) -> String { - format!( - r#" - (let ( - (stacker '{stacker}) - (func-name "{func_name}") - (stacker-info (stx-account stacker)) - (total-balance (stx-get-balance stacker)) - ) - {{ - ;; Function name - name: func-name, - ;; The principal of the stacker - stacker: stacker, - ;; The current available balance - balance: total-balance, - ;; The amount of locked STX - locked: (get locked stacker-info), - ;; The burnchain block height of when the tokens unlock. Zero if no tokens are locked. - burnchain-unlock-height: (get unlock-height stacker-info), - }} - ) - "#, - stacker = get_stacker(sender, function_name, args), - func_name = function_name - ) -} - -/// Craft the code snippet to evaluate a stack-aggregation-* function -fn create_event_info_aggregation_code(function_name: &str) -> String { - format!( - r#" - (let ( - (stacker-info (stx-account tx-sender)) - ) - {{ - ;; Function name - name: "{func_name}", - ;; who called this - ;; NOTE: these fields are required by downstream clients. - ;; Even though tx-sender is *not* a stacker, the field is - ;; called "stacker" and these clients know to treat it as - ;; the delegator. - stacker: tx-sender, - balance: (stx-get-balance tx-sender), - locked: (get locked stacker-info), - burnchain-unlock-height: (get unlock-height stacker-info), - - }} - ) - "#, - func_name = function_name - ) -} - -/// Craft the code snippet to generate the method-specific `data` payload -fn create_event_info_data_code(function_name: &str, args: &[Value]) -> String { - match function_name { - "stack-stx" => { - format!( - r#" - {{ - data: {{ - ;; amount of ustx to lock. - ;; equal to args[0] - lock-amount: {lock_amount}, - ;; burnchain height when the unlock finishes. - ;; derived from args[3] - unlock-burn-height: (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 {lock_period})), - ;; PoX address tuple. - ;; equal to args[1]. - pox-addr: {pox_addr}, - ;; start of lock-up. - ;; equal to args[2] - start-burn-height: {start_burn_height}, - ;; how long to lock, in burn blocks - ;; equal to args[3] - lock-period: {lock_period} - }} - }} - "#, - lock_amount = &args[0], - lock_period = &args[3], - pox_addr = &args[1], - start_burn_height = &args[2], - ) - } - "delegate-stack-stx" => { - format!( - r#" - {{ - data: {{ - ;; amount of ustx to lock. - ;; equal to args[1] - lock-amount: {lock_amount}, - ;; burnchain height when the unlock finishes. - ;; derived from args[4] - unlock-burn-height: (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 {lock_period})), - ;; PoX address tuple. - ;; equal to args[2] - pox-addr: {pox_addr}, - ;; start of lock-up - ;; equal to args[3] - start-burn-height: {start_burn_height}, - ;; how long to lock, in burn blocks - ;; equal to args[3] - lock-period: {lock_period}, - ;; delegator - delegator: tx-sender, - ;; stacker - ;; equal to args[0] - stacker: '{stacker} - }} - }} - "#, - stacker = &args[0], - lock_amount = &args[1], - pox_addr = &args[2], - start_burn_height = &args[3], - lock_period = &args[4], - ) - } - "stack-increase" => { - format!( - r#" - {{ - data: {{ - ;; amount to increase by - ;; equal to args[0] - increase-by: {increase_by}, - ;; new amount locked - ;; NOTE: the lock has not yet been applied! - ;; derived from args[0] - total-locked: (+ {increase_by} (get locked (stx-account tx-sender))), - ;; pox addr increased - pox-addr: (get pox-addr (unwrap-panic (map-get? stacking-state {{ stacker: tx-sender }}))) - }} - }} - "#, - increase_by = &args[0] - ) - } - "delegate-stack-increase" => { - format!( - r#" - {{ - data: {{ - ;; pox addr - ;; equal to args[1] - pox-addr: {pox_addr}, - ;; amount to increase by - ;; equal to args[2] - increase-by: {increase_by}, - ;; total amount locked now - ;; NOTE: the lock itself has not yet been applied! - ;; this is for the stacker, so args[0] - total-locked: (+ {increase_by} (get locked (stx-account '{stacker}))), - ;; delegator - delegator: tx-sender, - ;; stacker - ;; equal to args[0] - stacker: '{stacker} - }} - }} - "#, - stacker = &args[0], - pox_addr = &args[1], - increase_by = &args[2], - ) - } - "stack-extend" => { - format!( - r#" - (let ( - ;; variable declarations derived from pox-2 - (cur-cycle (current-pox-reward-cycle)) - (unlock-height (get unlock-height (stx-account tx-sender))) - (unlock-in-cycle (burn-height-to-reward-cycle unlock-height)) - (first-extend-cycle - (if (> (+ cur-cycle u1) unlock-in-cycle) - (+ cur-cycle u1) - unlock-in-cycle)) - (last-extend-cycle (- (+ first-extend-cycle {extend_count}) u1)) - (new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle))) - ) - {{ - data: {{ - ;; pox addr extended - ;; equal to args[1] - pox-addr: {pox_addr}, - ;; number of cycles extended - ;; equal to args[0] - extend-count: {extend_count}, - ;; new unlock burnchain block height - unlock-burn-height: new-unlock-ht - }} - }}) - "#, - extend_count = &args[0], - pox_addr = &args[1], - ) - } - "delegate-stack-extend" => { - format!( - r#" - (let ( - (unlock-height (get unlock-height (stx-account '{stacker}))) - (unlock-in-cycle (burn-height-to-reward-cycle unlock-height)) - (cur-cycle (current-pox-reward-cycle)) - (first-extend-cycle - (if (> (+ cur-cycle u1) unlock-in-cycle) - (+ cur-cycle u1) - unlock-in-cycle)) - (last-extend-cycle (- (+ first-extend-cycle {extend_count}) u1)) - (new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle))) - ) - {{ - data: {{ - ;; pox addr extended - ;; equal to args[1] - pox-addr: {pox_addr}, - ;; number of cycles extended - ;; equal to args[2] - extend-count: {extend_count}, - ;; new unlock burnchain block height - unlock-burn-height: new-unlock-ht, - ;; delegator locking this up - delegator: tx-sender, - ;; stacker - ;; equal to args[0] - stacker: '{stacker} - }} - }}) - "#, - stacker = &args[0], - pox_addr = &args[1], - extend_count = &args[2] - ) - } - "stack-aggregation-commit" - | "stack-aggregation-commit-indexed" - | "stack-aggregation-increase" => { - format!( - r#" - {{ - data: {{ - ;; pox addr locked up - ;; equal to args[0] in all methods - pox-addr: {pox_addr}, - ;; reward cycle locked up - ;; equal to args[1] in all methods - reward-cycle: {reward_cycle}, - ;; amount locked behind this PoX address by this method - amount-ustx: (get stacked-amount - (unwrap-panic (map-get? logged-partial-stacked-by-cycle - {{ pox-addr: {pox_addr}, sender: tx-sender, reward-cycle: {reward_cycle} }}))), - ;; delegator (this is the caller) - delegator: tx-sender - }} - }} - "#, - pox_addr = &args[0], - 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 }} }}"), - } -} - -/// Synthesize an events data tuple to return on the successful execution of a pox-2 or pox-3 stacking -/// function. It runs a series of Clarity queries against the PoX contract's data space (including -/// calling PoX functions). -fn synthesize_pox_2_or_3_event_info( - global_context: &mut GlobalContext, - contract_id: &QualifiedContractIdentifier, - sender_opt: Option<&PrincipalData>, - function_name: &str, - args: &[Value], -) -> std::result::Result, ChainstateError> { - let sender = match sender_opt { - Some(sender) => sender, - None => { - return Ok(None); - } - }; - let code_snippet_template_opt = match function_name { - "stack-stx" - | "delegate-stack-stx" - | "stack-extend" - | "delegate-stack-extend" - | "stack-increase" - | "delegate-stack-increase" - | "delegate-stx" => Some(create_event_info_stack_or_delegate_code( - sender, - function_name, - args, - )), - "stack-aggregation-commit" - | "stack-aggregation-commit-indexed" - | "stack-aggregation-increase" => Some(create_event_info_aggregation_code(function_name)), - _ => None, - }; - - if let Some(code_snippet) = code_snippet_template_opt { - let data_snippet = create_event_info_data_code(function_name, args); - - test_debug!("Evaluate snippet:\n{}", &code_snippet); - test_debug!("Evaluate data code:\n{}", &data_snippet); - - let pox_2_contract = global_context - .database - .get_contract(contract_id) - .expect("FATAL: could not load PoX contract metadata"); - - let event_info = global_context - .special_cc_handler_execute_read_only( - sender.clone(), - None, - pox_2_contract.contract_context, - |env| { - let base_event_info = env - .eval_read_only_with_rules( - contract_id, - &code_snippet, - ASTRules::PrecheckSize, - ) - .map_err(|e| { - error!( - "Failed to run event-info code snippet for '{}': {:?}", - function_name, &e - ); - ChainstateError::ClarityError(clarity_interpreter_error::Interpreter(e)) - })?; - - let data_event_info = env - .eval_read_only_with_rules( - contract_id, - &data_snippet, - ASTRules::PrecheckSize, - ) - .map_err(|e| { - error!( - "Failed to run data-info code snippet for '{}': {:?}", - function_name, &e - ); - ChainstateError::ClarityError(clarity_interpreter_error::Interpreter(e)) - })?; - - // merge them - let base_event_tuple = base_event_info.expect_tuple(); - let data_tuple = data_event_info.expect_tuple(); - let event_tuple = TupleData::shallow_merge(base_event_tuple, data_tuple) - .map_err(|e| { - error!("Failed to merge data-info and event-info: {:?}", &e); - ChainstateError::ClarityError(clarity_interpreter_error::Interpreter(e)) - })?; - - Ok(Value::Tuple(event_tuple)) - }, - ) - .map_err(|e: ChainstateError| { - error!("Failed to synthesize PoX event: {:?}", &e); - e - })?; - - test_debug!( - "Synthesized PoX event info for '{}''s call to '{}': {:?}", - sender, - function_name, - &event_info - ); - Ok(Some(event_info)) - } else { - Ok(None) - } -} - -/// Handle responses from stack-stx and delegate-stack-stx -- functions that *lock up* STX -fn handle_stack_lockup_pox_v2( - global_context: &mut GlobalContext, - function_name: &str, - value: &Value, -) -> Result> { - debug!( - "Handle special-case contract-call to {:?} {} (which returned {:?})", - boot_code_id(POX_2_NAME, global_context.mainnet), - function_name, - value - ); - // applying a pox lock at this point is equivalent to evaluating a transfer - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - match parse_pox_stacking_result(value) { - Ok((stacker, locked_amount, unlock_height)) => { - match StacksChainState::pox_lock_v2( - &mut global_context.database, - &stacker, - locked_amount, - unlock_height as u64, - ) { - Ok(_) => { - let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent( - STXLockEventData { - locked_amount, - unlock_height, - locked_address: stacker, - contract_identifier: boot_code_id("pox-2", global_context.mainnet), - }, - )); - return Ok(Some(event)); - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)); - } - Err(ChainstateError::PoxAlreadyLocked) => { - // the caller tried to lock tokens into both pox-1 and pox-2 - return Err(Error::Runtime(RuntimeErrorType::PoxAlreadyLocked, None)); - } - Err(e) => { - panic!( - "FATAL: failed to lock {} from {} until {}: '{:?}'", - locked_amount, stacker, unlock_height, &e - ); - } - } - } - Err(_) => { - // nothing to do -- the function failed - return Ok(None); - } - } -} - -/// Handle responses from stack-extend and delegate-stack-extend -- functions that *extend -/// already-locked* STX. -fn handle_stack_lockup_extension_pox_v2( - global_context: &mut GlobalContext, - function_name: &str, - value: &Value, -) -> Result> { - // in this branch case, the PoX-2 contract has stored the extension information - // and performed the extension checks. Now, the VM needs to update the account locks - // (because the locks cannot be applied directly from the Clarity code itself) - // applying a pox lock at this point is equivalent to evaluating a transfer - debug!( - "Handle special-case contract-call to {:?} {} (which returned {:?})", - boot_code_id("pox-2", global_context.mainnet), - function_name, - value - ); - - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - if let Ok((stacker, unlock_height)) = parse_pox_extend_result(value) { - match StacksChainState::pox_lock_extend_v2( - &mut global_context.database, - &stacker, - unlock_height as u64, - ) { - Ok(locked_amount) => { - let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent( - STXLockEventData { - locked_amount, - unlock_height, - locked_address: stacker, - contract_identifier: boot_code_id("pox-2", global_context.mainnet), - }, - )); - return Ok(Some(event)); - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)) - } - Err(e) => { - // Error results *other* than a DefunctPoxContract panic, because - // those errors should have been caught by the PoX contract before - // getting to this code path. - panic!( - "FATAL: failed to extend lock from {} until {}: '{:?}'", - stacker, unlock_height, &e - ); - } - } - } else { - // The stack-extend function returned an error: we do not need to apply a lock - // in this case, and can just return and let the normal VM codepath surface the - // error response type. - return Ok(None); - } -} - -/// Handle responses from stack-increase and delegate-stack-increase -- functions that *increase -/// already-locked* STX amounts. -fn handle_stack_lockup_increase_pox_v2( - global_context: &mut GlobalContext, - function_name: &str, - value: &Value, -) -> Result> { - // in this branch case, the PoX-2 contract has stored the increase information - // and performed the increase checks. Now, the VM needs to update the account locks - // (because the locks cannot be applied directly from the Clarity code itself) - // applying a pox lock at this point is equivalent to evaluating a transfer - debug!( - "Handle special-case contract-call"; - "contract" => ?boot_code_id("pox-2", global_context.mainnet), - "function" => function_name, - "return-value" => %value, - ); - - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - if let Ok((stacker, total_locked)) = parse_pox_increase(value) { - match StacksChainState::pox_lock_increase_v2( - &mut global_context.database, - &stacker, - total_locked, - ) { - Ok(new_balance) => { - let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent( - STXLockEventData { - locked_amount: new_balance.amount_locked(), - unlock_height: new_balance.unlock_height(), - locked_address: stacker, - contract_identifier: boot_code_id("pox-2", global_context.mainnet), - }, - )); - - return Ok(Some(event)); - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)) - } - Err(e) => { - // Error results *other* than a DefunctPoxContract panic, because - // those errors should have been caught by the PoX contract before - // getting to this code path. - panic!( - "FATAL: failed to increase lock from {}: '{:?}'", - stacker, &e - ); - } - } - } else { - Ok(None) - } -} - -/// Handle special cases when calling into the PoX API contract -fn handle_pox_v2_api_contract_call( - global_context: &mut GlobalContext, - sender_opt: Option<&PrincipalData>, - contract_id: &QualifiedContractIdentifier, - function_name: &str, - args: &[Value], - value: &Value, -) -> Result<()> { - // Generate a synthetic print event for all functions that alter stacking state - let print_event_opt = if let Value::Response(response) = value { - if response.committed { - // method succeeded. Synthesize event info, but default to no event report if we fail - // for some reason. - // Failure to synthesize an event due to a bug is *NOT* an excuse to crash the whole - // network! Event capture is not consensus-critical. - let event_info_opt = match synthesize_pox_2_or_3_event_info( - global_context, - contract_id, - sender_opt, - function_name, - args, - ) { - Ok(Some(event_info)) => Some(event_info), - Ok(None) => None, - Err(e) => { - error!("Failed to synthesize PoX-2 event info: {:?}", &e); - None - } - }; - if let Some(event_info) = event_info_opt { - let event_response = - Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); - let tx_event = - Environment::construct_print_transaction_event(contract_id, &event_response); - Some(tx_event) - } else { - None - } - } else { - None - } - } else { - None - }; - - // Execute function specific logic to complete the lock-up - let lock_event_opt = if function_name == "stack-stx" || function_name == "delegate-stack-stx" { - handle_stack_lockup_pox_v2(global_context, function_name, value)? - } else if function_name == "stack-extend" || function_name == "delegate-stack-extend" { - handle_stack_lockup_extension_pox_v2(global_context, function_name, value)? - } else if function_name == "stack-increase" || function_name == "delegate-stack-increase" { - handle_stack_lockup_increase_pox_v2(global_context, function_name, value)? - } else { - None - }; - - // append the lockup event, so it looks as if the print event happened before the lock-up - if let Some(batch) = global_context.event_batches.last_mut() { - if let Some(print_event) = print_event_opt { - batch.events.push(print_event); - } - if let Some(lock_event) = lock_event_opt { - batch.events.push(lock_event); - } - } - - Ok(()) -} - -/////////////// PoX-3 ////////////////////////////////////////// - -/// Handle responses from stack-stx and delegate-stack-stx in pox-3 -- functions that *lock up* STX -fn handle_stack_lockup_pox_v3( - global_context: &mut GlobalContext, - function_name: &str, - value: &Value, -) -> Result> { - debug!( - "Handle special-case contract-call to {:?} {} (which returned {:?})", - boot_code_id(POX_3_NAME, global_context.mainnet), - function_name, - value - ); - // applying a pox lock at this point is equivalent to evaluating a transfer - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - match parse_pox_stacking_result(value) { - Ok((stacker, locked_amount, unlock_height)) => { - match StacksChainState::pox_lock_v3( - &mut global_context.database, - &stacker, - locked_amount, - unlock_height as u64, - ) { - Ok(_) => { - let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent( - STXLockEventData { - locked_amount, - unlock_height, - locked_address: stacker, - contract_identifier: boot_code_id(POX_3_NAME, global_context.mainnet), - }, - )); - return Ok(Some(event)); - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)); - } - Err(ChainstateError::PoxAlreadyLocked) => { - // the caller tried to lock tokens into multiple pox contracts - return Err(Error::Runtime(RuntimeErrorType::PoxAlreadyLocked, None)); - } - Err(e) => { - panic!( - "FATAL: failed to lock {} from {} until {}: '{:?}'", - locked_amount, stacker, unlock_height, &e - ); - } - } - } - Err(_) => { - // nothing to do -- the function failed - return Ok(None); - } - } -} - -/// Handle responses from stack-extend and delegate-stack-extend in pox-3 -- functions that *extend -/// already-locked* STX. -fn handle_stack_lockup_extension_pox_v3( - global_context: &mut GlobalContext, - function_name: &str, - value: &Value, -) -> Result> { - // in this branch case, the PoX-3 contract has stored the extension information - // and performed the extension checks. Now, the VM needs to update the account locks - // (because the locks cannot be applied directly from the Clarity code itself) - // applying a pox lock at this point is equivalent to evaluating a transfer - debug!( - "Handle special-case contract-call to {:?} {} (which returned {:?})", - boot_code_id("pox-3", global_context.mainnet), - function_name, - value - ); - - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - if let Ok((stacker, unlock_height)) = parse_pox_extend_result(value) { - match StacksChainState::pox_lock_extend_v3( - &mut global_context.database, - &stacker, - unlock_height as u64, - ) { - Ok(locked_amount) => { - let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent( - STXLockEventData { - locked_amount, - unlock_height, - locked_address: stacker, - contract_identifier: boot_code_id(POX_3_NAME, global_context.mainnet), - }, - )); - return Ok(Some(event)); - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)) - } - Err(e) => { - // Error results *other* than a DefunctPoxContract panic, because - // those errors should have been caught by the PoX contract before - // getting to this code path. - panic!( - "FATAL: failed to extend lock from {} until {}: '{:?}'", - stacker, unlock_height, &e - ); - } - } - } else { - // The stack-extend function returned an error: we do not need to apply a lock - // in this case, and can just return and let the normal VM codepath surface the - // error response type. - return Ok(None); - } -} - -/// Handle responses from stack-increase and delegate-stack-increase in PoX-3 -- functions -/// that *increase already-locked* STX amounts. -fn handle_stack_lockup_increase_pox_v3( - global_context: &mut GlobalContext, - function_name: &str, - value: &Value, -) -> Result> { - // in this branch case, the PoX-3 contract has stored the increase information - // and performed the increase checks. Now, the VM needs to update the account locks - // (because the locks cannot be applied directly from the Clarity code itself) - // applying a pox lock at this point is equivalent to evaluating a transfer - debug!( - "Handle special-case contract-call"; - "contract" => ?boot_code_id("pox-3", global_context.mainnet), - "function" => function_name, - "return-value" => %value, - ); - - runtime_cost( - ClarityCostFunction::StxTransfer, - &mut global_context.cost_track, - 1, - )?; - - if let Ok((stacker, total_locked)) = parse_pox_increase(value) { - match StacksChainState::pox_lock_increase_v3( - &mut global_context.database, - &stacker, - total_locked, - ) { - Ok(new_balance) => { - let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent( - STXLockEventData { - locked_amount: new_balance.amount_locked(), - unlock_height: new_balance.unlock_height(), - locked_address: stacker, - contract_identifier: boot_code_id(POX_3_NAME, global_context.mainnet), - }, - )); - - return Ok(Some(event)); - } - Err(ChainstateError::DefunctPoxContract) => { - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)) - } - Err(e) => { - // Error results *other* than a DefunctPoxContract panic, because - // those errors should have been caught by the PoX contract before - // getting to this code path. - panic!( - "FATAL: failed to increase lock from {}: '{:?}'", - stacker, &e - ); - } - } - } else { - Ok(None) - } -} - -/// Handle special cases when calling into the PoX-3 API contract -fn handle_pox_v3_api_contract_call( - global_context: &mut GlobalContext, - sender_opt: Option<&PrincipalData>, - contract_id: &QualifiedContractIdentifier, - function_name: &str, - args: &[Value], - value: &Value, -) -> Result<()> { - // Generate a synthetic print event for all functions that alter stacking state - let print_event_opt = if let Value::Response(response) = value { - if response.committed { - // method succeeded. Synthesize event info, but default to no event report if we fail - // for some reason. - // Failure to synthesize an event due to a bug is *NOT* an excuse to crash the whole - // network! Event capture is not consensus-critical. - let event_info_opt = match synthesize_pox_2_or_3_event_info( - global_context, - contract_id, - sender_opt, - function_name, - args, - ) { - Ok(Some(event_info)) => Some(event_info), - Ok(None) => None, - Err(e) => { - error!("Failed to synthesize PoX-3 event info: {:?}", &e); - None - } - }; - if let Some(event_info) = event_info_opt { - let event_response = - Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); - let tx_event = - Environment::construct_print_transaction_event(contract_id, &event_response); - Some(tx_event) - } else { - None - } - } else { - None - } - } else { - None - }; - - // Execute function specific logic to complete the lock-up - let lock_event_opt = if function_name == "stack-stx" || function_name == "delegate-stack-stx" { - handle_stack_lockup_pox_v3(global_context, function_name, value)? - } else if function_name == "stack-extend" || function_name == "delegate-stack-extend" { - handle_stack_lockup_extension_pox_v3(global_context, function_name, value)? - } else if function_name == "stack-increase" || function_name == "delegate-stack-increase" { - handle_stack_lockup_increase_pox_v3(global_context, function_name, value)? - } else { - None - }; - - // append the lockup event, so it looks as if the print event happened before the lock-up - if let Some(batch) = global_context.event_batches.last_mut() { - if let Some(print_event) = print_event_opt { - batch.events.push(print_event); - } - if let Some(lock_event) = lock_event_opt { - batch.events.push(lock_event); - } - } - - Ok(()) -} - -/// Is a PoX-1 function read-only? -/// i.e. can we call it without incurring an error? -fn is_pox_v1_read_only(func_name: &str) -> bool { - func_name == "get-pox-rejection" - || func_name == "is-pox-active" - || func_name == "get-stacker-info" - || func_name == "get-reward-set-size" - || func_name == "get-total-ustx-stacked" - || func_name == "get-reward-set-pox-address" - || func_name == "get-stacking-minimum" - || func_name == "can-stack-stx" - || func_name == "minimal-can-stack-stx" - || func_name == "get-pox-info" -} - -fn is_pox_v2_read_only(func_name: &str) -> bool { - "get-pox-rejection" == func_name - || "is-pox-active" == func_name - || "burn-height-to-reward-cycle" == func_name - || "reward-cycle-to-burn-height" == func_name - || "current-pox-reward-cycle" == func_name - || "get-stacker-info" == func_name - || "get-check-delegation" == func_name - || "get-reward-set-size" == func_name - || "next-cycle-rejection-votes" == func_name - || "get-total-ustx-stacked" == func_name - || "get-reward-set-pox-address" == func_name - || "get-stacking-minimum" == func_name - || "check-pox-addr-version" == func_name - || "check-pox-addr-hashbytes" == func_name - || "check-pox-lock-period" == func_name - || "can-stack-stx" == func_name - || "minimal-can-stack-stx" == func_name - || "get-pox-info" == func_name - || "get-delegation-info" == func_name - || "get-allowance-contract-callers" == func_name - || "get-num-reward-set-pox-addresses" == func_name - || "get-partial-stacked-by-cycle" == func_name - || "get-total-pox-rejection" == func_name -} +use clarity::vm::contexts::GlobalContext; +use clarity::vm::errors::Error as ClarityError; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, Value}; /// Handle special cases of contract-calls -- namely, those into PoX that should lock up STX pub fn handle_contract_call_special_cases( global_context: &mut GlobalContext, sender: Option<&PrincipalData>, - _sponsor: Option<&PrincipalData>, + sponsor: Option<&PrincipalData>, contract_id: &QualifiedContractIdentifier, function_name: &str, args: &[Value], result: &Value, -) -> Result<()> { - if *contract_id == boot_code_id(POX_1_NAME, global_context.mainnet) { - if !is_pox_v1_read_only(function_name) - && global_context.database.get_v1_unlock_height() - <= global_context.database.get_current_burnchain_block_height() - { - // NOTE: get-pox-info is read-only, so it can call old pox v1 stuff - warn!("PoX-1 function call attempted on an account after v1 unlock height"; - "v1_unlock_ht" => global_context.database.get_v1_unlock_height(), - "current_burn_ht" => global_context.database.get_current_burnchain_block_height(), - "function_name" => function_name, - "contract_id" => %contract_id - ); - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)); - } - return handle_pox_v1_api_contract_call(global_context, sender, function_name, result); - } else if *contract_id == boot_code_id(POX_2_NAME, global_context.mainnet) { - if !is_pox_v2_read_only(function_name) && global_context.epoch_id >= StacksEpochId::Epoch22 - { - warn!("PoX-2 function call attempted on an account after Epoch 2.2"; - "v2_unlock_ht" => global_context.database.get_v2_unlock_height(), - "current_burn_ht" => global_context.database.get_current_burnchain_block_height(), - "function_name" => function_name, - "contract_id" => %contract_id - ); - return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)); - } - - return handle_pox_v2_api_contract_call( - global_context, - sender, - contract_id, - function_name, - args, - result, - ); - } else if *contract_id == boot_code_id(POX_3_NAME, global_context.mainnet) { - return handle_pox_v3_api_contract_call( - global_context, - sender, - contract_id, - function_name, - args, - result, - ); - } - - // TODO: insert more special cases here, as needed - Ok(()) +) -> Result<(), ClarityError> { + pox_locking::handle_contract_call_special_cases( + global_context, + sender, + sponsor, + contract_id, + function_name, + args, + result, + ) } From 51351d0a4f4672cf0bde4ac262933d4dc77299fc Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 19 Jul 2023 08:46:01 -0500 Subject: [PATCH 2/2] docs: add module documentation to pox-locking --- pox-locking/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pox-locking/src/lib.rs b/pox-locking/src/lib.rs index 17d921fa38..9d8da81482 100644 --- a/pox-locking/src/lib.rs +++ b/pox-locking/src/lib.rs @@ -14,6 +14,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! Special contract-call handling for updating PoX locks on user +//! accounts. +//! +//! This library provides a contract-call special case handler +//! `crate::handle_contract_call_special_cases()` which matches a +//! contract-call result against zero-address published contracts +//! `pox`, `pox-2`, and `pox-3`. For each of those contracts, it +//! checks if the function called requires applying or updating the +//! `STXBalance` struct's locks, and if the function was successfully +//! invoked. If so, it updates the PoX lock. + use clarity::boot_util::boot_code_id; use clarity::vm::contexts::GlobalContext; use clarity::vm::errors::{Error as ClarityError, RuntimeErrorType};