Spending constraints with OP_CHECKDATASIG

2018-11-26T18:07:37.000Z Honest Cash

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 OP_CHECKSIG (checkSig function in Spedn) and OP_CHECKDATASIG (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), sha256(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:

1. nVersion (4-byte little-endian)

2. hashPrevouts (32-byte)

3. hashSequence (32-byte)

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

5. scriptCode of the input (variable size)

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

7. nSequence of the input (4-byte little endian)

8. hashOutputs (32-bytes)

9. nLocktime of the tx (4-byte little endian)

10. 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 OP_SPLIT 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 OP_SIZE.

contract Constraint(Ripemd160 pkh) { 
   challenge spend(PubKey pk, Sig sig, bin preimage) { 
      verify hash160(pk) == pkh; 
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), sha256(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), sha256(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), sha256(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 OP_RETURN metadata in a particular scheme… And in that way, make OP_RETURN based tokens miner-enforceable.

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

Responses


RE: Spending constraints with OP_CHECKDATASIG

by @emergent_reasons

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


RE: RE: Spending constraints with OP_CHECKDATASIG

by @Licho

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.


Side chains with OP_CHECKDATASIG

by @dskloet

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.