Scenario 2: HTLC, timelocks and explained failures¶
Goal: Define a contract and list its spend paths. Try one path too early and read the cause of the failure. Then use a snapshot to try both paths.
#lang bitcoin/model
;; Scenario 2: HTLC branches, timelocks and explained failures.
(require rackunit)
(chain mainnet #:rules bitcoin)
(keys alice bob)
(contract htlc (sender receiver secret timeout)
(or [claim (and (pk receiver) (sha256 secret))]
[refund (and (pk sender) (older timeout))]))
(define s (secret 's1))
(define cb (first (mine 1 #:on mainnet #:to alice)))
(mine 100 #:on mainnet)
(define-tx fund
#:inputs ([cb #:sign alice])
#:outputs ([locked (htlc alice bob s 144) (btc 49.99)]))
(check-pred accepted? (confirm fund))
(check-equal? (map branch-name (branches locked)) '(claim refund))
(check-equal? (map branch-needs (branches locked))
`(((sig ,bob) (preimage ,s))
((sig ,alice) (age>= 144))))
(define refund
(spend locked #:path 'refund #:sign alice
#:outputs (list (output 'back (wpkh alice) (btc 49.98)))))
;; Too early: BIP68 holds the input back until the coin is 144 blocks deep.
(define early (try refund))
(check-equal? (rejected-rule early) 'sequence-lock)
(check-equal? (rejected-input early) 0)
(check-equal? (result-detail early 'need) 144)
(check-equal? (result-detail early 'have) 1)
;; The failing step in the explanation names the rule and carries its doc.
(define failing (last (explain (last-trace))))
(check-equal? (take failing 5) '(rule sequence-lock #:input 0 fail))
(check-not-false (memq '#:doc failing))
(define t0 (snapshot))
(mine 143 #:on mainnet)
(check-pred accepted? (try refund))
(restore t0)
(check-pred rejected? (try refund)) ; restore rewound the chain
(check-pred accepted?
(try (spend locked #:path 'claim #:sign bob #:reveal s
#:outputs (list (output 'claimed (wpkh bob) (btc 49.98))))))
What occurs¶
- The contract.
contractdefineshtlcas a function that makes a P2WSH lock from its parameters. The labels on the arms oforgive the names of the spend paths:claimandrefund. See Contracts. - The branches.
(branches locked)lists each path with the items that it needs. The claim needs the signature of Bob and the preimage. The refund needs the signature of Alice and an age of 144 blocks. -
Too early.
#:path 'refundsets the nSequence of the input to 144 and fills the witness. One block after the fund transaction, the input does not pass the relative lock of BIP68:(rejected #:chain mainnet #:rule sequence-lock #:input 0 #:need 144 #:have 1 …)The rule is
sequence-lock, not thecsvopcode. The nSequence is 144, thus OP_CSV passes. The relative lock of the input is the check that fails. A real node gives the same result (non-BIP68-final). -
Explain. The last step of
(explain (last-trace))gives the rule that failed and its doc:(rule sequence-lock #:input 0 fail #:need 144 #:have 1 #:doc "BIP68: an input whose nSequence encodes a relative lock waits that many blocks after its coin confirmed.") -
Snapshots.
snapshotcaptures the chain. After 143 more blocks, the refund is valid.restoreputs the chain back. Then the claim of Bob, with the preimage, is accepted.
Against a real node¶
tests/replay-2.rkt replays the log against Core. After the restore, the log holds the claim branch. The P2WSH script, the preimage, the CSV path and the BIP68 rejection all agree with Core.
Forms in this scenario: contract (pk, sha256, older, and, or), secret, branches, #:path, #:reveal, try, explain, snapshot, restore.