BCH Covenants with Spedn

2 84
Avatar for pein
Written by
  10
4 months ago (Last updated: 3 months ago)

This is an updated and refreshed version of the article originally publihed here.

The Great Schism in Christianity happened over a single word: filioque. What a silly thing, you might say - to fight over a word. But...

What is the good of words if they aren't important enough to quarrel over? Why do we choose one word more than another if there isn't any difference between them? If you called a woman a chimpanzee instead of an angel, wouldn't there be a quarrel about a word? If you're not going to argue about words, what are you going to argue about? Are you going to convey your meaning to me by moving your ears? The Church and the heresies always used to fight about words, because they are the only thing worth fighting about. [G.K. Chesterton]

Similarily, the Hashwar in Bitcoin Cash happened over a single opcode - a word in the Bitcoin Virtual Machine vocabluary. Was it worth fighting over? Let's explore some of the new capabilities that OP_CHECKDATASIG enables.

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, a high-level language for Bitcoin Cash smart contracts.

Simple Things

We'll start with a simple contract (a transaction output 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 thre differences between OP_CHECKSIG (checkSig function in Spedn) and OP_CHECKDATASIG (checkDataSig in Spedn) is that the former gets the signature preimage (a message to be signed) implicitly, the preimage is hashed twice with SHA256 rather than once 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, hash it and strip the sighash flag with toDataSig function. It might look like this:

contract Constraint(Ripemd160 pkh) { 
   challenge spend(PubKey pk, Sig sig, [byte] 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 content of the transaction 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 of the transaction (4-byte little endian)

  2. hashPrevouts (32-byte hash)

  3. hashSequence (32-byte hash)

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

  5. scriptCode of the input (serialized as scripts inside CTxOuts)

  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-byte hash)

  9. nLocktime of the transaction (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 the size of (5) is not fixed, 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,
                   [byte] preimage) {
      verify hash160(pk) == pkh; 
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), sha256(preimage), pk);

      (_, [byte;40] tail) = preimage @ size(preimage) - 40;
      ([byte;32] 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 hashOutputs's 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,
                   [byte] preimage, [byte] script, int amount) { 
      verify hash160(pk) == pkh; 
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), sha256(preimage), pk);

      (_, [byte;40] tail) = preimage @ size(preimage) - 40;
      ([byte;32] 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,
                   [byte] preimage, [byte] script, int amount) { 
      verify hash160(pk) == pkh; 
      verify checkSig(sig, pk);
      verify checkDataSig(toDataSig(sig), sha256(preimage), pk);

      (_, [byte;40] tail) = preimage @ size(preimage) - 40;
      ([byte;32] 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? Here are some projects that have been developed since the original publication of this article:

Last Will plugin for Electron Cash will help you to prepare for your death so thay your bitcoins will be reliably handed over to your inheritors.

Mecenas plugin for Electron Cash, named after Gaius Maecenas, is a non-custodial solution for recurring payments. Think - decentralized Patreon.

ChashCahnnels - a similar solution that lets users pre-approve future transactions of a specified amount, valued in any currency. The exchange rate is provided by an oracle - another functionality that OP_CHECKDATASIG enables.

Hamingja - non tradable, loyalty SLP tokens. You can spend them only in the shop that issued them.

Fancy Box

BITBOX with Spedn SDK is a great toolkit that will help you to create a working solution utilizing covenants in just one noon.

To compile a contract, use something like this:

const compiler = new Spedn();
const { Covenant } = await compiler.compileCode(`
  contract Covenant(Ripemd160 alice) {
    challenge spend(Sig sig, PubKey pubKey, [byte] preimage) {
      verify hash160(pubKey) == alice;
      verify checkSig(sig, pubKey);
      verify checkDataSig(toDataSig(sig), sha256(preimage), pubKey);
      // and your custom logic here
    }
  }
`);
compiler.dispose();

To find coins locked in this contract use:

const alice = bitbox.HDNode.derivePath(wallet, "m/44'/145'/0'/0/0");
const covenant = new Covenant({
  alice: alice.getIdentifier()
});
const coins = await covenant.findCoins("mainnet");

And to spend them use this:

const txid = await new TxBuilder("mainnet")
  .from(coins, (input, context) =>
    input.spend({
      sig: context.sign(alice.keyPair, SigHash.SIGHASH_ALL),
      pubKey: alice.getPublicKeyBuffer(),
      preimage: context.preimage(SigHash.SIGHASH_ALL)
    })
  )
  .to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
  .to(covenant.getAddress("mainnet")) // change address
  .broadcast();

As you can see, it's trivial to provide a signature or a preimage of the transaction you're crafting.

Now it's time to #buidl.

Sponsors of pein
empty
empty
empty

$ 15.97
$ 7.00 from @Read.Cash
$ 5.00 from @rosco
$ 1.00 from @emergent_reasons
+ 5
Avatar for pein
Written by
  10
4 months ago (Last updated: 3 months ago)
Enjoyed this article?  Earn Bitcoin Cash by sharing it! Explain
...and you will also help the author collect more tips.

Comments

The article that launched a thousand ships.

$ 0.00
4 months ago

Amazing work! Bookmarked!

$ 0.00
4 months ago
About us Rules What is Bitcoin Cash? Roadmap Affiliate program Get sponsored Self-host
hello@read.cash (PGP key) Reddit