diff --git a/mutation-testing/packages-output/pox-locking/missed.txt b/mutation-testing/packages-output/pox-locking/missed.txt index a9547b63f4..d941cecdeb 100644 --- a/mutation-testing/packages-output/pox-locking/missed.txt +++ b/mutation-testing/packages-output/pox-locking/missed.txt @@ -26,5 +26,23 @@ pox-locking/src/pox_3.rs:87: replace pox_lock_extend_v3 -> Result String with "xyzzy".into() pox-locking/src/pox_2.rs:473: replace handle_contract_call -> Result<(), ClarityError> with Ok(()) pox-locking/src/pox_2.rs:217: replace pox_lock_extend_v2 -> Result with Ok(0) +pox-locking/src/pox_4.rs:42: replace pox_lock_v3 -> Result<(), LockingError> with Ok(()) +pox-locking/src/pox_4.rs:72: replace pox_lock_extend_v3_not_tested -> Result with Ok(0) +pox-locking/src/pox_4.rs:72: replace pox_lock_extend_v3_not_tested -> Result with Ok(1) +pox-locking/src/pox_4.rs:87: replace pox_lock_extend_v3 -> Result with Ok(0) +pox-locking/src/pox_4.rs:87: replace pox_lock_extend_v3 -> Result with Ok(1) +pox-locking/src/pox_4.rs:169: replace handle_stack_lockup_pox_v3 -> Result, ClarityError> with Ok(None) +pox-locking/src/pox_4.rs:236: replace handle_stack_lockup_extension_pox_v3 -> Result, ClarityError> with Ok(None) +pox-locking/src/pox_4.rs:300: replace handle_stack_lockup_increase_pox_v3 -> Result, ClarityError> with Ok(None) +pox-locking/src/pox_4.rs:363: replace handle_contract_call -> Result<(), ClarityError> with Ok(()) pox-locking/src/pox_3.rs:72: replace pox_lock_extend_v3_not_tested -> Result with Ok(0) +pox-locking/src/pox_4_remove.rs:42: replace pox_lock_v3 -> Result<(), LockingError> with Ok(()) pox-locking/src/pox_3.rs:72: replace pox_lock_extend_v3_not_tested -> Result with Ok(1) +pox-locking/src/pox_4_remove.rs:72: replace pox_lock_extend_v3_not_tested -> Result with Ok(0) +pox-locking/src/pox_4_remove.rs:72: replace pox_lock_extend_v3_not_tested -> Result with Ok(1) +pox-locking/src/pox_4_remove.rs:87: replace pox_lock_extend_v3 -> Result with Ok(0) +pox-locking/src/pox_4_remove.rs:87: replace pox_lock_extend_v3 -> Result with Ok(1) +pox-locking/src/pox_4_remove.rs:169: replace handle_stack_lockup_pox_v3 -> Result, ClarityError> with Ok(None) +pox-locking/src/pox_4_remove.rs:236: replace handle_stack_lockup_extension_pox_v3 -> Result, ClarityError> with Ok(None) +pox-locking/src/pox_4_remove.rs:300: replace handle_stack_lockup_increase_pox_v3 -> Result, ClarityError> with Ok(None) +pox-locking/src/pox_4_remove.rs:363: replace handle_contract_call -> Result<(), ClarityError> with Ok(()) diff --git a/mutation-testing/packages-output/pox-locking/unviable.txt b/mutation-testing/packages-output/pox-locking/unviable.txt index f0f8e4328f..c5681493f6 100644 --- a/mutation-testing/packages-output/pox-locking/unviable.txt +++ b/mutation-testing/packages-output/pox-locking/unviable.txt @@ -20,3 +20,11 @@ pox-locking/src/pox_2.rs:64: replace parse_pox_stacking_result -> std::result::R pox-locking/src/pox_1.rs:36: replace parse_pox_stacking_result_v1 -> std::result::Result<(PrincipalData, u128, u64), i128> with Ok((Default::default(), 0, 0)) pox-locking/src/pox_2.rs:345: replace handle_stack_lockup_extension_pox_v2 -> Result, ClarityError> with Ok(Some(Default::default())) pox-locking/src/pox_2.rs:64: replace parse_pox_stacking_result -> std::result::Result<(PrincipalData, u128, u64), i128> with Ok((Default::default(), 1, 1)) +pox-locking/src/pox_4.rs:123: replace pox_lock_increase_v3 -> Result with Ok(Default::default()) +pox-locking/src/pox_4.rs:169: replace handle_stack_lockup_pox_v3 -> Result, ClarityError> with Ok(Some(Default::default())) +pox-locking/src/pox_4.rs:236: replace handle_stack_lockup_extension_pox_v3 -> Result, ClarityError> with Ok(Some(Default::default())) +pox-locking/src/pox_4.rs:300: replace handle_stack_lockup_increase_pox_v3 -> Result, ClarityError> with Ok(Some(Default::default())) +pox-locking/src/pox_4_remove.rs:123: replace pox_lock_increase_v3 -> Result with Ok(Default::default()) +pox-locking/src/pox_4_remove.rs:169: replace handle_stack_lockup_pox_v3 -> Result, ClarityError> with Ok(Some(Default::default())) +pox-locking/src/pox_4_remove.rs:236: replace handle_stack_lockup_extension_pox_v3 -> Result, ClarityError> with Ok(Some(Default::default())) +pox-locking/src/pox_4_remove.rs:300: replace handle_stack_lockup_increase_pox_v3 -> Result, ClarityError> with Ok(Some(Default::default())) diff --git a/mutation-testing/scripts/git-diff.sh b/mutation-testing/scripts/git-diff.sh index 90bf10c0c4..256db10290 100644 --- a/mutation-testing/scripts/git-diff.sh +++ b/mutation-testing/scripts/git-diff.sh @@ -1,8 +1,23 @@ # script that makes .git for the differences # it saves the .git on scripts folder -# run from scripts folder -cd .. +# add untracked files to git diff +# go to root folder +cd ./../.. + +# run git status on root +untracked_files=($(git ls-files --others --exclude-standard)) + +# for each file untracked -> run git add file path +echo "${untracked_files[@]}" +for file in "${untracked_files[@]}"; do + git add -N "$file" +done + +cd mutation-testing + + +# run from mutation-testing folder git diff > git.diff # it runs cargo mutants for those specific changed functions and outputs to /temp/mutants.out diff --git a/pox-locking/src/lib.rs b/pox-locking/src/lib.rs index b195f4cc9b..05db2adb1f 100644 --- a/pox-locking/src/lib.rs +++ b/pox-locking/src/lib.rs @@ -38,6 +38,7 @@ mod events; mod pox_1; mod pox_2; mod pox_3; +mod pox_4_remove; #[derive(Debug)] pub enum LockingError { diff --git a/pox-locking/src/pox_3.rs b/pox-locking/src/pox_3.rs index cccfbb2644..c3a0bc109e 100644 --- a/pox-locking/src/pox_3.rs +++ b/pox-locking/src/pox_3.rs @@ -65,6 +65,14 @@ pub fn pox_lock_v3( Ok(()) } +pub fn pox_lock_extend_v3_not_tested( + db: &mut ClarityDatabase, + principal: &PrincipalData, + unlock_burn_height: u64, +) -> Result { + Ok(1231) +} + /// Extend a STX lock up for PoX for a time. Does NOT touch the account nonce. /// Returns Ok(lock_amount) when successful /// diff --git a/pox-locking/src/pox_4_remove.rs b/pox-locking/src/pox_4_remove.rs new file mode 100644 index 0000000000..c3a0bc109e --- /dev/null +++ b/pox-locking/src/pox_4_remove.rs @@ -0,0 +1,423 @@ +// 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, RuntimeErrorType}; +use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use clarity::vm::{Environment, Value}; +use slog::{slog_debug, slog_error}; +use stacks_common::{debug, error}; + +use crate::events::synthesize_pox_2_or_3_event_info; +// 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}; +use crate::{LockingError, POX_3_NAME}; + +/////////////////////// 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(()) +} + +pub fn pox_lock_extend_v3_not_tested( + db: &mut ClarityDatabase, + principal: &PrincipalData, + unlock_burn_height: u64, +) -> Result { + Ok(1231) +} + +/// 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 +#[allow(clippy::needless_return)] +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, + ) { + 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. +#[allow(clippy::needless_return)] +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) { + 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. +#[allow(clippy::needless_return)] +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(()) +}