As shown in the transaction schematics, most of the covenants in the described contract
enforce the matching only on the part of the outputs data. One of the outputs is only
partially committed to by these covenants. In particular, the scriptPubKey field is not
committed to. This makes it possible to define the destination for that output at the
spending time, not at the commitment time. The explicit fee output that is present in
Elements transactions (but not in Bitcoin transactions) are not covered by the covenant
commitment at all. This makes it possible to set the fee for the transaction at the spending
time, and removes the difficulties related to trying to estimate the needed fee ahead of time,
that is present in other covenant constructions that fix the fee at the commitment time.
When the covenant script is created, the transaction template is constructed, and then
the outputs of the transaction template are blinded using a deterministic random source
that is derived according to BIP32 from the extended key shared between the participants
(the "shared blinding xkey"). This extended key is used to both derive the individual
blinding keys for the outputs, and to derive the deterministic random data used in the
blinding process itself. Different derivation paths are used for different purposes.
Not all outputs may be blinded. For example, it may be not worth the effort to blind
the outputs of the control assets, given that these outputs only reveal the structure of the
contract, that might also be revealed by another properties of the contract transactions.
The limitation of Elements script (that also exist in Bitcoin script) is that it is forbidden
to construct the data chunk of longer than 520 bytes on the stack. The blinded outputs take
much more space than non-blinded ones. With 520 byte limit on the data, only a few outputs
in the transaction bounded by the described covenant can be blinded. Streaming SHA256 opcodes
that are proposed to be included in Elements can be a way to workaround this
limitation.
During the covenant script construction, the blinded outputs are serialized as they would be
serialized when building a transaction to broadcast to the network. The chunk of that
serialized data is used as a template of the committed part of the outputs for the covenant.
This chunk of data consist of all the whole outputs that are committed to, and the partial
output that has the scriptPubKey excluded from the commitment. This data is then hashed
with SHA256 hash algorithm. The resulting hash is used to ensure that the data supplied
to the script at the spending time correspond to this "committed part" of the outputs data.
Along with the "committed part", the "free-floating" chunk of outputs data is supplied in the
input witness. This chunk have to contain the scriptPubKey for the partially-committed output,
and any other outputs that the spender needs to add, such as explicit fee output.
The script then combines the "committed part" with the "free-floating part" to create
the final image of the outputs data on the stack at run-time (or rather, spending time).
This final image is SHA256-hashed, and then combined with other data supplied in the
input witness. These additional data chunks are what the 'signature hash' algorithm uses
to create the final 'signature hash', that the signatures in the transaction inputs commit to.
The script might need to commit to several possible variants of the outputs. For example,
when the mutual-agreement case is allowed in the covenant, at least two variants for the
committed outputs will be present, the primary covenant case, and the mutual-agreement case.
This might be dealt with via the conditional execution using OP_IF/OP_ELSE/OP_ENDIF.
But a more succinct way is to just include all the enabled hash images (each 32byte in length)
in the script as a continuous data array (the length of which in bytes would be 32*n, where
n is the number of options). The spender then supplies a position in this data array, and
the script takes the 32bytes of the hash from this array at the supplied position using
the OP_SUBSTR opcode (which is enabled in Elements).
OUTPUTS_HASH_LOOKUP_OPS defined as:
# stack on entry:
# hashes_array (to be defined by the script)
# hash_position (to be supplied by the spender)
OP_SWAP # because the hashes_array comes from the script,
# and the offset comes from the witness data,
# they will be in reverse order, and we need to swap them first
# stack:
# hash_position
# hashes_array
5
# stack:
# 5
# hash_position
# hashes_array
OP_LSHIFT # left-shift to 5 binary places, same as multiplying by 32
# stack:
# offset
# hashes_array
32
# stack:
# 32
# offset
# hashes_array
OP_SUBSTR
# stack:
# chosen_hash
When the hash is chosen from the hashes_array (or directly given in the script, if there is
only one option), the data chunk that represents the outputs data portion that is committed to
by the covenant has to be matched against this hash, and then combined with the "free-floating"
part of the outputs data.
OUTPUTS_HASHCHECK_THEN_COMBINE_OPS defined as:
# stack on entry:
# chosen_hash
# committed_outs_data_chunk
# free_floating_outs_data_chunk
OP_OVER # get the committed_outs_data_chunk to the top
# stack:
# committed_outs_data_chunk
# chosen_hash
# committed_outs_data_chunk
# free_floating_outs_data_chunk
OP_SHA256 # take SHA256 of committed_outs_data_chunk
# stack:
# SHA256(committed_outs_data_chunk)
# chosen_hash
# committed_outs_data_chunk
# free_floating_outs_data_chunk
OP_EQUALVERIFY # check that the resulting hash matches the expected, fail otherwise
# stack:
# committed_outs_data_chunk
# free_floating_outs_data_chunk
OP_SWAP # swap the data chunks to be in correct order for OP_CAT
# stack:
# free_floating_outs_data_chunk
# committed_outs_data_chunk
OP_CAT # combine the data chunks to form the complete outputs data
# stack:
# outputs_data
The covenant script dynamically constructs the signature hash from the data
supplied in the input witness, and ensures that particular chunk of that data is the
same as it was in the transaction template when the covenant script was constructed.
Then, OP_CHECKSIGFROMSTACKVERIFY is used to verify a signature over this constructed signature hash,
with a signature that is partially supplied in the input witness. Then, the same signature
is supplied to OP_CHECKSIG, which checks this signature against the signature hash that
was constructed from the transaction currently being processed. This ensures that the
current transaction matches the template transaction in the part that the covenant checks.
Note that OP_CHECKSIGFROMSTACKVERIFY do additional SHA256 hashing over the data supplied to it.
This is because OP_CHECKSIG checks against a double-SHA256-hashed data. So technically, not a sighash,
but a hash preimage of the final sighash is supplied to OP_CHECKSIGFROMSTACKVERIFY.
Signature hash commits to the serialized spending script. This means that when we create the sighash
data dynamically, the data would need to include the covenant script itself, and this would make the
witness data big, and the length of the data will likely exceed the 520 byte limitation for the stack
item, that was discussed earlier. Fortunately, the OP_CODESEPARATOR opcode allows to split the script
for the purposes of sighash commitment. Only the opcodes that come after OP_CODESEPARATOR are used
in the construction of the signature hash. It makes sense to make this part minimal, and include
only the OP_CHECKSIGFROMSTACKVERIFY + OP_CHECKSIG combination. Note that while the sighash does
not commit to the whole script, the P2WSH scriptPubKey used for the covenant outputs commits to
the whole script. Thus, the spending condition for the input will require the whole script to execute
successfully.
POST_CODESEP_OPS defined as:
# stack on entry:
# pubkey
# sighash_preimage
# signature
# pubkey (the same one)
# signature+SIGHASH_ALL (the same signature, but concatenated with a SIGHASH type byte)
OP_CHECKSIGFROMSTACKVERIFY
# stack:
# pubkey
# signature+SIGHASH_ALL
OP_CHECKSIG
# stack:
# TRUE if the current transaction matches the template, FALSE otherwise
Because the signature checks are used as a way to enforce the match between the transaction template
and the actual transaction and not for spending authorization, it is not important what key is used
for these signatures. It may be a widely known key. If the signature matches both the sighash constructed
by the script and the sighash calculated by OP_CHECKSIG, this means that both hash values are equal.
Since we don't need to keep the key secret, the value of the nonce used in the signing process can
also be arbitrary.
It is therefore makes sense to exploit the fact that there is an elliptic point for the secp256k1 curve
with an anomalously small x coordinate, yet with a known logarithm (this fact is likely a
consequence of how the generator for the curve was chosen). Using this value as both
the nonce in the signing process and as a public key for the signing allows to save a bunch of bytes
in the witness data.
PUSH_SMALL_X defined as:
# stack on entry: <no arguments required>
DATA("3b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63")
# stack:
# DATA("3b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63")
When used as a public key, the value will be (encoded in hexadecimal):
"0200000000000000000000003b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63".
The private key for this public key is calculated as \((order + 1) / 2\) where
\(order\) is the order or the secp256k1 curve.
We can save additional 4 bytes in the witness by not pushing the 12-byte prefix "0200...00"
on the stack just as a data value, but by constructing it instead.
PUSH_SMALLPUB_PREFIX defined as:
# stack on entry: <no arguments required>
2
# stack:
# 2
1
# stack:
# 1
# 2
88 # 11*8
# stack:
# 88
# 1
# 2
OP_LSHIFT # take the value "1" and left-shift it to 88 binary places
# The result is the value 10000000000000000000000
# That is encoded as little-endian bytestring 000000000000000000000001
# stack:
# DATA("000000000000000000000001")
# 2
11
# stack:
# 11
# DATA("000000000000000000000001")
# 2
OP_LEFT # Take 11 bytes of the constructed long value
# so that only zeroes are left
# stack:
# DATA("0000000000000000000000")
# 2
OP_CAT # Concatenate 2 (represented as the byte "02")
# with a string of zeroes to construct the needed prefix
# stack:
# DATA("020000000000000000000000")
To satisfy the spending conditions of the covenant, the spender needs to sign the sighash of the
transaction with the key discussed above, and provide the \(S\) component of the signature in the
input witness. The covenant script will construct the final signature from the known \(R\) component
of the signature, the 4 bytes prefix that is also provided in the input witness, and the \(S\)
component provided by the spender. One of those 4 bytes in the prefix will depend on the length
of the \(S\) component, and thus it is more convenient to provide the whole 4 bytes as one witness
stack item rather than construct it byte by byte.
The other data that the spender needs to provide is two chunks of the outputs data, the "committed
part" and the "free-floating" part, and two data chunks representing the data that go into the
signature hash before (the 'prefix') and after (the 'suffix') of the hashOuts.
There are also two fields that go into the 'suffix': the nLockTime field and the sighash type field.
The later is added by the script itself, and the former is provided in the witness.
It is important to check that the size of nLockTime data provided is exactly 4 bytes.
Because the 'prefix' is supplied by the spender, allowing variable-sized data in the 'suffix'
would enable an attacker to 'hide' the real hashOuts inside one of the fields in the 'prefix', and
supply hashOuts of their liking.
The routine that checks the "committed part" against a chosen hash and the routine that combines
the "committed part" with the "free-floating part" were presented above.
The following routine takes the outputs data already as a whole field. It prepares everything for
the OP_CHECKSIGFROMSTACKVERIFY + OP_CHECKSIG pair in POST_CODESEP_OPS. This is the longest routine
in the covenant scripts.
OUTPUTS_FINAL_CHECK_OPS defined as:
# stack on entry:
# outputs_data # data of the outputs, checked and combined with OUTPUTS_HASHCHECK_THEN_COMBINE_OPS
# locktime_data # data representing the nLockTime field of the transaction
# sighash_data_prefix # all the other data that go into sighash before the outputs:
# # from nVersion to the asset issuances
# sig_prefix # first 4 byte of the signature, including the byte that
# # depends on the length of the S component
# sig_suffix # the S component of the signature with a couple of service bytes
OP_HASH256 # The sighash commits to the hash of the outputs data. Do the hashing.
# Note that HASH256 is SHA256(SHA256(data))
# stack:
# hashOuts # this is the HASH256(outputs_data)
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_SWAP # Prepare the stack for OP_CAT, so that locktime_data is appended to hashOuts
# stack:
# locktime_data
# hashOuts
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_SIZE # Get the size of locktime_data
# stack:
# locktime_data_size
# locktime_data
# hashOuts
# sighash_data_prefix
# sig_prefix
# sig_suffix
4 # will check that locktime_data_size is 4
# stack:
# 4
# locktime_data_size
# locktime_data
# hashOuts
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_EQUALVERIFY # Fail if locktime_data_size is not 4
# stack:
# locktime_data
# hashOuts
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_CAT # Append locktime_data to hashOuts
# stack:
# hashOuts+locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
DATA('01000000') # Push the value for SIGHASH_ALL on the stack.
# Note that sighash type takes 4 bytes in the sighash data,
# even if it is expressible in just 1 byte
# stack:
# SIGHASH_ALL
# hashOuts+locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_CAT # Append sighash type to the hashOuts+locktime_data chunk.
# We now completed the suffix of the data that the sighash commits to
# stack:
# sighash_data_suffix # consists of: hashOuts+locktime_data+SIGHASH_ALL
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_CAT # Combine the sighash data prefix and suffix, and get the complete
# sighash data on the stack
# stack:
# sighash_data
# sig_prefix
# sig_suffix
OP_SHA256 # hash the sighash data once (one round of SHA256)
# OP_CHECKSIGFROMSTACKVERIFY will do another round of SHA256,
# so we now have the preimage of the sighash
# stack:
# sighash_preimage
# sig_prefix
# sig_suffix
OP_TOALTSTACK # move the sighash_preimage to alternative stack
# stack:
# sig_prefix
# sig_suffix
# altstack:
# sighash_preimage
PUSH_SMALL_X # get the x coordinate of the 'special elliptic point'
# stack:
# SMALL_X
# sig_prefix
# sig_suffix
# altstack:
# sighash_preimage
PUSH_SMALLPUB_PREFIX # get the prefix for the 'special pubkey' on the stack
# stack:
# DATA("020000000000000000000000")
# SMALL_X
# sig_prefix
# sig_suffix
# altstack:
# sighash_preimage
OP_OVER # get SMALL_X to the top of the stack
# stack:
# SMALL_X
# DATA("020000000000000000000000")
# SMALL_X
# sig_prefix
# sig_suffix
# altstack:
# sighash_preimage
OP_CAT # construct the 'special pubkey' on the stack
# stack:
# pubkey
# SMALL_X
# sig_prefix
# sig_suffix
# altstack:
# sighash_preimage
OP_TOALTSTACK # move the 'special pubkey' to alternative stack
# stack:
# SMALL_X
# sig_prefix
# sig_suffix
# altstack:
# pubkey
# sighash_preimage
OP_CAT # Construct the first part of the signature
# stack:
# sig_prefix+SMALL_X
# sig_suffix
# altstack:
# pubkey
# sighash_preimage
OP_SWAP # Swap the two values for next OP_CAT
# stack:
# sig_suffix
# sig_prefix+SMALL_X
# altstack:
# pubkey
# sighash_preimage
OP_CAT # Combine the first part of the signature to the second part, completing it
# stack:
# signature
# altstack:
# pubkey
# sighash_preimage
OP_DUP # We will need one signature for OP_CHECKSIGFROMSTACKVERIFY and one for OP_CHECKSIG
# stack:
# signature
# signature
# altstack:
# pubkey
# sighash_preimage
OP_1 # the sighash type in the signature itself takes only one byte, as opposed
# to 4 bytes it takes within the sighash data.
# OP_1 will push byte 01 to the stack, which corresponds to SIGHASH_ALL
# stack:
# 01
# signature
# signature
# altstack:
# pubkey
# sighash_preimage
OP_CAT # Append the byte 01 to the signature, so we will have the signature with
# sighash type for OP_CHECKSIG, and without it, for OP_CHECKSIGFROMSTACKVERIFY
# Now we need to prepare the stack arguments for POST_CODESEP_OPS
# stack:
# signature+SIGHASH_ALL
# signature
# altstack:
# pubkey
# sighash_preimage
OP_FROMALTSTACK # get the 'special pubkey' from altstack
# stack:
# pubkey
# signature+SIGHASH_ALL
# signature
# altstack:
# sighash_preimage
OP_ROT # Rotate the top 3 items on the stack so that signature+SIGHASH_ALL becomes last
# stack:
# signature
# pubkey
# signature+SIGHASH_ALL
# altstack:
# sighash_preimage
OP_OVER # Duplicate the pubkey from the second position on the stack
# stack:
# pubkey
# signature
# pubkey
# signature+SIGHASH_ALL
# altstack:
# sighash_preimage
OP_FROMALTSTACK # Retrieve sighash_preimage from the stack
# stack:
# sighash_preimage
# pubkey
# signature
# pubkey
# signature+SIGHASH_ALL
OP_SWAP # Adjust argument positions to match what is expected by POST_CODESEP_OPS
# stack:
# pubkey
# sighash_preimage
# signature
# pubkey
# signature+SIGHASH_ALL
OP_CODESEPARATOR # Everything after this opcode will be included in the sighash data.
# The full script including everything before and after this opcode
# will be included in the input witness.
# stack:
# pubkey
# sighash_preimage
# signature
# pubkey
# signature+SIGHASH_ALL
POST_CODESEP_OPS # Do the checking via OP_CHECKSIGFROMSTACKVERIFY + OP_CHECKSIG
The script has two execution paths - for the lateral state transition, and for the vertical state transition.
The execution path are chosen by the top value on the witness stack. If the value is 0, the lateral state
progression path is chosen. If the value is 1, the vertical state progression path is chosen. The values
larger than 1 will mean the same as 1. Using other values than 1 may result in different size of witness data,
which can influence the transaction ordering in the mempool. But this is possible only when the transaction
standardness rules are not enforced (the MINIMALIF rule). This is not an issue, because only blocksigners
can bypass the standardness rules, and blocksigners already directly determine the transaction ordering.
MAIN_COVENANT_OPS defined as:
# stack on entry:
#
# for lateral state progression:
# 0
# offset_into_hash_array
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
#
# for vertical state progression:
# 1
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_IF
# stack:
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
<timeout> # the calculated timeout in blocks for when the vertical
# state progression becomes allowed
# stack:
# timeout
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_CHECKLOCKTIMEVERIFY # enforce the timelock
# stack:
# timeout
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OP_DROP # timeout was not dropped by OP_CHECKSIGFROMSTACKVERIFY, drop explicitly
# stack:
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
DATA(<hash_for_revocation>) # the hash for the committed part of outputs
# for the payment option revocation case
OP_ELSE # The lateral progression case
# stack:
# offset_into_hash_array
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
DATA(<hashes_array>) # The hashes_array will actually consist of up to three hashes:
# the hash of committed part of outputs for partial repayment,
# the hash of committed part of outputs for early full repayment,
# and the hash of committed part of outputs for mutual close case.
# stack:
# hashes_array
# offset_into_hash_array
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OUTPUTS_HASH_LOOKUP_OPS
OP_ENDIF # branch finished
# stack:
# outputs_hash
# committed_outs_data_chunk
# free_floating_outs_data_chunk
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OUTPUTS_HASHCHECK_THEN_COMBINE_OPS
# stack:
# outputs_data
# locktime_data
# sighash_data_prefix
# sig_prefix
# sig_suffix
OUTPUTS_FINAL_CHECK_OPS
The scripts construct the signature from the supplied \(S\) component of the signature and
known value for R component. The pubkey is fixed. The risk of the attack via this
particular input data is the same as the risk for the attack on the signature verification
code in the Elements client. Elements is based on Bitcoin Core codebase, and this
particular part of functionality does not deviate from the upstream behavior.
Given that the incentives to attack Bitcoin signature functionality is enormously high,
the level of risk that such a basic functionality will be broken in Elements is extremely low.
The scripts construct the signature hash from the inputs. Only a part of this data is checked
to be as expected. Part of outputs, and most of sighash data are supplied by the spender.
The covenants are built on the notion that only a part of outputs data is committed to by
the covenant script. The spender is allowed to attach extra inputs and add extra outputs.
This might be useful to extend the contract or combine it with another contracts. The assets,
amounts and (except for where scriptPubKey is not committed) the destinations of the committed
outputs will be as expected, and the contract terms will be enforced.
It is important that the committed-to outputs will be the first. If the attacker is able to
put their arbitrary outputs before the committed-to outputs, they can craft their data such that
their last custom output not complete: the data chunk in the witness would contain only OP_RETURN
opcode, but the size of scriptPubKey would be much bigger. In the transaction itself, this
output will contain OP_RETURN followed by the "committed-to" part of the outputs data. The
Attacker's outputs that completely bypass the covenant would go before that custom output.
Adding extra data means that participants can construct the transactions of different sizes.
But because it is the spender that will pay the fee for this extra size, this is not a concern.
The spender can supply the prefix for the sighash data, as well as nLockTime field. The size of
nLockTime field is checked by the script, while the size of the prefix data is not.
The attack might be imagined where the additional data influences the final hash of outputs
in such a way that the expected, "enforced" outputs do not correspond anymore to this influenced
hash, but another set of unrelated outputs are enabled. Would the hash function used for sighash
calculation be a weak one, we could talk about the possibility of a chosen prefix collision.
But even then, such attack would be hindered by the fact that this spender-supplied data would
need to correspond to a valid transaction that the spender can actually construct. There's no known
collision attacks on SHA256 hash function at the moment.
Footnotes: