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.