From 1e9cfefa79456efe4c1aab38a0b729342eb16947 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 30 Sep 2020 13:20:24 -0500 Subject: [PATCH 01/26] feat: accurate CheckError expressions in ReadOnlyChecker, enable `-` input in `clarity_cli check` --- src/clarity.rs | 17 +++++++++++++---- src/vm/analysis/errors.rs | 2 +- src/vm/analysis/read_only_checker/mod.rs | 5 ++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index 08fd8381a3e..c2e0f940535 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -540,10 +540,19 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { let contract_id = QualifiedContractIdentifier::transient(); - let content: String = friendly_expect( - fs::read_to_string(&args[1]), - &format!("Error reading file: {}", args[1]), - ); + let content: String = if &args[1] == "-" { + let mut buffer = String::new(); + friendly_expect( + io::stdin().read_to_string(&mut buffer), + "Error reading from stdin.", + ); + buffer + } else { + friendly_expect( + fs::read_to_string(&args[1]), + &format!("Error reading file: {}", args[1]), + ) + }; let mut ast = friendly_expect(parse(&contract_id, &content), "Failed to parse program"); diff --git a/src/vm/analysis/errors.rs b/src/vm/analysis/errors.rs index a09509f0f2e..4c1c75fc757 100644 --- a/src/vm/analysis/errors.rs +++ b/src/vm/analysis/errors.rs @@ -184,7 +184,7 @@ impl CheckError { self.expressions.replace(vec![expr.clone()]); } - pub fn set_expressions(&mut self, exprs: Vec) { + pub fn set_expressions(&mut self, exprs: &[SymbolicExpression]) { self.diagnostic.spans = exprs.iter().map(|e| e.span.clone()).collect(); self.expressions.replace(exprs.clone().to_vec()); } diff --git a/src/vm/analysis/read_only_checker/mod.rs b/src/vm/analysis/read_only_checker/mod.rs index aa65f30ce30..4c3b04343ca 100644 --- a/src/vm/analysis/read_only_checker/mod.rs +++ b/src/vm/analysis/read_only_checker/mod.rs @@ -303,7 +303,10 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { .match_atom() .ok_or(CheckErrors::NonFunctionApplication)?; - if let Some(result) = self.try_native_function_check(function_name, args) { + if let Some(mut result) = self.try_native_function_check(function_name, args) { + if let Err(ref mut check_err) = result { + check_err.set_expressions(expression); + } result } else { let is_function_read_only = self From a384fc27d079762d2ab6eaa74692945f6353b9ca Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 30 Sep 2020 13:33:45 -0500 Subject: [PATCH 02/26] first pass on pox pooling/delegation --- src/chainstate/stacks/boot/pox.clar | 305 +++++++++++++++++++++------- 1 file changed, 229 insertions(+), 76 deletions(-) diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index 32afbb55f3a..9815bb747af 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -14,6 +14,7 @@ (define-constant ERR_STACKING_ALREADY_REJECTED 17) (define-constant ERR_STACKING_INVALID_AMOUNT 18) (define-constant ERR_NOT_ALLOWED 19) +(define-constant ERR_STACKING_ALREADY_DELEGATED 20) ;; PoX disabling threshold (a percent) (define-constant POX_REJECTION_FRACTION u25) @@ -64,6 +65,22 @@ ) ) +;; Delegation relationships +(define-map delegation-state + ((stacker principal)) + ((amount-ustx uint) ;; how many uSTX delegated? + (delegated-to principal) ;; who are we delegating? + (until-burn-ht (optional uint)) ;; how long does the delegation last? + ;; does the delegator _need_ to use a specific + ;; pox recipient address? + (pox-addr (optional { version: (buff 1), + hashbytes: (buff 20) })))) + +;; Delegator's allowed contract-callers +(define-map delegator-contract-callers + ((delegator principal) (contract-caller principal)) + ((until-burn-ht (optional uint)))) + ;; How many uSTX are stacked in a given reward cycle. ;; Updated when a new PoX address is registered, or when more STX are granted ;; to it. @@ -87,16 +104,15 @@ ((len uint)) ) -;; When is a PoX address active? -;; Used to check of a PoX address is registered or not in a given -;; reward cycle. -(define-map pox-addr-reward-cycles - ((pox-addr (tuple (version (buff 1)) (hashbytes (buff 20))))) - ( - (first-reward-cycle uint) - (num-cycles uint) - ) -) +;; how much has been locked up for this address before +;; committing? +;; this map allows stackers to stack amounts < minimum +;; by paying the cost of aggregation during the commit +(define-map partial-stacked-by-cycle + ((pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (reward-cycle uint) + (sender principal)) + ((stacked-amount uint))) ;; Amount of uSTX that reject PoX, by reward cycle (define-map stacking-rejection @@ -153,8 +169,33 @@ ) ;; no state at all none - ) -) + )) + +(define-private (check-delegation-caller-allowed) + (or (is-eq tx-sender contract-caller) + (let ((delegation-caller-allowed + ;; if not in the delegation/caller map, return false + (unwrap! (map-get? delegator-contract-callers + { delegator: tx-sender, contract-caller: contract-caller }) + false))) + ;; is the caller allowance expired? + (if (< burn-block-height (unwrap! (get until-burn-ht delegation-caller-allowed) true)) + (begin (map-delete delegator-contract-callers + { delegator: tx-sender, contract-caller: contract-caller }) + false) + true)))) + +(define-private (get-check-delegation (stacker principal)) + (let ((delegation-info (try! (map-get? delegation-state { stacker: stacker })))) + ;; did the existing delegation expire? + (if (match (get until-burn-ht delegation-info) + until-burn-ht (> burn-block-height until-burn-ht) + false) + ;; it expired, delete the entry and return none + (begin (map-delete delegation-state { stacker: stacker }) + none) + ;; delegation is active + (some delegation-info)))) ;; Get the size of the reward set for a reward cycle. ;; Note that this does _not_ return duplicate PoX addresses. @@ -167,26 +208,6 @@ u0 (get len (map-get? reward-cycle-pox-address-list-len { reward-cycle: reward-cycle })))) -;; Is a PoX address registered anywhere in a given range of reward cycles? -;; Checkes the integer range [reward-cycle-start, reward-cycle-start + num-cycles) -;; Returns true if it's registered in at least one reward cycle in the given range. -;; Returns false if it's not registered in any reward cycle in the given range. -(define-private (is-pox-addr-registered (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) - (reward-cycle-start uint) - (num-cycles uint)) - (let ( - (pox-addr-range-opt (map-get? pox-addr-reward-cycles { pox-addr: pox-addr })) - ) - (match pox-addr-range-opt - ;; some range - pox-addr-range - (and (>= (+ reward-cycle-start num-cycles) (get first-reward-cycle pox-addr-range)) - (< reward-cycle-start (+ (get first-reward-cycle pox-addr-range) (get num-cycles pox-addr-range)))) - ;; none - false - )) -) - ;; Add a single PoX address to a single reward cycle. ;; Used to build up a set of per-reward-cycle PoX addresses. ;; No checking will be done -- don't call if this PoX address is already registered in this reward cycle! @@ -230,13 +251,14 @@ (amount-ustx uint) (i uint)))) (let ((reward-cycle (+ (get first-reward-cycle params) (get i params))) + (num-cycles (get num-cycles params)) (i (get i params))) { pox-addr: (get pox-addr params), first-reward-cycle: (get first-reward-cycle params), - num-cycles: (get num-cycles params), + num-cycles: num-cycles, amount-ustx: (get amount-ustx params), - i: (if (< i (get num-cycles params)) + i: (if (< i num-cycles) (let ((total-ustx (get-total-ustx-stacked reward-cycle))) ;; record how many uSTX this pox-addr will stack for in the given reward cycle (append-reward-cycle-pox-addr @@ -257,30 +279,58 @@ ;; Add a PoX address to a given sequence of reward cycle lists. ;; A PoX address can be added to at most 12 consecutive cycles. ;; No checking is done. -;; Returns the number of reward cycles added (define-private (add-pox-addr-to-reward-cycles (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) (first-reward-cycle uint) (num-cycles uint) (amount-ustx uint)) - (let ( - (cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11)) - ) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) ;; For safety, add up the number of times (add-principal-to-ith-reward-cycle) returns 1. ;; It _should_ be equal to num-cycles. (asserts! - (is-eq num-cycles (get i - (fold add-pox-addr-to-ith-reward-cycle cycle-indexes - { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) - (err ERR_STACKING_UNREACHABLE)) - - ;; mark address in use over this range - (map-set pox-addr-reward-cycles - { pox-addr: pox-addr } - { first-reward-cycle: first-reward-cycle, num-cycles: num-cycles } - ) - (ok true) - ) -) + (is-eq num-cycles + (get i (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) + (err ERR_STACKING_UNREACHABLE)) + (ok true))) + +(define-private (add-pox-partial-stacked-to-ith-cycle + (cycle-index uint) + (params { pox-addr: { version: (buff 1), hashbytes: (buff 20) }, + reward-cycle: uint, + num-cycles: uint, + amount-ustx: uint })) + (let ((pox-addr (get pox-addr params)) + (num-cycles (get num-cycles params)) + (reward-cycle (get reward-cycle params)) + (amount-ustx (get amount-ustx params))) + (let ((current-amount + (default-to u0 + (get stacked-amount + (map-get? partial-stacked-by-cycle { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle }))))) + (if (>= cycle-index num-cycles) + ;; do not add to cycles >= cycle-index + false + ;; otherwise, add to the partial-stacked-by-cycle + (map-set partial-stacked-by-cycle + { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle } + { stacked-amount: (+ amount-ustx current-amount) })) + ;; produce the next params tuple + { pox-addr: pox-addr, + reward-cycle: (+ u1 reward-cycle), + num-cycles: num-cycles, + amount-ustx: amount-ustx }))) + +;; Add a PoX address to a given sequence of partial reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-partial-stacked (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + (fold add-pox-partial-stacked-to-ith-cycle cycle-indexes + { pox-addr: pox-addr, reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx }) + (ok true))) ;; What is the minimum number of uSTX to be stacked in the given reward cycle? ;; Used internally by the Stacks node, and visible publicly. @@ -308,37 +358,44 @@ (amount-ustx uint) (first-reward-cycle uint) (num-cycles uint)) - (let ((is-registered (is-pox-addr-registered pox-addr first-reward-cycle num-cycles))) - ;; amount must be valid - (asserts! (> amount-ustx u0) - (err ERR_STACKING_INVALID_AMOUNT)) + (begin + ;; minimum uSTX must be met + (asserts! (<= (get-stacking-minimum) amount-ustx) + (err ERR_STACKING_THRESHOLD_NOT_MET)) - ;; tx-sender principal must not have rejected in this upcoming reward cycle - (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle)) - (err ERR_STACKING_ALREADY_REJECTED)) + (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles))) - ;; can't be registered yet - (asserts! (not is-registered) - (err ERR_STACKING_POX_ADDRESS_IN_USE)) +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (minimal-can-stack-stx + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; amount must be valid + (asserts! (> amount-ustx u0) + (err ERR_STACKING_INVALID_AMOUNT)) - ;; the Stacker must have sufficient unlocked funds - (asserts! (>= (stx-get-balance tx-sender) amount-ustx) - (err ERR_STACKING_INSUFFICIENT_FUNDS)) + ;; tx-sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) - ;; minimum uSTX must be met - (asserts! (<= (get-stacking-minimum) amount-ustx) - (err ERR_STACKING_THRESHOLD_NOT_MET)) + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) - ;; lock period must be in acceptable range. - (asserts! (check-pox-lock-period num-cycles) - (err ERR_STACKING_INVALID_LOCK_PERIOD)) + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) - ;; address version must be valid - (asserts! (check-pox-addr-version (get version pox-addr)) - (err ERR_STACKING_INVALID_POX_ADDRESS)) + ;; address version must be valid + (asserts! (check-pox-addr-version (get version pox-addr)) + (err ERR_STACKING_INVALID_POX_ADDRESS)) + (ok true))) - (ok true)) -) ;; Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX). ;; The STX will be locked for the given number of reward cycles (lock-period). @@ -357,10 +414,18 @@ ;; this stacker's first reward cycle is the _next_ reward cycle (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle)))) + ;; must be called directly by the tx-sender + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; tx-sender principal must not be stacking (asserts! (is-none (get-stacker-info tx-sender)) (err ERR_STACKING_ALREADY_STACKED)) + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + ;; ensure that stacking can be performed (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) @@ -380,6 +445,94 @@ (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })) ) +;; Commit partially stacked STX. +;; this allows a stacker/delegator to lock fewer STX than the minimal threshold in multiple transactions, +;; so long as the pox-addr is the same, and then this "commit" transaction is called _before_ the PoX anchor block +;; this ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, +;; but does not require it be all locked up within a single transaction +(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (reward-cycle uint)) + (let ((partial-stacked + ;; fetch the partial commitments + (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (err ERR_STACKING_NO_SUCH_PRINCIPAL)))) + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-delegation-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (let ((amount-ustx (get stacked-amount partial-stacked))) + (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1)) + ;; add the pox addr to the reward cycle + (add-pox-addr-to-ith-reward-cycle + u0 + { pox-addr: pox-addr, + first-reward-cycle: reward-cycle, + num-cycles: u1, + amount-ustx: amount-ustx, + i: u0 + }) + ;; don't update the stacking-state map, + ;; because it _already has_ this stacker's state + ;; don't lock the STX, because the STX is already locked + ;; + ;; clear the partial-stacked state + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (ok true)))) + +;; As a delegator, stack the given principal's STX using partial-stacked-by-cycle +;; Once the delegator has stacked > minimum, the delegator should call the stack-aggregation-commit +(define-public (delegator-stack-stx (stacker principal) + (amount-ustx uint) + (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-delegation-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must have delegated to the caller + (asserts! + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) + ;; must have delegated to tx-sender + (and (is-eq (get delegated-to delegation-info) tx-sender) + ;; must have delegated enough stx + (>= (get amount-ustx delegation-info) amount-ustx) + ;; if pox-addr is set, must be equal to pox-addr + (match (get pox-addr delegation-info) + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + ;; delegation must not expire before lock period + (match (get until-burn-ht delegation-info) + until-burn-ht (>= (+ u1 until-burn-ht) + (reward-cycle-to-burn-height unlock-burn-height)) + true))) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker principal must not be stacking + (asserts! (is-none (get-stacker-info stacker)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; ensure that stacking can be performed + (try! (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked + (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + + ;; add stacker record + (map-set stacking-state + { stacker: stacker } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: stacker, + lock-amount: amount-ustx, + unlock-burn-height: unlock-burn-height }))) + ;; Reject Stacking for this reward cycle. ;; tx-sender votes all its uSTX for rejection. ;; Note that unlike PoX, rejecting PoX does not lock the tx-sender's @@ -428,4 +581,4 @@ reward-cycle-length: (var-get pox-reward-cycle-length), rejection-fraction: (var-get pox-rejection-fraction) }) -) \ No newline at end of file +) From 7ea2898f1a40439df42735e8da4a041acf286469 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 30 Sep 2020 16:27:27 -0500 Subject: [PATCH 03/26] add contract-caller allowances to stack-stx, update boot::tests with allowances and multiple entries for PoX addrs, DRY test tx generation --- src/chainstate/stacks/boot/mod.rs | 381 +++++++++++++-------------- src/chainstate/stacks/boot/pox.clar | 52 ++-- src/chainstate/stacks/transaction.rs | 6 +- 3 files changed, 213 insertions(+), 226 deletions(-) diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 89bf410f484..41df22572e7 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -513,34 +513,50 @@ pub mod test { // (define-public (stack-stx (amount-ustx uint) // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) // (lock-period uint)) + make_pox_contract_call( + key, + nonce, + "stack-stx", + vec![ + Value::UInt(amount), + make_pox_addr(addr_version, addr_bytes), + Value::UInt(lock_period), + ], + ) + } + fn make_tx( + key: &StacksPrivateKey, + nonce: u64, + fee_rate: u64, + payload: TransactionPayload, + ) -> StacksTransaction { let auth = TransactionAuth::from_p2pkh(key).unwrap(); let addr = auth.origin().address_testnet(); - let mut pox_lockup = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::new_contract_call( - boot_code_addr(), - "pox", - "stack-stx", - vec![ - Value::UInt(amount), - make_pox_addr(addr_version, addr_bytes), - Value::UInt(lock_period), - ], - ) - .unwrap(), - ); - pox_lockup.chain_id = 0x80000000; - pox_lockup.auth.set_origin_nonce(nonce); - pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow); - pox_lockup.set_fee_rate(0); + let mut tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload); + tx.chain_id = 0x80000000; + tx.auth.set_origin_nonce(nonce); + tx.set_post_condition_mode(TransactionPostConditionMode::Allow); + tx.set_fee_rate(fee_rate); - let mut tx_signer = StacksTransactionSigner::new(&pox_lockup); + let mut tx_signer = StacksTransactionSigner::new(&tx); tx_signer.sign_origin(key).unwrap(); tx_signer.get_tx().unwrap() } + fn make_pox_contract_call( + key: &StacksPrivateKey, + nonce: u64, + function_name: &str, + args: Vec, + ) -> StacksTransaction { + let payload = + TransactionPayload::new_contract_call(boot_code_addr(), "pox", function_name, args) + .unwrap(); + + make_tx(key, nonce, 0, payload) + } + // make a stream of invalid pox-lockup transactions fn make_invalid_pox_lockups(key: &StacksPrivateKey, mut nonce: u64) -> Vec { let mut ret = vec![]; @@ -566,27 +582,12 @@ pub mod test { ); let generator = |amount, pox_addr, lock_period, nonce| { - let auth = TransactionAuth::from_p2pkh(key).unwrap(); - let addr = auth.origin().address_testnet(); - let mut pox_lockup = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::new_contract_call( - boot_code_addr(), - "pox", - "stack-stx", - vec![Value::UInt(amount), pox_addr, Value::UInt(lock_period)], - ) - .unwrap(), - ); - pox_lockup.chain_id = 0x80000000; - pox_lockup.auth.set_origin_nonce(nonce); - pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow); - pox_lockup.set_fee_rate(0); - - let mut tx_signer = StacksTransactionSigner::new(&pox_lockup); - tx_signer.sign_origin(key).unwrap(); - tx_signer.get_tx().unwrap() + make_pox_contract_call( + key, + nonce, + "stack-stx", + vec![Value::UInt(amount), pox_addr, Value::UInt(lock_period)], + ) }; let bad_pox_addr_tx = generator(amount, bad_pox_addr_version, lock_period, nonce); @@ -629,21 +630,8 @@ pub mod test { name: &str, code: &str, ) -> StacksTransaction { - let auth = TransactionAuth::from_p2pkh(key).unwrap(); - let addr = auth.origin().address_testnet(); - let mut bare_code = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::new_smart_contract(&name.to_string(), &code.to_string()).unwrap(), - ); - bare_code.chain_id = 0x80000000; - bare_code.auth.set_origin_nonce(nonce); - bare_code.set_post_condition_mode(TransactionPostConditionMode::Allow); - bare_code.set_fee_rate(fee_rate); - - let mut tx_signer = StacksTransactionSigner::new(&bare_code); - tx_signer.sign_origin(key).unwrap(); - tx_signer.get_tx().unwrap() + let payload = TransactionPayload::new_smart_contract(name, code).unwrap(); + make_tx(key, nonce, fee_rate, payload) } fn make_token_transfer( @@ -653,23 +641,8 @@ pub mod test { dest: PrincipalData, amount: u64, ) -> StacksTransaction { - let auth = TransactionAuth::from_p2pkh(key).unwrap(); - let addr = auth.origin().address_testnet(); - - let mut txn = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::TokenTransfer(dest, amount, TokenTransferMemo([0u8; 34])), - ); - - txn.chain_id = 0x80000000; - txn.auth.set_origin_nonce(nonce); - txn.set_post_condition_mode(TransactionPostConditionMode::Allow); - txn.set_fee_rate(fee_rate); - - let mut tx_signer = StacksTransactionSigner::new(&txn); - tx_signer.sign_origin(key).unwrap(); - tx_signer.get_tx().unwrap() + let payload = TransactionPayload::TokenTransfer(dest, amount, TokenTransferMemo([0u8; 34])); + make_tx(key, nonce, fee_rate, payload) } fn make_pox_lockup_contract( @@ -719,31 +692,18 @@ pub mod test { addr_bytes: Hash160, lock_period: u128, ) -> StacksTransaction { - let auth = TransactionAuth::from_p2pkh(key).unwrap(); - let addr = auth.origin().address_testnet(); - let mut pox_lockup = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::new_contract_call( - contract_addr.clone(), - name, - "do-contract-lockup", - vec![ - Value::UInt(amount), - make_pox_addr(addr_version, addr_bytes), - Value::UInt(lock_period), - ], - ) - .unwrap(), - ); - pox_lockup.chain_id = 0x80000000; - pox_lockup.auth.set_origin_nonce(nonce); - pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow); - pox_lockup.set_fee_rate(0); - - let mut tx_signer = StacksTransactionSigner::new(&pox_lockup); - tx_signer.sign_origin(key).unwrap(); - tx_signer.get_tx().unwrap() + let payload = TransactionPayload::new_contract_call( + contract_addr.clone(), + name, + "do-contract-lockup", + vec![ + Value::UInt(amount), + make_pox_addr(addr_version, addr_bytes), + Value::UInt(lock_period), + ], + ) + .unwrap(); + make_tx(key, nonce, 0, payload) } // call after make_pox_lockup_contract gets mined @@ -754,49 +714,19 @@ pub mod test { name: &str, amount: u128, ) -> StacksTransaction { - let auth = TransactionAuth::from_p2pkh(key).unwrap(); - let addr = auth.origin().address_testnet(); - let mut pox_lockup = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::new_contract_call( - contract_addr.clone(), - name, - "withdraw-stx", - vec![Value::UInt(amount)], - ) - .unwrap(), - ); - pox_lockup.chain_id = 0x80000000; - pox_lockup.auth.set_origin_nonce(nonce); - pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow); - pox_lockup.set_fee_rate(0); - - let mut tx_signer = StacksTransactionSigner::new(&pox_lockup); - tx_signer.sign_origin(key).unwrap(); - tx_signer.get_tx().unwrap() + let payload = TransactionPayload::new_contract_call( + contract_addr.clone(), + name, + "withdraw-stx", + vec![Value::UInt(amount)], + ) + .unwrap(); + make_tx(key, nonce, 0, payload) } fn make_pox_reject(key: &StacksPrivateKey, nonce: u64) -> StacksTransaction { // (define-public (reject-pox)) - let auth = TransactionAuth::from_p2pkh(key).unwrap(); - let addr = auth.origin().address_testnet(); - - let mut tx = StacksTransaction::new( - TransactionVersion::Testnet, - auth, - TransactionPayload::new_contract_call(boot_code_addr(), "pox", "reject-pox", vec![]) - .unwrap(), - ); - - tx.chain_id = 0x80000000; - tx.auth.set_origin_nonce(nonce); - tx.set_post_condition_mode(TransactionPostConditionMode::Allow); - tx.set_fee_rate(0); - - let mut tx_signer = StacksTransactionSigner::new(&tx); - tx_signer.sign_origin(key).unwrap(); - tx_signer.get_tx().unwrap() + make_pox_contract_call(key, nonce, "reject-pox", vec![]) } fn get_reward_addresses_with_par_tip( @@ -943,23 +873,31 @@ pub mod test { } if tenure_id == 8 { // alice locks 512_000_000 STX through her contract - let auth = TransactionAuth::from_p2pkh(&alice).unwrap(); - let addr = auth.origin().address_testnet(); - let mut contract_call = StacksTransaction::new(TransactionVersion::Testnet, auth, - TransactionPayload::new_contract_call(key_to_stacks_addr(&alice), - "nested-stacker", - "nested-stack-stx", - vec![]).unwrap()); - contract_call.chain_id = 0x80000000; - contract_call.auth.set_origin_nonce(2); - contract_call.set_post_condition_mode(TransactionPostConditionMode::Allow); - contract_call.set_fee_rate(0); - - let mut tx_signer = StacksTransactionSigner::new(&contract_call); - tx_signer.sign_origin(&alice).unwrap(); - let tx = tx_signer.get_tx().unwrap(); + let cc_payload = TransactionPayload::new_contract_call(key_to_stacks_addr(&alice), + "nested-stacker", + "nested-stack-stx", + vec![]).unwrap(); + let tx = make_tx(&alice, 2, 0, cc_payload.clone()); block_txs.push(tx); + + // the above tx _should_ error, because alice hasn't authorized that contract to stack + // try again with auth -> deauth -> auth + let alice_contract: Value = contract_id(&key_to_stacks_addr(&alice), "nested-stacker").into(); + + let alice_allowance = make_pox_contract_call(&alice, 3, "allow-contract-caller", vec![alice_contract.clone(), Value::none()]); + let alice_disallowance = make_pox_contract_call(&alice, 4, "disallow-contract-caller", vec![alice_contract.clone()]); + block_txs.push(alice_allowance); + block_txs.push(alice_disallowance); + + let tx = make_tx(&alice, 5, 0, cc_payload.clone()); + block_txs.push(tx); + + let alice_allowance = make_pox_contract_call(&alice, 6, "allow-contract-caller", vec![alice_contract.clone(), Value::none()]); + let tx = make_tx(&alice, 7, 0, cc_payload.clone()); // should be allowed! + block_txs.push(alice_allowance); + block_txs.push(tx); + } let block_builder = StacksBlockBuilder::make_block_builder(&parent_tip, vrf_proof, tip.total_burn, microblock_pubkeyhash).unwrap(); @@ -1794,55 +1732,60 @@ pub mod test { // will be rejected let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12); block_txs.push(alice_lockup_2); + + // let's make some allowances for contract-calls through smart contracts + // so that the tests in tenure_id == 3 don't just fail on permission checks + let alice_test = contract_id(&key_to_stacks_addr(&alice), "alice-test").into(); + let alice_allowance = make_pox_contract_call(&alice, 2, "allow-contract-caller", vec![alice_test, Value::none()]); + + let bob_test = contract_id(&key_to_stacks_addr(&bob), "bob-test").into(); + let bob_allowance = make_pox_contract_call(&bob, 0, "allow-contract-caller", vec![bob_test, Value::none()]); + + let charlie_test = contract_id(&key_to_stacks_addr(&charlie), "charlie-test").into(); + let charlie_allowance = make_pox_contract_call(&charlie, 0, "allow-contract-caller", vec![charlie_test, Value::none()]); + + block_txs.push(alice_allowance); + block_txs.push(bob_allowance); + block_txs.push(charlie_allowance); } if tenure_id == 2 { - // should fail -- Alice's PoX address is already in use, so Bob can't use it. - let bob_test_tx = make_bare_contract(&bob, 0, 0, "bob-test", &format!( - "(define-data-var bob-test-run bool false) - (let ( - (res - (contract-call? '{}.pox stack-stx u256000000 (tuple (version 0x00) (hashbytes 0xae1593226f85e49a7eaff5b633ff687695438cc9)) u12)) - ) - (begin - (asserts! (is-eq (err 12) res) - (err res)) - - (var-set bob-test-run true) - )) + // should pass -- there's no problem with Bob adding more stacking power to Alice's PoX address + let bob_test_tx = make_bare_contract(&bob, 1, 0, "bob-test", &format!( + "(define-data-var test-run bool false) + (define-data-var test-result int -1) + (let ((result + (contract-call? '{}.pox stack-stx u256000000 (tuple (version 0x00) (hashbytes 0xae1593226f85e49a7eaff5b633ff687695438cc9)) u12))) + (var-set test-result + (match result ok_value -1 err_value err_value)) + (var-set test-run true)) ", STACKS_BOOT_CODE_CONTRACT_ADDRESS)); block_txs.push(bob_test_tx); // should fail -- Alice has already stacked. - let alice_test_tx = make_bare_contract(&alice, 2, 0, "alice-test", &format!( - "(define-data-var alice-test-run bool false) - (let ( - (res - (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) u12)) - ) - (begin - (asserts! (is-eq (err 3) res) - (err res)) - - (var-set alice-test-run true) - )) + // expect err 3 + let alice_test_tx = make_bare_contract(&alice, 3, 0, "alice-test", &format!( + "(define-data-var test-run bool false) + (define-data-var test-result int -1) + (let ((result + (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) u12))) + (var-set test-result + (match result ok_value -1 err_value err_value)) + (var-set test-run true)) ", STACKS_BOOT_CODE_CONTRACT_ADDRESS)); block_txs.push(alice_test_tx); // should fail -- Charlie doesn't have enough uSTX - let charlie_test_tx = make_bare_contract(&charlie, 0, 0, "charlie-test", &format!( - "(define-data-var charlie-test-run bool false) - (let ( - (res - (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) u12)) - ) - (begin - (asserts! (is-eq (err 1) res) - (err res)) - - (var-set charlie-test-run true) - )) + // expect err 1 + let charlie_test_tx = make_bare_contract(&charlie, 1, 0, "charlie-test", &format!( + "(define-data-var test-run bool false) + (define-data-var test-result int -1) + (let ((result + (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) u12))) + (var-set test-result + (match result ok_value -1 err_value err_value)) + (var-set test-run true)) ", STACKS_BOOT_CODE_CONTRACT_ADDRESS)); block_txs.push(charlie_test_tx); @@ -1909,19 +1852,42 @@ pub mod test { &mut peer, &key_to_stacks_addr(&alice), "alice-test", - "(var-get alice-test-run)", + "(var-get test-run)", + ); + let bob_test_result = eval_contract_at_tip( + &mut peer, + &key_to_stacks_addr(&bob), + "bob-test", + "(var-get test-run)", + ); + let charlie_test_result = eval_contract_at_tip( + &mut peer, + &key_to_stacks_addr(&charlie), + "charlie-test", + "(var-get test-run)", + ); + + assert!(alice_test_result.expect_bool()); + assert!(bob_test_result.expect_bool()); + assert!(charlie_test_result.expect_bool()); + + let alice_test_result = eval_contract_at_tip( + &mut peer, + &key_to_stacks_addr(&alice), + "alice-test", + "(var-get test-result)", ); let bob_test_result = eval_contract_at_tip( &mut peer, &key_to_stacks_addr(&bob), "bob-test", - "(var-get bob-test-run)", + "(var-get test-result)", ); let charlie_test_result = eval_contract_at_tip( &mut peer, &key_to_stacks_addr(&charlie), "charlie-test", - "(var-get charlie-test-run)", + "(var-get test-result)", ); eprintln!( @@ -1929,9 +1895,9 @@ pub mod test { &alice_test_result, &bob_test_result, &charlie_test_result ); - assert!(alice_test_result.expect_bool()); - assert!(bob_test_result.expect_bool()); - assert!(charlie_test_result.expect_bool()); + assert_eq!(bob_test_result, Value::Int(-1)); + assert_eq!(alice_test_result, Value::Int(3)); + assert_eq!(charlie_test_result, Value::Int(1)); } } } @@ -3121,18 +3087,26 @@ pub mod test { // anything). let bob_reject = make_pox_reject(&bob, 0); block_txs.push(bob_reject); - } - else if tenure_id == 2 { + } else if tenure_id == 2 { // Charlie rejects + // this _should_ be included in the block let charlie_reject = make_pox_reject(&charlie, 0); block_txs.push(charlie_reject); + // allowance for the contract-caller + // this _should_ be included in the block + let charlie_contract: Value = contract_id(&key_to_stacks_addr(&charlie), "charlie-try-stack").into(); + let charlie_allowance = make_pox_contract_call(&charlie, 1, "allow-contract-caller", + vec![charlie_contract, Value::none()]); + block_txs.push(charlie_allowance); + // Charlie tries to stack, but it should fail. // Specifically, (stack-stx) should fail with (err 17). // If it's the case, then this tx will NOT be mined. - let charlie_stack = make_bare_contract(&charlie, 1, 0, "charlie-try-stack", + // Note: this behavior is a bug in the miner and block processor: see issue #? + let charlie_stack = make_bare_contract(&charlie, 2, 0, "charlie-try-stack", &format!( - "(asserts! (not (is-eq (contract-call? '{}.pox stack-stx u1 {{ version: 0x01, hashbytes: 0x1111111111111111111111111111111111111111 }} u1) (err 17))) (err 1))", + "(asserts! (not (is-eq (print (contract-call? '{}.pox stack-stx u1 {{ version: 0x01, hashbytes: 0x1111111111111111111111111111111111111111 }} u1)) (err 17))) (err 1))", boot_code_addr())); block_txs.push(charlie_stack); @@ -3143,7 +3117,7 @@ pub mod test { // If it's the case, then this tx will NOT be mined let alice_reject = make_bare_contract(&alice, 1, 0, "alice-try-reject", &format!( - "(asserts! (not (is-eq (contract-call? '{}.pox reject-pox) (err 3))) (err 1))", + "(asserts! (not (is-eq (print (contract-call? '{}.pox reject-pox)) (err 3))) (err 1))", boot_code_addr())); block_txs.push(alice_reject); @@ -3151,9 +3125,9 @@ pub mod test { // Charlie tries to reject again, but it should fail. // Specifically, (reject-pox) should fail with (err 17). // If it's the case, then this tx will NOT be mined. - let charlie_reject = make_bare_contract(&charlie, 1, 0, "charlie-try-reject", + let charlie_reject = make_bare_contract(&charlie, 3, 0, "charlie-try-reject", &format!( - "(asserts! (not (is-eq (contract-call? '{}.pox reject-pox) (err 17))) (err 1))", + "(asserts! (not (is-eq (print (contract-call? '{}.pox reject-pox)) (err 17))) (err 1))", boot_code_addr())); block_txs.push(charlie_reject); @@ -3163,7 +3137,8 @@ pub mod test { let (anchored_block, _size, _cost) = StacksBlockBuilder::make_anchored_block_from_txs(block_builder, chainstate, &sortdb.index_conn(), block_txs).unwrap(); if tenure_id == 2 { - assert_eq!(anchored_block.txs.len(), 2); + // block should be coinbase tx + 2 allowed txs + assert_eq!(anchored_block.txs.len(), 3); } (anchored_block, vec![]) diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index 9815bb747af..b18dbf36b96 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -76,9 +76,9 @@ (pox-addr (optional { version: (buff 1), hashbytes: (buff 20) })))) -;; Delegator's allowed contract-callers -(define-map delegator-contract-callers - ((delegator principal) (contract-caller principal)) +;; allowed contract-callers +(define-map allowance-contract-callers + ((sender principal) (contract-caller principal)) ((until-burn-ht (optional uint)))) ;; How many uSTX are stacked in a given reward cycle. @@ -171,17 +171,17 @@ none )) -(define-private (check-delegation-caller-allowed) +(define-private (check-caller-allowed) (or (is-eq tx-sender contract-caller) - (let ((delegation-caller-allowed - ;; if not in the delegation/caller map, return false - (unwrap! (map-get? delegator-contract-callers - { delegator: tx-sender, contract-caller: contract-caller }) + (let ((caller-allowed + ;; if not in the caller map, return false + (unwrap! (map-get? allowance-contract-callers + { sender: tx-sender, contract-caller: contract-caller }) false))) ;; is the caller allowance expired? - (if (< burn-block-height (unwrap! (get until-burn-ht delegation-caller-allowed) true)) - (begin (map-delete delegator-contract-callers - { delegator: tx-sender, contract-caller: contract-caller }) + (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true)) + (begin (map-delete allowance-contract-callers + { sender: tx-sender, contract-caller: contract-caller }) false) true)))) @@ -397,6 +397,20 @@ (ok true))) +(define-public (disallow-contract-caller (caller principal)) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller })))) + +(define-public (allow-contract-caller (caller principal) (until-burn-ht (optional uint))) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-set allowance-contract-callers + { sender: tx-sender, contract-caller: caller } + { until-burn-ht: until-burn-ht })))) + ;; Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX). ;; The STX will be locked for the given number of reward cycles (lock-period). ;; This is the self-service interface. tx-sender will be the Stacker. @@ -414,9 +428,9 @@ ;; this stacker's first reward cycle is the _next_ reward cycle (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle)))) - ;; must be called directly by the tx-sender - (asserts! (is-eq tx-sender contract-caller) - (err ERR_STACKING_PERMISSION_DENIED)) + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) ;; tx-sender principal must not be stacking (asserts! (is-none (get-stacker-info tx-sender)) @@ -435,8 +449,7 @@ ;; add stacker record (map-set stacking-state { stacker: tx-sender } - { - amount-ustx: amount-ustx, + { amount-ustx: amount-ustx, pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, lock-period: lock-period }) @@ -457,7 +470,7 @@ (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) (err ERR_STACKING_NO_SUCH_PRINCIPAL)))) ;; must be called directly by the tx-sender or by an allowed contract-caller - (asserts! (check-delegation-caller-allowed) + (asserts! (check-caller-allowed) (err ERR_STACKING_PERMISSION_DENIED)) (let ((amount-ustx (get stacked-amount partial-stacked))) (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1)) @@ -468,8 +481,7 @@ first-reward-cycle: reward-cycle, num-cycles: u1, amount-ustx: amount-ustx, - i: u0 - }) + i: u0 }) ;; don't update the stacking-state map, ;; because it _already has_ this stacker's state ;; don't lock the STX, because the STX is already locked @@ -489,7 +501,7 @@ (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) ;; must be called directly by the tx-sender or by an allowed contract-caller - (asserts! (check-delegation-caller-allowed) + (asserts! (check-caller-allowed) (err ERR_STACKING_PERMISSION_DENIED)) ;; stacker must have delegated to the caller diff --git a/src/chainstate/stacks/transaction.rs b/src/chainstate/stacks/transaction.rs index e9766eb674c..dd59912f994 100644 --- a/src/chainstate/stacks/transaction.rs +++ b/src/chainstate/stacks/transaction.rs @@ -235,10 +235,10 @@ impl TransactionPayload { })) } - pub fn new_smart_contract(name: &String, contract: &String) -> Option { + pub fn new_smart_contract(name: &str, contract: &str) -> Option { match ( - ContractName::try_from((*name).clone()), - StacksString::from_string(contract), + ContractName::try_from(name.to_string()), + StacksString::from_str(contract), ) { (Ok(s_name), Some(s_body)) => Some(TransactionPayload::SmartContract( TransactionSmartContract { From 1ba4efc83662d80ca2597de4615cc9d231159efd Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 1 Oct 2020 16:21:26 -0500 Subject: [PATCH 04/26] add delegation unit tests, some fixes, support in the special cc handler --- src/chainstate/stacks/boot/contract_tests.rs | 768 +++++++++++++++++++ src/chainstate/stacks/boot/mod.rs | 3 + src/chainstate/stacks/boot/pox.clar | 78 +- src/vm/functions/special.rs | 21 +- src/vm/tests/mod.rs | 4 +- 5 files changed, 841 insertions(+), 33 deletions(-) create mode 100644 src/chainstate/stacks/boot/contract_tests.rs diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs new file mode 100644 index 00000000000..821ff798255 --- /dev/null +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -0,0 +1,768 @@ +use std::collections::{HashMap, VecDeque}; +use std::convert::TryFrom; + +use vm::contracts::Contract; +use vm::errors::{ + CheckErrors, Error, IncomparableError, InterpreterError, InterpreterResult as Result, + RuntimeErrorType, +}; +use vm::types::{ + OptionalData, PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, + TupleTypeSignature, TypeSignature, Value, NONE, +}; + +use std::convert::TryInto; + +use burnchains::BurnchainHeaderHash; +use chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed}; +use chainstate::stacks::boot::STACKS_BOOT_CODE_CONTRACT_ADDRESS; +use chainstate::stacks::db::{MinerPaymentSchedule, StacksHeaderInfo}; +use chainstate::stacks::index::proofs::TrieMerkleProof; +use chainstate::stacks::index::MarfTrieId; +use chainstate::stacks::*; + +use util::db::{DBConn, FromRow}; +use util::hash::{Sha256Sum, Sha512Trunc256Sum}; +use vm::contexts::OwnedEnvironment; +use vm::costs::CostOverflowingMath; +use vm::database::*; +use vm::representations::SymbolicExpression; + +use util::hash::to_hex; +use vm::eval; +use vm::tests::{execute, is_committed, is_err_code, symbols_from_values}; + +use core::{ + FIRST_BURNCHAIN_BLOCK_HASH, FIRST_BURNCHAIN_BLOCK_HEIGHT, FIRST_BURNCHAIN_BLOCK_TIMESTAMP, + FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, POX_REWARD_CYCLE_LENGTH, +}; + +const BOOT_CODE_POX_BODY: &'static str = std::include_str!("pox.clar"); +const BOOT_CODE_POX_TESTNET_CONSTS: &'static str = std::include_str!("pox-testnet.clar"); +const BOOT_CODE_POX_MAINNET_CONSTS: &'static str = std::include_str!("pox-mainnet.clar"); +const BOOT_CODE_LOCKUP: &'static str = std::include_str!("lockup.clar"); + +const USTX_PER_HOLDER: u128 = 1_000_000; + +lazy_static! { + static ref BOOT_CODE_POX_MAINNET: String = + format!("{}\n{}", BOOT_CODE_POX_MAINNET_CONSTS, BOOT_CODE_POX_BODY); + static ref BOOT_CODE_POX_TESTNET: String = + format!("{}\n{}", BOOT_CODE_POX_TESTNET_CONSTS, BOOT_CODE_POX_BODY); + static ref FIRST_INDEX_BLOCK_HASH: StacksBlockId = StacksBlockHeader::make_index_block_hash( + &FIRST_BURNCHAIN_CONSENSUS_HASH, + &FIRST_STACKS_BLOCK_HASH + ); + static ref POX_CONTRACT: QualifiedContractIdentifier = + QualifiedContractIdentifier::parse(&format!("{}.pox", STACKS_BOOT_CODE_CONTRACT_ADDRESS)) + .unwrap(); + static ref USER_KEYS: Vec = + (0..50).map(|_| StacksPrivateKey::new()).collect(); + static ref POX_ADDRS: Vec = (0..50u64) + .map(|ix| execute(&format!( + "{{ version: 0x00, hashbytes: 0x000000000000000000000000{} }}", + &to_hex(&ix.to_le_bytes()) + ))) + .collect(); + static ref LIQUID_SUPPLY: u128 = USTX_PER_HOLDER * (POX_ADDRS.len() as u128); + static ref MIN_THRESHOLD: u128 = *LIQUID_SUPPLY / 480; +} + +impl From<&StacksPrivateKey> for StandardPrincipalData { + fn from(o: &StacksPrivateKey) -> StandardPrincipalData { + let stacks_addr = StacksAddress::from_public_keys( + C32_ADDRESS_VERSION_TESTNET_SINGLESIG, + &AddressHashMode::SerializeP2PKH, + 1, + &vec![StacksPublicKey::from_private(o)], + ) + .unwrap(); + StandardPrincipalData::from(stacks_addr) + } +} + +impl From<&StacksPrivateKey> for Value { + fn from(o: &StacksPrivateKey) -> Value { + Value::from(StandardPrincipalData::from(o)) + } +} + +struct ClarityTestSim { + marf: MarfedKV, + height: u64, +} + +struct TestSimHeadersDB { + height: u64, +} + +impl ClarityTestSim { + pub fn new() -> ClarityTestSim { + let mut marf = MarfedKV::temporary(); + marf.begin( + &StacksBlockId::sentinel(), + &StacksBlockId(test_sim_height_to_hash(0)), + ); + { + marf.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB) + .initialize(); + + let mut owned_env = + OwnedEnvironment::new(marf.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB)); + + for user_key in USER_KEYS.iter() { + owned_env.stx_faucet( + &StandardPrincipalData::from(user_key).into(), + USTX_PER_HOLDER, + ); + } + } + marf.test_commit(); + + ClarityTestSim { marf, height: 0 } + } + + pub fn execute_next_block(&mut self, f: F) -> R + where + F: FnOnce(&mut OwnedEnvironment) -> R, + { + self.marf.begin( + &StacksBlockId(test_sim_height_to_hash(self.height)), + &StacksBlockId(test_sim_height_to_hash(self.height + 1)), + ); + + let r = { + let headers_db = TestSimHeadersDB { + height: self.height + 1, + }; + let mut owned_env = + OwnedEnvironment::new(self.marf.as_clarity_db(&headers_db, &NULL_BURN_STATE_DB)); + f(&mut owned_env) + }; + + self.marf.test_commit(); + self.height += 1; + + r + } +} + +fn test_sim_height_to_hash(burn_height: u64) -> [u8; 32] { + let mut out = [0; 32]; + out[0..8].copy_from_slice(&burn_height.to_le_bytes()); + out +} + +fn test_sim_hash_to_height(in_bytes: &[u8; 32]) -> Option { + if &in_bytes[8..] != &[0; 24] { + None + } else { + let mut bytes = [0; 8]; + bytes.copy_from_slice(&in_bytes[0..8]); + Some(u64::from_le_bytes(bytes)) + } +} + +impl HeadersDB for TestSimHeadersDB { + fn get_burn_header_hash_for_block( + &self, + id_bhh: &StacksBlockId, + ) -> Option { + if *id_bhh == *FIRST_INDEX_BLOCK_HASH { + Some(FIRST_BURNCHAIN_BLOCK_HASH) + } else { + self.get_burn_block_height_for_block(id_bhh)?; + Some(BurnchainHeaderHash(id_bhh.0.clone())) + } + } + + fn get_vrf_seed_for_block(&self, _bhh: &StacksBlockId) -> Option { + None + } + + fn get_stacks_block_header_hash_for_block( + &self, + id_bhh: &StacksBlockId, + ) -> Option { + if *id_bhh == *FIRST_INDEX_BLOCK_HASH { + Some(FIRST_STACKS_BLOCK_HASH) + } else { + self.get_burn_block_height_for_block(id_bhh)?; + Some(BlockHeaderHash(id_bhh.0.clone())) + } + } + + fn get_burn_block_time_for_block(&self, id_bhh: &StacksBlockId) -> Option { + if *id_bhh == *FIRST_INDEX_BLOCK_HASH { + Some(FIRST_BURNCHAIN_BLOCK_TIMESTAMP) + } else { + let burn_block_height = self.get_burn_block_height_for_block(id_bhh)? as u64; + Some( + FIRST_BURNCHAIN_BLOCK_TIMESTAMP + burn_block_height + - FIRST_BURNCHAIN_BLOCK_HEIGHT as u64, + ) + } + } + fn get_burn_block_height_for_block(&self, id_bhh: &StacksBlockId) -> Option { + if *id_bhh == *FIRST_INDEX_BLOCK_HASH { + Some(FIRST_BURNCHAIN_BLOCK_HEIGHT) + } else { + let input_height = test_sim_hash_to_height(&id_bhh.0)?; + if input_height > self.height { + eprintln!("{} > {}", input_height, self.height); + None + } else { + Some( + (FIRST_BURNCHAIN_BLOCK_HEIGHT as u64 + input_height) + .try_into() + .unwrap(), + ) + } + } + } + fn get_miner_address(&self, _id_bhh: &StacksBlockId) -> Option { + None + } + fn get_total_liquid_ustx(&self, _id_bhh: &StacksBlockId) -> u128 { + *LIQUID_SUPPLY + } +} + +#[test] +fn delegation_tests() { + let mut sim = ClarityTestSim::new(); + let delegator = StacksPrivateKey::new(); + + sim.execute_next_block(|env| { + env.initialize_contract(POX_CONTRACT.clone(), &BOOT_CODE_POX_TESTNET) + .unwrap() + }); + sim.execute_next_block(|env| { + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(2 * USTX_PER_HOLDER), + (&delegator).into(), + Value::none(), + Value::none() + ]) + ) + .unwrap() + .0, + Value::okay_true() + ); + + // already delegating... + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(USTX_PER_HOLDER), + (&delegator).into(), + Value::none(), + Value::none() + ]) + ) + .unwrap() + .0, + Value::error(Value::Int(20)).unwrap() + ); + + assert_eq!( + env.execute_transaction( + (&USER_KEYS[1]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(USTX_PER_HOLDER), + (&delegator).into(), + Value::none(), + Value::some(POX_ADDRS[0].clone()).unwrap() + ]) + ) + .unwrap() + .0, + Value::okay_true() + ); + assert_eq!( + env.execute_transaction( + (&USER_KEYS[2]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(USTX_PER_HOLDER), + (&delegator).into(), + Value::some(Value::UInt(300)).unwrap(), + Value::none() + ]) + ) + .unwrap() + .0, + Value::okay_true() + ); + + assert_eq!( + env.execute_transaction( + (&USER_KEYS[3]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(USTX_PER_HOLDER), + (&delegator).into(), + Value::none(), + Value::none() + ]) + ) + .unwrap() + .0, + Value::okay_true() + ); + + assert_eq!( + env.execute_transaction( + (&USER_KEYS[4]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(USTX_PER_HOLDER), + (&delegator).into(), + Value::none(), + Value::none() + ]) + ) + .unwrap() + .0, + Value::okay_true() + ); + }); + // let's do some delegated stacking! + sim.execute_next_block(|env| { + // try to stack more than [0]'s delegated amount! + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[0]).into(), + Value::UInt(3 * USTX_PER_HOLDER), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 9)".to_string() + ); + + // try to stack more than [0] has! + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[0]).into(), + Value::UInt(2 * USTX_PER_HOLDER), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 1)".to_string() + ); + + // let's stack less than the threshold + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[0]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0, + execute(&format!( + "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", + Value::from(&USER_KEYS[0]), + Value::UInt(*MIN_THRESHOLD - 1), + Value::UInt(360) + )) + ); + + assert_eq!( + env.eval_read_only( + &POX_CONTRACT, + &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[0])) + ) + .unwrap() + .0, + Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD + 1) + ); + + // try to commit our partial stacking... + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "stack-aggregation-commit", + &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)]) + ) + .unwrap() + .0 + .to_string(), + "(err 11)".to_string() + ); + // not enough! we need to stack more... + // but POX_ADDR[1] cannot be used for USER_KEYS[1]... + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[1]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 9)".to_string() + ); + + // And USER_KEYS[0] is already stacking... + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[0]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 3)".to_string() + ); + + // USER_KEYS[2] won't want to stack past the delegation expiration... + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[2]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 9)".to_string() + ); + + // but for just one block will be fine + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[2]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[1].clone(), + Value::UInt(1) + ]) + ) + .unwrap() + .0, + execute(&format!( + "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", + Value::from(&USER_KEYS[2]), + Value::UInt(*MIN_THRESHOLD - 1), + Value::UInt(240) + )) + ); + + assert_eq!( + env.eval_read_only( + &POX_CONTRACT, + &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[2])) + ) + .unwrap() + .0, + Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD + 1) + ); + + assert_eq!( + env.eval_read_only( + &POX_CONTRACT, + &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[0])) + ) + .unwrap() + .0, + Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD + 1) + ); + + assert_eq!( + env.eval_read_only( + &POX_CONTRACT, + &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[1])) + ) + .unwrap() + .0, + Value::UInt(USTX_PER_HOLDER) + ); + + // try to commit our partial stacking again! + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "stack-aggregation-commit", + &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)]) + ) + .unwrap() + .0 + .to_string(), + "(ok true)".to_string() + ); + + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-size u1)") + .unwrap() + .0 + .to_string(), + "u1" + ); + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u1 u0)") + .unwrap() + .0, + execute(&format!( + "(some {{ pox-addr: {}, total-ustx: {} }})", + &POX_ADDRS[1], + &Value::UInt(2 * (*MIN_THRESHOLD - 1)) + )) + ); + + // can we double commit? I don't think so! + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "stack-aggregation-commit", + &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)]) + ) + .unwrap() + .0 + .to_string(), + "(err 4)".to_string() + ); + + // okay, let's try some more delegation situations... + // 1. we already locked user[0] up for round 2, so let's add some more stacks for round 2 from + // user[3]. in the process, this will add more stacks for lockup in round 1, so lets commit + // that as well. + + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[3]).into(), + Value::UInt(*MIN_THRESHOLD), + POX_ADDRS[1].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0, + execute(&format!( + "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", + Value::from(&USER_KEYS[3]), + Value::UInt(*MIN_THRESHOLD), + Value::UInt(360) + )) + ); + + assert_eq!( + env.eval_read_only( + &POX_CONTRACT, + &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[3])) + ) + .unwrap() + .0, + Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD) + ); + + // let's commit to round 2 now. + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "stack-aggregation-commit", + &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(2)]) + ) + .unwrap() + .0 + .to_string(), + "(ok true)".to_string() + ); + + // and we can commit to round 1 again as well! + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "stack-aggregation-commit", + &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)]) + ) + .unwrap() + .0 + .to_string(), + "(ok true)".to_string() + ); + + // check reward sets for round 2 and round 1... + + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-size u2)") + .unwrap() + .0 + .to_string(), + "u1" + ); + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u2 u0)") + .unwrap() + .0, + execute(&format!( + "(some {{ pox-addr: {}, total-ustx: {} }})", + &POX_ADDRS[1], + &Value::UInt((*MIN_THRESHOLD)) + )) + ); + + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-size u1)") + .unwrap() + .0 + .to_string(), + "u2" + ); + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u1 u0)") + .unwrap() + .0, + execute(&format!( + "(some {{ pox-addr: {}, total-ustx: {} }})", + &POX_ADDRS[1], + &Value::UInt(2 * (*MIN_THRESHOLD - 1)) + )) + ); + assert_eq!( + env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u1 u1)") + .unwrap() + .0, + execute(&format!( + "(some {{ pox-addr: {}, total-ustx: {} }})", + &POX_ADDRS[1], + &Value::UInt(*MIN_THRESHOLD) + )) + ); + + // 2. lets make sure we can lock up for user[1] so long as it goes to pox[0]. + + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[1]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[0].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0, + execute(&format!( + "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", + Value::from(&USER_KEYS[1]), + Value::UInt(*MIN_THRESHOLD), + Value::UInt(360) + )) + ); + + // 3. lets try to lock up user[4], but do some revocation first. + assert_eq!( + env.execute_transaction( + (&USER_KEYS[4]).into(), + POX_CONTRACT.clone(), + "revoke-delegate-stx", + &symbols_from_values(vec![(&delegator).into()]) + ) + .unwrap() + .0, + Value::okay_true() + ); + + // will run a second time, but return false + assert_eq!( + env.execute_transaction( + (&USER_KEYS[4]).into(), + POX_CONTRACT.clone(), + "revoke-delegate-stx", + &symbols_from_values(vec![(&delegator).into()]) + ) + .unwrap() + .0 + .to_string(), + "(ok false)".to_string() + ); + + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegator-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[4]).into(), + Value::UInt(*MIN_THRESHOLD - 1), + POX_ADDRS[0].clone(), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 9)".to_string() + ); + }); +} diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 41df22572e7..37ab7ef0d86 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -261,6 +261,9 @@ impl StacksChainState { } } +#[cfg(test)] +mod contract_tests; + #[cfg(test)] pub mod test { use chainstate::burn::db::sortdb::*; diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index b18dbf36b96..225a1b81002 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -335,8 +335,7 @@ ;; What is the minimum number of uSTX to be stacked in the given reward cycle? ;; Used internally by the Stacks node, and visible publicly. (define-read-only (get-stacking-minimum) - (/ stx-liquid-supply u20000) -) + (/ stx-liquid-supply STACKING_THRESHOLD_25)) ;; Is the address mode valid for a PoX burn address? (define-private (check-pox-addr-version (version (buff 1))) @@ -360,7 +359,7 @@ (num-cycles uint)) (begin ;; minimum uSTX must be met - (asserts! (<= (get-stacking-minimum) amount-ustx) + (asserts! (<= (print (get-stacking-minimum)) amount-ustx) (err ERR_STACKING_THRESHOLD_NOT_MET)) (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles))) @@ -379,14 +378,10 @@ (asserts! (> amount-ustx u0) (err ERR_STACKING_INVALID_AMOUNT)) - ;; tx-sender principal must not have rejected in this upcoming reward cycle + ;; sender principal must not have rejected in this upcoming reward cycle (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle)) (err ERR_STACKING_ALREADY_REJECTED)) - ;; the Stacker must have sufficient unlocked funds - (asserts! (>= (stx-get-balance tx-sender) amount-ustx) - (err ERR_STACKING_INSUFFICIENT_FUNDS)) - ;; lock period must be in acceptable range. (asserts! (check-pox-lock-period num-cycles) (err ERR_STACKING_INVALID_LOCK_PERIOD)) @@ -396,13 +391,17 @@ (err ERR_STACKING_INVALID_POX_ADDRESS)) (ok true))) - +;; Revoke contract-caller authorization to call stacking methods (define-public (disallow-contract-caller (caller principal)) (begin (asserts! (is-eq tx-sender contract-caller) (err ERR_STACKING_PERMISSION_DENIED)) (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller })))) +;; Give a contract-caller authorization to call stacking methods +;; normally, stacking methods may only be invoked by _direct_ transactions +;; (i.e., the tx-sender issues a direct contract-call to the stacking methods) +;; by issuing an allowance, the tx-sender may call through the allowed contract (define-public (allow-contract-caller (caller principal) (until-burn-ht (optional uint))) (begin (asserts! (is-eq tx-sender contract-caller) @@ -440,6 +439,10 @@ (asserts! (is-none (get-check-delegation tx-sender)) (err ERR_STACKING_ALREADY_DELEGATED)) + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + ;; ensure that stacking can be performed (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) @@ -458,6 +461,48 @@ (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })) ) +(define-public (revoke-delegate-stx (delegator principal)) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete delegation-state { stacker: tx-sender })))) + +;; Delegate to `delegator` the ability to stack from a given address. +;; This method _does not_ lock the funds, rather, it allows the delegator +;; to issue the stacking lock. +;; The caller specifies: +;; * amount-ustx: the total amount of ustx the delegator may be allowed to lock +;; * until-burn-ht: an optional burn height at which this delegation expiration +;; * pox-addr: an optional address to which any rewards *must* be sent +(define-public (delegate-stx (amount-ustx uint) + (delegator principal) + (until-burn-ht (optional uint)) + (pox-addr (optional { version: (buff 1), + hashbytes: (buff 20) }))) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; add delegation record + (map-set delegation-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + delegated-to: delegator, + until-burn-ht: until-burn-ht, + pox-addr: pox-addr }) + + (ok true))) + ;; Commit partially stacked STX. ;; this allows a stacker/delegator to lock fewer STX than the minimal threshold in multiple transactions, ;; so long as the pox-addr is the same, and then this "commit" transaction is called _before_ the PoX anchor block @@ -491,7 +536,7 @@ (ok true)))) ;; As a delegator, stack the given principal's STX using partial-stacked-by-cycle -;; Once the delegator has stacked > minimum, the delegator should call the stack-aggregation-commit +;; Once the delegator has stacked > minimum, the delegator should call stack-aggregation-commit (define-public (delegator-stack-stx (stacker principal) (amount-ustx uint) (pox-addr { version: (buff 1), hashbytes: (buff 20) }) @@ -517,8 +562,8 @@ true) ;; delegation must not expire before lock period (match (get until-burn-ht delegation-info) - until-burn-ht (>= (+ u1 until-burn-ht) - (reward-cycle-to-burn-height unlock-burn-height)) + until-burn-ht (>= until-burn-ht + unlock-burn-height) true))) (err ERR_STACKING_PERMISSION_DENIED)) @@ -526,11 +571,16 @@ (asserts! (is-none (get-stacker-info stacker)) (err ERR_STACKING_ALREADY_STACKED)) + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance stacker) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + ;; ensure that stacking can be performed (try! (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) - ;; register the PoX address with the amount stacked - (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + ;; register the PoX address with the amount stacked via partial stacking + ;; before it can be included in the reward set, this must be committed! + (try! (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx)) ;; add stacker record (map-set stacking-state diff --git a/src/vm/functions/special.rs b/src/vm/functions/special.rs index 9270d0aba07..8fc278c6b09 100644 --- a/src/vm/functions/special.rs +++ b/src/vm/functions/special.rs @@ -48,11 +48,11 @@ fn parse_pox_stacking_result( /// Handle special cases when calling into the PoX API contract fn handle_pox_api_contract_call( global_context: &mut GlobalContext, - sender_opt: Option<&PrincipalData>, + _sender_opt: Option<&PrincipalData>, function_name: &str, value: &Value, ) -> Result<()> { - if function_name == "stack-stx" { + if function_name == "stack-stx" || function_name == "delegator-stack-stx" { debug!( "Handle special-case contract-call to {:?} {} (which returned {:?})", boot_code_id("pox"), @@ -60,26 +60,13 @@ fn handle_pox_api_contract_call( value ); - // sender is required - let sender = match sender_opt { - None => { - return Err(RuntimeErrorType::NoSenderInContext.into()); - } - Some(sender) => (*sender).clone(), - }; - match parse_pox_stacking_result(value) { Ok((stacker, locked_amount, unlock_height)) => { - assert_eq!( - stacker, sender, - "BUG: tx-sender is not contract-call origin!" - ); - // if this fails, then there's a bug in the contract (since it already does // the necessary checks) match StacksChainState::pox_lock( &mut global_context.database, - &sender, + &stacker, locked_amount, unlock_height as u64, ) { @@ -96,7 +83,7 @@ fn handle_pox_api_contract_call( Err(e) => { panic!( "FATAL: failed to lock {} from {} until {}: '{:?}'", - locked_amount, sender, unlock_height, &e + locked_amount, stacker, unlock_height, &e ); } } diff --git a/src/vm/tests/mod.rs b/src/vm/tests/mod.rs index 86bca35b895..0dd629fcbab 100644 --- a/src/vm/tests/mod.rs +++ b/src/vm/tests/mod.rs @@ -93,7 +93,7 @@ pub fn symbols_from_values(mut vec: Vec) -> Vec { .collect() } -fn is_committed(v: &Value) -> bool { +pub fn is_committed(v: &Value) -> bool { eprintln!("is_committed?: {}", v); match v { @@ -102,7 +102,7 @@ fn is_committed(v: &Value) -> bool { } } -fn is_err_code(v: &Value, e: u128) -> bool { +pub fn is_err_code(v: &Value, e: u128) -> bool { eprintln!("is_err_code?: {}", v); match v { Value::Response(ref data) => !data.committed && *data.data == Value::UInt(e), From 2f1694cd31d3ae28fcad0ed12376b54236f1a26a Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 1 Oct 2020 19:37:26 -0500 Subject: [PATCH 05/26] fix min threshold constant for testnet --- src/chainstate/stacks/boot/contract_tests.rs | 2 +- src/chainstate/stacks/boot/mod.rs | 45 +++++++++----------- src/chainstate/stacks/boot/pox.clar | 4 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index 821ff798255..1bfd5221d69 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -664,7 +664,7 @@ fn delegation_tests() { execute(&format!( "(some {{ pox-addr: {}, total-ustx: {} }})", &POX_ADDRS[1], - &Value::UInt((*MIN_THRESHOLD)) + &Value::UInt(*MIN_THRESHOLD) )) ); diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 37ab7ef0d86..ebf38563244 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -1130,7 +1130,7 @@ pub mod test { chainstate.get_stacking_minimum(sortdb, &tip_index_block) }) .unwrap(); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -1196,7 +1196,7 @@ pub mod test { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. assert!(total_liquid_ustx > 4 * 1024 * 1000000); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } else { // still at 25% or more locked assert!(total_liquid_ustx <= 4 * 1024 * 1000000); @@ -1323,12 +1323,11 @@ pub mod test { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_balance, 1024 * 1000000); } - // stacking minimum should be floor(total-liquid-ustx / 20000) let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) }) .unwrap(); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -1396,7 +1395,7 @@ pub mod test { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. assert!(total_liquid_ustx > 4 * 1024 * 1000000); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } else { // still at 25% or more locked assert!(total_liquid_ustx <= 4 * 1024 * 1000000); @@ -1588,12 +1587,11 @@ pub mod test { assert_eq!(bob_balance, 1024 * 1000000); } - // stacking minimum should be floor(total-liquid-ustx / 20000) let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) }) .unwrap(); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -1667,7 +1665,7 @@ pub mod test { } // well over 25% locked, so this is always true - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // two reward addresses, and they're Alice's and Bob's. // They are present in sorted order @@ -1990,12 +1988,11 @@ pub mod test { assert_eq!(alice_balance, 1024 * 1000000); } - // stacking minimum should be floor(total-liquid-ustx / 20000) let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) }) .unwrap(); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -2059,7 +2056,7 @@ pub mod test { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. assert!(total_liquid_ustx > 4 * 1024 * 1000000); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } if cur_reward_cycle == alice_reward_cycle { @@ -2110,7 +2107,7 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // min STX is reset - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // Unlock is lazy let alice_account = @@ -2291,12 +2288,11 @@ pub mod test { assert_eq!(charlie_balance, 0); } - // stacking minimum should be floor(total-liquid-ustx / 20000) let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) }) .unwrap(); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses assert_eq!(reward_addrs.len(), 0); @@ -2320,13 +2316,13 @@ pub mod test { assert_eq!(charlie_balance, 1024 * 1000000); } else if tenure_id == 11 { // should have just re-locked - // stacking minimum should be floor(total-liquid-ustx / 20000), since we haven't + // stacking minimum should be minimum, since we haven't // locked up 25% of the tokens yet let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) }) .unwrap(); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses assert_eq!(reward_addrs.len(), 0); @@ -2349,13 +2345,13 @@ pub mod test { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. assert!(total_liquid_ustx > 4 * 1024 * 1000000); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } else if tenure_id >= 1 && cur_reward_cycle < first_reward_cycle { // still at 25% or more locked assert!(total_liquid_ustx <= 4 * 1024 * 1000000); } else if tenure_id < 1 { // nothing locked yet - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } if first_reward_cycle > 0 && second_reward_cycle == 0 { @@ -2447,7 +2443,7 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // min STX is reset - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // Unlock is lazy let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); @@ -2568,7 +2564,7 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // min STX is reset - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // Unlock is lazy let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); @@ -2894,8 +2890,7 @@ pub mod test { assert_eq!(balance, expected_balance); } } - // stacking minimum should be floor(total-liquid-ustx / 20000) - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -3011,7 +3006,7 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // min STX is reset - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } } @@ -3194,7 +3189,7 @@ pub mod test { assert_eq!(alice_account.stx_balance.unlock_height, 0); } - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); // no reward addresses assert_eq!(reward_addrs.len(), 0); @@ -3254,7 +3249,7 @@ pub mod test { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. assert!(total_liquid_ustx > 4 * 1024 * 1000000); - assert_eq!(min_ustx, total_liquid_ustx / 20000); + assert_eq!(min_ustx, total_liquid_ustx / 480); } else { // still at 25% or more locked assert!(total_liquid_ustx <= 4 * 1024 * 1000000); diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index 225a1b81002..cca6a56c42f 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -330,7 +330,7 @@ (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) (fold add-pox-partial-stacked-to-ith-cycle cycle-indexes { pox-addr: pox-addr, reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx }) - (ok true))) + true)) ;; What is the minimum number of uSTX to be stacked in the given reward cycle? ;; Used internally by the Stacks node, and visible publicly. @@ -580,7 +580,7 @@ ;; register the PoX address with the amount stacked via partial stacking ;; before it can be included in the reward set, this must be committed! - (try! (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx)) + (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx) ;; add stacker record (map-set stacking-state From 3a69a8d396791d23888ffd6af90e4232a1bf4d41 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 1 Oct 2020 20:13:28 -0500 Subject: [PATCH 06/26] test: fix delegation test case --- src/chainstate/stacks/boot/contract_tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index 1bfd5221d69..658d74fdb9c 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -664,7 +664,7 @@ fn delegation_tests() { execute(&format!( "(some {{ pox-addr: {}, total-ustx: {} }})", &POX_ADDRS[1], - &Value::UInt(*MIN_THRESHOLD) + &Value::UInt(2 * (*MIN_THRESHOLD) - 1) )) ); @@ -705,7 +705,7 @@ fn delegation_tests() { "delegator-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[1]).into(), - Value::UInt(*MIN_THRESHOLD - 1), + Value::UInt(*MIN_THRESHOLD), POX_ADDRS[0].clone(), Value::UInt(2) ]) From 8477e6c2524460409c04b42354a3610e1a7d1fbe Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 5 Oct 2020 14:57:27 -0500 Subject: [PATCH 07/26] threshold scaling + reward address repeating --- src/burnchains/mod.rs | 4 ++++ src/chainstate/coordinator/mod.rs | 25 +++++++++++++++++++++++-- src/chainstate/stacks/boot/mod.rs | 27 ++++++++++++++++++++++++++- src/chainstate/stacks/db/headers.rs | 11 +++-------- src/core/mod.rs | 2 ++ 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index c8c41a7450b..51508971990 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -308,6 +308,10 @@ impl PoxConstants { PoxConstants::new(10, 5, 3, 25) } + pub fn reward_slots(&self) -> u32 { + self.reward_cycle_length + } + pub fn mainnet_default() -> PoxConstants { PoxConstants::new(1000, 240, 192, 25) } diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index df5dd881666..f6ffc1467a1 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -1,5 +1,7 @@ use std::collections::VecDeque; -use std::convert::TryInto; +use std::convert::{ + TryInto, TryFrom +}; use std::time::Duration; use burnchains::{ @@ -163,7 +165,26 @@ impl RewardSetProvider for OnChainRewardSetProvider { ) -> Result, Error> { let res = chainstate.get_reward_addresses(burnchain, sortdb, current_burn_height, block_id)?; - let addresses = res.iter().map(|a| a.0).collect::>(); + let liquid_ustx = StacksChainState::get_stacks_block_header_info_by_index_block_hash( + chainstate.headers_db(), + block_id)? + .expect("CORRUPTION: Failed to look up block header info for PoX anchor block") + .total_liquid_ustx; + + let threshold = StacksChainState::get_reward_threshold( + &burnchain.pox_constants, + &res, + liquid_ustx); + let mut addresses = vec![]; + + for (address, stacked_amt) in res.iter() { + let slots_taken = u32::try_from(stacked_amt / threshold) + .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); + for _i in 0..slots_taken { + addresses.push(address.clone()); + } + } + Ok(addresses) } } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index ebf38563244..2f3cf9bfb97 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -24,8 +24,13 @@ use chainstate::stacks::StacksBlockHeader; use address::AddressHashMode; use burnchains::bitcoin::address::BitcoinAddress; -use burnchains::Address; +use burnchains::{ + Address, PoxConstants +}; +use core::{ + POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS +}; use chainstate::burn::db::sortdb::SortitionDB; use vm::types::{ @@ -44,6 +49,7 @@ use util::hash::Hash160; use std::boxed::Box; use std::convert::TryFrom; use std::convert::TryInto; +use std::cmp; pub const STACKS_BOOT_CODE_CONTRACT_ADDRESS: &'static str = "ST000000000000000000002AMW42H"; @@ -183,6 +189,25 @@ impl StacksChainState { .map(|value| value.expect_bool()) } + pub fn get_reward_threshold(pox_settings: &PoxConstants, addresses: &[(StacksAddress, u128)], liquid_ustx: u128) -> u128 { + let participation = addresses + .iter() + .fold(0, |agg, (_, stacked_amt)| agg + stacked_amt); + + assert!(participation <= liquid_ustx, "CORRUPTION: More stacking participation than liquid STX"); + + let scale_by = cmp::max(participation, liquid_ustx / POX_MAXIMAL_SCALING as u128); + + let reward_slots = pox_settings.reward_slots() as u128; + let threshold_precise = scale_by / reward_slots; + // compute the threshold as nearest 10k > threshold_precise + let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS { + 0 => 0, + remainder => POX_THRESHOLD_STEPS - remainder, + }; + threshold_precise + ceil_amount + } + /// Each address will have at least (get-stacking-minimum) tokens. pub fn get_reward_addresses( &mut self, diff --git a/src/chainstate/stacks/db/headers.rs b/src/chainstate/stacks/db/headers.rs index 1dd1ec89f01..6efd80ddde6 100644 --- a/src/chainstate/stacks/db/headers.rs +++ b/src/chainstate/stacks/db/headers.rs @@ -37,6 +37,7 @@ use vm::costs::ExecutionCost; use util::db::Error as db_error; use util::db::{ query_count, query_row, query_row_columns, query_rows, DBConn, FromColumn, FromRow, + query_row_panic }; use core::FIRST_BURNCHAIN_CONSENSUS_HASH; @@ -278,14 +279,8 @@ impl StacksChainState { index_block_hash: &StacksBlockId, ) -> Result, Error> { let sql = "SELECT * FROM block_headers WHERE index_block_hash = ?1".to_string(); - let mut rows = query_rows::(conn, &sql, &[&index_block_hash]) - .map_err(Error::DBError)?; - let cnt = rows.len(); - if cnt > 1 { - unreachable!("FATAL: multiple rows for the same block hash") // should be unreachable, since index_block_hash is unique - } - - Ok(rows.pop()) + query_row_panic(conn, &sql, &[&index_block_hash], || "FATAL: multiple rows for the same block hash".to_string()) + .map_err(Error::DBError) } /// Get an ancestor block header diff --git a/src/core/mod.rs b/src/core/mod.rs index 6c01838239c..b1eb12a90d3 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -63,6 +63,8 @@ pub const CHAINSTATE_VERSION: &'static str = "23.0.0.0"; pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240; pub const POX_REWARD_CYCLE_LENGTH: u32 = 1000; +pub const POX_MAXIMAL_SCALING: u128 = 4; +pub const POX_THRESHOLD_STEPS: u128 = 10_000; /// Synchronize burn transactions from the Bitcoin blockchain pub fn sync_burnchain_bitcoin( From c833b65ec1b68c8b8440fed93f89b5309546afe5 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 08:06:13 -0500 Subject: [PATCH 08/26] update stack-aggregation-commit comment --- src/chainstate/stacks/boot/pox.clar | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index cca6a56c42f..a989407d749 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -504,9 +504,10 @@ (ok true))) ;; Commit partially stacked STX. -;; this allows a stacker/delegator to lock fewer STX than the minimal threshold in multiple transactions, -;; so long as the pox-addr is the same, and then this "commit" transaction is called _before_ the PoX anchor block -;; this ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, +;; This allows a stacker/delegator to lock fewer STX than the minimal threshold in multiple transactions, +;; so long as: 1. The pox-addr is the same. +;; 2. This "commit" transaction is called _before_ the PoX anchor block. +;; This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, ;; but does not require it be all locked up within a single transaction (define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) }) (reward-cycle uint)) From 8f35d5a5892ebb924e815342049c0807878d10a1 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 09:56:24 -0500 Subject: [PATCH 09/26] #ignore test flake --- src/net/neighbors.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/net/neighbors.rs b/src/net/neighbors.rs index e691faccb95..d552cf5e8e2 100644 --- a/src/net/neighbors.rs +++ b/src/net/neighbors.rs @@ -4091,6 +4091,7 @@ mod test { } #[test] + #[ignore] fn test_step_walk_2_neighbors_rekey() { with_timeout(600, || { let mut peer_1_config = TestPeerConfig::from_port(32600); From d4aaa15e7d9aecaf9929b46c3225123670af9620 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 13:23:04 -0500 Subject: [PATCH 10/26] unit+int tests for threshold/addr repeats. write burnchain config to pox contract at boot. parameterize syncctl wait times for testing --- src/chainstate/coordinator/mod.rs | 24 +++- src/chainstate/stacks/boot/mod.rs | 58 ++++++++- src/chainstate/stacks/boot/pox.clar | 2 +- src/core/mod.rs | 4 +- .../burnchains/bitcoin_regtest_controller.rs | 114 ++++++++++++++++-- testnet/stacks-node/src/main.rs | 2 +- testnet/stacks-node/src/neon_node.rs | 15 ++- testnet/stacks-node/src/run_loop/neon.rs | 31 ++--- testnet/stacks-node/src/syncctl.rs | 14 ++- .../src/tests/neon_integrations.rs | 39 +++--- 10 files changed, 243 insertions(+), 60 deletions(-) diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index 5e0b84bc2e0..392ede05755 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -15,12 +15,13 @@ use chainstate::burn::{ }; use chainstate::stacks::{ db::{ClarityTx, StacksChainState, StacksHeaderInfo}, + boot::STACKS_BOOT_CODE_CONTRACT_ADDRESS, events::StacksTransactionReceipt, Error as ChainstateError, StacksAddress, StacksBlock, StacksBlockHeader, StacksBlockId, }; use monitoring::increment_stx_blocks_processed_counter; use util::db::Error as DBError; -use vm::{costs::ExecutionCost, types::PrincipalData}; +use vm::{costs::ExecutionCost, Value, types::{PrincipalData, QualifiedContractIdentifier}}; pub mod comm; use chainstate::stacks::index::MarfTrieId; @@ -217,7 +218,26 @@ impl<'a, T: BlockEventDispatcher> stacks_chain_id, chain_state_path, initial_balances, - boot_block_exec, + |clarity_tx| { + let burnchain = burnchain.clone(); + let contract = QualifiedContractIdentifier::parse(&format!("{}.pox", STACKS_BOOT_CODE_CONTRACT_ADDRESS)) + .expect("Failed to construct boot code contract address"); + let sender = PrincipalData::from(contract.clone()); + + clarity_tx.connection().as_transaction(|conn| { + conn.run_contract_call( + &sender, + &contract, + "set-burnchain-parameters", + &[Value::UInt(burnchain.first_block_height as u128), + Value::UInt(burnchain.pox_constants.prepare_length as u128), + Value::UInt(burnchain.pox_constants.reward_cycle_length as u128), + Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128)], + |_,_| false) + .expect("Failed to set burnchain parameters in PoX contract"); + }); + boot_block_exec(clarity_tx) + }, block_limit, ) .unwrap(); diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 6b9e3bcbae4..74b40affad6 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -29,7 +29,7 @@ use burnchains::{ }; use core::{ - POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS + POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX }; use chainstate::burn::db::sortdb::SortitionDB; @@ -201,11 +201,13 @@ impl StacksChainState { let reward_slots = pox_settings.reward_slots() as u128; let threshold_precise = scale_by / reward_slots; // compute the threshold as nearest 10k > threshold_precise - let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS { + let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS_USTX { 0 => 0, - remainder => POX_THRESHOLD_STEPS - remainder, + remainder => POX_THRESHOLD_STEPS_USTX - remainder, }; - threshold_precise + ceil_amount + let threshold = threshold_precise + ceil_amount; + info!("PoX participation threshold is {}, from {}", threshold, threshold_precise); + threshold } /// Each address will have at least (get-stacking-minimum) tokens. @@ -312,12 +314,60 @@ pub mod test { use vm::contracts::Contract; use vm::types::*; + use core::*; use std::convert::From; use std::fs; use util::hash::to_hex; + + #[test] + fn get_reward_threshold_units() { + // when the liquid amount = the threshold step, + // the threshold should always be the step size. + let liquid = POX_THRESHOLD_STEPS_USTX; + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + POX_THRESHOLD_STEPS_USTX); + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid), + POX_THRESHOLD_STEPS_USTX); + + let liquid = 200_000_000 * MICROSTACKS_PER_STACKS as u128; + // with zero participation, should scale to 25% of liquid + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + 50_000 * MICROSTACKS_PER_STACKS as u128); + // should be the same at 25% participation + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid / 4)], liquid), + 50_000 * MICROSTACKS_PER_STACKS as u128); + // but not at 30% participation + assert_eq!(StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid / 4), + (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128))], + liquid), + 60_000 * MICROSTACKS_PER_STACKS as u128); + + // bump by just a little bit, should go to the next threshold step + assert_eq!(StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid / 4), + (rand_addr(), (MICROSTACKS_PER_STACKS as u128))], + liquid), + 60_000 * MICROSTACKS_PER_STACKS as u128); + + // bump by just a little bit, should go to the next threshold step + assert_eq!(StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid)], + liquid), + 200_000 * MICROSTACKS_PER_STACKS as u128); + + } + + fn rand_addr() -> StacksAddress { + key_to_stacks_addr(&StacksPrivateKey::new()) + } + fn key_to_stacks_addr(key: &StacksPrivateKey) -> StacksAddress { StacksAddress::from_public_keys( C32_ADDRESS_VERSION_TESTNET_SINGLESIG, diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index cca6a56c42f..75366c3bfde 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -31,7 +31,7 @@ ;; This function can only be called once, when it boots up (define-public (set-burnchain-parameters (first-burn-height uint) (prepare-cycle-length uint) (reward-cycle-length uint) (rejection-fraction uint)) (begin - (asserts! (and is-in-regtest (not (var-get configured))) (err ERR_NOT_ALLOWED)) + (asserts! (not (var-get configured)) (err ERR_NOT_ALLOWED)) (var-set first-burnchain-block-height first-burn-height) (var-set pox-prepare-cycle-length prepare-cycle-length) (var-set pox-reward-cycle-length reward-cycle-length) diff --git a/src/core/mod.rs b/src/core/mod.rs index 9dab580e957..79d89e4bd4b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -61,10 +61,12 @@ pub const BURNCHAIN_BOOT_CONSENSUS_HASH: ConsensusHash = ConsensusHash([0xff; 20 pub const CHAINSTATE_VERSION: &'static str = "23.0.0.0"; +pub const MICROSTACKS_PER_STACKS: u32 = 1_000_000; + pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240; pub const POX_REWARD_CYCLE_LENGTH: u32 = 1000; pub const POX_MAXIMAL_SCALING: u128 = 4; -pub const POX_THRESHOLD_STEPS: u128 = 10_000; +pub const POX_THRESHOLD_STEPS_USTX: u128 = 10_000 * (MICROSTACKS_PER_STACKS as u128); /// Synchronize burn transactions from the Bitcoin blockchain pub fn sync_burnchain_bitcoin( diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index d704af243d5..413b06ad61d 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -53,12 +53,17 @@ pub struct BitcoinRegtestController { burnchain_db: Option, chain_tip: Option, use_coordinator: Option, + burnchain_config: Option, } const DUST_UTXO_LIMIT: u64 = 5500; impl BitcoinRegtestController { pub fn new(config: Config, coordinator_channel: Option) -> Self { + BitcoinRegtestController::with_burnchain(config, coordinator_channel, None) + } + + pub fn with_burnchain(config: Config, coordinator_channel: Option, burnchain_config: Option) -> Self { std::fs::create_dir_all(&config.node.get_burnchain_path()) .expect("Unable to create workdir"); @@ -93,11 +98,12 @@ impl BitcoinRegtestController { Self { use_coordinator: coordinator_channel, - config: config, + config, indexer_config, db: None, burnchain_db: None, chain_tip: None, + burnchain_config } } @@ -122,22 +128,28 @@ impl BitcoinRegtestController { Self { use_coordinator: None, - config: config, + config, indexer_config, db: None, burnchain_db: None, chain_tip: None, + burnchain_config: None } } fn setup_burnchain(&self) -> (Burnchain, BitcoinNetworkType) { let (network_name, network_type) = self.config.burnchain.get_bitcoin_network(); - let working_dir = self.config.get_burn_db_path(); - match Burnchain::new(&working_dir, &self.config.burnchain.chain, &network_name) { - Ok(burnchain) => (burnchain, network_type), - Err(e) => { - error!("Failed to instantiate burnchain: {}", e); - panic!() + match &self.burnchain_config { + Some(burnchain) => (burnchain.clone(), network_type), + None => { + let working_dir = self.config.get_burn_db_path(); + match Burnchain::new(&working_dir, &self.config.burnchain.chain, &network_name) { + Ok(burnchain) => (burnchain, network_type), + Err(e) => { + error!("Failed to instantiate burnchain: {}", e); + panic!() + } + } } } } @@ -312,6 +324,90 @@ impl BitcoinRegtestController { Ok((burnchain_tip, burnchain_height)) } + #[cfg(test)] + pub fn get_all_utxos(&self, public_key: &Secp256k1PublicKey) -> Vec { + // Configure UTXO filter + let pkh = Hash160::from_data(&public_key.to_bytes()) + .to_bytes() + .to_vec(); + let (_, network_id) = self.config.burnchain.get_bitcoin_network(); + let address = + BitcoinAddress::from_bytes(network_id, BitcoinAddressType::PublicKeyHash, &pkh) + .expect("Public key incorrect"); + let filter_addresses = vec![address.to_b58()]; + let _result = BitcoinRPCRequest::import_public_key(&self.config, &public_key); + + sleep_ms(1000); + + let min_conf = 0; + let max_conf = 9999999; + let minimum_amount = ParsedUTXO::sat_to_serialized_btc(1); + + let payload = BitcoinRPCRequest { + method: "listunspent".to_string(), + params: vec![ + min_conf.into(), + max_conf.into(), + filter_addresses.clone().into(), + true.into(), + json!({ "minimumAmount": minimum_amount }), + ], + id: "stacks".to_string(), + jsonrpc: "2.0".to_string(), + }; + + let mut res = BitcoinRPCRequest::send(&self.config, payload).unwrap(); + let mut result_vec = vec![]; + + if let Some(ref mut object) = res.as_object_mut() { + match object.get_mut("result") { + Some(serde_json::Value::Array(entries)) => { + while let Some(entry) = entries.pop() { + let parsed_utxo: ParsedUTXO = match serde_json::from_value(entry) { + Ok(utxo) => utxo, + Err(err) => { + warn!("Failed parsing UTXO: {}", err); + continue; + } + }; + let amount = match parsed_utxo.get_sat_amount() { + Some(amount) => amount, + None => continue, + }; + + if amount < 1 { + continue; + } + + let script_pub_key = match parsed_utxo.get_script_pub_key() { + Some(script_pub_key) => script_pub_key, + None => { + continue; + } + }; + + let txid = match parsed_utxo.get_txid() { + Some(amount) => amount, + None => continue, + }; + + result_vec.push(UTXO { + txid, + vout: parsed_utxo.vout, + script_pub_key, + amount, + }); + } + } + _ => { + warn!("Failed to get UTXOs"); + } + } + } + + result_vec + } + pub fn get_utxos( &self, public_key: &Secp256k1PublicKey, @@ -382,7 +478,7 @@ impl BitcoinRegtestController { let total_unspent: u64 = utxos.iter().map(|o| o.amount).sum(); if total_unspent < amount_required { - debug!( + warn!( "Total unspent {} < {} for {:?}", total_unspent, amount_required, diff --git a/testnet/stacks-node/src/main.rs b/testnet/stacks-node/src/main.rs index 62d04ceef62..a9cab5da31d 100644 --- a/testnet/stacks-node/src/main.rs +++ b/testnet/stacks-node/src/main.rs @@ -139,7 +139,7 @@ fn main() { || conf.burnchain.mode == "xenon" { let mut run_loop = neon::RunLoop::new(conf); - run_loop.start(num_round); + run_loop.start(num_round, None); } else { println!("Burnchain mode '{}' not supported", conf.burnchain.mode); } diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 39691f9b39c..a08c412c17d 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -85,6 +85,7 @@ pub struct NeonGenesisNode { pub config: Config, keychain: Keychain, event_dispatcher: EventDispatcher, + burnchain: Burnchain } #[cfg(test)] @@ -596,19 +597,13 @@ impl InitializedNeonNode { miner: bool, blocks_processed: BlocksProcessedCounter, coord_comms: CoordinatorChannels, + burnchain: Burnchain ) -> InitializedNeonNode { // we can call _open_ here rather than _connect_, since connect is first called in // make_genesis_block let sortdb = SortitionDB::open(&config.get_burn_db_file_path(), false) .expect("Error while instantiating sortition db"); - let burnchain = Burnchain::new( - &config.get_burn_db_path(), - &config.burnchain.chain, - "regtest", - ) - .expect("Error while instantiating burnchain"); - let view = { let ic = sortdb.index_conn(); let sortition_tip = SortitionDB::get_canonical_burn_chain_tip(&ic) @@ -1036,6 +1031,7 @@ impl InitializedNeonNode { /// Process a state coming from the burnchain, by extracting the validated KeyRegisterOp /// and inspecting if a sortition was won. + /// `ibd`: boolean indicating whether or not we are in the initial block download pub fn process_burnchain_state( &mut self, sortdb: &SortitionDB, @@ -1117,7 +1113,7 @@ impl InitializedNeonNode { impl NeonGenesisNode { /// Instantiate and initialize a new node, given a config - pub fn new(config: Config, mut event_dispatcher: EventDispatcher, boot_block_exec: F) -> Self + pub fn new(config: Config, mut event_dispatcher: EventDispatcher, burnchain: Burnchain, boot_block_exec: F) -> Self where F: FnOnce(&mut ClarityTx) -> (), { @@ -1151,6 +1147,7 @@ impl NeonGenesisNode { keychain, config, event_dispatcher, + burnchain } } @@ -1172,6 +1169,7 @@ impl NeonGenesisNode { true, blocks_processed, coord_comms, + self.burnchain ) } @@ -1193,6 +1191,7 @@ impl NeonGenesisNode { false, blocks_processed, coord_comms, + self.burnchain ) } } diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index b471beeab28..48489488c1b 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -33,6 +33,16 @@ pub struct RunLoop { coordinator_channels: Option<(CoordinatorReceivers, CoordinatorChannels)>, } +#[cfg(not(test))] +const BURNCHAIN_POLL_TIME: u64 = 30; // TODO: this is testnet-specific +#[cfg(test)] +const BURNCHAIN_POLL_TIME: u64 = 1; // TODO: this is testnet-specific + +#[cfg(not(test))] +const POX_SYNC_WAIT_MS: u64 = 1000; +#[cfg(test)] +const POX_SYNC_WAIT_MS: u64 = 0; + impl RunLoop { /// Sets up a runloop and node, given a config. #[cfg(not(test))] @@ -83,7 +93,7 @@ impl RunLoop { /// It will start the burnchain (separate thread), set-up a channel in /// charge of coordinating the new blocks coming from the burnchain and /// the nodes, taking turns on tenures. - pub fn start(&mut self, _expected_num_rounds: u64) { + pub fn start(&mut self, _expected_num_rounds: u64, burnchain_opt: Option) { let (coordinator_receivers, coordinator_senders) = self .coordinator_channels .take() @@ -91,7 +101,7 @@ impl RunLoop { // Initialize and start the burnchain. let mut burnchain = - BitcoinRegtestController::new(self.config.clone(), Some(coordinator_senders.clone())); + BitcoinRegtestController::with_burnchain(self.config.clone(), Some(coordinator_senders.clone()), burnchain_opt); let pox_constants = burnchain.get_pox_constants(); let is_miner = if self.config.node.miner { @@ -136,7 +146,7 @@ impl RunLoop { .iter() .map(|e| (e.address.clone(), e.amount)) .collect(); - let burnchain_poll_time = 30; // TODO: this is testnet-specific + let burnchain_poll_time = BURNCHAIN_POLL_TIME; // setup dispatcher let mut event_dispatcher = EventDispatcher::new(); @@ -145,17 +155,7 @@ impl RunLoop { } let mut coordinator_dispatcher = event_dispatcher.clone(); - let burnchain_config = match Burnchain::new( - &self.config.get_burn_db_path(), - &self.config.burnchain.chain, - "regtest", - ) { - Ok(burnchain) => burnchain, - Err(e) => { - error!("Failed to instantiate burnchain: {}", e); - panic!() - } - }; + let burnchain_config = burnchain.get_burnchain(); let chainstate_path = self.config.get_chainstate_path(); let coordinator_burnchain_config = burnchain_config.clone(); @@ -178,7 +178,7 @@ impl RunLoop { let mut block_height = burnchain_tip.block_snapshot.block_height; // setup genesis - let node = NeonGenesisNode::new(self.config.clone(), event_dispatcher, |_| {}); + let node = NeonGenesisNode::new(self.config.clone(), event_dispatcher, burnchain_config.clone(), |_| {}); let mut node = if is_miner { node.into_initialized_leader_node( burnchain_tip.clone(), @@ -214,6 +214,7 @@ impl RunLoop { chainstate_path, burnchain_poll_time, self.config.connection_options.timeout, + POX_SYNC_WAIT_MS ) .unwrap(); let mut burnchain_height = 1; diff --git a/testnet/stacks-node/src/syncctl.rs b/testnet/stacks-node/src/syncctl.rs index b46093b355b..67ccec1ba23 100644 --- a/testnet/stacks-node/src/syncctl.rs +++ b/testnet/stacks-node/src/syncctl.rs @@ -41,6 +41,8 @@ pub struct PoxSyncWatchdog { steady_state_resync_ts: u64, /// chainstate handle chainstate: StacksChainState, + /// ms to sleep on waits + wait_ms: u64 } impl PoxSyncWatchdog { @@ -50,6 +52,7 @@ impl PoxSyncWatchdog { chainstate_path: String, burnchain_poll_time: u64, download_timeout: u64, + wait_ms: u64, ) -> Result { let (chainstate, _) = match StacksChainState::open(mainnet, chain_id, &chainstate_path) { Ok(cs) => cs, @@ -75,6 +78,7 @@ impl PoxSyncWatchdog { steady_state_burnchain_sync_interval: burnchain_poll_time, steady_state_resync_ts: 0, chainstate: chainstate, + wait_ms }) } @@ -350,7 +354,7 @@ impl PoxSyncWatchdog { &self.max_samples ); } - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } @@ -359,7 +363,7 @@ impl PoxSyncWatchdog { { // still waiting for that first block in this reward cycle debug!("PoX watchdog: Still warming up: waiting until {}s for first Stacks block download (estimated download time: {}s)...", expected_first_block_deadline, self.estimated_block_download_time); - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } @@ -406,7 +410,7 @@ impl PoxSyncWatchdog { { debug!("PoX watchdog: Still processing blocks; waiting until at least min({},{})s before burnchain synchronization (estimated block-processing time: {}s)", get_epoch_time_secs() + 1, expected_last_block_deadline, self.estimated_block_process_time); - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } @@ -418,7 +422,7 @@ impl PoxSyncWatchdog { flat_attachable, flat_processed, &attachable_deviants, &processed_deviants); if !flat_attachable || !flat_processed { - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } } else { @@ -429,7 +433,7 @@ impl PoxSyncWatchdog { debug!("PoX watchdog: In steady-state; waiting until at least {} before burnchain synchronization", self.steady_state_resync_ts); steady_state = true; } - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 87a72fc2330..8e1bc388260 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -2,7 +2,7 @@ use super::{ make_contract_call, make_contract_publish, make_contract_publish_microblock_only, make_microblock, make_stacks_transfer_mblock_only, to_addr, ADDR_4, SK_1, }; -use stacks::burnchains::{Address, PublicKey}; +use stacks::burnchains::{Address, PublicKey, PoxConstants}; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::stacks::{ db::StacksChainState, StacksAddress, StacksBlock, StacksBlockHeader, StacksPrivateKey, @@ -14,6 +14,7 @@ use stacks::vm::costs::ExecutionCost; use stacks::vm::execute; use stacks::vm::types::PrincipalData; use stacks::vm::Value; +use stacks::core; use super::bitcoin_regtest::BitcoinCoreController; use crate::{ @@ -212,7 +213,7 @@ fn bitcoind_integration_test() { let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, None)); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -290,7 +291,7 @@ fn microblock_integration_test() { let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, None)); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -577,7 +578,7 @@ fn size_check_integration_test() { let client = reqwest::blocking::Client::new(); let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, None)); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -713,8 +714,8 @@ fn pox_integration_test() { let (mut conf, miner_account) = neon_integration_test_conf(); - let total_bal = 10_000_000_000; - let stacked_bal = 1_000_000_000; + let total_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let stacked_bal = 1_000_000_000 * (core::MICROSTACKS_PER_STACKS as u128); conf.initial_balances.push(InitialBalance { address: spender_addr.clone(), @@ -727,19 +728,23 @@ fn pox_integration_test() { .map_err(|_e| ()) .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); let http_origin = format!("http://{}", &conf.node.rpc_bind); + let mut burnchain_config = btc_regtest_controller.get_burnchain(); + burnchain_config.pox_constants = PoxConstants::new(10, 5, 4, 5); + btc_regtest_controller.bootstrap_chain(201); eprintln!("Chain bootstrapped..."); - let mut run_loop = neon::RunLoop::new(conf); + let mut run_loop = neon::RunLoop::new(conf.clone()); let blocks_processed = run_loop.get_blocks_processed_arc(); let client = reqwest::blocking::Client::new(); let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, Some(burnchain_config))); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -852,18 +857,24 @@ fn pox_integration_test() { assert_eq!(res.nonce, 1, "Spender address nonce should be 1"); } - // now let's mine until the next reward cycle starts ... - for _i in 0..35 { + let mut sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); + // now let's mine until the next reward cycle finishes ... + + while sort_height < 229 { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); } - // we should have received a Bitcoin commitment + // we should have received _three_ Bitcoin commitments, because our commitment was 3 * threshold let utxos = btc_regtest_controller - .get_utxos(&pox_pubkey, 1) - .expect("Should have been able to retrieve UTXOs for PoX recipient"); + .get_all_utxos(&pox_pubkey); eprintln!("Got UTXOs: {}", utxos.len()); - assert!(utxos.len() > 0, "Should have received an output during PoX"); + assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + + // okay, the threshold for participation should be channel.stop_chains_coordinator(); } From 49605a7a39b61dd6a70d439d62039cc902be4e84 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 17:21:58 -0500 Subject: [PATCH 11/26] don't remove expired entries --- src/chainstate/stacks/boot/pox.clar | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index a989407d749..0ad0960e3b2 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -180,9 +180,7 @@ false))) ;; is the caller allowance expired? (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true)) - (begin (map-delete allowance-contract-callers - { sender: tx-sender, contract-caller: contract-caller }) - false) + false true)))) (define-private (get-check-delegation (stacker principal)) @@ -191,9 +189,8 @@ (if (match (get until-burn-ht delegation-info) until-burn-ht (> burn-block-height until-burn-ht) false) - ;; it expired, delete the entry and return none - (begin (map-delete delegation-state { stacker: stacker }) - none) + ;; it expired, return none + none ;; delegation is active (some delegation-info)))) From d05980e7919b0e3ba16b9b326360b962c693ff94 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 18:01:01 -0500 Subject: [PATCH 12/26] sum stacked amounts across multiple entries from same address + int and unit tests --- src/chainstate/coordinator/mod.rs | 16 +-- src/chainstate/stacks/boot/mod.rs | 43 +++++- .../src/tests/neon_integrations.rs | 129 +++++++++++++++++- 3 files changed, 172 insertions(+), 16 deletions(-) diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index 392ede05755..d213a46143d 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -164,8 +164,9 @@ impl RewardSetProvider for OnChainRewardSetProvider { sortdb: &SortitionDB, block_id: &StacksBlockId, ) -> Result, Error> { - let res = + let registered_addrs = chainstate.get_reward_addresses(burnchain, sortdb, current_burn_height, block_id)?; + let liquid_ustx = StacksChainState::get_stacks_block_header_info_by_index_block_hash( chainstate.headers_db(), block_id)? @@ -174,19 +175,10 @@ impl RewardSetProvider for OnChainRewardSetProvider { let threshold = StacksChainState::get_reward_threshold( &burnchain.pox_constants, - &res, + ®istered_addrs, liquid_ustx); - let mut addresses = vec![]; - - for (address, stacked_amt) in res.iter() { - let slots_taken = u32::try_from(stacked_amt / threshold) - .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); - for _i in 0..slots_taken { - addresses.push(address.clone()); - } - } - Ok(addresses) + Ok(StacksChainState::make_reward_set(threshold, registered_addrs)) } } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 74b40affad6..6625dfb3425 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -189,6 +189,34 @@ impl StacksChainState { .map(|value| value.expect_bool()) } + /// Given a threshold and set of registered addresses, return a reward set where + /// every entry address has stacked more than the threshold, and addresses + /// are repeated floor(stacked_amt / threshold) times. + /// If an address appears in `addresses` multiple times, then the address's associated amounts + /// are summed. + pub fn make_reward_set(threshold: u128, mut addresses: Vec<(StacksAddress, u128)>) -> Vec { + let mut reward_set = vec![]; + // the way that we sum addresses relies on sorting. + addresses.sort_by_key(|k| k.0.bytes.0); + while let Some((address, mut stacked_amt)) = addresses.pop() { + // peak at the next address in the set, and see if we need to sum + while addresses.last().map(|x| &x.0) == Some(&address) { + let (_, additional_amt) = addresses.pop() + .expect("BUG: first() returned some, but pop() is none."); + stacked_amt = stacked_amt.checked_add(additional_amt) + .expect("CORRUPTION: Stacker stacked > u128 max amount"); + } + let slots_taken = u32::try_from(stacked_amt / threshold) + .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); + info!("Slots taken by {} = {}, on stacked_amt = {}", + &address, slots_taken, stacked_amt); + for _i in 0..slots_taken { + reward_set.push(address.clone()); + } + } + reward_set + } + pub fn get_reward_threshold(pox_settings: &PoxConstants, addresses: &[(StacksAddress, u128)], liquid_ustx: u128) -> u128 { let participation = addresses .iter() @@ -282,8 +310,6 @@ impl StacksChainState { ret.push((StacksAddress::new(version, hash), total_ustx)); } - ret.sort_by_key(|k| k.0.bytes.0); - Ok(ret) } } @@ -321,6 +347,15 @@ pub mod test { use util::hash::to_hex; + #[test] + fn make_reward_set_units() { + let threshold = 1_000; + let addresses = vec![(StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), 1500), + (StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), 500), + (StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), 1500), + (StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), 400)]; + assert_eq!(StacksChainState::make_reward_set(threshold, addresses).len(), 3); + } #[test] fn get_reward_threshold_units() { @@ -815,6 +850,10 @@ pub mod test { ) -> Result, Error> { let burn_block_height = get_par_burn_block_height(state, block_id); state.get_reward_addresses(burnchain, sortdb, burn_block_height, block_id) + .and_then(|mut addrs| { + addrs.sort_by_key(|k| k.0.bytes.0); + Ok(addrs) + }) } fn get_parent_tip( diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 8e1bc388260..f0b89a23612 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -702,6 +702,12 @@ fn pox_integration_test() { let spender_sk = StacksPrivateKey::new(); let spender_addr: PrincipalData = to_addr(&spender_sk).into(); + let spender_2_sk = StacksPrivateKey::new(); + let spender_2_addr: PrincipalData = to_addr(&spender_2_sk).into(); + + let spender_3_sk = StacksPrivateKey::new(); + let spender_3_addr: PrincipalData = to_addr(&spender_3_sk).into(); + let pox_pubkey = Secp256k1PublicKey::from_hex( "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", ) @@ -712,14 +718,36 @@ fn pox_integration_test() { .to_vec(), ); + let pox_2_pubkey = Secp256k1PublicKey::from_private(&StacksPrivateKey::new()); + let pox_2_pubkey_hash = bytes_to_hex( + &Hash160::from_data(&pox_2_pubkey.to_bytes()) + .to_bytes() + .to_vec(), + ); + + let (mut conf, miner_account) = neon_integration_test_conf(); let total_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + + let first_bal = 6_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let second_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let third_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); let stacked_bal = 1_000_000_000 * (core::MICROSTACKS_PER_STACKS as u128); conf.initial_balances.push(InitialBalance { address: spender_addr.clone(), - amount: total_bal, + amount: first_bal, + }); + + conf.initial_balances.push(InitialBalance { + address: spender_2_addr.clone(), + amount: second_bal, + }); + + conf.initial_balances.push(InitialBalance { + address: spender_3_addr.clone(), + amount: third_bal, }); let mut btcd_controller = BitcoinCoreController::new(conf.clone()); @@ -784,7 +812,7 @@ fn pox_integration_test() { .unwrap(); assert_eq!( u128::from_str_radix(&res.balance[2..], 16).unwrap(), - total_bal as u128 + first_bal as u128 ); assert_eq!(res.nonce, 0); @@ -830,6 +858,94 @@ fn pox_integration_test() { panic!(""); } + // now let's have sender_2 and sender_3 stack to pox addr 2 in + // two different txs, and make sure that they sum together in the reward set. + + let tx = make_contract_call( + &spender_2_sk, + 0, + 243, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox", + "stack-stx", + &[ + Value::UInt(stacked_bal / 2), + execute(&format!( + "{{ hashbytes: 0x{}, version: 0x00 }}", + pox_2_pubkey_hash + )) + .unwrap() + .unwrap(), + Value::UInt(3), + ], + ); + + // okay, let's push that stacking transaction! + let path = format!("{}/v2/transactions", &http_origin); + let res = client + .post(&path) + .header("Content-Type", "application/octet-stream") + .body(tx.clone()) + .send() + .unwrap(); + eprintln!("{:#?}", res); + if res.status().is_success() { + let res: String = res.json().unwrap(); + assert_eq!( + res, + StacksTransaction::consensus_deserialize(&mut &tx[..]) + .unwrap() + .txid() + .to_string() + ); + } else { + eprintln!("{}", res.text().unwrap()); + panic!(""); + } + + let tx = make_contract_call( + &spender_3_sk, + 0, + 243, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox", + "stack-stx", + &[ + Value::UInt(stacked_bal / 2), + execute(&format!( + "{{ hashbytes: 0x{}, version: 0x00 }}", + pox_2_pubkey_hash + )) + .unwrap() + .unwrap(), + Value::UInt(3), + ], + ); + + // okay, let's push that stacking transaction! + let path = format!("{}/v2/transactions", &http_origin); + let res = client + .post(&path) + .header("Content-Type", "application/octet-stream") + .body(tx.clone()) + .send() + .unwrap(); + eprintln!("{:#?}", res); + if res.status().is_success() { + let res: String = res.json().unwrap(); + assert_eq!( + res, + StacksTransaction::consensus_deserialize(&mut &tx[..]) + .unwrap() + .txid() + .to_string() + ); + } else { + eprintln!("{}", res.text().unwrap()); + panic!(""); + } + + // now let's mine a couple blocks, and then check the sender's nonce. // at the end of mining three blocks, there should be _one_ transaction from the microblock // only set that got mined (since the block before this one was empty, a microblock can @@ -874,6 +990,15 @@ fn pox_integration_test() { eprintln!("Got UTXOs: {}", utxos.len()); assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + // we should have received _three_ Bitcoin commitments to pox_2_pubkey, because our commitment was 3 * threshold + // note: that if the reward set "summing" isn't implemented, this recipient would only have received _2_ slots, + // because each `stack-stx` call only received enough to get 1 slot individually. + let utxos = btc_regtest_controller + .get_all_utxos(&pox_2_pubkey); + + eprintln!("Got UTXOs: {}", utxos.len()); + assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + // okay, the threshold for participation should be channel.stop_chains_coordinator(); From 196a2803302d39eb71835bcdaa854af743201c17 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 5 Oct 2020 14:57:27 -0500 Subject: [PATCH 13/26] threshold scaling + reward address repeating --- src/burnchains/mod.rs | 4 ++++ src/chainstate/coordinator/mod.rs | 25 +++++++++++++++++++++++-- src/chainstate/stacks/boot/mod.rs | 27 ++++++++++++++++++++++++++- src/chainstate/stacks/db/headers.rs | 11 +++-------- src/core/mod.rs | 2 ++ 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index c8c41a7450b..51508971990 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -308,6 +308,10 @@ impl PoxConstants { PoxConstants::new(10, 5, 3, 25) } + pub fn reward_slots(&self) -> u32 { + self.reward_cycle_length + } + pub fn mainnet_default() -> PoxConstants { PoxConstants::new(1000, 240, 192, 25) } diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index ca9e6ba68df..5e0b84bc2e0 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -1,5 +1,7 @@ use std::collections::VecDeque; -use std::convert::TryInto; +use std::convert::{ + TryInto, TryFrom +}; use std::time::Duration; use burnchains::{ @@ -163,7 +165,26 @@ impl RewardSetProvider for OnChainRewardSetProvider { ) -> Result, Error> { let res = chainstate.get_reward_addresses(burnchain, sortdb, current_burn_height, block_id)?; - let addresses = res.iter().map(|a| a.0).collect::>(); + let liquid_ustx = StacksChainState::get_stacks_block_header_info_by_index_block_hash( + chainstate.headers_db(), + block_id)? + .expect("CORRUPTION: Failed to look up block header info for PoX anchor block") + .total_liquid_ustx; + + let threshold = StacksChainState::get_reward_threshold( + &burnchain.pox_constants, + &res, + liquid_ustx); + let mut addresses = vec![]; + + for (address, stacked_amt) in res.iter() { + let slots_taken = u32::try_from(stacked_amt / threshold) + .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); + for _i in 0..slots_taken { + addresses.push(address.clone()); + } + } + Ok(addresses) } } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 4e0e357787f..6b9e3bcbae4 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -24,8 +24,13 @@ use chainstate::stacks::StacksBlockHeader; use address::AddressHashMode; use burnchains::bitcoin::address::BitcoinAddress; -use burnchains::Address; +use burnchains::{ + Address, PoxConstants +}; +use core::{ + POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS +}; use chainstate::burn::db::sortdb::SortitionDB; use vm::types::{ @@ -44,6 +49,7 @@ use util::hash::Hash160; use std::boxed::Box; use std::convert::TryFrom; use std::convert::TryInto; +use std::cmp; pub const STACKS_BOOT_CODE_CONTRACT_ADDRESS: &'static str = "ST000000000000000000002AMW42H"; @@ -183,6 +189,25 @@ impl StacksChainState { .map(|value| value.expect_bool()) } + pub fn get_reward_threshold(pox_settings: &PoxConstants, addresses: &[(StacksAddress, u128)], liquid_ustx: u128) -> u128 { + let participation = addresses + .iter() + .fold(0, |agg, (_, stacked_amt)| agg + stacked_amt); + + assert!(participation <= liquid_ustx, "CORRUPTION: More stacking participation than liquid STX"); + + let scale_by = cmp::max(participation, liquid_ustx / POX_MAXIMAL_SCALING as u128); + + let reward_slots = pox_settings.reward_slots() as u128; + let threshold_precise = scale_by / reward_slots; + // compute the threshold as nearest 10k > threshold_precise + let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS { + 0 => 0, + remainder => POX_THRESHOLD_STEPS - remainder, + }; + threshold_precise + ceil_amount + } + /// Each address will have at least (get-stacking-minimum) tokens. pub fn get_reward_addresses( &mut self, diff --git a/src/chainstate/stacks/db/headers.rs b/src/chainstate/stacks/db/headers.rs index 1dd1ec89f01..6efd80ddde6 100644 --- a/src/chainstate/stacks/db/headers.rs +++ b/src/chainstate/stacks/db/headers.rs @@ -37,6 +37,7 @@ use vm::costs::ExecutionCost; use util::db::Error as db_error; use util::db::{ query_count, query_row, query_row_columns, query_rows, DBConn, FromColumn, FromRow, + query_row_panic }; use core::FIRST_BURNCHAIN_CONSENSUS_HASH; @@ -278,14 +279,8 @@ impl StacksChainState { index_block_hash: &StacksBlockId, ) -> Result, Error> { let sql = "SELECT * FROM block_headers WHERE index_block_hash = ?1".to_string(); - let mut rows = query_rows::(conn, &sql, &[&index_block_hash]) - .map_err(Error::DBError)?; - let cnt = rows.len(); - if cnt > 1 { - unreachable!("FATAL: multiple rows for the same block hash") // should be unreachable, since index_block_hash is unique - } - - Ok(rows.pop()) + query_row_panic(conn, &sql, &[&index_block_hash], || "FATAL: multiple rows for the same block hash".to_string()) + .map_err(Error::DBError) } /// Get an ancestor block header diff --git a/src/core/mod.rs b/src/core/mod.rs index 1eb8c509d42..9dab580e957 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -63,6 +63,8 @@ pub const CHAINSTATE_VERSION: &'static str = "23.0.0.0"; pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240; pub const POX_REWARD_CYCLE_LENGTH: u32 = 1000; +pub const POX_MAXIMAL_SCALING: u128 = 4; +pub const POX_THRESHOLD_STEPS: u128 = 10_000; /// Synchronize burn transactions from the Bitcoin blockchain pub fn sync_burnchain_bitcoin( From 6d81a5e2a8d478ba22c41250a2f73dbe8a073e0e Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 13:23:04 -0500 Subject: [PATCH 14/26] unit+int tests for threshold/addr repeats. write burnchain config to pox contract at boot. parameterize syncctl wait times for testing --- src/chainstate/coordinator/mod.rs | 24 +++- src/chainstate/stacks/boot/mod.rs | 58 ++++++++- src/chainstate/stacks/boot/pox.clar | 2 +- src/core/mod.rs | 4 +- .../burnchains/bitcoin_regtest_controller.rs | 114 ++++++++++++++++-- testnet/stacks-node/src/main.rs | 2 +- testnet/stacks-node/src/neon_node.rs | 15 ++- testnet/stacks-node/src/run_loop/neon.rs | 31 ++--- testnet/stacks-node/src/syncctl.rs | 14 ++- .../src/tests/neon_integrations.rs | 39 +++--- 10 files changed, 243 insertions(+), 60 deletions(-) diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index 5e0b84bc2e0..392ede05755 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -15,12 +15,13 @@ use chainstate::burn::{ }; use chainstate::stacks::{ db::{ClarityTx, StacksChainState, StacksHeaderInfo}, + boot::STACKS_BOOT_CODE_CONTRACT_ADDRESS, events::StacksTransactionReceipt, Error as ChainstateError, StacksAddress, StacksBlock, StacksBlockHeader, StacksBlockId, }; use monitoring::increment_stx_blocks_processed_counter; use util::db::Error as DBError; -use vm::{costs::ExecutionCost, types::PrincipalData}; +use vm::{costs::ExecutionCost, Value, types::{PrincipalData, QualifiedContractIdentifier}}; pub mod comm; use chainstate::stacks::index::MarfTrieId; @@ -217,7 +218,26 @@ impl<'a, T: BlockEventDispatcher> stacks_chain_id, chain_state_path, initial_balances, - boot_block_exec, + |clarity_tx| { + let burnchain = burnchain.clone(); + let contract = QualifiedContractIdentifier::parse(&format!("{}.pox", STACKS_BOOT_CODE_CONTRACT_ADDRESS)) + .expect("Failed to construct boot code contract address"); + let sender = PrincipalData::from(contract.clone()); + + clarity_tx.connection().as_transaction(|conn| { + conn.run_contract_call( + &sender, + &contract, + "set-burnchain-parameters", + &[Value::UInt(burnchain.first_block_height as u128), + Value::UInt(burnchain.pox_constants.prepare_length as u128), + Value::UInt(burnchain.pox_constants.reward_cycle_length as u128), + Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128)], + |_,_| false) + .expect("Failed to set burnchain parameters in PoX contract"); + }); + boot_block_exec(clarity_tx) + }, block_limit, ) .unwrap(); diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 6b9e3bcbae4..74b40affad6 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -29,7 +29,7 @@ use burnchains::{ }; use core::{ - POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS + POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX }; use chainstate::burn::db::sortdb::SortitionDB; @@ -201,11 +201,13 @@ impl StacksChainState { let reward_slots = pox_settings.reward_slots() as u128; let threshold_precise = scale_by / reward_slots; // compute the threshold as nearest 10k > threshold_precise - let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS { + let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS_USTX { 0 => 0, - remainder => POX_THRESHOLD_STEPS - remainder, + remainder => POX_THRESHOLD_STEPS_USTX - remainder, }; - threshold_precise + ceil_amount + let threshold = threshold_precise + ceil_amount; + info!("PoX participation threshold is {}, from {}", threshold, threshold_precise); + threshold } /// Each address will have at least (get-stacking-minimum) tokens. @@ -312,12 +314,60 @@ pub mod test { use vm::contracts::Contract; use vm::types::*; + use core::*; use std::convert::From; use std::fs; use util::hash::to_hex; + + #[test] + fn get_reward_threshold_units() { + // when the liquid amount = the threshold step, + // the threshold should always be the step size. + let liquid = POX_THRESHOLD_STEPS_USTX; + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + POX_THRESHOLD_STEPS_USTX); + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid), + POX_THRESHOLD_STEPS_USTX); + + let liquid = 200_000_000 * MICROSTACKS_PER_STACKS as u128; + // with zero participation, should scale to 25% of liquid + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + 50_000 * MICROSTACKS_PER_STACKS as u128); + // should be the same at 25% participation + assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid / 4)], liquid), + 50_000 * MICROSTACKS_PER_STACKS as u128); + // but not at 30% participation + assert_eq!(StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid / 4), + (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128))], + liquid), + 60_000 * MICROSTACKS_PER_STACKS as u128); + + // bump by just a little bit, should go to the next threshold step + assert_eq!(StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid / 4), + (rand_addr(), (MICROSTACKS_PER_STACKS as u128))], + liquid), + 60_000 * MICROSTACKS_PER_STACKS as u128); + + // bump by just a little bit, should go to the next threshold step + assert_eq!(StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid)], + liquid), + 200_000 * MICROSTACKS_PER_STACKS as u128); + + } + + fn rand_addr() -> StacksAddress { + key_to_stacks_addr(&StacksPrivateKey::new()) + } + fn key_to_stacks_addr(key: &StacksPrivateKey) -> StacksAddress { StacksAddress::from_public_keys( C32_ADDRESS_VERSION_TESTNET_SINGLESIG, diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index 0ad0960e3b2..32fcb84ec63 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -31,7 +31,7 @@ ;; This function can only be called once, when it boots up (define-public (set-burnchain-parameters (first-burn-height uint) (prepare-cycle-length uint) (reward-cycle-length uint) (rejection-fraction uint)) (begin - (asserts! (and is-in-regtest (not (var-get configured))) (err ERR_NOT_ALLOWED)) + (asserts! (not (var-get configured)) (err ERR_NOT_ALLOWED)) (var-set first-burnchain-block-height first-burn-height) (var-set pox-prepare-cycle-length prepare-cycle-length) (var-set pox-reward-cycle-length reward-cycle-length) diff --git a/src/core/mod.rs b/src/core/mod.rs index 9dab580e957..79d89e4bd4b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -61,10 +61,12 @@ pub const BURNCHAIN_BOOT_CONSENSUS_HASH: ConsensusHash = ConsensusHash([0xff; 20 pub const CHAINSTATE_VERSION: &'static str = "23.0.0.0"; +pub const MICROSTACKS_PER_STACKS: u32 = 1_000_000; + pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240; pub const POX_REWARD_CYCLE_LENGTH: u32 = 1000; pub const POX_MAXIMAL_SCALING: u128 = 4; -pub const POX_THRESHOLD_STEPS: u128 = 10_000; +pub const POX_THRESHOLD_STEPS_USTX: u128 = 10_000 * (MICROSTACKS_PER_STACKS as u128); /// Synchronize burn transactions from the Bitcoin blockchain pub fn sync_burnchain_bitcoin( diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index d704af243d5..413b06ad61d 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -53,12 +53,17 @@ pub struct BitcoinRegtestController { burnchain_db: Option, chain_tip: Option, use_coordinator: Option, + burnchain_config: Option, } const DUST_UTXO_LIMIT: u64 = 5500; impl BitcoinRegtestController { pub fn new(config: Config, coordinator_channel: Option) -> Self { + BitcoinRegtestController::with_burnchain(config, coordinator_channel, None) + } + + pub fn with_burnchain(config: Config, coordinator_channel: Option, burnchain_config: Option) -> Self { std::fs::create_dir_all(&config.node.get_burnchain_path()) .expect("Unable to create workdir"); @@ -93,11 +98,12 @@ impl BitcoinRegtestController { Self { use_coordinator: coordinator_channel, - config: config, + config, indexer_config, db: None, burnchain_db: None, chain_tip: None, + burnchain_config } } @@ -122,22 +128,28 @@ impl BitcoinRegtestController { Self { use_coordinator: None, - config: config, + config, indexer_config, db: None, burnchain_db: None, chain_tip: None, + burnchain_config: None } } fn setup_burnchain(&self) -> (Burnchain, BitcoinNetworkType) { let (network_name, network_type) = self.config.burnchain.get_bitcoin_network(); - let working_dir = self.config.get_burn_db_path(); - match Burnchain::new(&working_dir, &self.config.burnchain.chain, &network_name) { - Ok(burnchain) => (burnchain, network_type), - Err(e) => { - error!("Failed to instantiate burnchain: {}", e); - panic!() + match &self.burnchain_config { + Some(burnchain) => (burnchain.clone(), network_type), + None => { + let working_dir = self.config.get_burn_db_path(); + match Burnchain::new(&working_dir, &self.config.burnchain.chain, &network_name) { + Ok(burnchain) => (burnchain, network_type), + Err(e) => { + error!("Failed to instantiate burnchain: {}", e); + panic!() + } + } } } } @@ -312,6 +324,90 @@ impl BitcoinRegtestController { Ok((burnchain_tip, burnchain_height)) } + #[cfg(test)] + pub fn get_all_utxos(&self, public_key: &Secp256k1PublicKey) -> Vec { + // Configure UTXO filter + let pkh = Hash160::from_data(&public_key.to_bytes()) + .to_bytes() + .to_vec(); + let (_, network_id) = self.config.burnchain.get_bitcoin_network(); + let address = + BitcoinAddress::from_bytes(network_id, BitcoinAddressType::PublicKeyHash, &pkh) + .expect("Public key incorrect"); + let filter_addresses = vec![address.to_b58()]; + let _result = BitcoinRPCRequest::import_public_key(&self.config, &public_key); + + sleep_ms(1000); + + let min_conf = 0; + let max_conf = 9999999; + let minimum_amount = ParsedUTXO::sat_to_serialized_btc(1); + + let payload = BitcoinRPCRequest { + method: "listunspent".to_string(), + params: vec![ + min_conf.into(), + max_conf.into(), + filter_addresses.clone().into(), + true.into(), + json!({ "minimumAmount": minimum_amount }), + ], + id: "stacks".to_string(), + jsonrpc: "2.0".to_string(), + }; + + let mut res = BitcoinRPCRequest::send(&self.config, payload).unwrap(); + let mut result_vec = vec![]; + + if let Some(ref mut object) = res.as_object_mut() { + match object.get_mut("result") { + Some(serde_json::Value::Array(entries)) => { + while let Some(entry) = entries.pop() { + let parsed_utxo: ParsedUTXO = match serde_json::from_value(entry) { + Ok(utxo) => utxo, + Err(err) => { + warn!("Failed parsing UTXO: {}", err); + continue; + } + }; + let amount = match parsed_utxo.get_sat_amount() { + Some(amount) => amount, + None => continue, + }; + + if amount < 1 { + continue; + } + + let script_pub_key = match parsed_utxo.get_script_pub_key() { + Some(script_pub_key) => script_pub_key, + None => { + continue; + } + }; + + let txid = match parsed_utxo.get_txid() { + Some(amount) => amount, + None => continue, + }; + + result_vec.push(UTXO { + txid, + vout: parsed_utxo.vout, + script_pub_key, + amount, + }); + } + } + _ => { + warn!("Failed to get UTXOs"); + } + } + } + + result_vec + } + pub fn get_utxos( &self, public_key: &Secp256k1PublicKey, @@ -382,7 +478,7 @@ impl BitcoinRegtestController { let total_unspent: u64 = utxos.iter().map(|o| o.amount).sum(); if total_unspent < amount_required { - debug!( + warn!( "Total unspent {} < {} for {:?}", total_unspent, amount_required, diff --git a/testnet/stacks-node/src/main.rs b/testnet/stacks-node/src/main.rs index 62d04ceef62..a9cab5da31d 100644 --- a/testnet/stacks-node/src/main.rs +++ b/testnet/stacks-node/src/main.rs @@ -139,7 +139,7 @@ fn main() { || conf.burnchain.mode == "xenon" { let mut run_loop = neon::RunLoop::new(conf); - run_loop.start(num_round); + run_loop.start(num_round, None); } else { println!("Burnchain mode '{}' not supported", conf.burnchain.mode); } diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 39691f9b39c..a08c412c17d 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -85,6 +85,7 @@ pub struct NeonGenesisNode { pub config: Config, keychain: Keychain, event_dispatcher: EventDispatcher, + burnchain: Burnchain } #[cfg(test)] @@ -596,19 +597,13 @@ impl InitializedNeonNode { miner: bool, blocks_processed: BlocksProcessedCounter, coord_comms: CoordinatorChannels, + burnchain: Burnchain ) -> InitializedNeonNode { // we can call _open_ here rather than _connect_, since connect is first called in // make_genesis_block let sortdb = SortitionDB::open(&config.get_burn_db_file_path(), false) .expect("Error while instantiating sortition db"); - let burnchain = Burnchain::new( - &config.get_burn_db_path(), - &config.burnchain.chain, - "regtest", - ) - .expect("Error while instantiating burnchain"); - let view = { let ic = sortdb.index_conn(); let sortition_tip = SortitionDB::get_canonical_burn_chain_tip(&ic) @@ -1036,6 +1031,7 @@ impl InitializedNeonNode { /// Process a state coming from the burnchain, by extracting the validated KeyRegisterOp /// and inspecting if a sortition was won. + /// `ibd`: boolean indicating whether or not we are in the initial block download pub fn process_burnchain_state( &mut self, sortdb: &SortitionDB, @@ -1117,7 +1113,7 @@ impl InitializedNeonNode { impl NeonGenesisNode { /// Instantiate and initialize a new node, given a config - pub fn new(config: Config, mut event_dispatcher: EventDispatcher, boot_block_exec: F) -> Self + pub fn new(config: Config, mut event_dispatcher: EventDispatcher, burnchain: Burnchain, boot_block_exec: F) -> Self where F: FnOnce(&mut ClarityTx) -> (), { @@ -1151,6 +1147,7 @@ impl NeonGenesisNode { keychain, config, event_dispatcher, + burnchain } } @@ -1172,6 +1169,7 @@ impl NeonGenesisNode { true, blocks_processed, coord_comms, + self.burnchain ) } @@ -1193,6 +1191,7 @@ impl NeonGenesisNode { false, blocks_processed, coord_comms, + self.burnchain ) } } diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index b471beeab28..48489488c1b 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -33,6 +33,16 @@ pub struct RunLoop { coordinator_channels: Option<(CoordinatorReceivers, CoordinatorChannels)>, } +#[cfg(not(test))] +const BURNCHAIN_POLL_TIME: u64 = 30; // TODO: this is testnet-specific +#[cfg(test)] +const BURNCHAIN_POLL_TIME: u64 = 1; // TODO: this is testnet-specific + +#[cfg(not(test))] +const POX_SYNC_WAIT_MS: u64 = 1000; +#[cfg(test)] +const POX_SYNC_WAIT_MS: u64 = 0; + impl RunLoop { /// Sets up a runloop and node, given a config. #[cfg(not(test))] @@ -83,7 +93,7 @@ impl RunLoop { /// It will start the burnchain (separate thread), set-up a channel in /// charge of coordinating the new blocks coming from the burnchain and /// the nodes, taking turns on tenures. - pub fn start(&mut self, _expected_num_rounds: u64) { + pub fn start(&mut self, _expected_num_rounds: u64, burnchain_opt: Option) { let (coordinator_receivers, coordinator_senders) = self .coordinator_channels .take() @@ -91,7 +101,7 @@ impl RunLoop { // Initialize and start the burnchain. let mut burnchain = - BitcoinRegtestController::new(self.config.clone(), Some(coordinator_senders.clone())); + BitcoinRegtestController::with_burnchain(self.config.clone(), Some(coordinator_senders.clone()), burnchain_opt); let pox_constants = burnchain.get_pox_constants(); let is_miner = if self.config.node.miner { @@ -136,7 +146,7 @@ impl RunLoop { .iter() .map(|e| (e.address.clone(), e.amount)) .collect(); - let burnchain_poll_time = 30; // TODO: this is testnet-specific + let burnchain_poll_time = BURNCHAIN_POLL_TIME; // setup dispatcher let mut event_dispatcher = EventDispatcher::new(); @@ -145,17 +155,7 @@ impl RunLoop { } let mut coordinator_dispatcher = event_dispatcher.clone(); - let burnchain_config = match Burnchain::new( - &self.config.get_burn_db_path(), - &self.config.burnchain.chain, - "regtest", - ) { - Ok(burnchain) => burnchain, - Err(e) => { - error!("Failed to instantiate burnchain: {}", e); - panic!() - } - }; + let burnchain_config = burnchain.get_burnchain(); let chainstate_path = self.config.get_chainstate_path(); let coordinator_burnchain_config = burnchain_config.clone(); @@ -178,7 +178,7 @@ impl RunLoop { let mut block_height = burnchain_tip.block_snapshot.block_height; // setup genesis - let node = NeonGenesisNode::new(self.config.clone(), event_dispatcher, |_| {}); + let node = NeonGenesisNode::new(self.config.clone(), event_dispatcher, burnchain_config.clone(), |_| {}); let mut node = if is_miner { node.into_initialized_leader_node( burnchain_tip.clone(), @@ -214,6 +214,7 @@ impl RunLoop { chainstate_path, burnchain_poll_time, self.config.connection_options.timeout, + POX_SYNC_WAIT_MS ) .unwrap(); let mut burnchain_height = 1; diff --git a/testnet/stacks-node/src/syncctl.rs b/testnet/stacks-node/src/syncctl.rs index b46093b355b..67ccec1ba23 100644 --- a/testnet/stacks-node/src/syncctl.rs +++ b/testnet/stacks-node/src/syncctl.rs @@ -41,6 +41,8 @@ pub struct PoxSyncWatchdog { steady_state_resync_ts: u64, /// chainstate handle chainstate: StacksChainState, + /// ms to sleep on waits + wait_ms: u64 } impl PoxSyncWatchdog { @@ -50,6 +52,7 @@ impl PoxSyncWatchdog { chainstate_path: String, burnchain_poll_time: u64, download_timeout: u64, + wait_ms: u64, ) -> Result { let (chainstate, _) = match StacksChainState::open(mainnet, chain_id, &chainstate_path) { Ok(cs) => cs, @@ -75,6 +78,7 @@ impl PoxSyncWatchdog { steady_state_burnchain_sync_interval: burnchain_poll_time, steady_state_resync_ts: 0, chainstate: chainstate, + wait_ms }) } @@ -350,7 +354,7 @@ impl PoxSyncWatchdog { &self.max_samples ); } - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } @@ -359,7 +363,7 @@ impl PoxSyncWatchdog { { // still waiting for that first block in this reward cycle debug!("PoX watchdog: Still warming up: waiting until {}s for first Stacks block download (estimated download time: {}s)...", expected_first_block_deadline, self.estimated_block_download_time); - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } @@ -406,7 +410,7 @@ impl PoxSyncWatchdog { { debug!("PoX watchdog: Still processing blocks; waiting until at least min({},{})s before burnchain synchronization (estimated block-processing time: {}s)", get_epoch_time_secs() + 1, expected_last_block_deadline, self.estimated_block_process_time); - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } @@ -418,7 +422,7 @@ impl PoxSyncWatchdog { flat_attachable, flat_processed, &attachable_deviants, &processed_deviants); if !flat_attachable || !flat_processed { - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } } else { @@ -429,7 +433,7 @@ impl PoxSyncWatchdog { debug!("PoX watchdog: In steady-state; waiting until at least {} before burnchain synchronization", self.steady_state_resync_ts); steady_state = true; } - sleep_ms(1000); + sleep_ms(self.wait_ms); continue; } } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 87a72fc2330..8e1bc388260 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -2,7 +2,7 @@ use super::{ make_contract_call, make_contract_publish, make_contract_publish_microblock_only, make_microblock, make_stacks_transfer_mblock_only, to_addr, ADDR_4, SK_1, }; -use stacks::burnchains::{Address, PublicKey}; +use stacks::burnchains::{Address, PublicKey, PoxConstants}; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::stacks::{ db::StacksChainState, StacksAddress, StacksBlock, StacksBlockHeader, StacksPrivateKey, @@ -14,6 +14,7 @@ use stacks::vm::costs::ExecutionCost; use stacks::vm::execute; use stacks::vm::types::PrincipalData; use stacks::vm::Value; +use stacks::core; use super::bitcoin_regtest::BitcoinCoreController; use crate::{ @@ -212,7 +213,7 @@ fn bitcoind_integration_test() { let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, None)); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -290,7 +291,7 @@ fn microblock_integration_test() { let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, None)); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -577,7 +578,7 @@ fn size_check_integration_test() { let client = reqwest::blocking::Client::new(); let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, None)); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -713,8 +714,8 @@ fn pox_integration_test() { let (mut conf, miner_account) = neon_integration_test_conf(); - let total_bal = 10_000_000_000; - let stacked_bal = 1_000_000_000; + let total_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let stacked_bal = 1_000_000_000 * (core::MICROSTACKS_PER_STACKS as u128); conf.initial_balances.push(InitialBalance { address: spender_addr.clone(), @@ -727,19 +728,23 @@ fn pox_integration_test() { .map_err(|_e| ()) .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); let http_origin = format!("http://{}", &conf.node.rpc_bind); + let mut burnchain_config = btc_regtest_controller.get_burnchain(); + burnchain_config.pox_constants = PoxConstants::new(10, 5, 4, 5); + btc_regtest_controller.bootstrap_chain(201); eprintln!("Chain bootstrapped..."); - let mut run_loop = neon::RunLoop::new(conf); + let mut run_loop = neon::RunLoop::new(conf.clone()); let blocks_processed = run_loop.get_blocks_processed_arc(); let client = reqwest::blocking::Client::new(); let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0)); + thread::spawn(move || run_loop.start(0, Some(burnchain_config))); // give the run loop some time to start up! wait_for_runloop(&blocks_processed); @@ -852,18 +857,24 @@ fn pox_integration_test() { assert_eq!(res.nonce, 1, "Spender address nonce should be 1"); } - // now let's mine until the next reward cycle starts ... - for _i in 0..35 { + let mut sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); + // now let's mine until the next reward cycle finishes ... + + while sort_height < 229 { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); } - // we should have received a Bitcoin commitment + // we should have received _three_ Bitcoin commitments, because our commitment was 3 * threshold let utxos = btc_regtest_controller - .get_utxos(&pox_pubkey, 1) - .expect("Should have been able to retrieve UTXOs for PoX recipient"); + .get_all_utxos(&pox_pubkey); eprintln!("Got UTXOs: {}", utxos.len()); - assert!(utxos.len() > 0, "Should have received an output during PoX"); + assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + + // okay, the threshold for participation should be channel.stop_chains_coordinator(); } From 98f42a83bc1a43e6d529a3ec0a7b3cf85615a707 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 6 Oct 2020 18:01:01 -0500 Subject: [PATCH 15/26] sum stacked amounts across multiple entries from same address + int and unit tests --- src/chainstate/coordinator/mod.rs | 16 +-- src/chainstate/stacks/boot/mod.rs | 43 +++++- .../src/tests/neon_integrations.rs | 129 +++++++++++++++++- 3 files changed, 172 insertions(+), 16 deletions(-) diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index 392ede05755..d213a46143d 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -164,8 +164,9 @@ impl RewardSetProvider for OnChainRewardSetProvider { sortdb: &SortitionDB, block_id: &StacksBlockId, ) -> Result, Error> { - let res = + let registered_addrs = chainstate.get_reward_addresses(burnchain, sortdb, current_burn_height, block_id)?; + let liquid_ustx = StacksChainState::get_stacks_block_header_info_by_index_block_hash( chainstate.headers_db(), block_id)? @@ -174,19 +175,10 @@ impl RewardSetProvider for OnChainRewardSetProvider { let threshold = StacksChainState::get_reward_threshold( &burnchain.pox_constants, - &res, + ®istered_addrs, liquid_ustx); - let mut addresses = vec![]; - - for (address, stacked_amt) in res.iter() { - let slots_taken = u32::try_from(stacked_amt / threshold) - .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); - for _i in 0..slots_taken { - addresses.push(address.clone()); - } - } - Ok(addresses) + Ok(StacksChainState::make_reward_set(threshold, registered_addrs)) } } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 74b40affad6..6625dfb3425 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -189,6 +189,34 @@ impl StacksChainState { .map(|value| value.expect_bool()) } + /// Given a threshold and set of registered addresses, return a reward set where + /// every entry address has stacked more than the threshold, and addresses + /// are repeated floor(stacked_amt / threshold) times. + /// If an address appears in `addresses` multiple times, then the address's associated amounts + /// are summed. + pub fn make_reward_set(threshold: u128, mut addresses: Vec<(StacksAddress, u128)>) -> Vec { + let mut reward_set = vec![]; + // the way that we sum addresses relies on sorting. + addresses.sort_by_key(|k| k.0.bytes.0); + while let Some((address, mut stacked_amt)) = addresses.pop() { + // peak at the next address in the set, and see if we need to sum + while addresses.last().map(|x| &x.0) == Some(&address) { + let (_, additional_amt) = addresses.pop() + .expect("BUG: first() returned some, but pop() is none."); + stacked_amt = stacked_amt.checked_add(additional_amt) + .expect("CORRUPTION: Stacker stacked > u128 max amount"); + } + let slots_taken = u32::try_from(stacked_amt / threshold) + .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); + info!("Slots taken by {} = {}, on stacked_amt = {}", + &address, slots_taken, stacked_amt); + for _i in 0..slots_taken { + reward_set.push(address.clone()); + } + } + reward_set + } + pub fn get_reward_threshold(pox_settings: &PoxConstants, addresses: &[(StacksAddress, u128)], liquid_ustx: u128) -> u128 { let participation = addresses .iter() @@ -282,8 +310,6 @@ impl StacksChainState { ret.push((StacksAddress::new(version, hash), total_ustx)); } - ret.sort_by_key(|k| k.0.bytes.0); - Ok(ret) } } @@ -321,6 +347,15 @@ pub mod test { use util::hash::to_hex; + #[test] + fn make_reward_set_units() { + let threshold = 1_000; + let addresses = vec![(StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), 1500), + (StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), 500), + (StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), 1500), + (StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), 400)]; + assert_eq!(StacksChainState::make_reward_set(threshold, addresses).len(), 3); + } #[test] fn get_reward_threshold_units() { @@ -815,6 +850,10 @@ pub mod test { ) -> Result, Error> { let burn_block_height = get_par_burn_block_height(state, block_id); state.get_reward_addresses(burnchain, sortdb, burn_block_height, block_id) + .and_then(|mut addrs| { + addrs.sort_by_key(|k| k.0.bytes.0); + Ok(addrs) + }) } fn get_parent_tip( diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 8e1bc388260..f0b89a23612 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -702,6 +702,12 @@ fn pox_integration_test() { let spender_sk = StacksPrivateKey::new(); let spender_addr: PrincipalData = to_addr(&spender_sk).into(); + let spender_2_sk = StacksPrivateKey::new(); + let spender_2_addr: PrincipalData = to_addr(&spender_2_sk).into(); + + let spender_3_sk = StacksPrivateKey::new(); + let spender_3_addr: PrincipalData = to_addr(&spender_3_sk).into(); + let pox_pubkey = Secp256k1PublicKey::from_hex( "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", ) @@ -712,14 +718,36 @@ fn pox_integration_test() { .to_vec(), ); + let pox_2_pubkey = Secp256k1PublicKey::from_private(&StacksPrivateKey::new()); + let pox_2_pubkey_hash = bytes_to_hex( + &Hash160::from_data(&pox_2_pubkey.to_bytes()) + .to_bytes() + .to_vec(), + ); + + let (mut conf, miner_account) = neon_integration_test_conf(); let total_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + + let first_bal = 6_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let second_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let third_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); let stacked_bal = 1_000_000_000 * (core::MICROSTACKS_PER_STACKS as u128); conf.initial_balances.push(InitialBalance { address: spender_addr.clone(), - amount: total_bal, + amount: first_bal, + }); + + conf.initial_balances.push(InitialBalance { + address: spender_2_addr.clone(), + amount: second_bal, + }); + + conf.initial_balances.push(InitialBalance { + address: spender_3_addr.clone(), + amount: third_bal, }); let mut btcd_controller = BitcoinCoreController::new(conf.clone()); @@ -784,7 +812,7 @@ fn pox_integration_test() { .unwrap(); assert_eq!( u128::from_str_radix(&res.balance[2..], 16).unwrap(), - total_bal as u128 + first_bal as u128 ); assert_eq!(res.nonce, 0); @@ -830,6 +858,94 @@ fn pox_integration_test() { panic!(""); } + // now let's have sender_2 and sender_3 stack to pox addr 2 in + // two different txs, and make sure that they sum together in the reward set. + + let tx = make_contract_call( + &spender_2_sk, + 0, + 243, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox", + "stack-stx", + &[ + Value::UInt(stacked_bal / 2), + execute(&format!( + "{{ hashbytes: 0x{}, version: 0x00 }}", + pox_2_pubkey_hash + )) + .unwrap() + .unwrap(), + Value::UInt(3), + ], + ); + + // okay, let's push that stacking transaction! + let path = format!("{}/v2/transactions", &http_origin); + let res = client + .post(&path) + .header("Content-Type", "application/octet-stream") + .body(tx.clone()) + .send() + .unwrap(); + eprintln!("{:#?}", res); + if res.status().is_success() { + let res: String = res.json().unwrap(); + assert_eq!( + res, + StacksTransaction::consensus_deserialize(&mut &tx[..]) + .unwrap() + .txid() + .to_string() + ); + } else { + eprintln!("{}", res.text().unwrap()); + panic!(""); + } + + let tx = make_contract_call( + &spender_3_sk, + 0, + 243, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox", + "stack-stx", + &[ + Value::UInt(stacked_bal / 2), + execute(&format!( + "{{ hashbytes: 0x{}, version: 0x00 }}", + pox_2_pubkey_hash + )) + .unwrap() + .unwrap(), + Value::UInt(3), + ], + ); + + // okay, let's push that stacking transaction! + let path = format!("{}/v2/transactions", &http_origin); + let res = client + .post(&path) + .header("Content-Type", "application/octet-stream") + .body(tx.clone()) + .send() + .unwrap(); + eprintln!("{:#?}", res); + if res.status().is_success() { + let res: String = res.json().unwrap(); + assert_eq!( + res, + StacksTransaction::consensus_deserialize(&mut &tx[..]) + .unwrap() + .txid() + .to_string() + ); + } else { + eprintln!("{}", res.text().unwrap()); + panic!(""); + } + + // now let's mine a couple blocks, and then check the sender's nonce. // at the end of mining three blocks, there should be _one_ transaction from the microblock // only set that got mined (since the block before this one was empty, a microblock can @@ -874,6 +990,15 @@ fn pox_integration_test() { eprintln!("Got UTXOs: {}", utxos.len()); assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + // we should have received _three_ Bitcoin commitments to pox_2_pubkey, because our commitment was 3 * threshold + // note: that if the reward set "summing" isn't implemented, this recipient would only have received _2_ slots, + // because each `stack-stx` call only received enough to get 1 slot individually. + let utxos = btc_regtest_controller + .get_all_utxos(&pox_2_pubkey); + + eprintln!("Got UTXOs: {}", utxos.len()); + assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + // okay, the threshold for participation should be channel.stop_chains_coordinator(); From 3c4aa04684a6fe65fdb4f7b0a1a061e92ea1ef6c Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 7 Oct 2020 09:29:17 -0500 Subject: [PATCH 16/26] delegator -> delegate --- src/chainstate/stacks/boot/contract_tests.rs | 24 ++++++++--------- src/chainstate/stacks/boot/pox.clar | 28 ++++++++++---------- src/vm/functions/special.rs | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index 658d74fdb9c..0ff62a7dd67 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -347,7 +347,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[0]).into(), Value::UInt(3 * USTX_PER_HOLDER), @@ -366,7 +366,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[0]).into(), Value::UInt(2 * USTX_PER_HOLDER), @@ -385,7 +385,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[0]).into(), Value::UInt(*MIN_THRESHOLD - 1), @@ -432,7 +432,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[1]).into(), Value::UInt(*MIN_THRESHOLD - 1), @@ -451,7 +451,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[0]).into(), Value::UInt(*MIN_THRESHOLD - 1), @@ -470,7 +470,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[2]).into(), Value::UInt(*MIN_THRESHOLD - 1), @@ -489,7 +489,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[2]).into(), Value::UInt(*MIN_THRESHOLD - 1), @@ -592,7 +592,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[3]).into(), Value::UInt(*MIN_THRESHOLD), @@ -702,7 +702,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[1]).into(), Value::UInt(*MIN_THRESHOLD), @@ -726,7 +726,7 @@ fn delegation_tests() { (&USER_KEYS[4]).into(), POX_CONTRACT.clone(), "revoke-delegate-stx", - &symbols_from_values(vec![(&delegator).into()]) + &[] ) .unwrap() .0, @@ -739,7 +739,7 @@ fn delegation_tests() { (&USER_KEYS[4]).into(), POX_CONTRACT.clone(), "revoke-delegate-stx", - &symbols_from_values(vec![(&delegator).into()]) + &[] ) .unwrap() .0 @@ -751,7 +751,7 @@ fn delegation_tests() { env.execute_transaction( (&delegator).into(), POX_CONTRACT.clone(), - "delegator-stack-stx", + "delegate-stack-stx", &symbols_from_values(vec![ (&USER_KEYS[4]).into(), Value::UInt(*MIN_THRESHOLD - 1), diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index 0ad0960e3b2..0dc23fe07e5 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -71,7 +71,7 @@ ((amount-ustx uint) ;; how many uSTX delegated? (delegated-to principal) ;; who are we delegating? (until-burn-ht (optional uint)) ;; how long does the delegation last? - ;; does the delegator _need_ to use a specific + ;; does the delegate _need_ to use a specific ;; pox recipient address? (pox-addr (optional { version: (buff 1), hashbytes: (buff 20) })))) @@ -458,22 +458,22 @@ (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })) ) -(define-public (revoke-delegate-stx (delegator principal)) +(define-public (revoke-delegate-stx) (begin ;; must be called directly by the tx-sender or by an allowed contract-caller (asserts! (check-caller-allowed) (err ERR_STACKING_PERMISSION_DENIED)) (ok (map-delete delegation-state { stacker: tx-sender })))) -;; Delegate to `delegator` the ability to stack from a given address. -;; This method _does not_ lock the funds, rather, it allows the delegator +;; Delegate to `delegate-to` the ability to stack from a given address. +;; This method _does not_ lock the funds, rather, it allows the delegate ;; to issue the stacking lock. ;; The caller specifies: -;; * amount-ustx: the total amount of ustx the delegator may be allowed to lock +;; * amount-ustx: the total amount of ustx the delegate may be allowed to lock ;; * until-burn-ht: an optional burn height at which this delegation expiration ;; * pox-addr: an optional address to which any rewards *must* be sent (define-public (delegate-stx (amount-ustx uint) - (delegator principal) + (delegate-to principal) (until-burn-ht (optional uint)) (pox-addr (optional { version: (buff 1), hashbytes: (buff 20) }))) @@ -494,14 +494,14 @@ (map-set delegation-state { stacker: tx-sender } { amount-ustx: amount-ustx, - delegated-to: delegator, + delegated-to: delegate-to, until-burn-ht: until-burn-ht, pox-addr: pox-addr }) (ok true))) ;; Commit partially stacked STX. -;; This allows a stacker/delegator to lock fewer STX than the minimal threshold in multiple transactions, +;; This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions, ;; so long as: 1. The pox-addr is the same. ;; 2. This "commit" transaction is called _before_ the PoX anchor block. ;; This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, @@ -533,12 +533,12 @@ (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) (ok true)))) -;; As a delegator, stack the given principal's STX using partial-stacked-by-cycle -;; Once the delegator has stacked > minimum, the delegator should call stack-aggregation-commit -(define-public (delegator-stack-stx (stacker principal) - (amount-ustx uint) - (pox-addr { version: (buff 1), hashbytes: (buff 20) }) - (lock-period uint)) +;; As a delegate, stack the given principal's STX using partial-stacked-by-cycle +;; Once the delegate has stacked > minimum, the delegate should call stack-aggregation-commit +(define-public (delegate-stack-stx (stacker principal) + (amount-ustx uint) + (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (lock-period uint)) ;; this stacker's first reward cycle is the _next_ reward cycle (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) diff --git a/src/vm/functions/special.rs b/src/vm/functions/special.rs index 8fc278c6b09..8556d59efe1 100644 --- a/src/vm/functions/special.rs +++ b/src/vm/functions/special.rs @@ -52,7 +52,7 @@ fn handle_pox_api_contract_call( function_name: &str, value: &Value, ) -> Result<()> { - if function_name == "stack-stx" || function_name == "delegator-stack-stx" { + if function_name == "stack-stx" || function_name == "delegate-stack-stx" { debug!( "Handle special-case contract-call to {:?} {} (which returned {:?})", boot_code_id("pox"), From 1ad5c6057fc5c08e9be9605273b858a87f0ab579 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 7 Oct 2020 11:56:16 -0500 Subject: [PATCH 17/26] docs: add docsgen for boot contract --- src/main.rs | 5 +++ src/vm/docs/mod.rs | 78 ++++++++++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index ae8c2429cad..705a4143225 100644 --- a/src/main.rs +++ b/src/main.rs @@ -346,6 +346,11 @@ fn main() { return; } + if argv[1] == "docgen_boot" { + println!("{}", vm::docs::contracts::make_json_boot_contracts_reference()); + return; + } + if argv[1] == "local" { clarity::invoke_command(&format!("{} {}", argv[0], argv[1]), &argv[2..]); return; diff --git a/src/vm/docs/mod.rs b/src/vm/docs/mod.rs index 224fd44d618..f91c36b1567 100644 --- a/src/vm/docs/mod.rs +++ b/src/vm/docs/mod.rs @@ -2,9 +2,11 @@ use vm::analysis::type_checker::natives::SimpleNativeFunction; use vm::analysis::type_checker::TypedNativeFunction; use vm::functions::define::DefineFunctions; use vm::functions::NativeFunctions; -use vm::types::{FixedFunction, FunctionType}; +use vm::types::{FixedFunction, FunctionType, Value}; use vm::variables::NativeVariables; +pub mod contracts; + #[derive(Serialize)] struct ReferenceAPIs { functions: Vec, @@ -292,6 +294,52 @@ const LESS_API: SimpleFunctionAPI = SimpleFunctionAPI { ", }; +pub fn get_input_type_string(function_type: &FunctionType) -> String { + match function_type { + FunctionType::Variadic(ref in_type, _) => format!("{}, ...", in_type), + FunctionType::Fixed(FixedFunction { ref args, .. }) => { + let in_types: Vec = + args.iter().map(|x| format!("{}", x.signature)).collect(); + in_types.join(", ") + } + FunctionType::UnionArgs(ref in_types, _) => { + let in_types: Vec = in_types.iter().map(|x| format!("{}", x)).collect(); + in_types.join(" | ") + } + FunctionType::ArithmeticVariadic => "int, ... | uint, ...".to_string(), + FunctionType::ArithmeticUnary => "int | uint".to_string(), + FunctionType::ArithmeticBinary | FunctionType::ArithmeticComparison => { + "int, int | uint, uint".to_string() + } + } +} + +pub fn get_output_type_string(function_type: &FunctionType) -> String { + match function_type { + FunctionType::Variadic(_, ref out_type) => format!("{}", out_type), + FunctionType::Fixed(FixedFunction { ref returns, .. }) => format!("{}", returns), + FunctionType::UnionArgs(_, ref out_type) => format!("{}", out_type), + FunctionType::ArithmeticVariadic + | FunctionType::ArithmeticUnary + | FunctionType::ArithmeticBinary => "int | uint".to_string(), + FunctionType::ArithmeticComparison => "bool".to_string(), + } +} + +pub fn get_signature(function_name: &str, function_type: &FunctionType) -> Option { + if let FunctionType::Fixed(FixedFunction { ref args, .. }) = function_type { + let in_names: Vec = + args.iter().map(|x| format!("{}", x.name.as_str())).collect(); + let arg_examples = in_names.join(" "); + Some(format!("({}{}{})", + function_name, + if arg_examples.len() == 0 { "" } else { " " }, + arg_examples)) + } else { + None + } +} + fn make_for_simple_native( api: &SimpleFunctionAPI, function: &NativeFunctions, @@ -301,32 +349,8 @@ fn make_for_simple_native( if let TypedNativeFunction::Simple(SimpleNativeFunction(function_type)) = TypedNativeFunction::type_native_function(&function) { - let input_type = match function_type { - FunctionType::Variadic(ref in_type, _) => format!("{}, ...", in_type), - FunctionType::Fixed(FixedFunction { ref args, .. }) => { - let in_types: Vec = - args.iter().map(|x| format!("{}", x.signature)).collect(); - in_types.join(", ") - } - FunctionType::UnionArgs(ref in_types, _) => { - let in_types: Vec = in_types.iter().map(|x| format!("{}", x)).collect(); - in_types.join(" | ") - } - FunctionType::ArithmeticVariadic => "int, ... | uint, ...".to_string(), - FunctionType::ArithmeticUnary => "int | uint".to_string(), - FunctionType::ArithmeticBinary | FunctionType::ArithmeticComparison => { - "int, int | uint, uint".to_string() - } - }; - let output_type = match function_type { - FunctionType::Variadic(_, ref out_type) => format!("{}", out_type), - FunctionType::Fixed(FixedFunction { ref returns, .. }) => format!("{}", returns), - FunctionType::UnionArgs(_, ref out_type) => format!("{}", out_type), - FunctionType::ArithmeticVariadic - | FunctionType::ArithmeticUnary - | FunctionType::ArithmeticBinary => "int | uint".to_string(), - FunctionType::ArithmeticComparison => "bool".to_string(), - }; + let input_type = get_input_type_string(&function_type); + let output_type = get_output_type_string(&function_type); (input_type, output_type) } else { panic!( From 4745049f2c197ef36c348b9452a8f4efc24285b2 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 7 Oct 2020 12:13:22 -0500 Subject: [PATCH 18/26] use more detailed error types --- src/chainstate/stacks/boot/contract_tests.rs | 6 ++-- src/chainstate/stacks/boot/pox.clar | 35 +++++++++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index 0ff62a7dd67..bf59f9af5a1 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -358,7 +358,7 @@ fn delegation_tests() { .unwrap() .0 .to_string(), - "(err 9)".to_string() + "(err 22)".to_string() ); // try to stack more than [0] has! @@ -443,7 +443,7 @@ fn delegation_tests() { .unwrap() .0 .to_string(), - "(err 9)".to_string() + "(err 23)".to_string() ); // And USER_KEYS[0] is already stacking... @@ -481,7 +481,7 @@ fn delegation_tests() { .unwrap() .0 .to_string(), - "(err 9)".to_string() + "(err 21)".to_string() ); // but for just one block will be fine diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index 0dc23fe07e5..e3aef4d843e 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -15,6 +15,9 @@ (define-constant ERR_STACKING_INVALID_AMOUNT 18) (define-constant ERR_NOT_ALLOWED 19) (define-constant ERR_STACKING_ALREADY_DELEGATED 20) +(define-constant ERR_DELEGATION_EXPIRES_DURING_LOCK 21) +(define-constant ERR_DELEGATION_TOO_MUCH_LOCKED 22) +(define-constant ERR_DELEGATION_POX_ADDR_REQUIRED 23) ;; PoX disabling threshold (a percent) (define-constant POX_REJECTION_FRACTION u25) @@ -548,22 +551,24 @@ (err ERR_STACKING_PERMISSION_DENIED)) ;; stacker must have delegated to the caller - (asserts! - (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) - ;; must have delegated to tx-sender - (and (is-eq (get delegated-to delegation-info) tx-sender) - ;; must have delegated enough stx - (>= (get amount-ustx delegation-info) amount-ustx) - ;; if pox-addr is set, must be equal to pox-addr - (match (get pox-addr delegation-info) - specified-pox-addr (is-eq pox-addr specified-pox-addr) + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) + ;; must have delegated to tx-sender + (asserts! (is-eq (get delegated-to delegation-info) tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; must have delegated enough stx + (asserts! (>= (get amount-ustx delegation-info) amount-ustx) + (err ERR_DELEGATION_TOO_MUCH_LOCKED)) + ;; if pox-addr is set, must be equal to pox-addr + (asserts! (match (get pox-addr delegation-info) + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + (err ERR_DELEGATION_POX_ADDR_REQUIRED)) + ;; delegation must not expire before lock period + (asserts! (match (get until-burn-ht delegation-info) + until-burn-ht (>= until-burn-ht + unlock-burn-height) true) - ;; delegation must not expire before lock period - (match (get until-burn-ht delegation-info) - until-burn-ht (>= until-burn-ht - unlock-burn-height) - true))) - (err ERR_STACKING_PERMISSION_DENIED)) + (err ERR_DELEGATION_EXPIRES_DURING_LOCK))) ;; stacker principal must not be stacking (asserts! (is-none (get-stacker-info stacker)) From 5cb5b9edbc6103915c08f931809ee1a748980752 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 7 Oct 2020 13:50:56 -0500 Subject: [PATCH 19/26] cargo fmt --- src/chainstate/coordinator/mod.rs | 51 +++--- src/chainstate/stacks/boot/mod.rs | 157 ++++++++++++------ src/chainstate/stacks/db/headers.rs | 10 +- .../burnchains/bitcoin_regtest_controller.rs | 10 +- testnet/stacks-node/src/neon_node.rs | 17 +- testnet/stacks-node/src/run_loop/neon.rs | 16 +- testnet/stacks-node/src/syncctl.rs | 4 +- .../src/tests/neon_integrations.rs | 27 +-- 8 files changed, 193 insertions(+), 99 deletions(-) diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index d213a46143d..a65bae045f8 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -1,7 +1,5 @@ use std::collections::VecDeque; -use std::convert::{ - TryInto, TryFrom -}; +use std::convert::{TryFrom, TryInto}; use std::time::Duration; use burnchains::{ @@ -14,14 +12,18 @@ use chainstate::burn::{ BlockHeaderHash, BlockSnapshot, ConsensusHash, }; use chainstate::stacks::{ - db::{ClarityTx, StacksChainState, StacksHeaderInfo}, boot::STACKS_BOOT_CODE_CONTRACT_ADDRESS, + db::{ClarityTx, StacksChainState, StacksHeaderInfo}, events::StacksTransactionReceipt, Error as ChainstateError, StacksAddress, StacksBlock, StacksBlockHeader, StacksBlockId, }; use monitoring::increment_stx_blocks_processed_counter; use util::db::Error as DBError; -use vm::{costs::ExecutionCost, Value, types::{PrincipalData, QualifiedContractIdentifier}}; +use vm::{ + costs::ExecutionCost, + types::{PrincipalData, QualifiedContractIdentifier}, + Value, +}; pub mod comm; use chainstate::stacks::index::MarfTrieId; @@ -169,16 +171,21 @@ impl RewardSetProvider for OnChainRewardSetProvider { let liquid_ustx = StacksChainState::get_stacks_block_header_info_by_index_block_hash( chainstate.headers_db(), - block_id)? - .expect("CORRUPTION: Failed to look up block header info for PoX anchor block") - .total_liquid_ustx; + block_id, + )? + .expect("CORRUPTION: Failed to look up block header info for PoX anchor block") + .total_liquid_ustx; let threshold = StacksChainState::get_reward_threshold( &burnchain.pox_constants, ®istered_addrs, - liquid_ustx); + liquid_ustx, + ); - Ok(StacksChainState::make_reward_set(threshold, registered_addrs)) + Ok(StacksChainState::make_reward_set( + threshold, + registered_addrs, + )) } } @@ -212,21 +219,27 @@ impl<'a, T: BlockEventDispatcher> initial_balances, |clarity_tx| { let burnchain = burnchain.clone(); - let contract = QualifiedContractIdentifier::parse(&format!("{}.pox", STACKS_BOOT_CODE_CONTRACT_ADDRESS)) - .expect("Failed to construct boot code contract address"); + let contract = QualifiedContractIdentifier::parse(&format!( + "{}.pox", + STACKS_BOOT_CODE_CONTRACT_ADDRESS + )) + .expect("Failed to construct boot code contract address"); let sender = PrincipalData::from(contract.clone()); - + clarity_tx.connection().as_transaction(|conn| { conn.run_contract_call( &sender, &contract, "set-burnchain-parameters", - &[Value::UInt(burnchain.first_block_height as u128), - Value::UInt(burnchain.pox_constants.prepare_length as u128), - Value::UInt(burnchain.pox_constants.reward_cycle_length as u128), - Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128)], - |_,_| false) - .expect("Failed to set burnchain parameters in PoX contract"); + &[ + Value::UInt(burnchain.first_block_height as u128), + Value::UInt(burnchain.pox_constants.prepare_length as u128), + Value::UInt(burnchain.pox_constants.reward_cycle_length as u128), + Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128), + ], + |_, _| false, + ) + .expect("Failed to set burnchain parameters in PoX contract"); }); boot_block_exec(clarity_tx) }, diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 6625dfb3425..6090ae415f2 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -24,14 +24,10 @@ use chainstate::stacks::StacksBlockHeader; use address::AddressHashMode; use burnchains::bitcoin::address::BitcoinAddress; -use burnchains::{ - Address, PoxConstants -}; +use burnchains::{Address, PoxConstants}; -use core::{ - POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX -}; use chainstate::burn::db::sortdb::SortitionDB; +use core::{POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX}; use vm::types::{ PrincipalData, QualifiedContractIdentifier, SequenceData, StandardPrincipalData, TupleData, @@ -47,9 +43,9 @@ use vm::representations::ContractName; use util::hash::Hash160; use std::boxed::Box; +use std::cmp; use std::convert::TryFrom; use std::convert::TryInto; -use std::cmp; pub const STACKS_BOOT_CODE_CONTRACT_ADDRESS: &'static str = "ST000000000000000000002AMW42H"; @@ -194,22 +190,29 @@ impl StacksChainState { /// are repeated floor(stacked_amt / threshold) times. /// If an address appears in `addresses` multiple times, then the address's associated amounts /// are summed. - pub fn make_reward_set(threshold: u128, mut addresses: Vec<(StacksAddress, u128)>) -> Vec { + pub fn make_reward_set( + threshold: u128, + mut addresses: Vec<(StacksAddress, u128)>, + ) -> Vec { let mut reward_set = vec![]; // the way that we sum addresses relies on sorting. addresses.sort_by_key(|k| k.0.bytes.0); while let Some((address, mut stacked_amt)) = addresses.pop() { // peak at the next address in the set, and see if we need to sum while addresses.last().map(|x| &x.0) == Some(&address) { - let (_, additional_amt) = addresses.pop() + let (_, additional_amt) = addresses + .pop() .expect("BUG: first() returned some, but pop() is none."); - stacked_amt = stacked_amt.checked_add(additional_amt) + stacked_amt = stacked_amt + .checked_add(additional_amt) .expect("CORRUPTION: Stacker stacked > u128 max amount"); } let slots_taken = u32::try_from(stacked_amt / threshold) .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); - info!("Slots taken by {} = {}, on stacked_amt = {}", - &address, slots_taken, stacked_amt); + info!( + "Slots taken by {} = {}, on stacked_amt = {}", + &address, slots_taken, stacked_amt + ); for _i in 0..slots_taken { reward_set.push(address.clone()); } @@ -217,12 +220,19 @@ impl StacksChainState { reward_set } - pub fn get_reward_threshold(pox_settings: &PoxConstants, addresses: &[(StacksAddress, u128)], liquid_ustx: u128) -> u128 { + pub fn get_reward_threshold( + pox_settings: &PoxConstants, + addresses: &[(StacksAddress, u128)], + liquid_ustx: u128, + ) -> u128 { let participation = addresses .iter() .fold(0, |agg, (_, stacked_amt)| agg + stacked_amt); - assert!(participation <= liquid_ustx, "CORRUPTION: More stacking participation than liquid STX"); + assert!( + participation <= liquid_ustx, + "CORRUPTION: More stacking participation than liquid STX" + ); let scale_by = cmp::max(participation, liquid_ustx / POX_MAXIMAL_SCALING as u128); @@ -234,7 +244,10 @@ impl StacksChainState { remainder => POX_THRESHOLD_STEPS_USTX - remainder, }; let threshold = threshold_precise + ceil_amount; - info!("PoX participation threshold is {}, from {}", threshold, threshold_precise); + info!( + "PoX participation threshold is {}, from {}", + threshold, threshold_precise + ); threshold } @@ -338,9 +351,9 @@ pub mod test { use util::*; + use core::*; use vm::contracts::Contract; use vm::types::*; - use core::*; use std::convert::From; use std::fs; @@ -350,11 +363,28 @@ pub mod test { #[test] fn make_reward_set_units() { let threshold = 1_000; - let addresses = vec![(StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), 1500), - (StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), 500), - (StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), 1500), - (StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), 400)]; - assert_eq!(StacksChainState::make_reward_set(threshold, addresses).len(), 3); + let addresses = vec![ + ( + StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), + 1500, + ), + ( + StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), + 500, + ), + ( + StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), + 1500, + ), + ( + StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), + 400, + ), + ]; + assert_eq!( + StacksChainState::make_reward_set(threshold, addresses).len(), + 3 + ); } #[test] @@ -362,41 +392,69 @@ pub mod test { // when the liquid amount = the threshold step, // the threshold should always be the step size. let liquid = POX_THRESHOLD_STEPS_USTX; - assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), - POX_THRESHOLD_STEPS_USTX); - assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid), - POX_THRESHOLD_STEPS_USTX); + assert_eq!( + StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + POX_THRESHOLD_STEPS_USTX + ); + assert_eq!( + StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid)], + liquid + ), + POX_THRESHOLD_STEPS_USTX + ); let liquid = 200_000_000 * MICROSTACKS_PER_STACKS as u128; // with zero participation, should scale to 25% of liquid - assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), - 50_000 * MICROSTACKS_PER_STACKS as u128); + assert_eq!( + StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + 50_000 * MICROSTACKS_PER_STACKS as u128 + ); // should be the same at 25% participation - assert_eq!(StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid / 4)], liquid), - 50_000 * MICROSTACKS_PER_STACKS as u128); + assert_eq!( + StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid / 4)], + liquid + ), + 50_000 * MICROSTACKS_PER_STACKS as u128 + ); // but not at 30% participation - assert_eq!(StacksChainState::get_reward_threshold( - &PoxConstants::new(1000, 1, 1, 1), - &[(rand_addr(), liquid / 4), - (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128))], - liquid), - 60_000 * MICROSTACKS_PER_STACKS as u128); + assert_eq!( + StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[ + (rand_addr(), liquid / 4), + (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128)) + ], + liquid + ), + 60_000 * MICROSTACKS_PER_STACKS as u128 + ); // bump by just a little bit, should go to the next threshold step - assert_eq!(StacksChainState::get_reward_threshold( - &PoxConstants::new(1000, 1, 1, 1), - &[(rand_addr(), liquid / 4), - (rand_addr(), (MICROSTACKS_PER_STACKS as u128))], - liquid), - 60_000 * MICROSTACKS_PER_STACKS as u128); - - // bump by just a little bit, should go to the next threshold step - assert_eq!(StacksChainState::get_reward_threshold( - &PoxConstants::new(1000, 1, 1, 1), - &[(rand_addr(), liquid)], - liquid), - 200_000 * MICROSTACKS_PER_STACKS as u128); + assert_eq!( + StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[ + (rand_addr(), liquid / 4), + (rand_addr(), (MICROSTACKS_PER_STACKS as u128)) + ], + liquid + ), + 60_000 * MICROSTACKS_PER_STACKS as u128 + ); + // bump by just a little bit, should go to the next threshold step + assert_eq!( + StacksChainState::get_reward_threshold( + &PoxConstants::new(1000, 1, 1, 1), + &[(rand_addr(), liquid)], + liquid + ), + 200_000 * MICROSTACKS_PER_STACKS as u128 + ); } fn rand_addr() -> StacksAddress { @@ -849,7 +907,8 @@ pub mod test { block_id: &StacksBlockId, ) -> Result, Error> { let burn_block_height = get_par_burn_block_height(state, block_id); - state.get_reward_addresses(burnchain, sortdb, burn_block_height, block_id) + state + .get_reward_addresses(burnchain, sortdb, burn_block_height, block_id) .and_then(|mut addrs| { addrs.sort_by_key(|k| k.0.bytes.0); Ok(addrs) diff --git a/src/chainstate/stacks/db/headers.rs b/src/chainstate/stacks/db/headers.rs index 6efd80ddde6..fc0dd849b1d 100644 --- a/src/chainstate/stacks/db/headers.rs +++ b/src/chainstate/stacks/db/headers.rs @@ -36,8 +36,8 @@ use vm::costs::ExecutionCost; use util::db::Error as db_error; use util::db::{ - query_count, query_row, query_row_columns, query_rows, DBConn, FromColumn, FromRow, - query_row_panic + query_count, query_row, query_row_columns, query_row_panic, query_rows, DBConn, FromColumn, + FromRow, }; use core::FIRST_BURNCHAIN_CONSENSUS_HASH; @@ -279,8 +279,10 @@ impl StacksChainState { index_block_hash: &StacksBlockId, ) -> Result, Error> { let sql = "SELECT * FROM block_headers WHERE index_block_hash = ?1".to_string(); - query_row_panic(conn, &sql, &[&index_block_hash], || "FATAL: multiple rows for the same block hash".to_string()) - .map_err(Error::DBError) + query_row_panic(conn, &sql, &[&index_block_hash], || { + "FATAL: multiple rows for the same block hash".to_string() + }) + .map_err(Error::DBError) } /// Get an ancestor block header diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 413b06ad61d..1c64200b68a 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -63,7 +63,11 @@ impl BitcoinRegtestController { BitcoinRegtestController::with_burnchain(config, coordinator_channel, None) } - pub fn with_burnchain(config: Config, coordinator_channel: Option, burnchain_config: Option) -> Self { + pub fn with_burnchain( + config: Config, + coordinator_channel: Option, + burnchain_config: Option, + ) -> Self { std::fs::create_dir_all(&config.node.get_burnchain_path()) .expect("Unable to create workdir"); @@ -103,7 +107,7 @@ impl BitcoinRegtestController { db: None, burnchain_db: None, chain_tip: None, - burnchain_config + burnchain_config, } } @@ -133,7 +137,7 @@ impl BitcoinRegtestController { db: None, burnchain_db: None, chain_tip: None, - burnchain_config: None + burnchain_config: None, } } diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index a08c412c17d..b6151eb9442 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -85,7 +85,7 @@ pub struct NeonGenesisNode { pub config: Config, keychain: Keychain, event_dispatcher: EventDispatcher, - burnchain: Burnchain + burnchain: Burnchain, } #[cfg(test)] @@ -597,7 +597,7 @@ impl InitializedNeonNode { miner: bool, blocks_processed: BlocksProcessedCounter, coord_comms: CoordinatorChannels, - burnchain: Burnchain + burnchain: Burnchain, ) -> InitializedNeonNode { // we can call _open_ here rather than _connect_, since connect is first called in // make_genesis_block @@ -1113,7 +1113,12 @@ impl InitializedNeonNode { impl NeonGenesisNode { /// Instantiate and initialize a new node, given a config - pub fn new(config: Config, mut event_dispatcher: EventDispatcher, burnchain: Burnchain, boot_block_exec: F) -> Self + pub fn new( + config: Config, + mut event_dispatcher: EventDispatcher, + burnchain: Burnchain, + boot_block_exec: F, + ) -> Self where F: FnOnce(&mut ClarityTx) -> (), { @@ -1147,7 +1152,7 @@ impl NeonGenesisNode { keychain, config, event_dispatcher, - burnchain + burnchain, } } @@ -1169,7 +1174,7 @@ impl NeonGenesisNode { true, blocks_processed, coord_comms, - self.burnchain + self.burnchain, ) } @@ -1191,7 +1196,7 @@ impl NeonGenesisNode { false, blocks_processed, coord_comms, - self.burnchain + self.burnchain, ) } } diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index 48489488c1b..e57be63eab1 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -100,8 +100,11 @@ impl RunLoop { .expect("Run loop already started, can only start once after initialization."); // Initialize and start the burnchain. - let mut burnchain = - BitcoinRegtestController::with_burnchain(self.config.clone(), Some(coordinator_senders.clone()), burnchain_opt); + let mut burnchain = BitcoinRegtestController::with_burnchain( + self.config.clone(), + Some(coordinator_senders.clone()), + burnchain_opt, + ); let pox_constants = burnchain.get_pox_constants(); let is_miner = if self.config.node.miner { @@ -178,7 +181,12 @@ impl RunLoop { let mut block_height = burnchain_tip.block_snapshot.block_height; // setup genesis - let node = NeonGenesisNode::new(self.config.clone(), event_dispatcher, burnchain_config.clone(), |_| {}); + let node = NeonGenesisNode::new( + self.config.clone(), + event_dispatcher, + burnchain_config.clone(), + |_| {}, + ); let mut node = if is_miner { node.into_initialized_leader_node( burnchain_tip.clone(), @@ -214,7 +222,7 @@ impl RunLoop { chainstate_path, burnchain_poll_time, self.config.connection_options.timeout, - POX_SYNC_WAIT_MS + POX_SYNC_WAIT_MS, ) .unwrap(); let mut burnchain_height = 1; diff --git a/testnet/stacks-node/src/syncctl.rs b/testnet/stacks-node/src/syncctl.rs index 67ccec1ba23..3c707ba5f7d 100644 --- a/testnet/stacks-node/src/syncctl.rs +++ b/testnet/stacks-node/src/syncctl.rs @@ -42,7 +42,7 @@ pub struct PoxSyncWatchdog { /// chainstate handle chainstate: StacksChainState, /// ms to sleep on waits - wait_ms: u64 + wait_ms: u64, } impl PoxSyncWatchdog { @@ -78,7 +78,7 @@ impl PoxSyncWatchdog { steady_state_burnchain_sync_interval: burnchain_poll_time, steady_state_resync_ts: 0, chainstate: chainstate, - wait_ms + wait_ms, }) } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index f0b89a23612..f47acdbdd1e 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -2,19 +2,19 @@ use super::{ make_contract_call, make_contract_publish, make_contract_publish_microblock_only, make_microblock, make_stacks_transfer_mblock_only, to_addr, ADDR_4, SK_1, }; -use stacks::burnchains::{Address, PublicKey, PoxConstants}; +use stacks::burnchains::{Address, PoxConstants, PublicKey}; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::stacks::{ db::StacksChainState, StacksAddress, StacksBlock, StacksBlockHeader, StacksPrivateKey, StacksPublicKey, StacksTransaction, }; +use stacks::core; use stacks::net::StacksMessageCodec; use stacks::util::secp256k1::Secp256k1PublicKey; use stacks::vm::costs::ExecutionCost; use stacks::vm::execute; use stacks::vm::types::PrincipalData; use stacks::vm::Value; -use stacks::core; use super::bitcoin_regtest::BitcoinCoreController; use crate::{ @@ -725,7 +725,6 @@ fn pox_integration_test() { .to_vec(), ); - let (mut conf, miner_account) = neon_integration_test_conf(); let total_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); @@ -756,7 +755,6 @@ fn pox_integration_test() { .map_err(|_e| ()) .expect("Failed starting bitcoind"); - let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); let http_origin = format!("http://{}", &conf.node.rpc_bind); @@ -945,7 +943,6 @@ fn pox_integration_test() { panic!(""); } - // now let's mine a couple blocks, and then check the sender's nonce. // at the end of mining three blocks, there should be _one_ transaction from the microblock // only set that got mined (since the block before this one was empty, a microblock can @@ -984,22 +981,28 @@ fn pox_integration_test() { } // we should have received _three_ Bitcoin commitments, because our commitment was 3 * threshold - let utxos = btc_regtest_controller - .get_all_utxos(&pox_pubkey); + let utxos = btc_regtest_controller.get_all_utxos(&pox_pubkey); eprintln!("Got UTXOs: {}", utxos.len()); - assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + assert_eq!( + utxos.len(), + 3, + "Should have received three outputs during PoX reward cycle" + ); // we should have received _three_ Bitcoin commitments to pox_2_pubkey, because our commitment was 3 * threshold // note: that if the reward set "summing" isn't implemented, this recipient would only have received _2_ slots, // because each `stack-stx` call only received enough to get 1 slot individually. - let utxos = btc_regtest_controller - .get_all_utxos(&pox_2_pubkey); + let utxos = btc_regtest_controller.get_all_utxos(&pox_2_pubkey); eprintln!("Got UTXOs: {}", utxos.len()); - assert_eq!(utxos.len(), 3, "Should have received three outputs during PoX reward cycle"); + assert_eq!( + utxos.len(), + 3, + "Should have received three outputs during PoX reward cycle" + ); - // okay, the threshold for participation should be + // okay, the threshold for participation should be channel.stop_chains_coordinator(); } From d8e5c8f0c4b8d3b47ea8f8fcaaf3a1f21df7f57c Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 8 Oct 2020 11:13:24 -0500 Subject: [PATCH 20/26] docs: autogeneration for boot contracts, add boot-contracts-reference.json to the PR robot. --- .github/actions/docsgen/Dockerfile.docsgen | 6 +- .github/workflows/docs-pr.yml | 12 +- src/main.rs | 5 +- src/vm/docs/contracts.rs | 237 +++++++++++++++++++++ src/vm/docs/mod.rs | 23 +- 5 files changed, 265 insertions(+), 18 deletions(-) create mode 100644 src/vm/docs/contracts.rs diff --git a/.github/actions/docsgen/Dockerfile.docsgen b/.github/actions/docsgen/Dockerfile.docsgen index 5f073a65869..9bed9ff4621 100644 --- a/.github/actions/docsgen/Dockerfile.docsgen +++ b/.github/actions/docsgen/Dockerfile.docsgen @@ -4,13 +4,15 @@ WORKDIR /src COPY . . -RUN apt-get update && apt-get install -y git +RUN apt-get update && apt-get install -y git jq RUN cargo build RUN mkdir /out -RUN /src/target/debug/blockstack-core docgen > /out/clarity-reference.json +RUN /src/target/debug/blockstack-core docgen | jq . > /out/clarity-reference.json +RUN /src/target/debug/blockstack-core docgen_boot | jq . > /out/boot-contracts-reference.json FROM scratch AS export-stage COPY --from=build /out/clarity-reference.json / +COPY --from=build /out/boot-contracts-reference.json / diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 05c2c6216c2..6f43ecb9c4f 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -43,18 +43,20 @@ jobs: id: push run: | cd docs.blockstack - git config user.email "robots@robots.actions" + git config user.email "kantai+robot@gmail.com" git config user.name "PR Robot" git fetch --unshallow git checkout -b $ROBOT_BRANCH cp ../docs-output/clarity-reference.json ./src/_data/clarity-reference.json - if $(git diff --quiet --exit-code); then - echo "No clarity-reference.json changes, stopping" + cp ../docs-output/boot-contracts-reference.json ./src/_data/boot-contracts-reference.json + git add src/_data/clarity-reference.json + git add src/_data/boot-contracts-reference.json + if $(git diff --staged --quiet --exit-code); then + echo "No reference.json changes, stopping" echo "::set-output name=open_pr::0" else git remote add robot https://github.com/$ROBOT_OWNER/$ROBOT_REPO - git add src/_data/clarity-reference.json - git commit -m "auto: update clarity-reference.json from stacks-blockchain@${GITHUB_SHA}" + git commit -m "auto: update Clarity references JSONs from stacks-blockchain@${GITHUB_SHA}" git push robot $ROBOT_BRANCH echo "::set-output name=open_pr::1" fi diff --git a/src/main.rs b/src/main.rs index 705a4143225..245b67253d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -347,7 +347,10 @@ fn main() { } if argv[1] == "docgen_boot" { - println!("{}", vm::docs::contracts::make_json_boot_contracts_reference()); + println!( + "{}", + vm::docs::contracts::make_json_boot_contracts_reference() + ); return; } diff --git a/src/vm/docs/contracts.rs b/src/vm/docs/contracts.rs new file mode 100644 index 00000000000..ab5e48bdeca --- /dev/null +++ b/src/vm/docs/contracts.rs @@ -0,0 +1,237 @@ +use chainstate::stacks::boot::STACKS_BOOT_CODE_MAINNET; +use vm::analysis::{mem_type_check, ContractAnalysis}; +use vm::docs::{get_input_type_string, get_output_type_string, get_signature}; +use vm::types::{FunctionType, Value}; + +use vm::execute; + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::iter::FromIterator; + +#[derive(Serialize)] +struct ContractRef { + public_functions: Vec, + read_only_functions: Vec, + error_codes: Vec, +} + +#[derive(Serialize)] +struct FunctionRef { + name: String, + input_type: String, + output_type: String, + signature: String, + description: String, +} + +#[derive(Serialize)] +struct ErrorCode { + name: String, + #[serde(rename = "type")] + value_type: String, + value: String, +} + +struct ContractSupportDocs { + descriptions: HashMap<&'static str, &'static str>, + skip_func_display: HashSet<&'static str>, +} + +fn make_contract_support_docs() -> HashMap<&'static str, ContractSupportDocs> { + let pox_descriptions = vec![ + ("disallow-contract-caller", "Revokes authorization from a contract to invoke stacking methods through contract-calls"), + ("allow-contract-caller", "Give a contract-caller authorization to call stacking methods. Normally, stacking methods may +only be invoked by _direct_ transactions (i.e., the `tx-sender` issues a direct `contract-call` to the stacking methods). +By issuing an allowance, the tx-sender may call through the allowed contract."), + ("stack-stx", "Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX). +The STX will be locked for the given number of reward cycles (lock-period). +This is the self-service interface. tx-sender will be the Stacker. + +* The given stacker cannot currently be stacking. +* You will need the minimum uSTX threshold. This isn't determined until the reward cycle begins, but this + method still requires stacking over the _absolute minimum_ amount, which can be obtained by calling `get-stacking-minimum`. + +The tokens will unlock and be returned to the Stacker (tx-sender) automatically."), + ("revoke-delegate-stx", "Revoke a Stacking delegate relationship. A particular Stacker may only have one delegate, +so this method does not take any parameters, and just revokes the Stacker's current delegate (if one exists)."), + ("delegate-stx", "Delegate to `delegate-to` the ability to stack from a given address. +This method _does not_ lock the funds, rather, it allows the delegate to issue the stacking lock. + +The caller specifies: + * amount-ustx: the total amount of ustx the delegate may be allowed to lock + * until-burn-ht: an optional burn height at which this delegation expiration + * pox-addr: an optional address to which any rewards *must* be sent"), + ("delegate-stack-stx", "As a delegate, stack the given principal's STX using `partial-stacked-by-cycle`. +Once the delegate has stacked > minimum, the delegate should call `stack-aggregation-commit`."), + ("stack-aggregation-commit", "Commit partially stacked STX. + +This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions, +so long as: + 1. The pox-addr is the same. + 2. This \"commit\" transaction is called _before_ the PoX anchor block. +This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, + but does not require it be all locked up within a single transaction"), + ("reject-pox", "Reject Stacking for this reward cycle. +`tx-sender` votes all its uSTX for rejection. +Note that unlike Stacking, rejecting PoX does not lock the tx-sender's tokens: PoX rejection acts like a coin vote."), + ("can-stack-stx", "Evaluate if a participant can stack an amount of STX for a given period."), + ("get-stacking-minimum", "Returns the absolute minimum amount that could be validly Stacked (the threshold to Stack in +a given reward cycle may be higher than this"), + ("get-pox-rejection", "Returns the amount of uSTX that a given principal used to reject a PoX cycle."), + ("is-pox-active", "Returns whether or not PoX has been rejected at a given PoX cycle."), + ("get-stacker-info", "Returns the _current_ stacking information for `stacker. If the information +is expired, or if there's never been such a stacker, then returns none."), + ("get-total-ustx-stacked", "Returns the amount of currently participating uSTX in the given cycle."), + ("get-pox-info", "Returns information about PoX status.") + ]; + + let pox_skip_display = vec![ + "set-burnchain-parameters", + "minimal-can-stack-stx", + "get-reward-set-size", + "get-reward-set-pox-address", + ]; + + HashMap::from_iter(vec![( + "pox", + ContractSupportDocs { + descriptions: HashMap::from_iter(pox_descriptions.into_iter()), + skip_func_display: HashSet::from_iter(pox_skip_display.into_iter()), + }, + )]) +} + +fn make_func_ref(func_name: &str, func_type: &FunctionType, description: &str) -> FunctionRef { + let input_type = get_input_type_string(func_type); + let output_type = get_output_type_string(func_type); + let signature = get_signature(func_name, func_type) + .expect("BUG: failed to build signature for boot contract"); + FunctionRef { + input_type, + output_type, + signature, + name: func_name.to_string(), + description: description.to_string(), + } +} + +fn get_constant_value(var_name: &str, contract_content: &str) -> Value { + let to_eval = format!("{}\n{}", contract_content, var_name); + execute(&to_eval) + .expect("BUG: failed to evaluate contract for constant value") + .expect("BUG: failed to return constant value") +} + +fn produce_docs() -> BTreeMap { + let mut docs = BTreeMap::new(); + let support_docs = make_contract_support_docs(); + + for (contract_name, content) in STACKS_BOOT_CODE_MAINNET.iter() { + let (_, contract_analysis) = + mem_type_check(content).expect("BUG: failed to type check boot contract"); + + if let Some(contract_support) = support_docs.get(*contract_name) { + let ContractAnalysis { + public_function_types, + read_only_function_types, + variable_types, + .. + } = contract_analysis; + let public_functions: Vec<_> = public_function_types + .iter() + .filter(|(func_name, _)| { + !contract_support + .skip_func_display + .contains(func_name.as_str()) + }) + .map(|(func_name, func_type)| { + let description = contract_support + .descriptions + .get(func_name.as_str()) + .expect(&format!("BUG: no description for {}", func_name.as_str())); + make_func_ref(func_name, func_type, description) + }) + .collect(); + + let read_only_functions: Vec<_> = read_only_function_types + .iter() + .filter(|(func_name, _)| { + !contract_support + .skip_func_display + .contains(func_name.as_str()) + }) + .map(|(func_name, func_type)| { + let description = contract_support + .descriptions + .get(func_name.as_str()) + .expect(&format!("BUG: no description for {}", func_name.as_str())); + make_func_ref(func_name, func_type, description) + }) + .collect(); + + let ecode_names = variable_types + .iter() + .enumerate() + .filter_map(|(ix, (var_name, _))| { + if var_name.starts_with("ERR_") { + Some(format!("{}: {}", var_name.as_str(), var_name.as_str())) + } else { + None + } + }) + .collect::>() + .join(", "); + let ecode_to_eval = format!("{}\n {{ {} }}", content, ecode_names); + let ecode_result = execute(&ecode_to_eval) + .expect("BUG: failed to evaluate contract for constant value") + .expect("BUG: failed to return constant value") + .expect_tuple(); + + let error_codes = variable_types + .iter() + .filter_map(|(var_name, type_signature)| { + if var_name.starts_with("ERR_") { + let value = ecode_result + .get(var_name) + .expect("BUG: failed to fetch tuple entry from ecode output") + .to_string(); + Some(ErrorCode { + name: var_name.to_string(), + value, + value_type: type_signature.to_string(), + }) + } else { + None + } + }) + .collect(); + + docs.insert( + contract_name.to_string(), + ContractRef { + public_functions, + read_only_functions, + error_codes, + }, + ); + } + } + + docs +} + +pub fn make_json_boot_contracts_reference() -> String { + let api_out = produce_docs(); + format!( + "{}", + serde_json::to_string(&api_out).expect("Failed to serialize documentation") + ) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_make_boot_contracts_reference() { + make_json_boot_contracts_reference(); + } +} diff --git a/src/vm/docs/mod.rs b/src/vm/docs/mod.rs index f91c36b1567..8b3d1972a3b 100644 --- a/src/vm/docs/mod.rs +++ b/src/vm/docs/mod.rs @@ -298,8 +298,7 @@ pub fn get_input_type_string(function_type: &FunctionType) -> String { match function_type { FunctionType::Variadic(ref in_type, _) => format!("{}, ...", in_type), FunctionType::Fixed(FixedFunction { ref args, .. }) => { - let in_types: Vec = - args.iter().map(|x| format!("{}", x.signature)).collect(); + let in_types: Vec = args.iter().map(|x| format!("{}", x.signature)).collect(); in_types.join(", ") } FunctionType::UnionArgs(ref in_types, _) => { @@ -320,21 +319,25 @@ pub fn get_output_type_string(function_type: &FunctionType) -> String { FunctionType::Fixed(FixedFunction { ref returns, .. }) => format!("{}", returns), FunctionType::UnionArgs(_, ref out_type) => format!("{}", out_type), FunctionType::ArithmeticVariadic - | FunctionType::ArithmeticUnary - | FunctionType::ArithmeticBinary => "int | uint".to_string(), + | FunctionType::ArithmeticUnary + | FunctionType::ArithmeticBinary => "int | uint".to_string(), FunctionType::ArithmeticComparison => "bool".to_string(), } } pub fn get_signature(function_name: &str, function_type: &FunctionType) -> Option { if let FunctionType::Fixed(FixedFunction { ref args, .. }) = function_type { - let in_names: Vec = - args.iter().map(|x| format!("{}", x.name.as_str())).collect(); + let in_names: Vec = args + .iter() + .map(|x| format!("{}", x.name.as_str())) + .collect(); let arg_examples = in_names.join(" "); - Some(format!("({}{}{})", - function_name, - if arg_examples.len() == 0 { "" } else { " " }, - arg_examples)) + Some(format!( + "({}{}{})", + function_name, + if arg_examples.len() == 0 { "" } else { " " }, + arg_examples + )) } else { None } From 4ebed59265ad23bdb706f2f66cede308d9cd39bb Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 8 Oct 2020 13:10:57 -0500 Subject: [PATCH 21/26] oops, need an import --- src/vm/docs/contracts.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vm/docs/contracts.rs b/src/vm/docs/contracts.rs index ab5e48bdeca..8ee081b66fc 100644 --- a/src/vm/docs/contracts.rs +++ b/src/vm/docs/contracts.rs @@ -171,8 +171,7 @@ fn produce_docs() -> BTreeMap { let ecode_names = variable_types .iter() - .enumerate() - .filter_map(|(ix, (var_name, _))| { + .filter_map(|(var_name, _)| { if var_name.starts_with("ERR_") { Some(format!("{}: {}", var_name.as_str(), var_name.as_str())) } else { @@ -230,6 +229,8 @@ pub fn make_json_boot_contracts_reference() -> String { #[cfg(test)] mod tests { + use vm::docs::contracts::make_json_boot_contracts_reference; + #[test] fn test_make_boot_contracts_reference() { make_json_boot_contracts_reference(); From 185d4d48a4119b413e0e4216bcf2ac55467f4672 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 8 Oct 2020 18:42:57 -0500 Subject: [PATCH 22/26] add burn-block-ht recency constraints, update existing tests --- src/burnchains/mod.rs | 12 ++++ src/chainstate/coordinator/mod.rs | 8 ++- src/chainstate/stacks/boot/contract_tests.rs | 11 +++ src/chainstate/stacks/boot/mod.rs | 69 ++++++++++--------- src/chainstate/stacks/boot/pox.clar | 18 ++++- .../src/tests/neon_integrations.rs | 11 ++- 6 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index 51508971990..6dcfd0df027 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -283,6 +283,9 @@ pub struct PoxConstants { /// fraction of liquid STX that must vote to reject PoX for /// it to revert to PoB in the next reward cycle pub pox_rejection_fraction: u64, + /// percentage of liquid STX that must participate for PoX + /// to occur + pub pox_participation_threshold_pct: u64, _shadow: PhantomData<()>, } @@ -300,6 +303,7 @@ impl PoxConstants { prepare_length, anchor_threshold, pox_rejection_fraction, + pox_participation_threshold_pct: 5, _shadow: PhantomData, } } @@ -312,6 +316,14 @@ impl PoxConstants { self.reward_cycle_length } + /// is participating_ustx enough to engage in PoX in the next reward cycle? + pub fn enough_participation(&self, participating_ustx: u128, liquid_ustx: u128) -> bool { + participating_ustx.checked_mul(self.pox_participation_threshold_pct as u128) + .expect("OVERFLOW: uSTX overflowed u128") > + liquid_ustx.checked_mul(100) + .expect("OVERFLOW: uSTX overflowed u128") + } + pub fn mainnet_default() -> PoxConstants { PoxConstants::new(1000, 240, 192, 25) } diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index a65bae045f8..f9d2d4ae079 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -176,12 +176,18 @@ impl RewardSetProvider for OnChainRewardSetProvider { .expect("CORRUPTION: Failed to look up block header info for PoX anchor block") .total_liquid_ustx; - let threshold = StacksChainState::get_reward_threshold( + let (threshold, participation) = StacksChainState::get_reward_threshold_and_participation( &burnchain.pox_constants, ®istered_addrs, liquid_ustx, ); + if !burnchain.pox_constants.enough_participation(participation, liquid_ustx) { + info!("PoX reward cycle did not have enough participation. Defaulting to burn. participation={}, liquid_ustx={}, burn_height={}", + participation, liquid_ustx, current_burn_height); + return Ok(vec![]) + } + Ok(StacksChainState::make_reward_set( threshold, registered_addrs, diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index bf59f9af5a1..01300bb0b63 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -343,6 +343,7 @@ fn delegation_tests() { // let's do some delegated stacking! sim.execute_next_block(|env| { // try to stack more than [0]'s delegated amount! + let burn_height = env.eval_raw("burn-block-height").unwrap().0; assert_eq!( env.execute_transaction( (&delegator).into(), @@ -352,6 +353,7 @@ fn delegation_tests() { (&USER_KEYS[0]).into(), Value::UInt(3 * USTX_PER_HOLDER), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -371,6 +373,7 @@ fn delegation_tests() { (&USER_KEYS[0]).into(), Value::UInt(2 * USTX_PER_HOLDER), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -390,6 +393,7 @@ fn delegation_tests() { (&USER_KEYS[0]).into(), Value::UInt(*MIN_THRESHOLD - 1), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -437,6 +441,7 @@ fn delegation_tests() { (&USER_KEYS[1]).into(), Value::UInt(*MIN_THRESHOLD - 1), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -456,6 +461,7 @@ fn delegation_tests() { (&USER_KEYS[0]).into(), Value::UInt(*MIN_THRESHOLD - 1), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -475,6 +481,7 @@ fn delegation_tests() { (&USER_KEYS[2]).into(), Value::UInt(*MIN_THRESHOLD - 1), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -494,6 +501,7 @@ fn delegation_tests() { (&USER_KEYS[2]).into(), Value::UInt(*MIN_THRESHOLD - 1), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(1) ]) ) @@ -597,6 +605,7 @@ fn delegation_tests() { (&USER_KEYS[3]).into(), Value::UInt(*MIN_THRESHOLD), POX_ADDRS[1].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -707,6 +716,7 @@ fn delegation_tests() { (&USER_KEYS[1]).into(), Value::UInt(*MIN_THRESHOLD), POX_ADDRS[0].clone(), + burn_height.clone(), Value::UInt(2) ]) ) @@ -756,6 +766,7 @@ fn delegation_tests() { (&USER_KEYS[4]).into(), Value::UInt(*MIN_THRESHOLD - 1), POX_ADDRS[0].clone(), + burn_height.clone(), Value::UInt(2) ]) ) diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 6090ae415f2..aa06491ffb8 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -220,11 +220,11 @@ impl StacksChainState { reward_set } - pub fn get_reward_threshold( + pub fn get_reward_threshold_and_participation( pox_settings: &PoxConstants, addresses: &[(StacksAddress, u128)], liquid_ustx: u128, - ) -> u128 { + ) -> (u128, u128) { let participation = addresses .iter() .fold(0, |agg, (_, stacked_amt)| agg + stacked_amt); @@ -248,7 +248,7 @@ impl StacksChainState { "PoX participation threshold is {}, from {}", threshold, threshold_precise ); - threshold + (threshold, participation) } /// Each address will have at least (get-stacking-minimum) tokens. @@ -393,66 +393,66 @@ pub mod test { // the threshold should always be the step size. let liquid = POX_THRESHOLD_STEPS_USTX; assert_eq!( - StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + StacksChainState::get_reward_threshold_and_participation(&PoxConstants::new(1000, 1, 1, 1), &[], liquid).0, POX_THRESHOLD_STEPS_USTX ); assert_eq!( - StacksChainState::get_reward_threshold( + StacksChainState::get_reward_threshold_and_participation( &PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid - ), + ).0, POX_THRESHOLD_STEPS_USTX ); let liquid = 200_000_000 * MICROSTACKS_PER_STACKS as u128; // with zero participation, should scale to 25% of liquid assert_eq!( - StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid), + StacksChainState::get_reward_threshold_and_participation(&PoxConstants::new(1000, 1, 1, 1), &[], liquid).0, 50_000 * MICROSTACKS_PER_STACKS as u128 ); // should be the same at 25% participation assert_eq!( - StacksChainState::get_reward_threshold( + StacksChainState::get_reward_threshold_and_participation( &PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid / 4)], liquid - ), + ).0, 50_000 * MICROSTACKS_PER_STACKS as u128 ); // but not at 30% participation assert_eq!( - StacksChainState::get_reward_threshold( + StacksChainState::get_reward_threshold_and_participation( &PoxConstants::new(1000, 1, 1, 1), &[ (rand_addr(), liquid / 4), (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128)) ], liquid - ), + ).0, 60_000 * MICROSTACKS_PER_STACKS as u128 ); // bump by just a little bit, should go to the next threshold step assert_eq!( - StacksChainState::get_reward_threshold( + StacksChainState::get_reward_threshold_and_participation( &PoxConstants::new(1000, 1, 1, 1), &[ (rand_addr(), liquid / 4), (rand_addr(), (MICROSTACKS_PER_STACKS as u128)) ], liquid - ), + ).0, 60_000 * MICROSTACKS_PER_STACKS as u128 ); // bump by just a little bit, should go to the next threshold step assert_eq!( - StacksChainState::get_reward_threshold( + StacksChainState::get_reward_threshold_and_participation( &PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid - ), + ).0, 200_000 * MICROSTACKS_PER_STACKS as u128 ); } @@ -680,6 +680,7 @@ pub mod test { addr_version: AddressHashMode, addr_bytes: Hash160, lock_period: u128, + burn_ht: u64, ) -> StacksTransaction { // (define-public (stack-stx (amount-ustx uint) // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) @@ -691,6 +692,7 @@ pub mod test { vec![ Value::UInt(amount), make_pox_addr(addr_version, addr_bytes), + Value::UInt(burn_ht as u128), Value::UInt(lock_period), ], ) @@ -832,7 +834,7 @@ pub mod test { ;; this contract stacks the stx given to it (as-contract - (contract-call? '{}.pox stack-stx amount-ustx pox-addr lock-period)) + (contract-call? '{}.pox stack-stx amount-ustx pox-addr burn-block-height lock-period)) )) ) @@ -1037,13 +1039,13 @@ pub mod test { ]; if tenure_id == 1 { - let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1); + let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, tip.block_height); block_txs.push(alice_lockup_1); } if tenure_id == 2 { let alice_test_tx = make_bare_contract(&alice, 1, 0, "nested-stacker", &format!( "(define-public (nested-stack-stx) - (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) u1))", STACKS_BOOT_CODE_CONTRACT_ADDRESS)); + (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) burn-block-height u1))", STACKS_BOOT_CODE_CONTRACT_ADDRESS)); block_txs.push(alice_test_tx); } @@ -1256,6 +1258,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, + tip.block_height ); block_txs.push(alice_lockup); } @@ -1706,6 +1709,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, + tip.block_height ); block_txs.push(alice_lockup); @@ -1717,6 +1721,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&bob).bytes, 12, + tip.block_height ); block_txs.push(bob_lockup); } @@ -1900,11 +1905,11 @@ pub mod test { if tenure_id == 1 { // Alice locks up exactly 12.5% of the liquid STX supply, twice. // Only the first one succeeds. - let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12); + let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); block_txs.push(alice_lockup_1); // will be rejected - let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12); + let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); block_txs.push(alice_lockup_2); // let's make some allowances for contract-calls through smart contracts @@ -1928,7 +1933,7 @@ pub mod test { "(define-data-var test-run bool false) (define-data-var test-result int -1) (let ((result - (contract-call? '{}.pox stack-stx u256000000 (tuple (version 0x00) (hashbytes 0xae1593226f85e49a7eaff5b633ff687695438cc9)) u12))) + (contract-call? '{}.pox stack-stx u256000000 (tuple (version 0x00) (hashbytes 0xae1593226f85e49a7eaff5b633ff687695438cc9)) burn-block-height u12))) (var-set test-result (match result ok_value -1 err_value err_value)) (var-set test-run true)) @@ -1942,7 +1947,7 @@ pub mod test { "(define-data-var test-run bool false) (define-data-var test-result int -1) (let ((result - (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) u12))) + (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) burn-block-height u12))) (var-set test-result (match result ok_value -1 err_value err_value)) (var-set test-run true)) @@ -1956,7 +1961,7 @@ pub mod test { "(define-data-var test-run bool false) (define-data-var test-result int -1) (let ((result - (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) u12))) + (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) burn-block-height u12))) (var-set test-result (match result ok_value -1 err_value err_value)) (var-set test-run true)) @@ -2122,7 +2127,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - ); + tip.block_height); block_txs.push(alice_lockup); } @@ -2356,7 +2361,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - ); + tip.block_height); block_txs.push(alice_lockup); // Bob creates a locking contract @@ -2383,7 +2388,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - ); + tip.block_height); block_txs.push(alice_lockup); // Charlie locks up half of his tokens @@ -2825,7 +2830,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - ); + tip.block_height); block_txs.push(alice_lockup); let bob_lockup = make_pox_lockup( @@ -2835,7 +2840,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&bob).bytes, 1, - ); + tip.block_height); block_txs.push(bob_lockup); let charlie_lockup = make_pox_lockup( @@ -2845,7 +2850,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&charlie).bytes, 1, - ); + tip.block_height); block_txs.push(charlie_lockup); let danielle_lockup = make_pox_lockup( @@ -2855,7 +2860,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&danielle).bytes, 1, - ); + tip.block_height); block_txs.push(danielle_lockup); let bob_contract = make_pox_lockup_contract(&bob, 1, "do-lockup"); @@ -3251,7 +3256,7 @@ pub mod test { if tenure_id == 1 { // Alice locks up exactly 25% of the liquid STX supply, so this should succeed. - let alice_lockup = make_pox_lockup(&alice, 0, 1024 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12); + let alice_lockup = make_pox_lockup(&alice, 0, 1024 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); block_txs.push(alice_lockup); // Bob rejects with exactly 25% of the liquid STX supply (shouldn't affect @@ -3277,7 +3282,7 @@ pub mod test { // Note: this behavior is a bug in the miner and block processor: see issue #? let charlie_stack = make_bare_contract(&charlie, 2, 0, "charlie-try-stack", &format!( - "(asserts! (not (is-eq (print (contract-call? '{}.pox stack-stx u1 {{ version: 0x01, hashbytes: 0x1111111111111111111111111111111111111111 }} u1)) (err 17))) (err 1))", + "(asserts! (not (is-eq (print (contract-call? '{}.pox stack-stx u1 {{ version: 0x01, hashbytes: 0x1111111111111111111111111111111111111111 }} burn-block-height u1)) (err 17))) (err 1))", boot_code_addr())); block_txs.push(charlie_stack); diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar index dd5a6cd5fdd..28db2338289 100644 --- a/src/chainstate/stacks/boot/pox.clar +++ b/src/chainstate/stacks/boot/pox.clar @@ -18,6 +18,7 @@ (define-constant ERR_DELEGATION_EXPIRES_DURING_LOCK 21) (define-constant ERR_DELEGATION_TOO_MUCH_LOCKED 22) (define-constant ERR_DELEGATION_POX_ADDR_REQUIRED 23) +(define-constant ERR_INVALID_START_BURN_HEIGHT 24) ;; PoX disabling threshold (a percent) (define-constant POX_REJECTION_FRACTION u25) @@ -425,13 +426,22 @@ ;; at the time this method is called. ;; * You may need to increase the amount of uSTX locked up later, since the minimum uSTX threshold ;; may increase between reward cycles. +;; * The Stacker will receive rewards in the reward cycle following `start-burn-ht`. +;; Importantly, `start-burn-ht` may not be further into the future than the next reward cycle, +;; and in most cases should be set to the current burn block height. ;; ;; The tokens will unlock and be returned to the Stacker (tx-sender) automatically. (define-public (stack-stx (amount-ustx uint) (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (start-burn-ht uint) (lock-period uint)) ;; this stacker's first reward cycle is the _next_ reward cycle - (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle)))) + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) ;; must be called directly by the tx-sender or by an allowed contract-caller (asserts! (check-caller-allowed) @@ -547,10 +557,16 @@ (define-public (delegate-stack-stx (stacker principal) (amount-ustx uint) (pox-addr { version: (buff 1), hashbytes: (buff 20) }) + (start-burn-ht uint) (lock-period uint)) ;; this stacker's first reward cycle is the _next_ reward cycle (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) ;; must be called directly by the tx-sender or by an allowed contract-caller (asserts! (check-caller-allowed) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index f47acdbdd1e..7e76f8a9f18 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -784,6 +784,8 @@ fn pox_integration_test() { // second block will be the first mined Stacks block next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + let sort_height = channel.get_sortitions_processed(); + // let's query the miner's account nonce: eprintln!("Miner account: {}", miner_account); @@ -817,7 +819,7 @@ fn pox_integration_test() { let tx = make_contract_call( &spender_sk, 0, - 243, + 260, &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), "pox", "stack-stx", @@ -829,6 +831,7 @@ fn pox_integration_test() { )) .unwrap() .unwrap(), + Value::UInt(sort_height as u128), Value::UInt(3), ], ); @@ -862,7 +865,7 @@ fn pox_integration_test() { let tx = make_contract_call( &spender_2_sk, 0, - 243, + 260, &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), "pox", "stack-stx", @@ -874,6 +877,7 @@ fn pox_integration_test() { )) .unwrap() .unwrap(), + Value::UInt(sort_height as u128), Value::UInt(3), ], ); @@ -904,7 +908,7 @@ fn pox_integration_test() { let tx = make_contract_call( &spender_3_sk, 0, - 243, + 260, &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), "pox", "stack-stx", @@ -916,6 +920,7 @@ fn pox_integration_test() { )) .unwrap() .unwrap(), + Value::UInt(sort_height as u128), Value::UInt(3), ], ); From 6772aab419e597ae21b63bcfbcc978b5fb0350b5 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 9 Oct 2020 08:56:11 -0500 Subject: [PATCH 23/26] add some docs+comments for threshold scaling consts and calculations --- src/chainstate/stacks/boot/mod.rs | 2 ++ src/core/mod.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 6090ae415f2..5fc86595661 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -234,6 +234,8 @@ impl StacksChainState { "CORRUPTION: More stacking participation than liquid STX" ); + // set the lower limit on reward scaling at 25% of liquid_ustx + // (i.e., liquid_ustx / POX_MAXIMAL_SCALING) let scale_by = cmp::max(participation, liquid_ustx / POX_MAXIMAL_SCALING as u128); let reward_slots = pox_settings.reward_slots() as u128; diff --git a/src/core/mod.rs b/src/core/mod.rs index 79d89e4bd4b..47e370aed4a 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -65,7 +65,13 @@ pub const MICROSTACKS_PER_STACKS: u32 = 1_000_000; pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240; pub const POX_REWARD_CYCLE_LENGTH: u32 = 1000; +/// The maximum amount that PoX rewards can be scaled by. +/// That is, if participation is very low, rewards are: +/// POX_MAXIMAL_SCALING x (rewards with 100% participation) +/// Set a 4x, this implies the lower bound of participation for scaling +/// is 25% pub const POX_MAXIMAL_SCALING: u128 = 4; +/// This is the amount that PoX threshold adjustments are stepped by. pub const POX_THRESHOLD_STEPS_USTX: u128 = 10_000 * (MICROSTACKS_PER_STACKS as u128); /// Synchronize burn transactions from the Bitcoin blockchain From eed4dda091875b98fac34423a9a3768c62baf75f Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 9 Oct 2020 13:26:30 -0500 Subject: [PATCH 24/26] add integration and unit tests for the participation threshold --- src/burnchains/mod.rs | 8 ++- src/chainstate/coordinator/mod.rs | 7 +- src/chainstate/stacks/boot/contract_tests.rs | 68 +++++++++++++++++++ src/chainstate/stacks/boot/mod.rs | 56 ++++++++++----- .../src/tests/neon_integrations.rs | 63 +++++++++-------- 5 files changed, 150 insertions(+), 52 deletions(-) diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index 6dcfd0df027..da0c7c866fc 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -318,10 +318,12 @@ impl PoxConstants { /// is participating_ustx enough to engage in PoX in the next reward cycle? pub fn enough_participation(&self, participating_ustx: u128, liquid_ustx: u128) -> bool { - participating_ustx.checked_mul(self.pox_participation_threshold_pct as u128) - .expect("OVERFLOW: uSTX overflowed u128") > - liquid_ustx.checked_mul(100) + participating_ustx + .checked_mul(100) .expect("OVERFLOW: uSTX overflowed u128") + > liquid_ustx + .checked_mul(self.pox_participation_threshold_pct as u128) + .expect("OVERFLOW: uSTX overflowed u128") } pub fn mainnet_default() -> PoxConstants { diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index f9d2d4ae079..3ea10b7bdc4 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -182,10 +182,13 @@ impl RewardSetProvider for OnChainRewardSetProvider { liquid_ustx, ); - if !burnchain.pox_constants.enough_participation(participation, liquid_ustx) { + if !burnchain + .pox_constants + .enough_participation(participation, liquid_ustx) + { info!("PoX reward cycle did not have enough participation. Defaulting to burn. participation={}, liquid_ustx={}, burn_height={}", participation, liquid_ustx, current_burn_height); - return Ok(vec![]) + return Ok(vec![]); } Ok(StacksChainState::make_reward_set( diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index 01300bb0b63..26f24523df0 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -228,6 +228,74 @@ impl HeadersDB for TestSimHeadersDB { } } +#[test] +fn recency_tests() { + let mut sim = ClarityTestSim::new(); + let delegator = StacksPrivateKey::new(); + + sim.execute_next_block(|env| { + env.initialize_contract(POX_CONTRACT.clone(), &BOOT_CODE_POX_TESTNET) + .unwrap() + }); + sim.execute_next_block(|env| { + // try to issue a far future stacking tx + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + POX_CONTRACT.clone(), + "stack-stx", + &symbols_from_values(vec![ + Value::UInt(USTX_PER_HOLDER), + POX_ADDRS[0].clone(), + Value::UInt(3000), + Value::UInt(3), + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 24)".to_string() + ); + // let's delegate, and check if the delegate can issue a far future + // stacking tx + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + POX_CONTRACT.clone(), + "delegate-stx", + &symbols_from_values(vec![ + Value::UInt(2 * USTX_PER_HOLDER), + (&delegator).into(), + Value::none(), + Value::none() + ]) + ) + .unwrap() + .0, + Value::okay_true() + ); + + assert_eq!( + env.execute_transaction( + (&delegator).into(), + POX_CONTRACT.clone(), + "delegate-stack-stx", + &symbols_from_values(vec![ + (&USER_KEYS[0]).into(), + Value::UInt(USTX_PER_HOLDER), + POX_ADDRS[1].clone(), + Value::UInt(3000), + Value::UInt(2) + ]) + ) + .unwrap() + .0 + .to_string(), + "(err 24)".to_string() + ); + }); +} + #[test] fn delegation_tests() { let mut sim = ClarityTestSim::new(); diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index a2eeefb4d0e..7b61f58ab1f 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -395,7 +395,12 @@ pub mod test { // the threshold should always be the step size. let liquid = POX_THRESHOLD_STEPS_USTX; assert_eq!( - StacksChainState::get_reward_threshold_and_participation(&PoxConstants::new(1000, 1, 1, 1), &[], liquid).0, + StacksChainState::get_reward_threshold_and_participation( + &PoxConstants::new(1000, 1, 1, 1), + &[], + liquid + ) + .0, POX_THRESHOLD_STEPS_USTX ); assert_eq!( @@ -403,14 +408,20 @@ pub mod test { &PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid - ).0, + ) + .0, POX_THRESHOLD_STEPS_USTX ); let liquid = 200_000_000 * MICROSTACKS_PER_STACKS as u128; // with zero participation, should scale to 25% of liquid assert_eq!( - StacksChainState::get_reward_threshold_and_participation(&PoxConstants::new(1000, 1, 1, 1), &[], liquid).0, + StacksChainState::get_reward_threshold_and_participation( + &PoxConstants::new(1000, 1, 1, 1), + &[], + liquid + ) + .0, 50_000 * MICROSTACKS_PER_STACKS as u128 ); // should be the same at 25% participation @@ -419,7 +430,8 @@ pub mod test { &PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid / 4)], liquid - ).0, + ) + .0, 50_000 * MICROSTACKS_PER_STACKS as u128 ); // but not at 30% participation @@ -431,7 +443,8 @@ pub mod test { (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128)) ], liquid - ).0, + ) + .0, 60_000 * MICROSTACKS_PER_STACKS as u128 ); @@ -444,7 +457,8 @@ pub mod test { (rand_addr(), (MICROSTACKS_PER_STACKS as u128)) ], liquid - ).0, + ) + .0, 60_000 * MICROSTACKS_PER_STACKS as u128 ); @@ -454,7 +468,8 @@ pub mod test { &PoxConstants::new(1000, 1, 1, 1), &[(rand_addr(), liquid)], liquid - ).0, + ) + .0, 200_000 * MICROSTACKS_PER_STACKS as u128 ); } @@ -1260,7 +1275,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, - tip.block_height + tip.block_height, ); block_txs.push(alice_lockup); } @@ -1711,7 +1726,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, - tip.block_height + tip.block_height, ); block_txs.push(alice_lockup); @@ -1723,7 +1738,7 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&bob).bytes, 12, - tip.block_height + tip.block_height, ); block_txs.push(bob_lockup); } @@ -2129,7 +2144,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(alice_lockup); } @@ -2363,7 +2379,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(alice_lockup); // Bob creates a locking contract @@ -2390,7 +2407,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(alice_lockup); // Charlie locks up half of his tokens @@ -2832,7 +2850,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(alice_lockup); let bob_lockup = make_pox_lockup( @@ -2842,7 +2861,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&bob).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(bob_lockup); let charlie_lockup = make_pox_lockup( @@ -2852,7 +2872,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&charlie).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(charlie_lockup); let danielle_lockup = make_pox_lockup( @@ -2862,7 +2883,8 @@ pub mod test { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&danielle).bytes, 1, - tip.block_height); + tip.block_height, + ); block_txs.push(danielle_lockup); let bob_contract = make_pox_lockup_contract(&bob, 1, "do-lockup"); diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 7e76f8a9f18..cae65d2ed23 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -759,7 +759,9 @@ fn pox_integration_test() { let http_origin = format!("http://{}", &conf.node.rpc_bind); let mut burnchain_config = btc_regtest_controller.get_burnchain(); - burnchain_config.pox_constants = PoxConstants::new(10, 5, 4, 5); + let mut pox_constants = PoxConstants::new(10, 5, 4, 5); + pox_constants.pox_participation_threshold_pct = 15; + burnchain_config.pox_constants = pox_constants; btc_regtest_controller.bootstrap_chain(201); @@ -859,6 +861,18 @@ fn pox_integration_test() { panic!(""); } + let mut sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); + + // now let's mine until the next reward cycle starts ... + while sort_height < 222 { + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); + } + + // let's stack with spender 2 and spender 3... + // now let's have sender_2 and sender_3 stack to pox addr 2 in // two different txs, and make sure that they sum together in the reward set. @@ -948,38 +962,27 @@ fn pox_integration_test() { panic!(""); } - // now let's mine a couple blocks, and then check the sender's nonce. - // at the end of mining three blocks, there should be _one_ transaction from the microblock - // only set that got mined (since the block before this one was empty, a microblock can - // be added), - // and _two_ transactions from the two anchor blocks that got mined (and processed) - // - // this one wakes up our node, so that it'll mine a microblock _and_ an anchor block. - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - // this one will contain the sortition from above anchor block, - // which *should* have also confirmed the microblock. - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // let's figure out how many micro-only and anchor-only txs got accepted - // by examining our account nonces: - let path = format!("{}/v2/accounts/{}?proof=0", &http_origin, spender_addr); - let res = client - .get(&path) - .send() - .unwrap() - .json::() - .unwrap(); - if res.nonce != 1 { - assert_eq!(res.nonce, 1, "Spender address nonce should be 1"); + // mine until the end of the current reward cycle. + sort_height = channel.get_sortitions_processed(); + while sort_height < 229 { + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + sort_height = channel.get_sortitions_processed(); + eprintln!("Sort height: {}", sort_height); } - let mut sort_height = channel.get_sortitions_processed(); - eprintln!("Sort height: {}", sort_height); - // now let's mine until the next reward cycle finishes ... + // we should have received _no_ Bitcoin commitments, because the pox participation threshold + // was not met! + let utxos = btc_regtest_controller.get_all_utxos(&pox_pubkey); + eprintln!("Got UTXOs: {}", utxos.len()); + assert_eq!( + utxos.len(), + 0, + "Should have received no outputs during PoX reward cycle" + ); - while sort_height < 229 { + // mine until the end of the next reward cycle, + // the participation threshold now should be met. + while sort_height < 239 { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); sort_height = channel.get_sortitions_processed(); eprintln!("Sort height: {}", sort_height); From 33ec31b3920312b0bca9878f4a74ecfb82bb134d Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 13 Oct 2020 11:09:58 -0500 Subject: [PATCH 25/26] use config option to control sample size, hardcode sample wait --- testnet/stacks-node/src/config.rs | 12 ++++++++++++ testnet/stacks-node/src/run_loop/neon.rs | 16 ++-------------- testnet/stacks-node/src/syncctl.rs | 16 +++++++--------- .../stacks-node/src/tests/neon_integrations.rs | 5 +++-- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index f997b63b2de..57abb707374 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -355,6 +355,9 @@ impl Config { .wait_time_for_microblocks .unwrap_or(default_node_config.wait_time_for_microblocks), prometheus_bind: node.prometheus_bind, + pox_sync_sample_secs: node + .pox_sync_sample_secs + .unwrap_or(default_node_config.pox_sync_sample_secs), }; node_config.set_bootstrap_node(node.bootstrap_node); node_config @@ -412,6 +415,9 @@ impl Config { .burnchain_op_tx_fee .unwrap_or(default_burnchain_config.burnchain_op_tx_fee), process_exit_at_block_height: burnchain.process_exit_at_block_height, + poll_time_secs: burnchain + .poll_time_secs + .unwrap_or(default_burnchain_config.poll_time_secs), } } None => default_burnchain_config, @@ -719,6 +725,7 @@ pub struct BurnchainConfig { pub local_mining_public_key: Option, pub burnchain_op_tx_fee: u64, pub process_exit_at_block_height: Option, + pub poll_time_secs: u64, } impl BurnchainConfig { @@ -741,6 +748,7 @@ impl BurnchainConfig { local_mining_public_key: None, burnchain_op_tx_fee: MINIMUM_DUST_FEE, process_exit_at_block_height: None, + poll_time_secs: 30, // TODO: this is a testnet specific value. } } @@ -791,6 +799,7 @@ pub struct BurnchainConfigFile { pub local_mining_public_key: Option, pub burnchain_op_tx_fee: Option, pub process_exit_at_block_height: Option, + pub poll_time_secs: Option, } #[derive(Clone, Debug, Default)] @@ -808,6 +817,7 @@ pub struct NodeConfig { pub mine_microblocks: bool, pub wait_time_for_microblocks: u64, pub prometheus_bind: Option, + pub pox_sync_sample_secs: u64, } impl NodeConfig { @@ -841,6 +851,7 @@ impl NodeConfig { mine_microblocks: false, wait_time_for_microblocks: 15000, prometheus_bind: None, + pox_sync_sample_secs: 30, } } @@ -939,6 +950,7 @@ pub struct NodeConfigFile { pub mine_microblocks: Option, pub wait_time_for_microblocks: Option, pub prometheus_bind: Option, + pub pox_sync_sample_secs: Option, } #[derive(Clone, Deserialize, Default)] diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index e57be63eab1..01d137b0cc2 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -33,16 +33,6 @@ pub struct RunLoop { coordinator_channels: Option<(CoordinatorReceivers, CoordinatorChannels)>, } -#[cfg(not(test))] -const BURNCHAIN_POLL_TIME: u64 = 30; // TODO: this is testnet-specific -#[cfg(test)] -const BURNCHAIN_POLL_TIME: u64 = 1; // TODO: this is testnet-specific - -#[cfg(not(test))] -const POX_SYNC_WAIT_MS: u64 = 1000; -#[cfg(test)] -const POX_SYNC_WAIT_MS: u64 = 0; - impl RunLoop { /// Sets up a runloop and node, given a config. #[cfg(not(test))] @@ -149,7 +139,6 @@ impl RunLoop { .iter() .map(|e| (e.address.clone(), e.amount)) .collect(); - let burnchain_poll_time = BURNCHAIN_POLL_TIME; // setup dispatcher let mut event_dispatcher = EventDispatcher::new(); @@ -220,9 +209,8 @@ impl RunLoop { mainnet, chainid, chainstate_path, - burnchain_poll_time, - self.config.connection_options.timeout, - POX_SYNC_WAIT_MS, + self.config.burnchain.poll_time_secs, + self.config.node.pox_sync_sample_secs, ) .unwrap(); let mut burnchain_height = 1; diff --git a/testnet/stacks-node/src/syncctl.rs b/testnet/stacks-node/src/syncctl.rs index 3c707ba5f7d..e0becb83297 100644 --- a/testnet/stacks-node/src/syncctl.rs +++ b/testnet/stacks-node/src/syncctl.rs @@ -41,10 +41,10 @@ pub struct PoxSyncWatchdog { steady_state_resync_ts: u64, /// chainstate handle chainstate: StacksChainState, - /// ms to sleep on waits - wait_ms: u64, } +const PER_SAMPLE_WAIT_MS: u64 = 1000; + impl PoxSyncWatchdog { pub fn new( mainnet: bool, @@ -52,7 +52,6 @@ impl PoxSyncWatchdog { chainstate_path: String, burnchain_poll_time: u64, download_timeout: u64, - wait_ms: u64, ) -> Result { let (chainstate, _) = match StacksChainState::open(mainnet, chain_id, &chainstate_path) { Ok(cs) => cs, @@ -78,7 +77,6 @@ impl PoxSyncWatchdog { steady_state_burnchain_sync_interval: burnchain_poll_time, steady_state_resync_ts: 0, chainstate: chainstate, - wait_ms, }) } @@ -354,7 +352,7 @@ impl PoxSyncWatchdog { &self.max_samples ); } - sleep_ms(self.wait_ms); + sleep_ms(PER_SAMPLE_WAIT_MS); continue; } @@ -363,7 +361,7 @@ impl PoxSyncWatchdog { { // still waiting for that first block in this reward cycle debug!("PoX watchdog: Still warming up: waiting until {}s for first Stacks block download (estimated download time: {}s)...", expected_first_block_deadline, self.estimated_block_download_time); - sleep_ms(self.wait_ms); + sleep_ms(PER_SAMPLE_WAIT_MS); continue; } @@ -410,7 +408,7 @@ impl PoxSyncWatchdog { { debug!("PoX watchdog: Still processing blocks; waiting until at least min({},{})s before burnchain synchronization (estimated block-processing time: {}s)", get_epoch_time_secs() + 1, expected_last_block_deadline, self.estimated_block_process_time); - sleep_ms(self.wait_ms); + sleep_ms(PER_SAMPLE_WAIT_MS); continue; } @@ -422,7 +420,7 @@ impl PoxSyncWatchdog { flat_attachable, flat_processed, &attachable_deviants, &processed_deviants); if !flat_attachable || !flat_processed { - sleep_ms(self.wait_ms); + sleep_ms(PER_SAMPLE_WAIT_MS); continue; } } else { @@ -433,7 +431,7 @@ impl PoxSyncWatchdog { debug!("PoX watchdog: In steady-state; waiting until at least {} before burnchain synchronization", self.steady_state_resync_ts); steady_state = true; } - sleep_ms(self.wait_ms); + sleep_ms(PER_SAMPLE_WAIT_MS); continue; } } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index f47acdbdd1e..1490486ab76 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -45,6 +45,9 @@ fn neon_integration_test_conf() -> (Config, StacksAddress) { Some(keychain.generate_op_signer().get_public_key().to_hex()); conf.burnchain.commit_anchor_block_within = 0; + conf.burnchain.poll_time_secs = 1; + conf.node.pox_sync_sample_secs = 1; + let miner_account = keychain.origin_address().unwrap(); (conf, miner_account) @@ -727,8 +730,6 @@ fn pox_integration_test() { let (mut conf, miner_account) = neon_integration_test_conf(); - let total_bal = 10_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); - let first_bal = 6_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); let second_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); let third_bal = 2_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); From 5a88fe89369ac00e0dc60e03842798bc58779564 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 13 Oct 2020 13:07:50 -0500 Subject: [PATCH 26/26] add arg to PoxConstants::new --- src/burnchains/mod.rs | 9 +++++---- src/chainstate/coordinator/tests.rs | 2 +- src/chainstate/stacks/boot/mod.rs | 15 ++++++++------- src/net/inv.rs | 4 ++-- src/net/mod.rs | 2 +- .../stacks-node/src/tests/neon_integrations.rs | 3 +-- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index da0c7c866fc..46979a4cc91 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -295,6 +295,7 @@ impl PoxConstants { prepare_length: u32, anchor_threshold: u32, pox_rejection_fraction: u64, + pox_participation_threshold_pct: u64, ) -> PoxConstants { assert!(anchor_threshold > (prepare_length / 2)); @@ -303,13 +304,13 @@ impl PoxConstants { prepare_length, anchor_threshold, pox_rejection_fraction, - pox_participation_threshold_pct: 5, + pox_participation_threshold_pct, _shadow: PhantomData, } } #[cfg(test)] pub fn test_default() -> PoxConstants { - PoxConstants::new(10, 5, 3, 25) + PoxConstants::new(10, 5, 3, 25, 5) } pub fn reward_slots(&self) -> u32 { @@ -327,11 +328,11 @@ impl PoxConstants { } pub fn mainnet_default() -> PoxConstants { - PoxConstants::new(1000, 240, 192, 25) + PoxConstants::new(1000, 240, 192, 25, 5) } pub fn testnet_default() -> PoxConstants { - PoxConstants::new(120, 30, 20, 3333333333333333) // total liquid supply is 40000000000000000 µSTX + PoxConstants::new(120, 30, 20, 3333333333333333, 5) // total liquid supply is 40000000000000000 µSTX } } diff --git a/src/chainstate/coordinator/tests.rs b/src/chainstate/coordinator/tests.rs index d6347a1b459..8ac5afdba97 100644 --- a/src/chainstate/coordinator/tests.rs +++ b/src/chainstate/coordinator/tests.rs @@ -252,7 +252,7 @@ fn make_reward_set_coordinator<'a>( pub fn get_burnchain(path: &str) -> Burnchain { let mut b = Burnchain::new(&format!("{}/burnchain/db/", path), "bitcoin", "regtest").unwrap(); - b.pox_constants = PoxConstants::new(5, 3, 3, 25); + b.pox_constants = PoxConstants::new(5, 3, 3, 25, 5); b } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 7b61f58ab1f..f9fcd8f4705 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -391,12 +391,13 @@ pub mod test { #[test] fn get_reward_threshold_units() { + let test_pox_constants = PoxConstants::new(1000, 1, 1, 1, 5); // when the liquid amount = the threshold step, // the threshold should always be the step size. let liquid = POX_THRESHOLD_STEPS_USTX; assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[], liquid ) @@ -405,7 +406,7 @@ pub mod test { ); assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[(rand_addr(), liquid)], liquid ) @@ -417,7 +418,7 @@ pub mod test { // with zero participation, should scale to 25% of liquid assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[], liquid ) @@ -427,7 +428,7 @@ pub mod test { // should be the same at 25% participation assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[(rand_addr(), liquid / 4)], liquid ) @@ -437,7 +438,7 @@ pub mod test { // but not at 30% participation assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[ (rand_addr(), liquid / 4), (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128)) @@ -451,7 +452,7 @@ pub mod test { // bump by just a little bit, should go to the next threshold step assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[ (rand_addr(), liquid / 4), (rand_addr(), (MICROSTACKS_PER_STACKS as u128)) @@ -465,7 +466,7 @@ pub mod test { // bump by just a little bit, should go to the next threshold step assert_eq!( StacksChainState::get_reward_threshold_and_participation( - &PoxConstants::new(1000, 1, 1, 1), + &test_pox_constants, &[(rand_addr(), liquid)], liquid ) diff --git a/src/net/inv.rs b/src/net/inv.rs index 2d038f24f9a..b867f619913 100644 --- a/src/net/inv.rs +++ b/src/net/inv.rs @@ -2559,7 +2559,7 @@ mod test { #[test] fn test_inv_merge_pox_inv() { let mut burnchain = Burnchain::new("unused", "bitcoin", "regtest").unwrap(); - burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25); + burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25, 5); let mut peer_inv = PeerBlocksInv::new(vec![0x01], vec![0x01], vec![0x01], 1, 1, 0); for i in 0..32 { @@ -2577,7 +2577,7 @@ mod test { #[test] fn test_inv_truncate_pox_inv() { let mut burnchain = Burnchain::new("unused", "bitcoin", "regtest").unwrap(); - burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25); + burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25, 5); let mut peer_inv = PeerBlocksInv::new(vec![0x01], vec![0x01], vec![0x01], 1, 1, 0); for i in 0..5 { diff --git a/src/net/mod.rs b/src/net/mod.rs index a5c78cf5a95..38d75f43fb8 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -2015,7 +2015,7 @@ pub mod test { ) .unwrap(), ); - burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25); + burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25, 5); let spending_account = TestMinerFactory::new().next_miner( &burnchain, diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index cae65d2ed23..edb17ab6c4d 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -759,8 +759,7 @@ fn pox_integration_test() { let http_origin = format!("http://{}", &conf.node.rpc_bind); let mut burnchain_config = btc_regtest_controller.get_burnchain(); - let mut pox_constants = PoxConstants::new(10, 5, 4, 5); - pox_constants.pox_participation_threshold_pct = 15; + let mut pox_constants = PoxConstants::new(10, 5, 4, 5, 15); burnchain_config.pox_constants = pox_constants; btc_regtest_controller.bootstrap_chain(201);