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_MULTISHA256 or OP_CAT, it’s possible to deal with full amounts. I’ve written some (untested!) script code below.

The Vexing Problem of Amounts

Using 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_MULTISHA256 (or OP_CAT 3 times and OP_SHA256) to concatentate them to form an 8-byte little-endian number, for comparison against the format used by OP_TXHASH.

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 OP_SWAP.

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.

Summary

While this shows that OP_CAT/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: OP_ADD64, OP_SUB64, OP_MUL64, OP_DIV64, OP_NEG64, OP_LESSTHAN64, OP_LESSTHANOREQUAL64, OP_GREATERTHAN64, OP_GREATERTHANOREQUAL64.

(They also reenabled the bitwise opcodes (OP_XOR etc) to work just fine with these. They also implemented OP_SCRIPTNUMTOLE64, OP_LE64TOSCRIPTNUM and 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_LESSTHAN64, OP_ADD64 and OP_NEG64 to allow 64-bit comparison, addition and subtraction.

But, with only OP_CAT or OP_MULTISHA256, it’s possible to deal with amounts. It’s just not pretty!

Thanks for reading!