These examples are in a human-readable form and would need to be transpiled
through the compile_script
function to produce the byte code tape that can
then be executed with run_script
.
Note that these are currently untested; this was a series of thought experiments to help inform the design/implementation process. Once the system is complete, these will be thoroughly tested/vetted, updated/replaced as necessary, and expanded upon.
The below example includes a branching locking script that references preconditions (i.e. is run before the main locking script) and the two unlocking scripts. To make the scripts execute successfully, one of the two unlocking scripts will have to be used, as well as the signature from the relevant private key.
"example 1"
# precondition script #
OP_DEF 0 {
OP_PUSH d1694791613
OP_CHECK_TIMESTAMP_VERIFY
OP_PUSH x<hex pubkey0>
OP_CHECK_SIG x00
OP_VERIFY
}
OP_DEF 1 {
OP_PUSH x<hex pubkey1>
OP_CHECK_SIG x00
OP_VERIFY
}
# unlocking script 0 #
OP_PUSH x<hex signature from pubkey0>
OP_TRUE
# unlocking script 1 #
OP_PUSH x<hex signature from pubkey1>
OP_FALSE
# locking script #
OP_IF (
OP_CALL d0
) ELSE (
OP_CALL d1
)
This example shows a pay-to-script-hash locking script and the associated unlocking script.
"example 2: P2SH"
# committed conditional P2PK script: 86 bytes #
OP_IF (
# branch A: 43 bytes #
OP_PUSH d1694791613
OP_CHECK_TIMESTAMP_VERIFY
OP_PUSH x<hex pubkey0>
OP_CHECK_SIG x00
) ELSE (
# branch B: 36 bytes #
OP_PUSH x<hex pubkey1>
OP_CHECK_SIG x00
)
# unlocking script: 155 bytes #
OP_PUSH x<hex signature from pubkey0>
OP_TRUE
OP_PUSH x<hex committed P2PK script src>
# locking script: 27 bytes #
OP_DUP
OP_SHAKE256 d20
OP_PUSH x<hex script shake256 hash>
OP_EQUAL_VERIFY
OP_EVAL
This example shows how a complex derivative might be implemented using a committed contract script, keeping the contract terms private until execution. This branching logic is for three spending paths, and the contract code can be further obfuscated with more commitments, e.g. by replacing the first branch with a committment similar to the locking script to save several hundred bytes for executions of the other two spending paths.
"example 3: underwriting/CDS on obligation"
# committed script: multisig contract #
OP_IF (
OP_PUSH d<unix epoch of maturation + grace period>
OP_DUP
OP_CHECK_TIMESTAMP_VERIFY
OP_CHECK_EPOCH_VERIFY
OP_PUSH x<funding source>
OP_PUSH x<funding destination>
OP_PUSH x<encoded txn constraints>
OP_PUSH d<amount>
OP_PUSH x<hex contract hash>
OP_PUSH d1
OP_CHECK_TRANSFER
OP_VERIFY
OP_PUSH x<hex pubkey_CDS_purchaser>
OP_CHECK_SIG x00
) ELSE (
OP_IF (
OP_PUSH d<unix epoch of maturation + grace period + 30 days>
OP_CHECK_EPOCH_VERIFY
OP_PUSH x<hex pubkey_CDS_issuer>
OP_CHECK_SIG x00
) ELSE (
OP_PUSH x<hex pubkey_2of2>
OP_CHECK_SIG x00
)
)
# unlocking script: CDS redemption #
OP_PUSH x<hex signature from pubkey_CDS_purchaser>
OP_PUSH x<hex transfer proof from contract>
OP_TRUE
OP_PUSH x<hex committed P2PK script src>
# unlocking script: CDS expiration #
OP_PUSH x<hex signature from pubkey_CDS_issuer>
OP_TRUE
OP_FALSE
OP_PUSH x<hex committed P2PK script src>
# unlocking script: CDS transfer #
OP_PUSH x<hex signature from pubkey_2of2>
OP_FALSE
OP_FALSE
OP_PUSH x<hex committed P2PK script src>
# locking script #
OP_DUP
OP_SHAKE256 d20
OP_PUSH x<hex committed script shake256 hash>
OP_EQUAL_VERIFY
OP_EVAL
"example 4: nostro/vostro account encumbrance"
# committed script #
OP_IF (
OP_PUSH x<hex musig pubkey>
OP_CHECK_SIG x00
) ELSE (
OP_PUSH x<hex pubkey1>
OP_PUSH x<hex pubkey2>
OP_SWAP d1 d2
OP_CHECK_SIG_VERIFY x00
OP_CHECK_SIG x00
)
# locking script #
OP_DUP
OP_SHAKE256 d20
OP_PUSH x<hex committed script shake256 hash>
OP_EQUAL_VERIFY
OP_EVAL
# unlocking script: musig #
OP_PUSH x<hex signature from musig pubkey>
OP_TRUE
OP_PUSH x<hex committed script src>
# unlocking script: 2 signatures #
OP_PUSH x<hex signature from pubkey1>
OP_PUSH x<hex signature from pubkey2>
OP_FALSE
OP_PUSH x<hex committed script src>
This is an example of a streamlined branching script where unexecuted branches remain hidden behind cryptographic commitments. Only those script branches included in the root commitment can be executed.
# locking script: 33 bytes #
OP_MERKLEVAL x<hex 32 byte root commitment>
# committed script branch A: 36 bytes #
OP_PUSH x<hex pubkey0>
OP_CHECK_SIG x00
# unlocking script A: 139 bytes #
OP_PUSH x<hex signature from pubkey0>
OP_PUSH x<hex 32 byte branch B sha256 hash>
OP_PUSH x<hex branch A script>
# committed script branch B: 33 bytes #
OP_MERKLEVAL x<hex 32 byte branch B root commitment>
# committed script branch BA: 36 bytes #
OP_PUSH x<hex pubkey1>
OP_CHECK_SIG x00
# unlocking script BA: 147 bytes #
OP_PUSH x<hex signature from pubkey1>
OP_PUSH x<hex 32 byte branch BB sha256 hash>
OP_PUSH x<hex branch BA script>
OP_PUSH x<hex 32 byte branch A sha256 hash>
OP_PUSH x<hex branch B script>
# committed script branch BB: 36 bytes #
OP_PUSH x<hex pubkey2>
OP_CHECK_SIG x00
# unlocking script BB: 147 bytes #
OP_PUSH x<hex signature from pubkey2>
OP_PUSH x<hex 32 byte branch BA sha256 hash>
OP_PUSH x<hex branch BB script>
OP_PUSH x<hex 32 byte branch A sha256 hash>
OP_PUSH x<hex branch B script>
This functionality can be replicated with the other ops, but it will have more
overhead since OP_MERKLEVAL
reads the tape and runs 9 ops and 1 stack push.
Note that 5 bytes can be shaved from the commitment scripts by using OP_SHAKE256
with digest length 26, reducing the commitment security from 256 to 208 bits;
this reduces the size of unlocking scripts A by 10 bytes and unlocking scripts
BA and BB by 20 bytes. If we decreased the commitment security down to 180 bits
using OP_SHAKE256 d20
, this shaves an additional 6 bytes from the locking
script, 12 bytes from unlocking script A, and 24 bytes from unlocking scripts BA
and BB (final byte counts of 27, 174, and 232 respectively). The OP_MERKLEVAL
option is both more secure and a more efficient branching script solution. The
below example of this was not added as a test vector because it is strictly
inferior to the OP_MERKLEVAL
method above, and it would have been a lot of
time and effort that I would rather put elsewhere.
# locking script: 38 bytes #
OP_DUP
OP_SHA256
OP_PUSH x<hex 32 byte root sha256 hash>
OP_EQUAL_VERIFY
OP_EVAL
# committed script root: 83 bytes #
OP_IF (
# committment to branch A: 38 bytes #
OP_DUP
OP_SHA256
OP_PUSH x<hex 32 byte branch A sha256 hash>
OP_EQUAL_VERIFY
OP_EVAL
) ELSE (
# committed script branch B: 38 bytes #
OP_DUP
OP_SHA256
OP_PUSH x<hex 32 byte branch B root sha256 hash>
OP_EQUAL_VERIFY
OP_EVAL
)
# committed script branch A: 36 bytes #
OP_PUSH x<hex pubkeyA>
OP_CHECK_SIG x00
# unlocking script A: 190 bytes #
OP_PUSH x<hex signature from pubkeyA>
OP_PUSH x<hex committed script branch A>
OP_TRUE
OP_PUSH x<hex committed script root>
# committed script branch B: 83 bytes #
OP_IF (
# commitment to script branch BA: 38 bytes #
OP_DUP
OP_SHA256
OP_PUSH x<hex 32 byte branch A sha256 hash>
OP_EQUAL_VERIFY
OP_EVAL
) ELSE (
# commitment to script branch BB: 38 bytes #
OP_DUP
OP_SHA256
OP_PUSH x<hex 32 byte branch A sha256 hash>
OP_EQUAL_VERIFY
OP_EVAL
)
# committed script branch BA: 36 bytes #
OP_PUSH x<hex pubkeyBA>
OP_CHECK_SIG x00
# unlocking script BA: 276 bytes #
OP_PUSH x<hex signature from pubkeyBA>
OP_PUSH x<hex committed script branch BA>
OP_TRUE
OP_PUSH x<hex committed script branch B>
OP_FALSE
OP_PUSH x<hex committed script root>
# committed script branch BB: 36 bytes #
OP_PUSH x<hex pubkeyBB>
OP_CHECK_SIG x00
# unlocking script BB: 276 bytes #
OP_PUSH x<hex signature from pubkeyBB>
OP_PUSH x<hex committed script branch BB>
OP_FALSE
OP_PUSH x<hex committed script branch B>
OP_FALSE
OP_PUSH x<hex committed script root>
Since the v0.5.0 upgrade, OP_MERKLEVAL
now uses a root commitment calculated
as xor(sha256(sha256(branch1)), sha256(sha256(branch2)))
. Validation to
execute branch1 requires providing sha256(branch2)
, which is then hashed and
xor
ed with the double sha256 of branch1; this additional hashing step is to
prevent being able to execute arbitrary scripts since XOR is a reversible
operation -- the extra sha256 step requires that one must find the preimage for
a sha256 hash in order to validate the execution of arbitrary code. Without the
step, we could do this: given some root_commitment
and arbitrary code
branchA
, calculate branchB
= xor(root_commitment, sha256(branchA))
;
branchA
would then be executable by providing this calculated branchB
value.
With the additional hashing operation, the calculation is instead
branchB = sha256_preimage(xor(root_commitment, sha256(sha256(branchA))))
,
which cannot be calculated.
However, there is one case for which this construction is vulnerable: any
symmetrical tree will have a root of all null bytes. Given some code branchA
,
if a tree is constructed with that branch twice, then the root commitment will
be xor(sha256(sha256(branchA)), sha256(sha256(branchA)))
; since anything XOR
itself is all null bits, every symmetrical tree will share the same root, which
means that any arbitrary code can be validated and executed against that root by
constructing another symmetrical tree. This is proven in the
security
test file.
This example shows how the features of tapescript can be used to implement the eltoo off-chain protocol using on-chain primitives. This example was implemented as an e2e test here. This assumes instant confirmation of transactions once broadcast for the sake of convenience -- the Unix timestamp based constraints can be adapted for a system that enforces causal ordering, e.g. a blockchain or other logical clock.
In the original paper, designed for Bitcoin and introducing a new sighash flag and a change to how sequence numbers are interpreted, the locking scripts were as follows:
- setup:
2 <pubkey A> <pubkey B> 2 OP_CHECKMULTISIGVERIFY
- trigger and update:
OP_IF
<N> OP_CSV
2 <pubkey A_(s,i)> <pubkey B_(s,i)> 2 OP_CHECKMULTISIGVERIFY
ELSE
<S_i + 1> OP_CLTV
2 <pubkey A_u> <pubkey B_u> 2 OP_CHECKMULTISIGVERIFY
ENDIF
(OP_CSV
= OP_CHECKSEQUENCEVERIFY
; OP_CLTV
= OP_CHECKLOCKTIMEVERIFY
)
<pubkey A>
and <pubkey B>
are public keys used by the channel participants
to set up the channel. <pubkey A_u>
and <pubkey B_u>` are public keys used for signing update transactions. `<pubkey A_(s,i)>` and
<pubkey B_(s,i)>` are
settlement keys calculated using a seed and a state counter used to sign
settlement transactions.
Before broadcasting the setup transaction to open the channel, a trigger txn is
signed that spends the setup UTXO, and a settlement txn is signed that spends
the trigger UTXO to return funds to the channel participants. Both participants
retain the trigger txn and the initial settlement txn. The update txns are
signed using the proposed SIGHASH_NOINPUT
sighash-flag, which blanks the
previous input field during signature creation and verification, allowing the
signature to be used for any matching locking script without committing to spend
a specific UTXO.
To update the channel, an update txn is created with an incremented sequence
number, and a new settlement txn is also created and signed using the new
settlement keys for this state. The state counter is held in the txn sequence
field. Invalidation of earlier update txns is enforced using OP_CLTV
(i.e. an
earlier update txn cannot spend the UTXO of a later update txn), and settlement
txns must wait N
blocks after the update txn is confirmed on the blockchain
before becoming valid (enforced by the OP_CSV
), allowing either participant to
broadcast a later update txn before the prior settlement txn becomes valid.
Importantly, the sequence number must be included in signature generation and
verification.
To open a channel, only the setup txn is broadcast and confirmed. The trigger, update, and settlement txns are all held by the participants until they decide to close the payment channel and settle, at which point the trigger txn is broadcast and confirmed on the blockchain, then the latest update txn, then finally the settlement txn. If one participant attempts to cheat the other by broadcasting an old settlement transaction, it will first have to broadcast and confirm the trigger and corresponding update transaction; the timeout on the settlement transaction will allow the other participant to detect the attempted fraud and broadcast the most recent update txn, invalidating the old settlement txn before it can be confirmed. This also allows for synchronization between participants in the case that one node experiences a fault, whereas the current Lightning Network protocol causes a total loss of funds for a faulty node.
Transactions will consist of a list of entries. Each entry will consist of the following:
inputs
: ordered list of IDs of funding UTXOs in the form[(txn_id, index), ...]
outputs
: ordered list of tuples of locking scripts of the new UTXOs to be generated and the values assigned to each, i.e.[(lock, val), ...]
witnesses
: the unlocking scripts that satisfy the locking scripts of the inputs
Each transaction will contain the following fields:
state
: unsigned 32-bit integertimestamp
: Unix epoch timestamp at time of transaction creationentries
:[entry1, ...]
The following values must be held as read-only in the cache at execution time:
time
: the Unix epoch timestamp at time of executionsigfield[1-8]
: the relevant signature fieldsinput_ts
: greatest txn timestamp from entry inputs
For validating signatures, the following values will be held in the sigfields:
sigfield1
: entry inputssigfield2
: entry outputssigfield3
: transaction sequencesigfield4
: transaction timestamp
An important note is that because the tapescript CHECK_SIG
and
CHECK_SIG_VERIFY
ops take a parameter encoding allowable sighash flags,
invalidating any signatures that use a disallowed flag to exclude a required
sigfield, the same public keys can be used for all locking scripts. The original
eltoo proposal was made for the Bitcoin script system, which does not include
the ability to selectively enable sighash flags in locking scripts, so the
authors had to use another scheme to ensure that signatures could not be bound
to settlement transactions without any constraints, hence the use of unique keys
for each settlement txn spending path in the update txn locking scripts. If
instead the same keys were used for all locking scripts, an update txn signature
could be bound to any settlement txn.
By disallowing all sighash flags in the settlement path locking scripts, each
settlement transaction in the tapescript implementation is bound solely to the
corresponding update txn, while allowing the exclusion of sigfield1
containing
the inputs in the update spending path allows the signatures for update txns to
couple only to the state counter and outputs.
Locking script:
PUSH x<pubkey A>
PUSH x<pubkey B>
CHECK_MULTISIG x00 d2 d2
Txn entry:
inputs
:[funding inputs]
outputs
:[(lock, total value from inputs less fee)]
witnesses
:[unlocking scripts]
Transaction:
state
:0
timestamp
: uint32entries
:[txn entry]
Locking script:
IF (
# txn timestamp must be 24 hours greater than youngest input #
VAL s"sigfield4"
VAL s"input_ts" PUSH d43200 ADD_INTS d2
LESS VERIFY
# current time must be greater than or equal to txn timestamp #
VAL s"time"
VAL s"sigfield4"
LEQ VERIFY
PUSH x<pubkey A>
PUSH x<pubkey B>
CHECK_MULTISIG x00 d2 d2
) ELSE (
VAL s"sigfield3"
PUSH d0
LESS VERIFY
PUSH x<pubkey A>
PUSH x<pubkey B>
CHECK_MULTISIG x01 d2 d2
)
Witness matching setup locking script:
PUSH x<signature from pubkey B>
PUSH x<signature from pubkey A>
Txn entry:
inputs
:[setup UTXO]
outputs
:[(locking script, value)]
witnesses
:[witness]
Transaction:
sequence
:0
timestamp
: uint32entries
:[txn entry]
Locking script:
IF (
# txn timestamp must be 24 hours greater than youngest input #
VAL s"sigfield4"
VAL s"input_ts" PUSH d43200 ADD_INTS d2
LESS VERIFY
# current time must be greater than or equal to txn timestamp #
VAL s"time"
VAL s"sigfield4"
LEQ VERIFY
PUSH x<pubkey A>
PUSH x<pubkey B>
CHECK_MULTISIG x00 d2 d2
) ELSE (
VAL s"sigfield3"
PUSH d<i>
LESS VERIFY
PUSH x<pubkey A>
PUSH x<pubkey B>
CHECK_MULTISIG x01 d2 d2
)
Witness matching trigger locking script:
PUSH x<signature from pubkey B + x01>
PUSH x<signature from pubkey A + x01>
Txn entry:
inputs
:[trigger UTXO]
outputs
:[(locking script, value)]
witnesses
:[witness]
Transaction:
sequence
:previous state + 1
timestamp
: uint32entries
:[txn entry]
Lock_A locking script: PUSH x<pubkey A> CHECK_SIG x00
Lock_B locking script: PUSH x<pubkey B> CHECK_SIG x00
Witness matching update locking script:
PUSH x<signature from pubkey B + x01>
PUSH x<signature from pubkey A + x01>
Txn entry:
inputs
:[update UTXO]
outputs
:[(lock_A, val_A), (lock_B, val_B)]
witnesses
:[witness]
Transaction:
sequence
:previous state + 1
timestamp
: uint32entries
:[settlement txn entry]