Spending constraints with OP_CHECKDATASIG

Sep 4, 19

Hashwar is over (wow, those 2 years of no trade have passed so quickly!). The dust settles. Let's explore some of the new capabilities of the opcode that we were fighting over during this war: OP_CHECKDATASIG.

One of the limitations of Bitcoin Script was that you could only specify if one can spend the coin but there was no way of constraining how. In this article, I’ll demonstrate that this is possible now. For better readability, all code will be expressed in Spedn, an experimental, high-level language for Bitcoin Cash smart contracts.

Simple things

We'll start with a simple contract (TXO definition) in that is basically an ordinary Pay to Public Key Hash.

contract Constraint(Ripemd160 pkh) {    challenge spend(PubKey pk, Sig sig) {
      verify hash160(pk) == pkh;
      verify checkSig(sig, pk);
   }
}

So in plain English, we check whether a public key (pk) provided in input's scriptSig field matches the hash (pkh) specified in this output and then, that a signature (sig) also provided in in scriptSig matches the key (pk) and the serialized transaction body. Nothing fancy, this is how most of the transactions in Bitcoin Cash are constructed.


Fancy things

Now the fancy thing. the only two differences between OPCHECKSIG (checkSig function in Spedn) and OPCHECKDATASIG (checkDataSig in Spedn) is that the former gets the signature preimage (signed message) implicitly and the signature contains additional sighash flag. So it is possible for checkDataSig to mimic plain old checkSig, we just have to provide the same serialized tx body in the scriptSig and strip the sighash flag with toDataSig function. It might look like this:

contract Constraint(Ripemd160 pkh) {    challenge spend(PubKey pk, Sig sig, bin preimage) {
      verify hash160(pk) == pkh;
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), preimage, pk);
  }
}

Here, we've just ensured that the preimage provided in scriptSig is exactly the same as the one used to perform checkSig operation.

But, what's the point? Well, our contract has just become aware of the transaction content that is spending it. And because of that, we can now introspect it. And, for example, check whether the tx outputs meet our conditions.

Fancier things

According to the spec, here are the components of the preimage:

nVersion (4-byte little-endian)

hashPrevouts (32-byte)

hashSequence (32-byte)

outpoint (32-byte hash + 4-byte little endian)

scriptCode of the input (variable size)

value of the output spent by this input (8-byte little endian)

nSequence of the input (4-byte little endian)

hashOutputs (32-bytes)

nLocktime of the tx (4-byte little endian)

sighash type of the signature (4-byte little-endian)

If we want to inspect the outputs, here we have (8). We can cut it out with OPSPLIT applied twice. Because there is a variable in length part before it, we'll have to count bytes from the end. For that, we can measure the preimage size with OPSIZE.

contract Constraint(Ripemd160 pkh) {    challenge spend(PubKey pk, Sig sig, bin preimage) {
      verify hash160(pk) == pkh;
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), preimage, pk); 
      bin [_, tail] = preimage @ size(preimage) - 40; 
      bin [hashOutputs, _] = tail @ 32;
  }
}

Unfortunately, it's only a hash, we can't see what outputs produced it. But we can repeat the trick that we have already done once with the preimage as a whole - we can require the hash preimage to be put in scriptSig and check if it matches the hash. For the sake of this demonstration, we'll assume the transaction spending this contract will use sighash type set to Single which mean the hashOutputs will be made from a single output corresponding to the input spending the contract. The output serialization for the hashOutputs is concatenated 8-bytes little endian amount in satoshis and scriptPubKey (script).

contract Constraint(Ripemd160 pkh) {    challenge spend(PubKey pk, Sig sig, bin preimage, bin script, int amount) {
      verify hash160(pk) == pkh;
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), preimage, pk); 
      bin [_, tail] = preimage @ size(preimage) - 40; 
      bin [hashOutputs, _] = tail @ 32;
      verify hash256(num2bin(amount, 8) . script) == Sha256(hashOutputs);
   }
}

So we do the same in Script. We convert a number to an 8-bytes long little endian form with num2bin, concatenate it with script, hash it and compare the result with hashOutputs. Because Spedn is strongly typed, we also had to cast hashOutputs to the matching type explicitly.

The fanciest

All the above have led us to the point where the contract knows what is the amount and script this contract will be spent to. So now we could impose some constraints on that. For example:

contract Constraint(Ripemd160 pkh, int minimum) {    challenge spend(PubKey pk, Sig sig, bin preimage, bin script, int amount) {
      verify hash160(pk) == pkh;
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), preimage, pk); 
      bin [_, tail] = preimage @ size(preimage) - 40; 
      bin [hashOutputs, _] = tail @ 32;
      verify hash256(num2bin(amount, 8) . script) == Sha256(hashOutputs); 
      verify amount >= minimum;
   }
}

With this, we can impose the particular output of the transaction to have some minimal value. Maybe not the most useful thing but very simple and therefore good for the demonstration purpose. What else can be done? We can further introspect the provided script and check if it matches some pattern, for example - if it contains valid OPRETURN metadata in a particular scheme… And in that way, make OPRETURN based tokens miner-enforceable.

War is over. It's time to #buidl.

Developer of Spedn



Comments (2)
sort by  /

6 Aug 19 10:48

I'm going to upvote this every time I have to come back for reference :D

6   1  

7 Aug 19 07:48

You can set up Mecenas contract for @pein_sama and use spending constraints he described here to back him up :) I've created Mecenas contract thanks to this article.

4   0  

1 Dec 18 07:10

This is really cool!

And if I'm not mistaken this would even let you implement side chains by allowing to unlock an amount of coins by presenting proof that the same amount of coins has been locked on another chain.

0   0