Skip to content

Scenario 7: Conformance replay against regtest

Goal: Take a scenario from the model and lower it to real keys, signatures and transaction bytes. Then replay it against real nodes, step by step.

#lang bitcoin/conform
(define run1
  (replay (scenario-log)
          #:targets (hash 'mainnet (regtest)
                          'signet  (regtest #:build 'inquisition))))
(summary run1)   ; => ((confirmed 14) (disagree 0) (unverified 0))

(sighash-matrix #:spend-types '(legacy wpkh tr-key tr-script)
                #:flags 'all
                #:target (regtest))
; => ((confirmed #:type wpkh #:flags (all) #:edit none #:model accepted)
;     …
;     (unsupported #:type legacy))

Each scenario that runs has a replay test. This is the replay test of Scenario 4, against Core and Inquisition:

#lang bitcoin/conform
;; Scenario 4, replayed: mainnet against Core, the CTV chain against
;; Bitcoin Inquisition, and the CTV chain against Core (which cannot check
;; CTV steps).

(require rackunit)

(define-consensus ctv-rules #:extends bitcoin
  #:opcodes (upgrade nop4 #:to ctv))

(chain mainnet #:rules bitcoin)
(chain signet #:rules ctv-rules)
(keys alice cold mallory)
(check-equal? (format "~s" (diff-consensus bitcoin ctv-rules)) "((opcode #xb3 nop4 -> ctv))")

(define tmpl
  (template #:outputs (list (output 'to-cold (wpkh cold) (btc 49.98)))))
(contract vault () (ctv tmpl))

(define results
  (for/list ([ch (list mainnet signet)])
    (define cb (first (mine 1 #:on ch #:to alice)))
    (void (mine 100 #:on ch))
    (define-tx lock
      #:inputs  ([cb #:sign alice])
      #:outputs ([vaulted (vault) (btc 49.99)]))
    (confirm lock)
    (list (try (spend vaulted            ; thief ignores the template
                 #:outputs (list (output 'stolen (wpkh mallory) (btc 49.98)))))
          (try (spend vaulted            ; the honest spend follows it
                 #:outputs (list (output 'to-cold (wpkh cold) (btc 49.98))))))))

(define thief (map first results))
(check-pred accepted? (first thief))                                ; covenant unenforced
(check-equal? (rejected-rule (second thief)) 'ctv-template-mismatch)
(check-equal? (rejected-chain (second thief)) 'signet)
(check-true (andmap accepted? (map second results)))

(check-equal? (audit (vault) #:on mainnet)
              '((warning #:rule-unenforced ctv #:chain mainnet #:runs-as nop4)))
(check-equal? (audit (vault) #:on signet) '())

(define inquisition (regtest #:build 'inquisition))
(define have-core (find-executable-path "bitcoind"))
(define have-inquisition (find-executable-path (target-bitcoind inquisition)))

(define (statuses r) (map (λ (s) (list (step-n s) (step-status s))) (run-steps r)))

(if (and have-core have-inquisition)
    (let ([r (replay (scenario-log) #:targets (hash 'mainnet (regtest) 'signet inquisition))])
      (check-equal? (disagreements r) '())
      (check-equal? (cadr (assq 'unverified (summary r))) 0 (format "~s" (run-steps r)))
      ;; The thief's NOP4 spend on mainnet: Core's mempool refuses it as
      ;; non-standard, but a block would accept it, as the model says.
      (check-not-false
       (for/or ([s (run-steps r)])
         (and (eq? (step-status s) 'confirmed) (memq '#:mempool-only (step-detail s))))))
    (displayln "skipping Core+Inquisition replay: a binary is missing"))

(if have-core
    (let* ([r (replay (scenario-log) #:targets (hash 'mainnet (regtest) 'signet (regtest)))]
           [unverified (filter (λ (s) (eq? (step-status s) 'unverified)) (run-steps r))])
      (check-equal? (disagreements r) '())
      ;; Only the two signet spends that run CTV are unverified.
      (check-equal? (map step-detail unverified)
                    '((#:reason (model-only-rule ctv)) (#:reason (model-only-rule ctv)))))
    (displayln "skipping Core replay: bitcoind not found"))

This is the test of the sighash matrix:

#lang bitcoin/conform
;; sighash-matrix: every spend type x flag set x edit agrees with Core,
;; and the matrix covers both accepted and rejected cells.

(require rackunit)

(if (find-executable-path "bitcoind")
    (let* ([rows (sighash-matrix #:spend-types '(wpkh tr-key tr-script legacy))]
           [cells (filter (λ (r) (not (eq? (car r) 'unsupported))) rows)]
           [find (λ (type flags e)
                   (for/first ([r (in-list cells)]
                               #:when (and (equal? (cadr (memq '#:type r)) type)
                                           (equal? (cadr (memq '#:flags r)) flags)
                                           (equal? (cadr (memq '#:edit r)) e)))
                     (cadr (memq '#:model r))))])
      (check-equal? (length cells) (+ (* 6 13) (* 2 7 13)))
      (check-true (andmap (λ (r) (eq? (car r) 'confirmed)) cells))
      (check-equal? (filter (λ (r) (eq? (car r) 'unsupported)) rows) '((unsupported #:type legacy)))
      (check-true (> (length (filter (λ (r) (eq? (cadr (memq '#:model r)) 'accepted)) cells)) 26))
      (check-true (> (length (filter (λ (r) (pair? (cadr (memq '#:model r)))) cells)) 26))
      (check-equal? (find 'wpkh '(all anyonecanpay) '(inputs append)) 'accepted)
      (check-equal? (find 'wpkh '(all) '(inputs append)) '(rejected eval-false))
      (check-equal? (find 'tr-key '(none) '(output 0 lock)) 'accepted)
      (check-equal? (find 'tr-script '(single) '(output 1 amount)) '(rejected checksig))
      (check-equal? (scenario-log) '()))
    (displayln "skipping sighash matrix: bitcoind not found"))

What you learn

You learn which results of the model a real node supports. A step that uses rules that no target has returns unverified. Replay does not confirm such a step. A disagreement is a bug in the model, or an idea that depends on rules that do not exist. During the development, replay found real bugs: an error in the key format of taproot script-path signatures, and a special case of SIGHASH_SINGLE.

Conformance replay explains how replay lowers values, the targets, the difference between policy and consensus, and the matrix.

Forms in this scenario: replay, regtest, summary, disagreements, unverified-steps, run-steps, sighash-matrix.