diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c8b8a17f2..9bab7a19d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,10 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - New `/new_pox_anchor` endpoint for broadcasting PoX anchor block processing. - Stacker bitvec in NakamotoBlock - New [`pox-4` contract](./stackslib/src/chainstate/stacks/boot/pox-4.clar) that reflects changes in how Stackers are signers in Nakamoto: - - `stack-stx`, `stack-extend`, and `stack-aggregation-commit` now include a `signer-key` parameter, which represents the public key used by the Signer. This key is used for determining the signer set in Nakamoto. + - `stack-stx`, `stack-extend`, `stack-increase` and `stack-aggregation-commit` now include a `signer-key` parameter, which represents the public key used by the Signer. This key is used for determining the signer set in Nakamoto. - Functions that include a `signer-key` parameter also include a `signer-sig` parameter to demonstrate that the owner of `signer-key` is approving that particular Stacking operation. For more details, refer to the `verify-signer-key-sig` method in the `pox-4` contract. - Signer key authorizations can be added via `set-signer-key-authorization` to omit the need for `signer-key` signatures + - A `max-amount` field is a field in signer key authorizations and defines the maximum amount of STX that can be locked in a single transaction. ### Modified diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index 907a643206f..a9afa338273 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -239,6 +239,12 @@ pub struct GenerateStackingSignatureArgs { /// Use `1` for stack-aggregation-commit #[arg(long)] pub period: u64, + /// The max amount of uSTX that can be used in this unique transaction + #[arg(long)] + pub max_amount: u128, + /// A unique identifier to prevent re-using this authorization + #[arg(long)] + pub auth_id: u128, } /// Parse the contract ID diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 38cfea57871..e9c0af22f2e 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -315,6 +315,8 @@ fn handle_generate_stacking_signature( args.method.topic(), config.network.to_chain_id(), args.period.into(), + args.max_amount, + args.auth_id, ) .expect("Failed to generate signature"); @@ -403,11 +405,14 @@ pub mod tests { lock_period: u128, public_key: &Secp256k1PublicKey, signature: Vec, + amount: u128, + max_amount: u128, + auth_id: u128, ) -> bool { let program = format!( r#" {} - (verify-signer-key-sig {} u{} "{}" u{} (some 0x{}) 0x{}) + (verify-signer-key-sig {} u{} "{}" u{} (some 0x{}) 0x{} u{} u{} u{}) "#, &*POX_4_CODE, //s Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), //p @@ -416,6 +421,9 @@ pub mod tests { lock_period, to_hex(signature.as_slice()), to_hex(public_key.to_bytes_compressed().as_slice()), + amount, + max_amount, + auth_id, ); execute_v2(&program) .expect("FATAL: could not execute program") @@ -436,6 +444,8 @@ pub mod tests { reward_cycle: 6, method: Pox4SignatureTopic::StackStx.into(), period: 12, + max_amount: u128::MAX, + auth_id: 1, }; let signature = handle_generate_stacking_signature(args.clone(), false); @@ -448,6 +458,9 @@ pub mod tests { args.period.into(), &public_key, signature.to_rsv(), + 100, + args.max_amount, + args.auth_id, ); assert!(valid); @@ -455,6 +468,8 @@ pub mod tests { args.period = 6; args.method = Pox4SignatureTopic::AggregationCommit.into(); args.reward_cycle = 7; + args.auth_id = 2; + args.max_amount = 100; let signature = handle_generate_stacking_signature(args.clone(), false); let public_key = Secp256k1PublicKey::from_private(&config.stacks_private_key); @@ -466,6 +481,9 @@ pub mod tests { args.period.into(), &public_key, signature.to_rsv(), + 100, + args.max_amount, + args.auth_id, ); assert!(valid); } @@ -480,6 +498,8 @@ pub mod tests { reward_cycle: 6, method: Pox4SignatureTopic::StackStx.into(), period: 12, + max_amount: u128::MAX, + auth_id: 1, }; let signature = handle_generate_stacking_signature(args.clone(), false); @@ -492,6 +512,8 @@ pub mod tests { &Pox4SignatureTopic::StackStx, CHAIN_ID_TESTNET, args.period.into(), + args.max_amount, + args.auth_id, ); let verify_result = public_key.verify(&message_hash.0, &signature); diff --git a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs index 721149789a2..f2dcfe4c181 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs @@ -97,6 +97,8 @@ fn advance_to_nakamoto( 6, &Pox4SignatureTopic::StackStx, 12_u128, + u128::MAX, + 1, ); let signing_key = StacksPublicKey::from_private(&test_stacker.signer_private_key); @@ -109,6 +111,8 @@ fn advance_to_nakamoto( &signing_key, 34, Some(signature), + u128::MAX, + 1, ) }) .collect() diff --git a/stackslib/src/chainstate/stacks/boot/mod.rs b/stackslib/src/chainstate/stacks/boot/mod.rs index 7908f3abd7d..cd18e4aa5a6 100644 --- a/stackslib/src/chainstate/stacks/boot/mod.rs +++ b/stackslib/src/chainstate/stacks/boot/mod.rs @@ -1845,6 +1845,8 @@ pub mod test { signer_key: &StacksPublicKey, burn_ht: u64, signature_opt: Option>, + max_amount: u128, + auth_id: u128, ) -> StacksTransaction { let addr_tuple = Value::Tuple(addr.as_clarity_tuple().unwrap()); let signature = match signature_opt { @@ -1862,6 +1864,8 @@ pub mod test { Value::UInt(lock_period), signature, Value::buff_from(signer_key.to_bytes_compressed()).unwrap(), + Value::UInt(max_amount), + Value::UInt(auth_id), ], ) .unwrap(); @@ -2005,6 +2009,8 @@ pub mod test { lock_period: u128, signer_key: StacksPublicKey, signature_opt: Option>, + max_amount: u128, + auth_id: u128, ) -> StacksTransaction { let addr_tuple = Value::Tuple(addr.as_clarity_tuple().unwrap()); let signature = match signature_opt { @@ -2020,6 +2026,8 @@ pub mod test { addr_tuple, signature, Value::buff_from(signer_key.to_bytes_compressed()).unwrap(), + Value::UInt(max_amount), + Value::UInt(auth_id), ], ) .unwrap(); @@ -2114,6 +2122,8 @@ pub mod test { reward_cycle: u128, signature_opt: Option>, signer_key: &Secp256k1PublicKey, + max_amount: u128, + auth_id: u128, ) -> StacksTransaction { let addr_tuple = Value::Tuple(pox_addr.as_clarity_tuple().unwrap()); let signature = match signature_opt { @@ -2129,6 +2139,8 @@ pub mod test { Value::UInt(reward_cycle), signature, Value::buff_from(signer_key.to_bytes_compressed()).unwrap(), + Value::UInt(max_amount), + Value::UInt(auth_id), ], ) .unwrap(); @@ -2140,12 +2152,25 @@ pub mod test { key: &StacksPrivateKey, nonce: u64, amount: u128, + signer_key: &Secp256k1PublicKey, + signature_opt: Option>, + max_amount: u128, + auth_id: u128, ) -> StacksTransaction { + let signature = signature_opt + .map(|sig| Value::some(Value::buff_from(sig).unwrap()).unwrap()) + .unwrap_or_else(|| Value::none()); let payload = TransactionPayload::new_contract_call( boot_code_test_addr(), POX_4_NAME, "stack-increase", - vec![Value::UInt(amount)], + vec![ + Value::UInt(amount), + signature, + Value::buff_from(signer_key.to_bytes_compressed()).unwrap(), + Value::UInt(max_amount), + Value::UInt(auth_id), + ], ) .unwrap(); @@ -2192,6 +2217,8 @@ pub mod test { reward_cycle: u128, topic: &Pox4SignatureTopic, period: u128, + max_amount: u128, + auth_id: u128, ) -> Vec { let signature = make_pox_4_signer_key_signature( pox_addr, @@ -2200,6 +2227,8 @@ pub mod test { topic, CHAIN_ID_TESTNET, period, + max_amount, + auth_id, ) .unwrap(); @@ -2215,6 +2244,8 @@ pub mod test { enabled: bool, nonce: u64, sender_key: Option<&StacksPrivateKey>, + max_amount: u128, + auth_id: u128, ) -> StacksTransaction { let signer_pubkey = StacksPublicKey::from_private(signer_key); let payload = TransactionPayload::new_contract_call( @@ -2228,6 +2259,8 @@ pub mod test { Value::string_ascii_from_bytes(topic.get_name_str().into()).unwrap(), Value::buff_from(signer_pubkey.to_bytes_compressed()).unwrap(), Value::Bool(enabled), + Value::UInt(max_amount), + Value::UInt(auth_id), ], ) .unwrap(); diff --git a/stackslib/src/chainstate/stacks/boot/pox-4.clar b/stackslib/src/chainstate/stacks/boot/pox-4.clar index 77c8ef2550b..681f8d9eab1 100644 --- a/stackslib/src/chainstate/stacks/boot/pox-4.clar +++ b/stackslib/src/chainstate/stacks/boot/pox-4.clar @@ -33,6 +33,9 @@ (define-constant ERR_INVALID_SIGNATURE_PUBKEY 35) (define-constant ERR_INVALID_SIGNATURE_RECOVER 36) (define-constant ERR_INVALID_REWARD_CYCLE 37) +(define-constant ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH 38) +(define-constant ERR_SIGNER_AUTH_USED 39) +(define-constant ERR_INVALID_INCREASE 40) ;; Valid values for burnchain address versions. ;; These first four correspond to address hash modes in Stacks 2.1, @@ -230,14 +233,34 @@ ;; this refers to `extend-count`. For `stack-aggregation-commit`, this is `u1`. period: uint, ;; A string representing the function where this authorization is valid. Either - ;; `stack-stx`, `stack-extend`, or `agg-commit`. - topic: (string-ascii 12), + ;; `stack-stx`, `stack-extend`, `stack-increase` or `agg-commit`. + topic: (string-ascii 14), ;; The PoX address that can be used with this signer key pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + ;; The unique auth-id for this authorization + auth-id: uint, + ;; The maximum amount of uSTX that can be used (per tx) with this signer key + max-amount: uint, } bool ;; Whether the authorization can be used or not ) +;; State for tracking used signer key authorizations. This prevents re-use +;; of the same signature or pre-set authorization for multiple transactions. +;; Refer to the `signer-key-authorizations` map for the documentation on these fields +(define-map used-signer-key-authorizations + { + signer-key: (buff 33), + reward-cycle: uint, + period: uint, + topic: (string-ascii 14), + pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + auth-id: uint, + max-amount: uint, + } + bool ;; Whether the field has been used or not +) + ;; What's the reward cycle number of the burnchain block height? ;; Will runtime-abort if height is less than the first burnchain block (this is intentional) (define-read-only (burn-height-to-reward-cycle (height uint)) @@ -597,6 +620,10 @@ ;; * 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. +;; +;; To ensure that the Stacker is authorized to use the provided `signer-key`, the stacker +;; must provide either a signature have an authorization already saved. Refer to +;; `verify-signer-key-sig` for more information. ;; ;; The tokens will unlock and be returned to the Stacker (tx-sender) automatically. (define-public (stack-stx (amount-ustx uint) @@ -604,7 +631,9 @@ (start-burn-ht uint) (lock-period uint) (signer-sig (optional (buff 65))) - (signer-key (buff 33))) + (signer-key (buff 33)) + (max-amount uint) + (auth-id 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)))) @@ -630,7 +659,7 @@ (err ERR_STACKING_INSUFFICIENT_FUNDS)) ;; Validate ownership of the given signer key - (try! (verify-signer-key-sig pox-addr (- first-reward-cycle u1) "stack-stx" lock-period signer-sig signer-key)) + (try! (consume-signer-key-authorization pox-addr (- first-reward-cycle u1) "stack-stx" lock-period signer-sig signer-key amount-ustx max-amount auth-id)) ;; ensure that stacking can be performed (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) @@ -709,12 +738,14 @@ ;; Generate a message hash for validating a signer key. ;; The message hash follows SIP018 for signing structured data. The structured data -;; is the tuple `{ pox-addr: { version, hashbytes }, reward-cycle }`. The domain is -;; `{ name: "pox-4-signer", version: "1.0.0", chain-id: chain-id }`. +;; is the tuple `{ pox-addr: { version, hashbytes }, reward-cycle, auth-id, max-amount }`. +;; The domain is `{ name: "pox-4-signer", version: "1.0.0", chain-id: chain-id }`. (define-read-only (get-signer-key-message-hash (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (reward-cycle uint) - (topic (string-ascii 12)) - (period uint)) + (topic (string-ascii 14)) + (period uint) + (max-amount uint) + (auth-id uint)) (sha256 (concat SIP018_MSG_PREFIX (concat @@ -725,6 +756,8 @@ reward-cycle: reward-cycle, topic: topic, period: period, + auth-id: auth-id, + max-amount: max-amount, }))))))) ;; Verify a signature from the signing key for this specific stacker. @@ -735,6 +768,10 @@ ;; the lock period are inflexible, which means that the stacker must confirm their transaction ;; during the exact reward cycle and with the exact period that the signature or authorization was ;; generated for. +;; +;; The `amount` field is checked to ensure it is not larger than `max-amount`, which is +;; a field in the authorization. `auth-id` is a random uint to prevent authorization +;; replays. ;; ;; This function does not verify the payload of the authorization. The caller of ;; this function must ensure that the payload (reward cycle, period, topic, and pox-addr) @@ -743,26 +780,64 @@ ;; When `signer-sig` is present, the public key is recovered from the signature ;; and compared to `signer-key`. If `signer-sig` is `none`, the function verifies that an authorization was previously ;; added for this key. +;; +;; This function checks to ensure that the authorization hasn't been used yet, but it +;; does _not_ store the authorization as used. The function `consume-signer-key-authorization` +;; handles that, and this read-only function is exposed for client-side verification. (define-read-only (verify-signer-key-sig (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (reward-cycle uint) - (topic (string-ascii 12)) + (topic (string-ascii 14)) (period uint) (signer-sig-opt (optional (buff 65))) - (signer-key (buff 33))) - (match signer-sig-opt - ;; `signer-sig` is present, verify the signature - signer-sig (ok (asserts! - (is-eq - (unwrap! (secp256k1-recover? - (get-signer-key-message-hash pox-addr reward-cycle topic period) - signer-sig) (err ERR_INVALID_SIGNATURE_RECOVER)) - signer-key) - (err ERR_INVALID_SIGNATURE_PUBKEY))) - ;; `signer-sig` is not present, verify that an authorization was previously added for this key - (ok (asserts! (default-to false (map-get? signer-key-authorizations - { signer-key: signer-key, reward-cycle: reward-cycle, period: period, topic: topic, pox-addr: pox-addr })) - (err ERR_NOT_ALLOWED))) + (signer-key (buff 33)) + (amount uint) + (max-amount uint) + (auth-id uint)) + (begin + ;; Validate that amount is less than or equal to `max-amount` + (asserts! (>= max-amount amount) (err ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH)) + (asserts! (is-none (map-get? used-signer-key-authorizations { signer-key: signer-key, reward-cycle: reward-cycle, topic: topic, period: period, pox-addr: pox-addr, auth-id: auth-id, max-amount: max-amount })) + (err ERR_SIGNER_AUTH_USED)) + (match signer-sig-opt + ;; `signer-sig` is present, verify the signature + signer-sig (ok (asserts! + (is-eq + (unwrap! (secp256k1-recover? + (get-signer-key-message-hash pox-addr reward-cycle topic period max-amount auth-id) + signer-sig) (err ERR_INVALID_SIGNATURE_RECOVER)) + signer-key) + (err ERR_INVALID_SIGNATURE_PUBKEY))) + ;; `signer-sig` is not present, verify that an authorization was previously added for this key + (ok (asserts! (default-to false (map-get? signer-key-authorizations + { signer-key: signer-key, reward-cycle: reward-cycle, period: period, topic: topic, pox-addr: pox-addr, auth-id: auth-id, max-amount: max-amount })) + (err ERR_NOT_ALLOWED))) )) + ) + +;; This function does two things: +;; +;; - Verify that a signer key is authorized to be used +;; - Updates the `used-signer-key-authorizations` map to prevent reuse +;; +;; This "wrapper" method around `verify-signer-key-sig` allows that function to remain +;; read-only, so that it can be used by clients as a sanity check before submitting a transaction. +(define-private (consume-signer-key-authorization (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (reward-cycle uint) + (topic (string-ascii 14)) + (period uint) + (signer-sig-opt (optional (buff 65))) + (signer-key (buff 33)) + (amount uint) + (max-amount uint) + (auth-id uint)) + (begin + ;; verify the authorization + (try! (verify-signer-key-sig pox-addr reward-cycle topic period signer-sig-opt signer-key amount max-amount auth-id)) + ;; update the `used-signer-key-authorizations` map + (asserts! (map-insert used-signer-key-authorizations + { signer-key: signer-key, reward-cycle: reward-cycle, topic: topic, period: period, pox-addr: pox-addr, auth-id: auth-id, max-amount: max-amount } true) + (err ERR_SIGNER_AUTH_USED)) + (ok true))) ;; Commit partially stacked STX and allocate a new PoX reward address slot. ;; This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions, @@ -779,7 +854,9 @@ (define-private (inner-stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (reward-cycle uint) (signer-sig (optional (buff 65))) - (signer-key (buff 33))) + (signer-key (buff 33)) + (max-amount uint) + (auth-id 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 }) @@ -787,8 +864,8 @@ ;; must be called directly by the tx-sender or by an allowed contract-caller (asserts! (check-caller-allowed) (err ERR_STACKING_PERMISSION_DENIED)) - (try! (verify-signer-key-sig pox-addr reward-cycle "agg-commit" u1 signer-sig signer-key)) (let ((amount-ustx (get stacked-amount partial-stacked))) + (try! (consume-signer-key-authorization pox-addr reward-cycle "agg-commit" u1 signer-sig signer-key amount-ustx max-amount auth-id)) (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1)) ;; Add the pox addr to the reward cycle, and extract the index of the PoX address ;; so the delegator can later use it to call stack-aggregation-increase. @@ -822,8 +899,10 @@ (define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (reward-cycle uint) (signer-sig (optional (buff 65))) - (signer-key (buff 33))) - (match (inner-stack-aggregation-commit pox-addr reward-cycle signer-sig signer-key) + (signer-key (buff 33)) + (max-amount uint) + (auth-id uint)) + (match (inner-stack-aggregation-commit pox-addr reward-cycle signer-sig signer-key max-amount auth-id) pox-addr-index (ok true) commit-err (err commit-err))) @@ -832,8 +911,10 @@ (define-public (stack-aggregation-commit-indexed (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (reward-cycle uint) (signer-sig (optional (buff 65))) - (signer-key (buff 33))) - (inner-stack-aggregation-commit pox-addr reward-cycle signer-sig signer-key)) + (signer-key (buff 33)) + (max-amount uint) + (auth-id uint)) + (inner-stack-aggregation-commit pox-addr reward-cycle signer-sig signer-key max-amount auth-id)) ;; Commit partially stacked STX to a PoX address which has already received some STX (more than the Stacking min). ;; This allows a delegator to lock up marginally more STX from new delegates, even if they collectively do not @@ -1000,19 +1081,23 @@ ;; `(some stacker)` as the listed stacker, and must be an upcoming reward cycle. (define-private (increase-reward-cycle-entry (reward-cycle-index uint) - (updates (optional { first-cycle: uint, reward-cycle: uint, stacker: principal, add-amount: uint }))) + (updates (optional { first-cycle: uint, reward-cycle: uint, stacker: principal, add-amount: uint, signer-key: (buff 33) }))) (let ((data (try! updates)) (first-cycle (get first-cycle data)) - (reward-cycle (get reward-cycle data))) + (reward-cycle (get reward-cycle data)) + (passed-signer-key (get signer-key data))) (if (> first-cycle reward-cycle) ;; not at first cycle to process yet - (some { first-cycle: first-cycle, reward-cycle: (+ u1 reward-cycle), stacker: (get stacker data), add-amount: (get add-amount data) }) + (some { first-cycle: first-cycle, reward-cycle: (+ u1 reward-cycle), stacker: (get stacker data), add-amount: (get add-amount data), signer-key: (get signer-key data) }) (let ((existing-entry (unwrap-panic (map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: reward-cycle-index }))) (existing-total (unwrap-panic (map-get? reward-cycle-total-stacked { reward-cycle: reward-cycle }))) + (existing-signer-key (get signer existing-entry)) (add-amount (get add-amount data)) (total-ustx (+ (get total-ustx existing-total) add-amount))) ;; stacker must match (asserts! (is-eq (get stacker existing-entry) (some (get stacker data))) none) + ;; signer-key must match + (asserts! (is-eq existing-signer-key passed-signer-key) none) ;; update the pox-address list (map-set reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: reward-cycle-index } @@ -1028,13 +1113,22 @@ (some { first-cycle: first-cycle, reward-cycle: (+ u1 reward-cycle), stacker: (get stacker data), - add-amount: (get add-amount data) }))))) + add-amount: (get add-amount data), + signer-key: passed-signer-key }))))) ;; Increase the number of STX locked. ;; *New in Stacks 2.1* ;; This method locks up an additional amount of STX from `tx-sender`'s, indicated -;; by `increase-by`. The `tx-sender` must already be Stacking. -(define-public (stack-increase (increase-by uint)) +;; by `increase-by`. The `tx-sender` must already be Stacking & must not be +;; straddling more than one signer-key for the cycles effected. +;; Refer to `verify-signer-key-sig` for more information on the authorization parameters +;; included here. +(define-public (stack-increase + (increase-by uint) + (signer-sig (optional (buff 65))) + (signer-key (buff 33)) + (max-amount uint) + (auth-id uint)) (let ((stacker-info (stx-account tx-sender)) (amount-stacked (get locked stacker-info)) (amount-unlocked (get unlocked stacker-info)) @@ -1043,7 +1137,9 @@ (first-increased-cycle (+ cur-cycle u1)) (stacker-state (unwrap! (map-get? stacking-state { stacker: tx-sender }) - (err ERR_STACK_INCREASE_NOT_LOCKED)))) + (err ERR_STACK_INCREASE_NOT_LOCKED))) + (cur-pox-addr (get pox-addr stacker-state)) + (cur-period (get lock-period stacker-state))) ;; tx-sender must be currently locked (asserts! (> amount-stacked u0) (err ERR_STACK_INCREASE_NOT_LOCKED)) @@ -1062,14 +1158,22 @@ ;; stacker must not be delegating (asserts! (is-none (get delegated-to stacker-state)) (err ERR_STACKING_IS_DELEGATED)) + + ;; Validate that amount is less than or equal to `max-amount` + (asserts! (>= max-amount (+ increase-by amount-stacked)) (err ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH)) + + ;; Verify signature from delegate that allows this sender for this cycle + (try! (consume-signer-key-authorization cur-pox-addr cur-cycle "stack-increase" cur-period signer-sig signer-key increase-by max-amount auth-id)) + ;; update reward cycle amounts (asserts! (is-some (fold increase-reward-cycle-entry (get reward-set-indexes stacker-state) (some { first-cycle: first-increased-cycle, reward-cycle: (get first-reward-cycle stacker-state), stacker: tx-sender, - add-amount: increase-by }))) - (err ERR_STACKING_UNREACHABLE)) + add-amount: increase-by, + signer-key: signer-key }))) + (err ERR_INVALID_INCREASE)) ;; NOTE: stacking-state map is unchanged: it does not track amount-stacked in PoX-4 (ok { stacker: tx-sender, total-locked: (+ amount-stacked increase-by)}))) @@ -1078,10 +1182,15 @@ ;; This method extends the `tx-sender`'s current lockup for an additional `extend-count` ;; and associates `pox-addr` with the rewards, The `signer-key` will be the key ;; used for signing. The `tx-sender` can thus decide to change the key when extending. +;; +;; Because no additional STX are locked in this function, the `amount` field used +;; to verify the signer key authorization is zero. Refer to `verify-signer-key-sig` for more information. (define-public (stack-extend (extend-count uint) (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (signer-sig (optional (buff 65))) - (signer-key (buff 33))) + (signer-key (buff 33)) + (max-amount uint) + (auth-id uint)) (let ((stacker-info (stx-account tx-sender)) ;; to extend, there must already be an etry in the stacking-state (stacker-state (unwrap! (get-stacker-info tx-sender) (err ERR_STACK_EXTEND_NOT_LOCKED))) @@ -1107,7 +1216,7 @@ (err ERR_STACKING_IS_DELEGATED)) ;; Verify signature from delegate that allows this sender for this cycle - (try! (verify-signer-key-sig pox-addr cur-cycle "stack-extend" extend-count signer-sig signer-key)) + (try! (consume-signer-key-authorization pox-addr cur-cycle "stack-extend" extend-count signer-sig signer-key u0 max-amount auth-id)) ;; TODO: add more assertions to sanity check the `stacker-info` values with ;; the `stacker-state` values @@ -1355,15 +1464,19 @@ ;; in `stack-stx` and `stack-extend`, the `reward-cycle` refers to the reward cycle ;; where the transaction is confirmed, **not** the reward cycle where stacking begins. ;; The `period` parameter must match the exact lock period (or extend count) used -;; in the stacking transaction. +;; in the stacking transaction. The `max-amount` parameter specifies the maximum amount +;; of STX that can be locked in an individual stacking transaction. `auth-id` is a +;; random uint to prevent replays. ;; ;; *New in Stacks 3.0* (define-public (set-signer-key-authorization (pox-addr { version: (buff 1), hashbytes: (buff 32)}) (period uint) (reward-cycle uint) - (topic (string-ascii 12)) + (topic (string-ascii 14)) (signer-key (buff 33)) - (allowed bool)) + (allowed bool) + (max-amount uint) + (auth-id uint)) (begin ;; Validate that `tx-sender` has the same pubkey hash as `signer-key` (asserts! (is-eq @@ -1373,7 +1486,7 @@ (asserts! (>= period u1) (err ERR_STACKING_INVALID_LOCK_PERIOD)) ;; Must be current or future reward cycle (asserts! (>= reward-cycle (current-pox-reward-cycle)) (err ERR_INVALID_REWARD_CYCLE)) - (map-set signer-key-authorizations { pox-addr: pox-addr, period: period, reward-cycle: reward-cycle, topic: topic, signer-key: signer-key } allowed) + (map-set signer-key-authorizations { pox-addr: pox-addr, period: period, reward-cycle: reward-cycle, topic: topic, signer-key: signer-key, auth-id: auth-id, max-amount: max-amount } allowed) (ok allowed))) ;; Get the _current_ PoX stacking delegation information for a stacker. If the information diff --git a/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs index 4f51b81e82a..a785fe2f6ad 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs @@ -41,6 +41,7 @@ use stacks_common::types::chainstate::{ use stacks_common::types::{Address, PrivateKey}; use stacks_common::util::hash::{hex_bytes, to_hex, Sha256Sum, Sha512Trunc256Sum}; use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey}; +use stdext::num::integer::Integer; use wsts::curve::point::{Compressed, Point}; use super::test::*; @@ -491,6 +492,7 @@ fn pox_extend_transition() { AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, ); + let auth_id = 1; let alice_signature = make_signer_key_signature( &alice_pox_addr, @@ -498,6 +500,8 @@ fn pox_extend_transition() { reward_cycle, &Pox4SignatureTopic::StackStx, 4_u128, + u128::MAX, + auth_id, ); let alice_lockup = make_pox_4_lockup( &alice, @@ -511,6 +515,8 @@ fn pox_extend_transition() { &alice_signer_key, tip.block_height, Some(alice_signature), + u128::MAX, + auth_id, ); let alice_pox_4_lock_nonce = 2; let alice_first_pox_4_unlock_height = @@ -569,6 +575,8 @@ fn pox_extend_transition() { reward_cycle, &Pox4SignatureTopic::StackStx, 3_u128, + u128::MAX, + 2, ); let tip = get_tip(peer.sortdb.as_ref()); @@ -581,6 +589,8 @@ fn pox_extend_transition() { &StacksPublicKey::from_private(&bob_signer_private), tip.block_height, Some(bob_signature), + u128::MAX, + 2, ); // new signing key needed @@ -593,6 +603,8 @@ fn pox_extend_transition() { reward_cycle, &Pox4SignatureTopic::StackExtend, 6_u128, + u128::MAX, + 3, ); // Alice can stack-extend in PoX v2 @@ -603,6 +615,8 @@ fn pox_extend_transition() { 6, alice_signer_key, Some(alice_signature), + u128::MAX, + 3, ); let alice_pox_4_extend_nonce = 3; @@ -864,6 +878,8 @@ fn pox_lock_unlock() { reward_cycle, &Pox4SignatureTopic::StackStx, lock_period.into(), + u128::MAX, + 1, ); txs.push(make_pox_4_lockup( key, @@ -874,6 +890,8 @@ fn pox_lock_unlock() { &StacksPublicKey::from_private(&signer_key), tip_height, Some(signature), + u128::MAX, + 1, )); pox_addr }) @@ -1455,6 +1473,9 @@ fn verify_signer_key_sig( reward_cycle: u128, period: u128, topic: &Pox4SignatureTopic, + amount: u128, + max_amount: u128, + auth_id: u128, ) -> Value { let result: Value = with_sortdb(peer, |ref mut chainstate, ref mut sortdb| { chainstate @@ -1469,13 +1490,16 @@ fn verify_signer_key_sig( LimitedCostTracker::new_free(), |env| { let program = format!( - "(verify-signer-key-sig {} u{} \"{}\" u{} (some 0x{}) 0x{})", + "(verify-signer-key-sig {} u{} \"{}\" u{} (some 0x{}) 0x{} u{} u{} u{})", Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), reward_cycle, topic.get_name_str(), period, to_hex(&signature), signing_key.to_hex(), + amount, + max_amount, + auth_id ); env.eval_read_only(&boot_code_id("pox-4", false), &program) }, @@ -1543,8 +1567,15 @@ fn verify_signer_key_signatures() { // Test 1: invalid reward cycle used in signature let last_reward_cycle = reward_cycle - 1; - let signature = - make_signer_key_signature(&bob_pox_addr, &bob, last_reward_cycle, &topic, period); + let signature = make_signer_key_signature( + &bob_pox_addr, + &bob, + last_reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); let result = verify_signer_key_sig( &signature, @@ -1555,12 +1586,23 @@ fn verify_signer_key_signatures() { reward_cycle, period, &topic, + 1, + u128::MAX, + 1, ); assert_eq!(result, expected_error); // Test 2: Invalid pox-addr used in signature - let signature = make_signer_key_signature(&alice_pox_addr, &bob, reward_cycle, &topic, period); + let signature = make_signer_key_signature( + &alice_pox_addr, + &bob, + reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); let result = verify_signer_key_sig( &signature, @@ -1571,13 +1613,24 @@ fn verify_signer_key_signatures() { reward_cycle, period, &topic, + 1, + u128::MAX, + 1, ); assert_eq!(result, expected_error); // Test 3: Invalid signer key used in signature - let signature = make_signer_key_signature(&bob_pox_addr, &alice, reward_cycle, &topic, period); + let signature = make_signer_key_signature( + &bob_pox_addr, + &alice, + reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); let result = verify_signer_key_sig( &signature, @@ -1588,6 +1641,9 @@ fn verify_signer_key_signatures() { reward_cycle, period, &topic, + 1, + u128::MAX, + 1, ); assert_eq!(result, expected_error); @@ -1599,6 +1655,8 @@ fn verify_signer_key_signatures() { reward_cycle, &Pox4SignatureTopic::StackStx, period, + u128::MAX, + 1, ); let result = verify_signer_key_sig( &signature, @@ -1609,12 +1667,23 @@ fn verify_signer_key_signatures() { reward_cycle, period, &Pox4SignatureTopic::StackExtend, // different + 1, + u128::MAX, + 1, ); assert_eq!(result, expected_error); // Test 5: invalid period - let signature = make_signer_key_signature(&bob_pox_addr, &bob, reward_cycle, &topic, period); + let signature = make_signer_key_signature( + &bob_pox_addr, + &bob, + reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); let result = verify_signer_key_sig( &signature, &bob_public_key, @@ -1624,13 +1693,100 @@ fn verify_signer_key_signatures() { reward_cycle, period + 1, // different &topic, + 1, + u128::MAX, + 1, + ); + + assert_eq!(result, expected_error); + + // Test incorrect auth-id + let signature = make_signer_key_signature( + &bob_pox_addr, + &bob, + reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); + let result = verify_signer_key_sig( + &signature, + &bob_public_key, + &bob_pox_addr, + &mut peer, + &latest_block, + reward_cycle, + period, + &topic, + 1, + u128::MAX, + 2, // different ); + assert_eq!(result, expected_error); + // Test incorrect max-amount + let signature = make_signer_key_signature( + &bob_pox_addr, + &bob, + reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); + let result = verify_signer_key_sig( + &signature, + &bob_public_key, + &bob_pox_addr, + &mut peer, + &latest_block, + reward_cycle, + period, + &topic, + 1, + 11111, // different + 1, + ); assert_eq!(result, expected_error); - // Test 6: using a valid signature + // Test amount > max-amount + let signature = make_signer_key_signature( + &bob_pox_addr, + &bob, + reward_cycle, + &topic, + period, + 4, // less than max to invalidate `amount` + 1, + ); + let result = verify_signer_key_sig( + &signature, + &bob_public_key, + &bob_pox_addr, + &mut peer, + &latest_block, + reward_cycle, + period, + &topic, + 5, // different + 4, // less than amount + 1, + ); + // Different error code + assert_eq!(result, Value::error(Value::Int(38)).unwrap()); + + // Test using a valid signature - let signature = make_signer_key_signature(&bob_pox_addr, &bob, reward_cycle, &topic, period); + let signature = make_signer_key_signature( + &bob_pox_addr, + &bob, + reward_cycle, + &topic, + period, + u128::MAX, + 1, + ); let result = verify_signer_key_sig( &signature, @@ -1641,6 +1797,9 @@ fn verify_signer_key_signatures() { reward_cycle, period, &topic, + 1, + u128::MAX, + 1, ); assert_eq!(result, Value::okay_true()); @@ -1681,6 +1840,8 @@ fn stack_stx_verify_signer_sig() { reward_cycle - 1, &topic, lock_period, + u128::MAX, + 1, ); let invalid_cycle_nonce = stacker_nonce; let invalid_cycle_stack = make_pox_4_lockup( @@ -1692,6 +1853,8 @@ fn stack_stx_verify_signer_sig() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, ); // test 2: invalid pox addr @@ -1702,9 +1865,11 @@ fn stack_stx_verify_signer_sig() { reward_cycle, &topic, lock_period, + u128::MAX, + 1, ); - let invalid_stacker_nonce = stacker_nonce; - let invalid_stacker_tx = make_pox_4_lockup( + let invalid_pox_addr_nonce = stacker_nonce; + let invalid_pox_addr_tx = make_pox_4_lockup( &stacker_key, stacker_nonce, min_ustx, @@ -1713,6 +1878,8 @@ fn stack_stx_verify_signer_sig() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, ); // Test 3: invalid key used to sign @@ -1723,6 +1890,8 @@ fn stack_stx_verify_signer_sig() { reward_cycle, &topic, lock_period, + u128::MAX, + 1, ); let invalid_key_nonce = stacker_nonce; let invalid_key_tx = make_pox_4_lockup( @@ -1734,6 +1903,8 @@ fn stack_stx_verify_signer_sig() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, ); // Test 4: invalid topic @@ -1744,6 +1915,8 @@ fn stack_stx_verify_signer_sig() { reward_cycle, &Pox4SignatureTopic::StackExtend, // wrong topic lock_period, + u128::MAX, + 1, ); let invalid_topic_nonce = stacker_nonce; let invalid_topic_tx = make_pox_4_lockup( @@ -1755,6 +1928,8 @@ fn stack_stx_verify_signer_sig() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, ); // Test 5: invalid period @@ -1765,6 +1940,8 @@ fn stack_stx_verify_signer_sig() { reward_cycle, &topic, lock_period + 1, // wrong period + u128::MAX, + 1, ); let invalid_period_nonce = stacker_nonce; let invalid_period_tx = make_pox_4_lockup( @@ -1776,12 +1953,96 @@ fn stack_stx_verify_signer_sig() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, + ); + + // Test invalid auth-id + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); + let invalid_auth_id_nonce = stacker_nonce; + let invalid_auth_id_tx = make_pox_4_lockup( + &stacker_key, + stacker_nonce, + min_ustx, + &pox_addr, + lock_period, + &signer_public_key, + block_height, + Some(signature), + u128::MAX, + 2, // wrong auth-id + ); + + // Test invalid amount + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + min_ustx.saturating_sub(1), + 1, + ); + let invalid_amount_nonce = stacker_nonce; + let invalid_amount_tx = make_pox_4_lockup( + &stacker_key, + stacker_nonce, + min_ustx, + &pox_addr, + lock_period, + &signer_public_key, + block_height, + Some(signature), + min_ustx.saturating_sub(1), + 1, + ); + + // Test invalid max-amount + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + u128::MAX.saturating_sub(1), + 1, + ); + let invalid_max_amount_nonce = stacker_nonce; + let invalid_max_amount_tx = make_pox_4_lockup( + &stacker_key, + stacker_nonce, + min_ustx, + &pox_addr, + lock_period, + &signer_public_key, + block_height, + Some(signature), + u128::MAX, // different than signature + 1, ); - // Test 6: valid signature + // Test: valid signature stacker_nonce += 1; - let signature = - make_signer_key_signature(&pox_addr, &signer_key, reward_cycle, &topic, lock_period); + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); let valid_nonce = stacker_nonce; let valid_tx = make_pox_4_lockup( &stacker_key, @@ -1791,19 +2052,24 @@ fn stack_stx_verify_signer_sig() { lock_period, &signer_public_key, block_height, - Some(signature), + Some(signature.clone()), + u128::MAX, + 1, ); let txs = vec![ invalid_cycle_stack, - invalid_stacker_tx, + invalid_pox_addr_tx, invalid_key_tx, invalid_topic_tx, invalid_period_tx, + invalid_auth_id_tx, + invalid_amount_tx, + invalid_max_amount_tx, valid_tx, ]; - peer.tenure_with_txs(&txs, &mut coinbase_nonce); + let latest_block = peer.tenure_with_txs(&txs, &mut coinbase_nonce); let stacker_txs = get_last_block_sender_transactions(&observer, stacker_addr); let expected_error = Value::error(Value::Int(35)).unwrap(); @@ -1812,15 +2078,51 @@ fn stack_stx_verify_signer_sig() { let tx_result = |nonce: u64| -> Value { stacker_txs.get(nonce as usize).unwrap().result.clone() }; assert_eq!(tx_result(invalid_cycle_nonce), expected_error); - assert_eq!(tx_result(invalid_stacker_nonce), expected_error); + assert_eq!(tx_result(invalid_pox_addr_nonce), expected_error); assert_eq!(tx_result(invalid_key_nonce), expected_error); assert_eq!(tx_result(invalid_period_nonce), expected_error); assert_eq!(tx_result(invalid_topic_nonce), expected_error); + assert_eq!(tx_result(invalid_auth_id_nonce), expected_error); + assert_eq!(tx_result(invalid_max_amount_nonce), expected_error); + assert_eq!( + tx_result(invalid_amount_nonce), + Value::error(Value::Int(38)).unwrap() + ); // valid tx should succeed tx_result(valid_nonce) .expect_result_ok() .expect("Expected ok result from tx"); + + // Ensure that the used signature cannot be re-used + let result = verify_signer_key_sig( + &signature, + &signer_public_key, + &pox_addr, + &mut peer, + &latest_block, + reward_cycle, + lock_period, + &topic, + min_ustx, + u128::MAX, + 1, + ); + let expected_error = Value::error(Value::Int(39)).unwrap(); + assert_eq!(result, expected_error); + + // Ensure the authorization is stored as used + let entry = get_signer_key_authorization_used_pox_4( + &mut peer, + &latest_block, + &pox_addr, + reward_cycle.try_into().unwrap(), + &topic, + lock_period, + &signer_public_key, + u128::MAX, + 1, + ); } #[test] @@ -1850,6 +2152,8 @@ fn stack_extend_verify_sig() { reward_cycle, &Pox4SignatureTopic::StackStx, lock_period, + u128::MAX, + 1, ); let stack_nonce = stacker_nonce; let stack_tx = make_pox_4_lockup( @@ -1861,6 +2165,8 @@ fn stack_extend_verify_sig() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, ); // We need a new signer-key for the extend tx @@ -1874,6 +2180,8 @@ fn stack_extend_verify_sig() { reward_cycle - 1, &topic, lock_period, + u128::MAX, + 1, ); stacker_nonce += 1; let invalid_cycle_nonce = stacker_nonce; @@ -1884,6 +2192,8 @@ fn stack_extend_verify_sig() { lock_period, signer_public_key.clone(), Some(signature), + u128::MAX, + 1, ); // Test 2: invalid pox-addr @@ -1895,22 +2205,33 @@ fn stack_extend_verify_sig() { reward_cycle, &topic, lock_period, + u128::MAX, + 1, ); - let invalid_stacker_nonce = stacker_nonce; - let invalid_stacker_tx = make_pox_4_extend( + let invalid_pox_addr_nonce = stacker_nonce; + let invalid_pox_addr_tx = make_pox_4_extend( &stacker_key, stacker_nonce, pox_addr.clone(), lock_period, signer_public_key.clone(), Some(signature), + u128::MAX, + 1, ); // Test 3: invalid key used to sign stacker_nonce += 1; let other_key = Secp256k1PrivateKey::new(); - let signature = - make_signer_key_signature(&pox_addr, &other_key, reward_cycle, &topic, lock_period); + let signature = make_signer_key_signature( + &pox_addr, + &other_key, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); let invalid_key_nonce = stacker_nonce; let invalid_key_tx = make_pox_4_extend( &stacker_key, @@ -1919,28 +2240,87 @@ fn stack_extend_verify_sig() { lock_period, signer_public_key.clone(), Some(signature), + u128::MAX, + 1, ); - // Test 4: valid stack-extend + // Test invalid auth-id stacker_nonce += 1; - let signature = - make_signer_key_signature(&pox_addr, &signer_key, reward_cycle, &topic, lock_period); - let valid_nonce = stacker_nonce; - let valid_tx = make_pox_4_extend( + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); + let invalid_auth_id_nonce = stacker_nonce; + let invalid_auth_id_tx = make_pox_4_extend( &stacker_key, stacker_nonce, - pox_addr, + pox_addr.clone(), lock_period, signer_public_key.clone(), Some(signature), + u128::MAX, + 2, // wrong auth-id ); - peer.tenure_with_txs( - &[ - stack_tx, - invalid_cycle_tx, - invalid_stacker_tx, - invalid_key_tx, + // Test invalid max-amount + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + u128::MAX.saturating_sub(1), + 1, + ); + let invalid_max_amount_nonce = stacker_nonce; + let invalid_max_amount_tx = make_pox_4_extend( + &stacker_key, + stacker_nonce, + pox_addr.clone(), + lock_period, + signer_public_key.clone(), + Some(signature), + u128::MAX, // different than signature + 1, + ); + + // Test: valid stack-extend + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_key, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); + let valid_nonce = stacker_nonce; + let valid_tx = make_pox_4_extend( + &stacker_key, + stacker_nonce, + pox_addr.clone(), + lock_period, + signer_public_key.clone(), + Some(signature.clone()), + u128::MAX, + 1, + ); + + let latest_block = peer.tenure_with_txs( + &[ + stack_tx, + invalid_cycle_tx, + invalid_pox_addr_tx, + invalid_key_tx, + invalid_auth_id_tx, + invalid_max_amount_tx, valid_tx, ], &mut coinbase_nonce, @@ -1956,11 +2336,45 @@ fn stack_extend_verify_sig() { .expect_result_ok() .expect("Expected ok result from tx"); assert_eq!(tx_result(invalid_cycle_nonce), expected_error); - assert_eq!(tx_result(invalid_stacker_nonce), expected_error); + assert_eq!(tx_result(invalid_pox_addr_nonce), expected_error); assert_eq!(tx_result(invalid_key_nonce), expected_error); + assert_eq!(tx_result(invalid_auth_id_nonce), expected_error); + assert_eq!(tx_result(invalid_max_amount_nonce), expected_error); + + // valid tx should succeed tx_result(valid_nonce) .expect_result_ok() .expect("Expected ok result from tx"); + + // Ensure that the used signature cannot be re-used + let result = verify_signer_key_sig( + &signature, + &signer_public_key, + &pox_addr, + &mut peer, + &latest_block, + reward_cycle, + lock_period, + &topic, + min_ustx, + u128::MAX, + 1, + ); + let expected_error = Value::error(Value::Int(39)).unwrap(); + assert_eq!(result, expected_error); + + // Ensure the authorization is stored as used + let entry = get_signer_key_authorization_used_pox_4( + &mut peer, + &latest_block, + &pox_addr, + reward_cycle.try_into().unwrap(), + &topic, + lock_period, + &signer_public_key, + u128::MAX, + 1, + ); } #[test] @@ -2017,143 +2431,686 @@ fn stack_agg_commit_verify_sig() { let topic = Pox4SignatureTopic::AggregationCommit; - // Test 1: invalid reward cycle - delegate_nonce += 1; - let next_reward_cycle = reward_cycle + 1; + // Test 1: invalid reward cycle + delegate_nonce += 1; + let next_reward_cycle = reward_cycle + 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, // wrong cycle + &topic, + 1_u128, + u128::MAX, + 1, + ); + let invalid_cycle_nonce = delegate_nonce; + let invalid_cycle_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX, + 1, + ); + + // Test 2: invalid pox addr + delegate_nonce += 1; + let other_pox_addr = pox_addr_from(&Secp256k1PrivateKey::new()); + let signature = make_signer_key_signature( + &other_pox_addr, + &signer_sk, + next_reward_cycle, + &topic, + 1_u128, + u128::MAX, + 1, + ); + let invalid_pox_addr_nonce = delegate_nonce; + let invalid_pox_addr_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX, + 1, + ); + + // Test 3: invalid private key + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &delegate_key, + next_reward_cycle, + &topic, + 1_u128, + u128::MAX, + 1, + ); + let invalid_key_nonce = delegate_nonce; + let invalid_key_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX, + 1, + ); + + // Test 4: invalid period in signature + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + next_reward_cycle, + &topic, + 2_u128, // wrong period + u128::MAX, + 1, + ); + let invalid_period_nonce = delegate_nonce; + let invalid_period_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX, + 1, + ); + + // Test 5: invalid topic in signature + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + next_reward_cycle, + &Pox4SignatureTopic::StackStx, // wrong topic + 1_u128, + u128::MAX, + 1, + ); + let invalid_topic_nonce = delegate_nonce; + let invalid_topic_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX, + 1, + ); + + // Test using incorrect auth-id + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + next_reward_cycle, + &topic, + 1_u128, + u128::MAX, + 2, // wrong auth-id + ); + let invalid_auth_id_nonce = delegate_nonce; + let invalid_auth_id_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX, + 1, // different auth-id + ); + + // Test incorrect max-amount + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + next_reward_cycle, + &topic, + 1_u128, + u128::MAX, + 1, + ); + let invalid_max_amount_nonce = delegate_nonce; + let invalid_max_amount_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + u128::MAX - 1, // different max-amount + 1, + ); + + // Test amount > max-amount + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + next_reward_cycle, + &topic, + 1_u128, + min_ustx.saturating_sub(1), // amount > max-amount + 1, + ); + let invalid_amount_nonce = delegate_nonce; + let invalid_amount_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature), + &signer_pk, + min_ustx.saturating_sub(1), // amount > max-amount + 1, + ); + + // Test with valid signature + delegate_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + next_reward_cycle, + &topic, + 1_u128, + u128::MAX, + 1, + ); + let valid_nonce = delegate_nonce; + let valid_tx = make_pox_4_aggregation_commit_indexed( + &delegate_key, + delegate_nonce, + &pox_addr, + next_reward_cycle, + Some(signature.clone()), + &signer_pk, + u128::MAX, + 1, + ); + + let latest_block = peer.tenure_with_txs( + &[ + delegate_tx, + delegate_stack_stx_tx, + invalid_cycle_tx, + invalid_pox_addr_tx, + invalid_key_tx, + invalid_period_tx, + invalid_topic_tx, + invalid_auth_id_tx, + invalid_max_amount_tx, + invalid_amount_tx, + valid_tx, + ], + &mut coinbase_nonce, + ); + + let txs = get_last_block_sender_transactions(&observer, delegate_addr); + + let tx_result = |nonce: u64| -> Value { txs.get(nonce as usize).unwrap().result.clone() }; + + let expected_error = Value::error(Value::Int(35)).unwrap(); + let amount_too_high_error = Value::error(Value::Int(38)).unwrap(); + + tx_result(delegate_stack_stx_nonce) + .expect_result_ok() + .expect("Expected ok result from tx"); + assert_eq!(tx_result(invalid_cycle_nonce), expected_error); + assert_eq!(tx_result(invalid_pox_addr_nonce), expected_error); + assert_eq!(tx_result(invalid_key_nonce), expected_error); + assert_eq!(tx_result(invalid_period_nonce), expected_error); + assert_eq!(tx_result(invalid_topic_nonce), expected_error); + assert_eq!(tx_result(invalid_auth_id_nonce), expected_error); + assert_eq!(tx_result(invalid_max_amount_nonce), expected_error); + assert_eq!(tx_result(invalid_amount_nonce), amount_too_high_error); + tx_result(valid_nonce) + .expect_result_ok() + .expect("Expected ok result from tx"); + + // Ensure that the used signature cannot be re-used + let result = verify_signer_key_sig( + &signature, + &signer_pk, + &pox_addr, + &mut peer, + &latest_block, + next_reward_cycle, + 1, + &topic, + min_ustx, + u128::MAX, + 1, + ); + let expected_error = Value::error(Value::Int(39)).unwrap(); + assert_eq!(result, expected_error); + + // Ensure the authorization is stored as used + let entry = get_signer_key_authorization_used_pox_4( + &mut peer, + &latest_block, + &pox_addr, + next_reward_cycle.try_into().unwrap(), + &topic, + 1, + &signer_pk, + u128::MAX, + 1, + ); +} + +#[test] +fn stack_increase_verify_signer_key() { + let lock_period = 1; + let observer = TestEventObserver::new(); + let (burnchain, mut peer, keys, latest_block, block_height, coinbase_nonce) = + prepare_pox4_test(function_name!(), Some(&observer)); + + let mut coinbase_nonce = coinbase_nonce; + + let mut stacker_nonce = 0; + let stacker_key = &keys[0]; + let min_ustx = get_stacking_minimum(&mut peer, &latest_block); + let stacker_addr = key_to_stacks_addr(&stacker_key); + let signer_sk = &keys[1]; + let signer_pk = StacksPublicKey::from_private(signer_sk); + let pox_addr = pox_addr_from(&signer_sk); + + let reward_cycle = get_current_reward_cycle(&peer, &burnchain); + let topic = Pox4SignatureTopic::StackIncrease; + + // Setup: stack-stx + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &Pox4SignatureTopic::StackStx, + lock_period, + u128::MAX, + 1, + ); + let stack_nonce = stacker_nonce; + let stack_tx = make_pox_4_lockup( + &stacker_key, + stacker_nonce, + min_ustx, + &pox_addr, + lock_period, + &signer_pk, + block_height, + Some(signature), + u128::MAX, + 1, + ); + + // invalid reward cycle + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle - 1, // invalid + &topic, + lock_period, + u128::MAX, + 1, + ); + let invalid_cycle_nonce = stacker_nonce; + let invalid_cycle_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + // invalid pox addr + stacker_nonce += 1; + let other_pox_addr = pox_addr_from(&Secp256k1PrivateKey::new()); + let signature = make_signer_key_signature( + &other_pox_addr, // different than existing + &signer_sk, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); + let invalid_pox_addr_nonce = stacker_nonce; + let invalid_pox_addr_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + // invalid private key + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &stacker_key, // different than signer + reward_cycle, + &topic, + lock_period, + u128::MAX, + 1, + ); + let invalid_key_nonce = stacker_nonce; + let invalid_key_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + // invalid period + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &topic, + lock_period + 1, // wrong + u128::MAX, + 1, + ); + let invalid_period_nonce = stacker_nonce; + let invalid_period_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + // invalid topic + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &Pox4SignatureTopic::StackExtend, // wrong topic + lock_period, + u128::MAX, + 1, + ); + let invalid_topic_nonce = stacker_nonce; + let invalid_topic_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + // invalid auth-id + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &topic, + lock_period, + u128::MAX, + 2, // wrong auth-id + ); + let invalid_auth_id_nonce = stacker_nonce; + let invalid_auth_id_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + // invalid max-amount + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &topic, + lock_period, + u128::MAX.saturating_sub(1), + 1, + ); + let invalid_max_amount_nonce = stacker_nonce; + let invalid_max_amount_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, // different than signature + 1, + ); + + // invalid amount + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &topic, + lock_period, + min_ustx.saturating_sub(1), + 1, + ); + let invalid_amount_nonce = stacker_nonce; + let invalid_amount_tx = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + min_ustx.saturating_sub(1), + 1, + ); + + // Valid tx + stacker_nonce += 1; + let signature = make_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle, + &Pox4SignatureTopic::StackIncrease, + lock_period, + u128::MAX, + 1, + ); + let valid_nonce = stacker_nonce; + let stack_increase = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, + &signer_pk, + Some(signature), + u128::MAX, + 1, + ); + + let latest_block = peer.tenure_with_txs( + &[ + stack_tx, + invalid_cycle_tx, + invalid_pox_addr_tx, + invalid_key_tx, + invalid_period_tx, + invalid_topic_tx, + invalid_auth_id_tx, + invalid_max_amount_tx, + invalid_amount_tx, + stack_increase, + ], + &mut coinbase_nonce, + ); + + let txs = get_last_block_sender_transactions(&observer, stacker_addr); + let tx_result = |nonce: u64| -> Value { txs.get(nonce as usize).unwrap().result.clone() }; + let signature_error = Value::error(Value::Int(35)).unwrap(); + + // stack-stx should work + tx_result(stack_nonce) + .expect_result_ok() + .expect("Expected ok result from tx"); + assert_eq!(tx_result(invalid_cycle_nonce), signature_error); + assert_eq!(tx_result(invalid_pox_addr_nonce), signature_error); + assert_eq!(tx_result(invalid_key_nonce), signature_error); + assert_eq!(tx_result(invalid_period_nonce), signature_error); + assert_eq!(tx_result(invalid_topic_nonce), signature_error); + assert_eq!(tx_result(invalid_auth_id_nonce), signature_error); + assert_eq!(tx_result(invalid_max_amount_nonce), signature_error); + assert_eq!( + tx_result(invalid_amount_nonce), + Value::error(Value::Int(38)).unwrap() + ); + + // valid tx should succeed + tx_result(valid_nonce) + .expect_result_ok() + .expect("Expected ok result from tx"); +} + +#[test] +/// Verify that when calling `stack-increase`, the function +/// fails if the signer key for each cycle being updated is not the same +/// as the provided `signer-key` argument +fn stack_increase_different_signer_keys() { + let lock_period = 1; + let observer = TestEventObserver::new(); + let (burnchain, mut peer, keys, latest_block, block_height, coinbase_nonce) = + prepare_pox4_test(function_name!(), Some(&observer)); + + let mut coinbase_nonce = coinbase_nonce; + + let mut stacker_nonce = 0; + let stacker_key = &keys[0]; + let min_ustx = get_stacking_minimum(&mut peer, &latest_block); + let stacker_addr = key_to_stacks_addr(&stacker_key); + let signer_sk = &keys[1]; + let signer_pk = StacksPublicKey::from_private(signer_sk); + let pox_addr = pox_addr_from(&signer_sk); + + // Second key is used in `stack-extend` + let second_signer_sk = &keys[2]; + let second_signer_pk = StacksPublicKey::from_private(second_signer_sk); + + let reward_cycle = get_current_reward_cycle(&peer, &burnchain); + + // Setup: stack-stx let signature = make_signer_key_signature( &pox_addr, &signer_sk, - reward_cycle, // wrong cycle - &topic, - 1_u128, - ); - let invalid_cycle_nonce = delegate_nonce; - let invalid_cycle_tx = make_pox_4_aggregation_commit_indexed( - &delegate_key, - delegate_nonce, - &pox_addr, - next_reward_cycle, - Some(signature), - &signer_pk, - ); - - // Test 2: invalid pox addr - delegate_nonce += 1; - let other_pox_addr = pox_addr_from(&Secp256k1PrivateKey::new()); - let signature = make_signer_key_signature( - &other_pox_addr, - &signer_sk, - next_reward_cycle, - &topic, - 1_u128, + reward_cycle, + &Pox4SignatureTopic::StackStx, + lock_period, + u128::MAX, + 1, ); - let invalid_pox_addr_nonce = delegate_nonce; - let invalid_stacker_tx = make_pox_4_aggregation_commit_indexed( - &delegate_key, - delegate_nonce, + let stack_nonce = stacker_nonce; + let stack_tx = make_pox_4_lockup( + &stacker_key, + stacker_nonce, + min_ustx, &pox_addr, - next_reward_cycle, - Some(signature), + lock_period, &signer_pk, - ); - - // Test 3: invalid signature - delegate_nonce += 1; - let signature = - make_signer_key_signature(&pox_addr, &delegate_key, next_reward_cycle, &topic, 1_u128); - let invalid_key_nonce = delegate_nonce; - let invalid_key_tx = make_pox_4_aggregation_commit_indexed( - &delegate_key, - delegate_nonce, - &pox_addr, - next_reward_cycle, + block_height, Some(signature), - &signer_pk, + u128::MAX, + 1, ); - // Test 4: invalid period in signature - delegate_nonce += 1; + stacker_nonce += 1; let signature = make_signer_key_signature( &pox_addr, - &signer_sk, - next_reward_cycle, - &topic, - 2_u128, // wrong period + &second_signer_sk, + reward_cycle, + &Pox4SignatureTopic::StackExtend, + lock_period, + u128::MAX, + 1, ); - let invalid_period_nonce = delegate_nonce; - let invalid_period_tx = make_pox_4_aggregation_commit_indexed( - &delegate_key, - delegate_nonce, - &pox_addr, - next_reward_cycle, - Some(signature), - &signer_pk, + let extend_nonce = stacker_nonce; + let extend_tx = make_pox_4_extend( + &stacker_key, + stacker_nonce, + pox_addr.clone(), + lock_period, + second_signer_pk.clone(), + Some(signature.clone()), + u128::MAX, + 1, ); - // Test 5: invalid topic in signature - delegate_nonce += 1; + stacker_nonce += 1; let signature = make_signer_key_signature( &pox_addr, &signer_sk, - next_reward_cycle, - &Pox4SignatureTopic::StackStx, // wrong topic - 1_u128, + reward_cycle, + &Pox4SignatureTopic::StackIncrease, + 2, // 2 cycles total (1 from stack-stx, 1 from extend) + u128::MAX, + 1, ); - let invalid_topic_nonce = delegate_nonce; - let invalid_topic_tx = make_pox_4_aggregation_commit_indexed( - &delegate_key, - delegate_nonce, - &pox_addr, - next_reward_cycle, - Some(signature), + let increase_nonce = stacker_nonce; + let stack_increase = make_pox_4_stack_increase( + &stacker_key, + stacker_nonce, + min_ustx, &signer_pk, - ); - - // Test 6: valid signature - delegate_nonce += 1; - let signature = - make_signer_key_signature(&pox_addr, &signer_sk, next_reward_cycle, &topic, 1_u128); - let valid_nonce = delegate_nonce; - let valid_tx = make_pox_4_aggregation_commit_indexed( - &delegate_key, - delegate_nonce, - &pox_addr, - next_reward_cycle, Some(signature), - &signer_pk, + u128::MAX, + 1, ); - peer.tenure_with_txs( - &[ - delegate_tx, - delegate_stack_stx_tx, - invalid_cycle_tx, - invalid_stacker_tx, - invalid_key_tx, - invalid_period_tx, - invalid_topic_tx, - valid_tx, - ], - &mut coinbase_nonce, - ); + let latest_block = + peer.tenure_with_txs(&[stack_tx, extend_tx, stack_increase], &mut coinbase_nonce); - let txs = get_last_block_sender_transactions(&observer, delegate_addr); + let txs = get_last_block_sender_transactions(&observer, stacker_addr.clone()); let tx_result = |nonce: u64| -> Value { txs.get(nonce as usize).unwrap().result.clone() }; - let expected_error = Value::error(Value::Int(35)).unwrap(); - - tx_result(delegate_stack_stx_nonce) + // stack-stx should work + tx_result(stack_nonce) .expect_result_ok() .expect("Expected ok result from tx"); - assert_eq!(tx_result(invalid_cycle_nonce), expected_error); - assert_eq!(tx_result(invalid_pox_addr_nonce), expected_error); - assert_eq!(tx_result(invalid_key_nonce), expected_error); - assert_eq!(tx_result(invalid_period_nonce), expected_error); - assert_eq!(tx_result(invalid_topic_nonce), expected_error); - tx_result(valid_nonce) + // `stack-extend` should work + tx_result(extend_nonce) .expect_result_ok() .expect("Expected ok result from tx"); + let increase_result = tx_result(increase_nonce); + + // Validate that the error is not due to the signature + assert_ne!( + tx_result(increase_nonce), + Value::error(Value::Int(35)).unwrap() + ); + assert_eq!(increase_result, Value::error(Value::Int(40)).unwrap()) } pub fn assert_latest_was_burn(peer: &mut TestPeer) { @@ -2256,6 +3213,8 @@ fn stack_stx_signer_key() { reward_cycle, &Pox4SignatureTopic::StackStx, 2_u128, + u128::MAX, + 1, ); let txs = vec![make_pox_4_contract_call( @@ -2269,6 +3228,8 @@ fn stack_stx_signer_key() { Value::UInt(2), Value::some(Value::buff_from(signature.clone()).unwrap()).unwrap(), signer_key_val.clone(), + Value::UInt(u128::MAX), + Value::UInt(1), ], )]; @@ -2346,6 +3307,8 @@ fn stack_stx_signer_auth() { &signer_public_key, block_height, None, + u128::MAX, + 1, ); let enable_auth_nonce = signer_nonce; @@ -2358,6 +3321,8 @@ fn stack_stx_signer_auth() { true, signer_nonce, None, + u128::MAX, + 1, ); // Ensure that stack-stx succeeds with auth @@ -2372,6 +3337,8 @@ fn stack_stx_signer_auth() { &signer_public_key, block_height, None, + u128::MAX, + 1, ); let txs = vec![failed_stack_tx, enable_auth_tx, valid_stack_tx]; @@ -2478,6 +3445,8 @@ fn stack_agg_commit_signer_auth() { next_reward_cycle, None, &signer_pk, + u128::MAX, + 1, ); // Signer enables auth @@ -2491,6 +3460,8 @@ fn stack_agg_commit_signer_auth() { true, enable_auth_nonce, None, + u128::MAX, + 1, ); // Stack agg works with auth @@ -2503,6 +3474,8 @@ fn stack_agg_commit_signer_auth() { next_reward_cycle, None, &signer_pk, + u128::MAX, + 1, ); let txs = vec![ @@ -2557,6 +3530,8 @@ fn stack_extend_signer_auth() { reward_cycle, &Pox4SignatureTopic::StackStx, lock_period, + u128::MAX, + 1, ); let stack_nonce = stacker_nonce; let stack_tx = make_pox_4_lockup( @@ -2568,6 +3543,8 @@ fn stack_extend_signer_auth() { &signer_public_key, block_height, Some(signature), + u128::MAX, + 1, ); // Stack-extend should fail without auth @@ -2580,6 +3557,8 @@ fn stack_extend_signer_auth() { lock_period, signer_public_key.clone(), None, + u128::MAX, + 1, ); // Enable authorization @@ -2593,6 +3572,8 @@ fn stack_extend_signer_auth() { true, enable_auth_nonce, None, + u128::MAX, + 1, ); // Stack-extend should work with auth @@ -2605,6 +3586,8 @@ fn stack_extend_signer_auth() { lock_period, signer_public_key.clone(), None, + u128::MAX, + 1, ); let txs = vec![stack_tx, invalid_cycle_tx, enable_auth_tx, valid_tx]; @@ -2658,6 +3641,8 @@ fn test_set_signer_key_auth() { true, invalid_enable_nonce, Some(&alice_key), + u128::MAX, + 1, ); // Test that period is at least u1 @@ -2672,6 +3657,8 @@ fn test_set_signer_key_auth() { false, signer_invalid_period_nonce, Some(&signer_key), + u128::MAX, + 1, ); let signer_invalid_cycle_nonce = signer_nonce; @@ -2686,6 +3673,8 @@ fn test_set_signer_key_auth() { false, signer_invalid_cycle_nonce, Some(&signer_key), + u128::MAX, + 1, ); // Disable auth for `signer-key` @@ -2698,6 +3687,8 @@ fn test_set_signer_key_auth() { false, signer_nonce, None, + u128::MAX, + 1, ); let latest_block = peer.tenure_with_txs( @@ -2755,6 +3746,8 @@ fn test_set_signer_key_auth() { &Pox4SignatureTopic::StackStx, lock_period.try_into().unwrap(), &signer_public_key, + u128::MAX, + 1, ); assert_eq!(signer_key_enabled.unwrap(), false); @@ -2771,6 +3764,8 @@ fn test_set_signer_key_auth() { true, enable_auth_nonce, None, + u128::MAX, + 1, ); let latest_block = peer.tenure_with_txs(&[enable_auth_tx], &mut coinbase_nonce); @@ -2783,6 +3778,8 @@ fn test_set_signer_key_auth() { &Pox4SignatureTopic::StackStx, lock_period.try_into().unwrap(), &signer_public_key, + u128::MAX, + 1, ); assert_eq!(signer_key_enabled.unwrap(), true); @@ -2799,6 +3796,8 @@ fn test_set_signer_key_auth() { false, disable_auth_nonce, None, + u128::MAX, + 1, ); let latest_block = peer.tenure_with_txs(&[disable_auth_tx], &mut coinbase_nonce); @@ -2811,6 +3810,8 @@ fn test_set_signer_key_auth() { &Pox4SignatureTopic::StackStx, lock_period.try_into().unwrap(), &signer_public_key, + u128::MAX, + 1, ); assert_eq!(signer_key_enabled.unwrap(), false); @@ -2851,6 +3852,8 @@ fn stack_extend_signer_key() { reward_cycle, &Pox4SignatureTopic::StackStx, lock_period, + u128::MAX, + 1, ); let txs = vec![make_pox_4_lockup( @@ -2862,6 +3865,8 @@ fn stack_extend_signer_key() { &signer_key, block_height, Some(signature), + u128::MAX, + 1, )]; stacker_nonce += 1; @@ -2874,21 +3879,19 @@ fn stack_extend_signer_key() { reward_cycle, &Pox4SignatureTopic::StackExtend, 1_u128, + u128::MAX, + 1, ); - // (define-public (stack-extend (extend-count uint) - // (pox-addr { version: (buff 1), hashbytes: (buff 32) }) - // (signer-key (buff 33))) - let update_txs = vec![make_pox_4_contract_call( - stacker_key, + let update_txs = vec![make_pox_4_extend( + &stacker_key, stacker_nonce, - "stack-extend", - vec![ - Value::UInt(1), - pox_addr_val.clone(), - Value::some(Value::buff_from(signature.clone()).unwrap()).unwrap(), - signer_extend_key_val.clone(), - ], + pox_addr.clone(), + 1, + signer_extend_key.clone(), + Some(signature), + u128::MAX, + 1, )]; latest_block = peer.tenure_with_txs(&update_txs, &mut coinbase_nonce); @@ -2959,6 +3962,8 @@ fn delegate_stack_stx_signer_key() { next_reward_cycle.into(), &Pox4SignatureTopic::AggregationCommit, 1_u128, + u128::MAX, + 1, ); let txs = vec![ @@ -2996,6 +4001,8 @@ fn delegate_stack_stx_signer_key() { Value::UInt(next_reward_cycle.into()), Value::some(Value::buff_from(signature).unwrap()).unwrap(), signer_key_val.clone(), + Value::UInt(u128::MAX), + Value::UInt(1), ], ), ]; @@ -3146,6 +4153,8 @@ fn delegate_stack_stx_extend_signer_key() { next_reward_cycle.into(), &Pox4SignatureTopic::AggregationCommit, 1_u128, + u128::MAX, + 1, ); let delegate_stack_extend = make_pox_4_delegate_stack_extend( @@ -3165,6 +4174,8 @@ fn delegate_stack_stx_extend_signer_key() { Value::UInt(next_reward_cycle.into()), Value::some(Value::buff_from(signature).unwrap()).unwrap(), signer_key_val.clone(), + Value::UInt(u128::MAX), + Value::UInt(1), ], ); @@ -3174,6 +4185,8 @@ fn delegate_stack_stx_extend_signer_key() { extend_cycle.into(), &Pox4SignatureTopic::AggregationCommit, 1_u128, + u128::MAX, + 2, ); let agg_tx_1 = make_pox_4_contract_call( @@ -3185,6 +4198,8 @@ fn delegate_stack_stx_extend_signer_key() { Value::UInt(extend_cycle.into()), Value::some(Value::buff_from(extend_signature).unwrap()).unwrap(), signer_extend_key_val.clone(), + Value::UInt(u128::MAX), + Value::UInt(2), ], ); @@ -3250,6 +4265,8 @@ fn stack_increase() { reward_cycle, &Pox4SignatureTopic::StackStx, lock_period, + u128::MAX, + 1, ); let stack_stx = make_pox_4_lockup( @@ -3261,6 +4278,8 @@ fn stack_increase() { &signing_pk, block_height as u64, Some(signature), + u128::MAX, + 1, ); // Initial tx arr includes a stack_stx pox_4 helper found in mod.rs @@ -3277,8 +4296,25 @@ fn stack_increase() { alice_nonce += 1; - let stack_increase = - make_pox_4_stack_increase(alice_stacking_private_key, alice_nonce, min_ustx); + let signature = make_signer_key_signature( + &pox_addr, + &signing_sk, + reward_cycle, + &Pox4SignatureTopic::StackIncrease, + lock_period, + u128::MAX, + 1, + ); + + let stack_increase = make_pox_4_stack_increase( + alice_stacking_private_key, + alice_nonce, + min_ustx, + &signing_pk, + Some(signature), + u128::MAX, + 1, + ); // Next tx arr includes a stack_increase pox_4 helper found in mod.rs let txs = vec![stack_increase]; let latest_block = peer.tenure_with_txs(&txs, &mut coinbase_nonce); @@ -3389,6 +4425,8 @@ fn delegate_stack_increase() { next_reward_cycle.into(), &Pox4SignatureTopic::AggregationCommit, 1_u128, + u128::MAX, + 1, ); let agg_tx = make_pox_4_contract_call( @@ -3400,6 +4438,8 @@ fn delegate_stack_increase() { Value::UInt(next_reward_cycle.into()), (Value::some(Value::buff_from(signature).unwrap()).unwrap()), signer_key_val.clone(), + Value::UInt(u128::MAX), + Value::UInt(1), ], ); @@ -3461,6 +4501,37 @@ pub fn get_stacking_state_pox_4( }) } +pub fn make_signer_key_authorization_lookup_key( + pox_addr: &PoxAddress, + reward_cycle: u64, + topic: &Pox4SignatureTopic, + period: u128, + signer_key: &StacksPublicKey, + max_amount: u128, + auth_id: u128, +) -> Value { + TupleData::from_data(vec![ + ( + "pox-addr".into(), + pox_addr.as_clarity_tuple().unwrap().into(), + ), + ("reward-cycle".into(), Value::UInt(reward_cycle.into())), + ( + "topic".into(), + Value::string_ascii_from_bytes(topic.get_name_str().into()).unwrap(), + ), + ("period".into(), Value::UInt(period.into())), + ( + "signer-key".into(), + Value::buff_from(signer_key.to_bytes_compressed()).unwrap(), + ), + ("max-amount".into(), Value::UInt(max_amount)), + ("auth-id".into(), Value::UInt(auth_id)), + ]) + .unwrap() + .into() +} + pub fn get_signer_key_authorization_pox_4( peer: &mut TestPeer, tip: &StacksBlockId, @@ -3469,42 +4540,70 @@ pub fn get_signer_key_authorization_pox_4( topic: &Pox4SignatureTopic, period: u128, signer_key: &StacksPublicKey, + max_amount: u128, + auth_id: u128, ) -> Option { with_clarity_db_ro(peer, tip, |db| { - let lookup_tuple = TupleData::from_data(vec![ - ( - "pox-addr".into(), - pox_addr.as_clarity_tuple().unwrap().into(), - ), - ("reward-cycle".into(), Value::UInt(reward_cycle.into())), - ( - "topic".into(), - Value::string_ascii_from_bytes(topic.get_name_str().into()).unwrap(), - ), - ("period".into(), Value::UInt(period.into())), - ( - "signer-key".into(), - Value::buff_from(signer_key.to_bytes_compressed()).unwrap(), - ), - ]) + let lookup_tuple = make_signer_key_authorization_lookup_key( + &pox_addr, + reward_cycle, + &topic, + period, + &signer_key, + max_amount, + auth_id, + ); + let epoch = db.get_clarity_epoch_version().unwrap(); + db.fetch_entry_unknown_descriptor( + &boot_code_id(boot::POX_4_NAME, false), + "signer-key-authorizations", + &lookup_tuple, + &epoch, + ) .unwrap() - .into(); + .expect_optional() + .unwrap() + .map(|v| v.expect_bool().unwrap()) + }) +} + +/// Lookup in the `used-signer-key-authorizations` map +/// for a specific signer key authorization. If no entry is +/// found, `false` is returned. +pub fn get_signer_key_authorization_used_pox_4( + peer: &mut TestPeer, + tip: &StacksBlockId, + pox_addr: &PoxAddress, + reward_cycle: u64, + topic: &Pox4SignatureTopic, + period: u128, + signer_key: &StacksPublicKey, + max_amount: u128, + auth_id: u128, +) -> bool { + with_clarity_db_ro(peer, tip, |db| { + let lookup_tuple = make_signer_key_authorization_lookup_key( + &pox_addr, + reward_cycle, + &topic, + period, + &signer_key, + max_amount, + auth_id, + ); let epoch = db.get_clarity_epoch_version().unwrap(); - let map_entry = db - .fetch_entry_unknown_descriptor( - &boot_code_id(boot::POX_4_NAME, false), - "signer-key-authorizations", - &lookup_tuple, - &epoch, - ) - .unwrap() - .expect_optional() - .unwrap(); - match map_entry { - Some(v) => Some(v.expect_bool().unwrap()), - None => None, - } + db.fetch_entry_unknown_descriptor( + &boot_code_id(boot::POX_4_NAME, false), + "used-signer-key-authorizations", + &lookup_tuple, + &epoch, + ) + .unwrap() + .expect_optional() + .unwrap() + .map(|v| v.expect_bool().unwrap()) }) + .unwrap_or(false) } pub fn get_partially_stacked_state_pox_4( diff --git a/stackslib/src/net/tests/mod.rs b/stackslib/src/net/tests/mod.rs index 9111f10fc0f..67212840d7f 100644 --- a/stackslib/src/net/tests/mod.rs +++ b/stackslib/src/net/tests/mod.rs @@ -396,6 +396,8 @@ impl NakamotoBootPlan { reward_cycle.into(), &crate::util_lib::signed_structured_data::pox4::Pox4SignatureTopic::StackStx, 12_u128, + u128::MAX, + 1, ); make_pox_4_lockup( &test_stacker.stacker_private_key, @@ -406,6 +408,8 @@ impl NakamotoBootPlan { &StacksPublicKey::from_private(&test_stacker.signer_private_key), 34, Some(signature), + u128::MAX, + 1, ) }) .collect(); diff --git a/stackslib/src/util_lib/signed_structured_data.rs b/stackslib/src/util_lib/signed_structured_data.rs index 019443842d0..9cc0eaa0f1f 100644 --- a/stackslib/src/util_lib/signed_structured_data.rs +++ b/stackslib/src/util_lib/signed_structured_data.rs @@ -84,18 +84,22 @@ pub mod pox4 { StackStx("stack-stx"), AggregationCommit("agg-commit"), StackExtend("stack-extend"), + StackIncrease("stack-increase"), }); pub fn make_pox_4_signed_data_domain(chain_id: u32) -> Value { make_structured_data_domain("pox-4-signer", "1.0.0", chain_id) } + #[cfg_attr(test, mutants::skip)] pub fn make_pox_4_signer_key_message_hash( pox_addr: &PoxAddress, reward_cycle: u128, topic: &Pox4SignatureTopic, chain_id: u32, period: u128, + max_amount: u128, + auth_id: u128, ) -> Sha256Sum { let domain_tuple = make_pox_4_signed_data_domain(chain_id); let data_tuple = Value::Tuple( @@ -110,6 +114,8 @@ pub mod pox4 { "topic".into(), Value::string_ascii_from_bytes(topic.get_name_str().into()).unwrap(), ), + ("auth-id".into(), Value::UInt(auth_id)), + ("max-amount".into(), Value::UInt(max_amount)), ]) .unwrap(), ); @@ -117,16 +123,19 @@ pub mod pox4 { } impl Into for &'static str { + #[cfg_attr(test, mutants::skip)] fn into(self) -> Pox4SignatureTopic { match self { "stack-stx" => Pox4SignatureTopic::StackStx, "agg-commit" => Pox4SignatureTopic::AggregationCommit, "stack-extend" => Pox4SignatureTopic::StackExtend, + "stack-increase" => Pox4SignatureTopic::StackIncrease, _ => panic!("Invalid pox-4 signature topic"), } } } + #[cfg_attr(test, mutants::skip)] pub fn make_pox_4_signer_key_signature( pox_addr: &PoxAddress, signer_key: &StacksPrivateKey, @@ -134,9 +143,18 @@ pub mod pox4 { topic: &Pox4SignatureTopic, chain_id: u32, period: u128, + max_amount: u128, + auth_id: u128, ) -> Result { - let msg_hash = - make_pox_4_signer_key_message_hash(pox_addr, reward_cycle, topic, chain_id, period); + let msg_hash = make_pox_4_signer_key_message_hash( + pox_addr, + reward_cycle, + topic, + chain_id, + period, + max_amount, + auth_id, + ); signer_key.sign(msg_hash.as_bytes()) } @@ -166,6 +184,8 @@ pub mod pox4 { topic: &Pox4SignatureTopic, lock_period: u128, sender: &PrincipalData, + max_amount: u128, + auth_id: u128, ) -> Vec { let pox_contract_id = boot_code_id(POX_4_NAME, false); sim.execute_next_block_as_conn(|conn| { @@ -178,11 +198,13 @@ pub mod pox4 { LimitedCostTracker::new_free(), |env| { let program = format!( - "(get-signer-key-message-hash {} u{} \"{}\" u{})", + "(get-signer-key-message-hash {} u{} \"{}\" u{} u{} u{})", Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), //p reward_cycle, topic.get_name_str(), - lock_period + lock_period, + max_amount, + auth_id, ); env.eval_read_only(&pox_contract_id, &program) }, @@ -242,6 +264,8 @@ pub mod pox4 { let reward_cycle: u128 = 1; let topic = Pox4SignatureTopic::StackStx; let lock_period = 12; + let auth_id = 111; + let max_amount = u128::MAX; let expected_hash_vec = make_pox_4_signer_key_message_hash( &pox_addr, @@ -249,6 +273,8 @@ pub mod pox4 { &Pox4SignatureTopic::StackStx, CHAIN_ID_TESTNET, lock_period, + max_amount, + auth_id, ); let expected_hash = expected_hash_vec.as_bytes(); @@ -261,6 +287,8 @@ pub mod pox4 { &topic, lock_period, &principal, + max_amount, + auth_id, ); assert_eq!(expected_hash.clone(), result.as_slice()); @@ -276,6 +304,8 @@ pub mod pox4 { &topic, lock_period, &principal, + max_amount, + auth_id, ); assert_ne!(expected_hash.clone(), result.as_slice()); @@ -287,6 +317,8 @@ pub mod pox4 { &topic, lock_period, &principal, + max_amount, + auth_id, ); assert_ne!(expected_hash.clone(), result.as_slice()); @@ -298,6 +330,8 @@ pub mod pox4 { &Pox4SignatureTopic::AggregationCommit, lock_period, &principal, + max_amount, + auth_id, ); assert_ne!(expected_hash.clone(), result.as_slice()); @@ -309,6 +343,34 @@ pub mod pox4 { &topic, 0, &principal, + max_amount, + auth_id, + ); + assert_ne!(expected_hash.clone(), result.as_slice()); + + // Test 5: invalid max amount + let result = call_get_signer_message_hash( + &mut sim, + &pox_addr, + reward_cycle, + &topic, + lock_period, + &principal, + 1010101, + auth_id, + ); + assert_ne!(expected_hash.clone(), result.as_slice()); + + // Test 6: invalid auth id + let result = call_get_signer_message_hash( + &mut sim, + &pox_addr, + reward_cycle, + &topic, + lock_period, + &principal, + max_amount, + 10101, ); assert_ne!(expected_hash.clone(), result.as_slice()); } @@ -316,12 +378,14 @@ pub mod pox4 { #[test] /// Fixture message hash to test against in other libraries fn test_sig_hash_fixture() { - let fixture = "3dd864afd98609df3911a7ab6f0338ace129e56ad394d85866d298a7eda3ad98"; + let fixture = "ec5b88aa81a96a6983c26cdba537a13d253425348ffc0ba6b07130869b025a2d"; let pox_addr = PoxAddress::standard_burn_address(false); let pubkey_hex = "0206952cd8813a64f7b97144c984015490a8f9c5778e8f928fbc8aa6cbf02f48e6"; let pubkey = Secp256k1PublicKey::from_hex(pubkey_hex).unwrap(); let reward_cycle: u128 = 1; let lock_period = 12; + let auth_id = 111; + let max_amount = u128::MAX; let message_hash = make_pox_4_signer_key_message_hash( &pox_addr, @@ -329,6 +393,8 @@ pub mod pox4 { &Pox4SignatureTopic::StackStx, CHAIN_ID_TESTNET, lock_period, + max_amount, + auth_id, ); assert_eq!(to_hex(message_hash.as_bytes()), fixture); diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 16b0583b4ab..012cf911f85 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -538,6 +538,8 @@ pub fn boot_to_epoch_3( &Pox4SignatureTopic::StackStx, CHAIN_ID_TESTNET, 12_u128, + u128::MAX, + 1, ) .unwrap() .to_rsv(); @@ -559,6 +561,8 @@ pub fn boot_to_epoch_3( clarity::vm::Value::some(clarity::vm::Value::buff_from(signature).unwrap()) .unwrap(), clarity::vm::Value::buff_from(signer_pk.to_bytes_compressed()).unwrap(), + clarity::vm::Value::UInt(u128::MAX), + clarity::vm::Value::UInt(1), ], ); submit_tx(&http_origin, &stacking_tx); @@ -812,6 +816,8 @@ pub fn boot_to_epoch_3_reward_set( &Pox4SignatureTopic::StackStx, CHAIN_ID_TESTNET, lock_period, + u128::MAX, + 1, ) .unwrap() .to_rsv(); @@ -832,6 +838,8 @@ pub fn boot_to_epoch_3_reward_set( clarity::vm::Value::some(clarity::vm::Value::buff_from(signature).unwrap()) .unwrap(), clarity::vm::Value::buff_from(signer_pk.to_bytes_compressed()).unwrap(), + clarity::vm::Value::UInt(u128::MAX), + clarity::vm::Value::UInt(1), ], ); submit_tx(&http_origin, &stacking_tx); @@ -1424,6 +1432,8 @@ fn correct_burn_outs() { &Pox4SignatureTopic::StackStx, CHAIN_ID_TESTNET, 1_u128, + u128::MAX, + 1, ) .unwrap() .to_rsv(); @@ -1443,6 +1453,8 @@ fn correct_burn_outs() { clarity::vm::Value::some(clarity::vm::Value::buff_from(signature).unwrap()) .unwrap(), clarity::vm::Value::buff_from(pk_bytes).unwrap(), + clarity::vm::Value::UInt(u128::MAX), + clarity::vm::Value::UInt(1), ], ); let txid = submit_tx(&http_origin, &stacking_tx);