Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions NBitcoin.Tests/sidechains_tests.cs
Original file line number Diff line number Diff line change
@@ -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<BlockHeader> {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;
}
}
}
8 changes: 8 additions & 0 deletions NBitcoin/PartialMerkleTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ public PartialMerkleTree()
{

}

public PartialMerkleTree(byte[] bytes)
: this()
{
this.FromBytes(bytes);
}


uint _TransactionCount;
public uint TransactionCount
{
Expand Down
5 changes: 3 additions & 2 deletions NBitcoin/Script.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
97 changes: 94 additions & 3 deletions NBitcoin/ScriptEvaluationContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using NBitcoin.Crypto;
using NBitcoin.Crypto;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -62,6 +61,19 @@ public enum ScriptError
NullFail,
MinimalIf,
WitnessPubkeyType,

/* sidechains */
WithdrawVerifyFormat,
WithdrawVerifyBlock,
WithdrawVerifyLockTx,
WithdrawVerifyOutput,
WithdrawVerifyOutputScriptdest,
WithdrawVerifyRelockScriptval,
WithdrawVerifyOutputval,
WithdrawVerifyOutputscript,

WithdrawVerifyBlockUnconfirmed,
WithdrawVerifyBlindedAmounts,
}

public class TransactionChecker
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<byte[]>
{
_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);
}
Expand Down
127 changes: 127 additions & 0 deletions NBitcoin/SpvProof.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace NBitcoin
{
/// <summary>
/// An SpvProof (Simplified Payment Verification) that is used to proof an output is locked (undependable).
/// </summary>
public class SpvProof
{
/// <summary>
/// The genesis hash of the chain where the withdraw lock was created.
/// </summary>
public uint256 Genesis;

/// <summary>
/// A list of headers on the chain that confirmed the withdraw.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public SpvHeaders SpvHeaders;

/// <summary>
/// A transaction that locked some coins to a special op code <see cref="OpcodeType.OP_WITHDRAWPROOFVERIFY"/>.
/// </summary>
public Transaction Lock;

/// <summary>
/// The index of the output in the that lock transaction that is being refereed to by this SPV Proof.
/// </summary>
public int OutputIndex;

/// <summary>
/// The coinbase of the block that confirmed the locked transaction.
/// </summary>
/// <remarks>
/// The coinbase can be used to know the block height of the locked transaction.
/// </remarks>
public Transaction CoinBase;

/// <summary>
/// A proof that verifies the locking transaction was included in a block.
/// </summary>
public PartialMerkleTree MerkleProof;

/// <summary>
/// The recipient of the locking transaction.
/// </summary>
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<byte[]> 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();
}
}
}

/// <summary>
/// A class to serialize a list of block headers.
/// </summary>
public class SpvHeaders : IBitcoinSerializable
{
public List<BlockHeader> Headers;

public SpvHeaders()
{ }

public SpvHeaders(byte[] bytes)
{
this.FromBytes(bytes);
}

public void ReadWrite(BitcoinStream stream)
{
stream.ReadWrite(ref this.Headers);
}
}


}