Covenants are a construction to allow introspection: a transaction output can place conditions on the transaction which spends it (beyond the specific “must provide a valid signature of itself and a particular pubkey”).
I previously looked at Examining ScriptPubkeys, but another useful thing covenants want to enforce is amounts. This is easy for equality, but consider the case where you are allowed to merge inputs: perhaps the first output amount must be the sum of the first and second inputs.
The problem is that Bitcoin Script deals in signed ones-complement values, and 31 bits limits us to 21.47483648 bitcoin. However, using
OP_CAT, it’s possible to deal with full amounts. I’ve written some (untested!) script code below.
The Vexing Problem of Amounts
OP_TXHASH, we can get SHA256(input amount) and SHA256(output amount) on the stack. Since this involves hashing, we can’t evaluate the number for anything but equality, so as in other cases where we don’t have Fully Complete Covenants we need to have the user supply the actual values on the witness stack, and we test those for the conditions we want, and then make sure they match what
OP_TXHASH says is in the transaction. I usually object to this backwards form (just give me the value on the stack!), but as you’ll see, we couldn’t natively use 64 bit values from
OP_TX anyway (I had proposed pushing two values, which is its own kind of ugly).
A Value Form Bitcoin Script Can Deal With
21M BTC is just under 2^51 satoshis.
We split these bits into a pair of stack values:
- lower 24 bits
- upper bits (27, but we allow up to 31)
I call this tuple “Script-friendly pair” (SFP) form. Note that all script numbers on stack are represented in little-endian, with a sign bit (0x80 on the last byte). This is a nasty format to work with, unfortunately.
Converting A Script-Friendly Pair to an 8-byte Little-Endian Value
Here’s the code to takes a positive CScriptNum, and produces two stack values which can be concatenated to make a 4 byte unsigned value:
# !UNTESTED CODE! # Stack (top to bottom): lower, upper OP_SWAP # Generate required prefix to append to stack value to make it 4 bytes long. OP_SIZE OP_DUP OP_NOTIF # 0 -> 00000000 OP_DROP 4 OP_PUSHDATA1 0x00 0x00 0x00 0x00 OP_ELSE OP_DUP 1 OP_EQUAL OP_IF # Single byte: prepend 0x00 0x00 0x00 OP_DROP 3 OP_PUSHDATA1 0x00 0x00 0x00 OP_ELSE OP_DUP 2 OP_EQUAL OP_IF # Two bytes: prepend 0x00 0x00 2 OP_PUSHDATA1 0x00 0x00 OP_ELSE 3 OP_EQUAL OP_IF # Three bytes: prepend 0x00 1 OP_PUSHDATA1 0x00 OP_ELSE # Prepend nothing. 0 OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_SWAP # Stack (top to bottom): upper, pad, lower
That 46 bytes handles upper. Now lower is a CScriptNum between 0 and 16777215, and we want to produce two stack values which can be concatenated to make an 3 byte unsigned value. Here we have to remove the zero-padding in the four-byte case:
# !UNTESTED CODE! # Stack (top to bottom): upper, pad, lower OP_ROT # Generate required prefix to append to stack value to make it 3 bytes long. OP_SIZE OP_DUP OP_NOTIF # 0 -> 000000 OP_DROP 3 OP_PUSHDATA1 0x00 0x00 0x00 OP_ELSE OP_DUP 1 OP_EQUAL OP_IF # Single byte: prepend 0x00 0x00 OP_DROP 2 OP_PUSHDATA1 0x00 0x00 OP_ELSE OP_DUP 2 OP_EQUAL OP_IF # Two bytes. Now maybe final byte is 0x00 simply so it doesn't # appear negative, but we don't care. 1 OP_PUSHDATA1 0x00 OP_ELSE # Three bytes: empty append below 3 OP_EQUAL OP_NOTIF # Four bytes, e.g. 0xff 0xff 0xff 0x00 # Convert to three byte version: negate and add 2^23 # => 0xff 0xff 0xff OP_NEG 4 OP_PUSHDATA1 0x00 0x00 0x80 0x00 OP_ADD OP_ENDIF # Prepend nothing. 0 OP_ENDIF OP_ENDIF OP_ENDIF OP_SWAP # Stack (top to bottom): lower, pad, upper, pad
You can optimize these 47 bytes a little, but I’ll leave that as an exercise for the reader!
Now we use
OP_CAT 3 times and
concatentate them to form an 8-byte little-endian number, for
comparison against the format used by
Basically, 95 bytes to compare our tuple to a hashed value.
Adding Two Script-Friendly Pairs
Let’s write some code to add two well-formed Script-Friendly Pairs!
# !UNTESTED CODE! # Stack (top to bottom): a_lower, a_upper, b_lower, b_upper OP_ROT OP_ADD OP_DUP 4 OP_PUSHDATA1 0x00 0x00 0x00 0x01 OP_GREATERTHANOREQUAL OP_IF # lower overflow, bump upper. # FIXME: We can OP_TUCK this constant above! 4 OP_PUSHDATA1 0x00 0x00 0x00 0x01 OP_SUB OP_SWAP OP_1ADD OP_ELSE OP_SWAP OP_ENDIF # Stack now: a_upper(w/carry), lower_sum, b_upper. OP_ROT OP_ADD OP_SWAP # Stack now: lower_sum, upper_sum
Note that these 26 bytes don’t check that upper doesn’t overflow: if we’re dealing with verified amounts, we can add 16 times before it’s even possible (and it’s never possible with distinct amounts of course). Still, we can add
OP_DUP 0 OP_GREATERTHANOREQUAL OP_VERIFY before the final
Checking Script-Friendly Pairs
The code above assumes well-formed pairs, but since the pairs will come from the witness stack, we need to have a routine to check that a pair is wel-formed:
# !UNTESTED CODE! # Stack: lower, upper OP_DUP # lower must be 0 - 0xFFFFFF inclusive 0 4 OP_PUSHDATA1 0xFF 0xFF 0xFF 0x00 OP_WITHIN OP_VERIFY OP_OVER # upper must be 0 - 0x7FFFFFF inclusive 0 4 OP_PUSHDATA1 0xFF 0xFF 0xFF 0x07 OP_WITHIN OP_VERIFY
This ensures the ranges are all within spec: no negative numbers, no giant numbers.
While this shows that
OP_MULTISHA256 is sufficient to deal with bitcoin amounts in Script, the size (about 250 bytes to validate that two inputs equals one output) makes a fairly compelling case for optimization.
It’s worth noting that this is why Liquid chose to add the following 64-bit opcodes to bitscoin script:
(They also reenabled the bitwise opcodes (
OP_XOR etc) to work just fine with these. They also implemented
OP_LE32TOLE64 for conversion.)
In my previous post I proposed
OP_LESS which works on arbitrary values, which doen’t work for these because the endian is wrong! As a minimum, we’d need to add
OP_NEG64 to allow 64-bit comparison, addition and subtraction.
But, with only
OP_MULTISHA256, it’s possible to deal with amounts. It’s just not pretty!
Thanks for reading!