read.cash Log in
@pein edited

BCH Covenants with Spedn

*This is an updated and refreshed version of the article originally publihed* *here**.* https://honest.cash/v2/pein_sama/spending-constraints-with-op_checkdatasig-172 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. http://spedn.readthedocs.io/ 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: https://www.bitcoincash.org/spec/replay-protected-sighash.html#digest-algorithm nVersion of the transaction (4-byte little endian) hashPrevouts (32-byte hash) hashSequence (32-byte hash) outpoint (32-byte hash + 4-byte little endian) scriptCode of the input (serialized as scripts inside CTxOuts) value of the output spent by this input (8-byte little endian) nSequence of the input (4-byte little endian) hashOutputs (32-byte hash) nLocktime of the transaction (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 `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. https://github.com/KarolTrzeszczkowski/Electron-Cash-Last-Will-Plugin Mecenas plugin for Electron Cash, named after Gaius Maecenas, is a non-custodial solution for recurring payments. Think - decentralized Patreon. https://github.com/KarolTrzeszczkowski/Mecenas-recurring-payment-EC-plugin https://en.wikipedia.org/wiki/Gaius_Maecenas 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. https://blog.bitjson.com/cashchannels-recurring-payments-for-bitcoin-cash-3b274fbfa6e2 Hamingja - non tradable, loyalty SLP tokens. You can spend them only in the shop that issued them. https://slpvh.github.io/hamingja/ 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. https://developer.bitcoin.com/bitbox/docs/getting-started https://spedn.readthedocs.io/en/latest/bitbox.html 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]

$15.47

3 comments

Log in to join in Reading is open to everyone. Replying needs an account.