diff --git a/NBitcoin.Tests/sidechains_tests.cs b/NBitcoin.Tests/sidechains_tests.cs new file mode 100644 index 0000000000..35d9ce4e4f --- /dev/null +++ b/NBitcoin.Tests/sidechains_tests.cs @@ -0,0 +1,101 @@ +using NBitcoin.DataEncoders; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace NBitcoin.Tests +{ + public class sidechains + { + + [Fact] + [Trait("UnitTest", "UnitTest")] + public static void CreateWithdrawScript() + { + // build a sidechain genesis with a locked output of 1000 coins + + Transaction sidechainGenesis = new Transaction(); + sidechainGenesis.Version = 1; + sidechainGenesis.Time = (uint) DateTime.UtcNow.ToUnixTimestamp(); + sidechainGenesis.AddInput(new TxIn + { + ScriptSig = new Script(Op.GetPushOp(Encoders.ASCII.DecodeData("Testing a sidechain"))) + }); + sidechainGenesis.AddOutput(new TxOut() + { + Value = Money.Coins(1000), + ScriptPubKey = new Script(new Op + { + Code = OpcodeType.OP_WITHDRAWPROOFVERIFY, + }) + }); + + // build a withdraw lock transaction from parent chain that spends 500 coins + + var key = new Key(); + var network = Network.RegTest; + + // a block that has an output. + var block = CreateBlockWithCoinbase(network, network.GetGenesis(), key, 1); + var lockTrx = new Transaction(); + lockTrx.AddInput(new TxIn(new OutPoint(uint256.Zero, 0))); // a fake input that spends 250 + lockTrx.AddInput(new TxIn(new OutPoint(uint256.Zero, 0))); // a fake input that spends 250 + lockTrx.AddOutput(new TxOut(Money.Coins(500), + new Script(new Op {Code = OpcodeType.OP_WITHDRAWPROOFVERIFY}))); // lock the output + // TODO: how do we represent the target on the sidechain? + block.AddTransaction(lockTrx); + block.UpdateMerkleRoot(); + + // mine two more blocks + var block1 = CreateBlockWithCoinbase(network, block, key, 2); + var block2 = CreateBlockWithCoinbase(network, block1, key, 3); + + // Create an SPV proof, a transaction that can withdraw to the sidechain + var proof = new SpvProof(); + proof.CoinBase = block.Transactions.First(); + proof.Lock = lockTrx; + proof.OutputIndex = 0; + var merkleBlock = new MerkleBlock(block, new[] {lockTrx.GetHash()}); + proof.MerkleProof = merkleBlock.PartialMerkleTree; + proof.SpvHeaders = new SpvHeaders {Headers = new List {block.Header, block1.Header, block2.Header}}; + proof.Genesis = network.GenesisHash; + proof.DestinationScript = key.ScriptPubKey; + // verify the transaction script + + var scriptSignature = SpvProof.CreateScript(proof); + + var withdrawTrx = new Transaction(); + withdrawTrx.AddInput(new TxIn(new OutPoint(sidechainGenesis, 0), scriptSignature)); + withdrawTrx.AddOutput(new TxOut(500, key.ScriptPubKey)); + withdrawTrx.AddOutput(new TxOut(500, new Script(new Op {Code = OpcodeType.OP_WITHDRAWPROOFVERIFY}))); + + var scriptSig = withdrawTrx.Inputs.First().ScriptSig; + var output = sidechainGenesis.Outputs.First(); + var scriptPubKey = sidechainGenesis.Outputs.First().ScriptPubKey; + + + var result = Script.VerifyScript(scriptSig, scriptPubKey, withdrawTrx, 0, output.Value); + Assert.True(result); + } + + + private static Block CreateBlockWithCoinbase(Network network, Block previous, Key key, int index) + { + Block block = new Block(); + block.Header.HashPrevBlock = previous.GetHash(); + var tip = new ChainedBlock(previous.Header, index); + block.Header.Bits = block.Header.GetWorkRequired(network, tip); + block.Header.UpdateTime(network, tip); + + var coinbase = new Transaction(); + coinbase.AddInput(TxIn.CreateCoinbase(tip.Height + 1)); + coinbase.AddOutput(new TxOut(network.GetReward(tip.Height + 1), key)); + block.AddTransaction(coinbase); + + block.UpdateMerkleRoot(); + + return block; + } + } +} diff --git a/NBitcoin/PartialMerkleTree.cs b/NBitcoin/PartialMerkleTree.cs index ac1290b880..0e44086e74 100644 --- a/NBitcoin/PartialMerkleTree.cs +++ b/NBitcoin/PartialMerkleTree.cs @@ -11,6 +11,14 @@ public PartialMerkleTree() { } + + public PartialMerkleTree(byte[] bytes) + : this() + { + this.FromBytes(bytes); + } + + uint _TransactionCount; public uint TransactionCount { diff --git a/NBitcoin/Script.cs b/NBitcoin/Script.cs index 0b6b6cf7d8..0b0d2a3e5d 100644 --- a/NBitcoin/Script.cs +++ b/NBitcoin/Script.cs @@ -1,10 +1,10 @@ -using System.Runtime.InteropServices; -using NBitcoin.Crypto; +using NBitcoin.Crypto; using NBitcoin.DataEncoders; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; namespace NBitcoin @@ -308,6 +308,7 @@ public enum OpcodeType : byte OP_NOP2 = 0xb1, OP_NOP3 = 0xb2, OP_NOP4 = 0xb3, + OP_WITHDRAWPROOFVERIFY = OP_NOP4, OP_NOP5 = 0xb4, OP_NOP6 = 0xb5, OP_NOP7 = 0xb6, diff --git a/NBitcoin/ScriptEvaluationContext.cs b/NBitcoin/ScriptEvaluationContext.cs index 8ce102bceb..636c47d30c 100644 --- a/NBitcoin/ScriptEvaluationContext.cs +++ b/NBitcoin/ScriptEvaluationContext.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using NBitcoin.Crypto; +using NBitcoin.Crypto; using System; using System.Collections; using System.Collections.Generic; @@ -62,6 +61,19 @@ public enum ScriptError NullFail, MinimalIf, WitnessPubkeyType, + + /* sidechains */ + WithdrawVerifyFormat, + WithdrawVerifyBlock, + WithdrawVerifyLockTx, + WithdrawVerifyOutput, + WithdrawVerifyOutputScriptdest, + WithdrawVerifyRelockScriptval, + WithdrawVerifyOutputval, + WithdrawVerifyOutputscript, + + WithdrawVerifyBlockUnconfirmed, + WithdrawVerifyBlindedAmounts, } public class TransactionChecker @@ -823,7 +835,7 @@ bool EvalScript(Script s, TransactionChecker checker, int hashversion) } - case OpcodeType.OP_NOP4: + //case OpcodeType.OP_NOP4: case OpcodeType.OP_NOP5: case OpcodeType.OP_NOP6: case OpcodeType.OP_NOP7: @@ -1463,6 +1475,85 @@ bool EvalScript(Script s, TransactionChecker checker, int hashversion) } break; } + + case OpcodeType.OP_WITHDRAWPROOFVERIFY: + + { + // This op code expects the following on the stack + // 1. a list of spv headers + // 2. a MerkleProof of the locking trx on the parent chain + // 3. the OutputIndex of the locking trx + // 4. the locking trx itself + // 5. the CoinBase of the block the lock was confirmed in + // 6. Genesis of parent + // 7. ScriptPubKey of the destination script + + if (_stack.Count != 7) + return SetError(ScriptError.InvalidStackOperation); + + var arrayList = new List + { + _stack.Pop(), // SpvHeaders + _stack.Pop(), // MerkleProof + _stack.Pop(), // OutputIndex + _stack.Pop(), // Lock + _stack.Pop(), // CoinBase + _stack.Pop(), // Genesis + _stack.Pop(), // SciptPubkey target + }; + + arrayList.Reverse(); + var proof = SpvProof.CreateProof(arrayList); + + // validate the target is one of the outputs. + // currently only allow two outputs: + // - the spending of the lock + // - the re-lock of the result + + if(checker.Transaction.Outputs.Count != 2) + return SetError(ScriptError.WithdrawVerifyOutput); + + var spender = checker.Transaction.Outputs.Where(o => o.ScriptPubKey == proof.DestinationScript); + if (spender.Count() != 1) + return SetError(ScriptError.WithdrawVerifyOutputscript); + + // check that the output value is within the unlocked coins + var withdrawScript = new Script(new[] {new Op {Code = OpcodeType.OP_WITHDRAWPROOFVERIFY}}); + var relocks = checker.Transaction.Outputs.Where(o => o.ScriptPubKey == withdrawScript); + + if (relocks.Count() != 1) + return SetError(ScriptError.WithdrawVerifyOutput); + + if (relocks.First().Value > checker.Amount) + return SetError(ScriptError.WithdrawVerifyOutputval); + + // check that the Lock transaction is in the first header + var first = proof.SpvHeaders.Headers.First(); + if (!proof.MerkleProof.Check(first.HashMerkleRoot)) + return SetError(ScriptError.WithdrawVerifyLockTx); + + // check that the coinbase is in the first header + if (!proof.MerkleProof.Check(first.HashMerkleRoot)) + return SetError(ScriptError.WithdrawVerifyLockTx); + + // verify the amount of work done + proof.SpvHeaders.Headers.Reverse(); + var current = proof.SpvHeaders.Headers.First(); + + foreach (var header in proof.SpvHeaders.Headers.Skip(1)) + { + if(current.HashPrevBlock != header.GetHash()) + return SetError(ScriptError.WithdrawVerifyBlock); + current = header; + + // TODO: check the work + } + + _stack.Push(vchTrue); + + break; + } + default: return SetError(ScriptError.BadOpCode); } diff --git a/NBitcoin/SpvProof.cs b/NBitcoin/SpvProof.cs new file mode 100644 index 0000000000..a1972954e6 --- /dev/null +++ b/NBitcoin/SpvProof.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace NBitcoin +{ + /// + /// An SpvProof (Simplified Payment Verification) that is used to proof an output is locked (undependable). + /// + public class SpvProof + { + /// + /// The genesis hash of the chain where the withdraw lock was created. + /// + public uint256 Genesis; + + /// + /// A list of headers on the chain that confirmed the withdraw. + /// + /// + /// The first header represents the block that contains the withdraw lock. + /// The rest of the headers represent work (how many blocks the withdraw is berried under). + /// The headers are expected to be a chained of block headers. + /// + public SpvHeaders SpvHeaders; + + /// + /// A transaction that locked some coins to a special op code . + /// + public Transaction Lock; + + /// + /// The index of the output in the that lock transaction that is being refereed to by this SPV Proof. + /// + public int OutputIndex; + + /// + /// The coinbase of the block that confirmed the locked transaction. + /// + /// + /// The coinbase can be used to know the block height of the locked transaction. + /// + public Transaction CoinBase; + + /// + /// A proof that verifies the locking transaction was included in a block. + /// + public PartialMerkleTree MerkleProof; + + /// + /// The recipient of the locking transaction. + /// + public Script DestinationScript; + + public static Script CreateScript(SpvProof proof) + { + var scriptSignature = new Script( + Op.GetPushOp(proof.Genesis.ToBytes()), + Op.GetPushOp(proof.CoinBase.ToBytes()), + Op.GetPushOp(WriteIndex(proof.OutputIndex)), + Op.GetPushOp(proof.Lock.ToBytes()), + Op.GetPushOp(proof.MerkleProof.ToBytes()), + Op.GetPushOp(proof.SpvHeaders.ToBytes()), + Op.GetPushOp(proof.DestinationScript.ToBytes())); + + return scriptSignature; + } + + public static SpvProof CreateProof(IEnumerable stack) + { + var items = stack.ToArray(); + var proof = new SpvProof(); + proof.Genesis = new uint256(items[0]); + proof.CoinBase = new Transaction(items[1]); + proof.OutputIndex = ReadIndex(items[2]); + proof.Lock = new Transaction(items[3]); + proof.MerkleProof = new PartialMerkleTree(items[4]); + proof.SpvHeaders = new SpvHeaders(items[5]); + proof.DestinationScript = new Script(items[6]); + return proof; + } + + private static int ReadIndex(byte[] array) + { + using (var mem = new MemoryStream(array)) + { + int ret = 0; + var stream = new BitcoinStream(mem, false); + stream.ReadWrite(ref ret); + return ret; + } + } + + private static byte[] WriteIndex(int number) + { + using (var mem = new MemoryStream()) + { + var stream = new BitcoinStream(mem, true); + stream.ReadWrite(ref number); + return mem.ToArray(); + } + } + } + + /// + /// A class to serialize a list of block headers. + /// + public class SpvHeaders : IBitcoinSerializable + { + public List Headers; + + public SpvHeaders() + { } + + public SpvHeaders(byte[] bytes) + { + this.FromBytes(bytes); + } + + public void ReadWrite(BitcoinStream stream) + { + stream.ReadWrite(ref this.Headers); + } + } + + +}