Skip to content

Scenario 3: Sighash and fee bumps

Goal: Find the sighash flags that let Carol add a fee input to the signed payment of Alice. The signature of Alice must stay valid.

#lang bitcoin/model
;; Scenario 3: sighash exploration and fee bumping.

(require rackunit)

(chain mainnet #:rules bitcoin)
(keys alice bob carol)

(define cb (first (mine 1 #:on mainnet #:to alice)))
(define cc (first (mine 1 #:on mainnet #:to carol)))
(mine 100 #:on mainnet)

(define-tx split                ; give carol a small fee coin
  #:inputs  ([cc #:sign carol])
  #:outputs ([fee-coin (wpkh carol) (btc 0.01)]
             [rest     (wpkh carol) (btc 49.98)]))
(check-pred accepted? (confirm split))

(define-tx pay
  #:inputs  ([cb #:sign alice #:sighash '(all anyonecanpay)])
  #:outputs ([to-bob (wpkh bob) (btc 49.99)]))

(check-equal? (commits (sig-of pay 0))
              '(version
                (own-input outpoint) (own-prevout script) (own-prevout amount) (own-input sequence)
                (outputs all)
                locktime))

(check-equal? (free-fields pay) '((inputs append) (inputs remove-others)))

(define bumped (add-input pay fee-coin #:sign carol))
(check-pred accepted? (try bumped))
(check-equal? (fee bumped) (btc 0.02))

(check-equal? (breaks-entries (mutate pay '(output to-bob amount) (btc 49.0)))
              `((sig ,alice 0 #:fields ((outputs all)))))

;; Which flags let anyone add an input while Alice's outputs stay fixed?
(check-equal? (sighash-search pay
                #:goal  (can (add-input))
                #:keep  (fixed (outputs all))
                #:over  '(wpkh tr-key tr-script))
              '((wpkh (all anyonecanpay))
                (tr-key (all anyonecanpay))
                (tr-script (all anyonecanpay))))

;; Adding outputs too needs SINGLE or NONE; with SINGLE her own output stays fixed.
(check-equal? (sighash-search pay #:goal (can (add-input) (add-output)) #:keep (fixed (output to-bob))
                              #:over '(wpkh))
              '((wpkh (single anyonecanpay))))

;; With plain SIGHASH_ALL the same bump breaks alice's signature.
(define-tx pay-all
  #:inputs  ([cb #:sign alice])
  #:outputs ([to-bob-all (wpkh bob) (btc 49.99)]))
(check-equal? (free-fields pay-all) '())
(define bad (try (add-input pay-all fee-coin #:sign carol)))
(check-equal? (rejected-rule bad) 'eval-false)
(check-equal? (result-detail bad 'cause) 'commitment-mismatch)

What occurs

  • ANYONECANPAY. Alice signs with '(all anyonecanpay). commits shows that her signature commits to her own input and to all outputs, but not to the other inputs. Thus free-fields gives (inputs append) and (inputs remove-others).
  • The fee bump. add-input adds the 0.01 BTC coin of Carol and signs only that input. The new transaction is accepted, with a fee of 0.02 BTC.
  • The break. mutate changes the amount for Bob. It shows that the signature of Alice breaks on (outputs all).
  • The search. sighash-search signs hypothetical copies of the payment again. It uses P2WPKH, taproot key-path and taproot script-path spends, and each valid flag set. With fixed outputs, only (all anyonecanpay) lets a different person add an input, for all three spend types. To also add outputs and keep the output of Bob fixed, the signer must use (single anyonecanpay).
  • The contrast. With SIGHASH_ALL only, free-fields is empty. The input of Carol gives a rejection with eval-false, #:cause commitment-mismatch and the changed #:fields.

Signatures and sighash gives the field names and explains the queries.

Against a real node

tests/replay-3.rkt replays the session against Core. A real node also accepts the ANYONECANPAY fee bump and rejects the SIGHASH_ALL fee bump. sighash-matrix checks the full table of flags and edits.

Forms in this scenario: #:sighash, sig-of, commits, free-fields, mutate, add-input, sighash-search, can, fixed.