Skip to content

Commit 79ddeff

Browse files
committed
Support "getblock" with verbosity level 3
1 parent 3a62d20 commit 79ddeff

3 files changed

Lines changed: 192 additions & 52 deletions

File tree

‎NBitcoin.Tests/RPCClientTests.cs‎

Lines changed: 104 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,46 +1767,111 @@ public async Task GetBlockVerboseTests()
17671767
await node.StartAsync();
17681768
var cli = node.CreateRPCClient();
17691769

1770+
BitcoinAddress addr;
1771+
17701772
// case 1: genesis block
1771-
var verboseGenesis = await cli.GetBlockAsync(Network.RegTest.GenesisHash, GetBlockVerbosity.WithFullTx);
1772-
Assert.True(verboseGenesis.Block.ToBytes().SequenceEqual(Network.RegTest.GetGenesis().ToBytes()));
1773-
Assert.Equal(0, verboseGenesis.Height);
1774-
var height = await cli.GetBlockCountAsync();
1775-
Assert.Equal(height + 1, verboseGenesis.Confirmations);
1776-
Assert.Equal(285, verboseGenesis.StrippedSize);
1777-
Assert.Equal(285, verboseGenesis.Size);
1778-
Assert.Equal(1140, verboseGenesis.Weight);
1779-
Assert.Equal(0, verboseGenesis.Height);
1780-
Assert.Equal("00000001", verboseGenesis.VersionHex);
1781-
Assert.Equal(1, verboseGenesis.Block.Header.Version);
1782-
Assert.Equal(Network.RegTest.GenesisHash, verboseGenesis.Block.GetHash());
1783-
Assert.Equal(uint256.Parse("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"), verboseGenesis.Block.Transactions.First().GetHash());
1784-
Assert.Single(verboseGenesis.Block.Transactions);
1785-
Assert.Equal(verboseGenesis.MedianTime, verboseGenesis.Block.Header.BlockTime);
1786-
Assert.Equal(2u, verboseGenesis.Block.Header.Nonce);
1787-
Assert.Equal(new Target(0x207fffff), verboseGenesis.Block.Header.Bits);
1788-
Assert.Equal(4.656542373906925e-10, verboseGenesis.Difficulty);
1789-
Assert.Equal(uint256.Parse("0000000000000000000000000000000000000000000000000000000000000002"), verboseGenesis.ChainWork);
1790-
1791-
// NextBlockHash must be included iff the block is not on the tip.
1792-
Assert.Null(verboseGenesis.NextBlockHash);
1793-
var addr = await cli.GetNewAddressAsync();
1794-
await cli.GenerateToAddressAsync(1, addr);
1795-
verboseGenesis = await cli.GetBlockAsync(Network.RegTest.GenesisHash, GetBlockVerbosity.WithOnlyTxId);
1796-
Assert.NotNull(verboseGenesis.NextBlockHash);
1797-
Assert.Null(verboseGenesis.Block); // there will be no Block if we specify false to second argument.
1798-
Assert.NotNull(verboseGenesis.TxIds); // But txids are still there.
1799-
Assert.Single(verboseGenesis.TxIds);
1800-
1801-
// case 2: next block.
1802-
var secondBlockHash = await cli.GetBestBlockHashAsync();
1803-
var verboseBestBlock = await cli.GetBlockAsync(secondBlockHash, GetBlockVerbosity.WithOnlyTxId);
1804-
Assert.Equal(Network.RegTest.GenesisHash, verboseBestBlock.Header.HashPrevBlock);
1805-
Assert.Null(verboseBestBlock.NextBlockHash);
1806-
1807-
await cli.GenerateToAddressAsync(1, addr);
1808-
verboseBestBlock = await cli.GetBlockAsync(secondBlockHash, GetBlockVerbosity.WithOnlyTxId);
1809-
Assert.NotNull(verboseBestBlock.NextBlockHash);
1773+
{
1774+
var verboseGenesis = await cli.GetBlockAsync(Network.RegTest.GenesisHash, GetBlockVerbosity.WithFullTx);
1775+
Assert.True(verboseGenesis.Block.ToBytes().SequenceEqual(Network.RegTest.GetGenesis().ToBytes()));
1776+
Assert.Equal(0, verboseGenesis.Height);
1777+
var height = await cli.GetBlockCountAsync();
1778+
Assert.Equal(height + 1, verboseGenesis.Confirmations);
1779+
Assert.Equal(285, verboseGenesis.StrippedSize);
1780+
Assert.Equal(285, verboseGenesis.Size);
1781+
Assert.Equal(1140, verboseGenesis.Weight);
1782+
Assert.Equal(0, verboseGenesis.Height);
1783+
Assert.Equal("00000001", verboseGenesis.VersionHex);
1784+
Assert.Equal(1, verboseGenesis.Block.Header.Version);
1785+
Assert.Equal(Network.RegTest.GenesisHash, verboseGenesis.Block.GetHash());
1786+
Assert.Equal(uint256.Parse("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"), verboseGenesis.Block.Transactions.First().GetHash());
1787+
Assert.Single(verboseGenesis.Block.Transactions);
1788+
Assert.Equal(verboseGenesis.MedianTime, verboseGenesis.Block.Header.BlockTime);
1789+
Assert.Equal(2u, verboseGenesis.Block.Header.Nonce);
1790+
Assert.Equal(new Target(0x207fffff), verboseGenesis.Block.Header.Bits);
1791+
Assert.Equal(4.656542373906925e-10, verboseGenesis.Difficulty);
1792+
Assert.Equal(uint256.Parse("0000000000000000000000000000000000000000000000000000000000000002"), verboseGenesis.ChainWork);
1793+
1794+
// NextBlockHash must be included iff the block is not on the tip.
1795+
Assert.Null(verboseGenesis.NextBlockHash);
1796+
addr = await cli.GetNewAddressAsync();
1797+
await cli.GenerateToAddressAsync(1, addr);
1798+
verboseGenesis = await cli.GetBlockAsync(Network.RegTest.GenesisHash, GetBlockVerbosity.WithOnlyTxId);
1799+
Assert.NotNull(verboseGenesis.NextBlockHash);
1800+
Assert.Null(verboseGenesis.Block); // there will be no Block if we specify false to second argument.
1801+
Assert.NotNull(verboseGenesis.TxIds); // But txids are still there.
1802+
Assert.Single(verboseGenesis.TxIds);
1803+
}
1804+
1805+
// case 2: Get second block with information about transaction (verbosity 2).
1806+
{
1807+
var secondBlockHash = await cli.GetBestBlockHashAsync();
1808+
GetBlockRPCResponse verboseBestBlock = await cli.GetBlockAsync(secondBlockHash, GetBlockVerbosity.WithOnlyTxId);
1809+
Assert.Equal(Network.RegTest.GenesisHash, verboseBestBlock.Header.HashPrevBlock);
1810+
Assert.Null(verboseBestBlock.NextBlockHash);
1811+
1812+
await cli.GenerateToAddressAsync(1, addr);
1813+
verboseBestBlock = await cli.GetBlockAsync(secondBlockHash, GetBlockVerbosity.WithOnlyTxId);
1814+
Assert.NotNull(verboseBestBlock.NextBlockHash);
1815+
Assert.Null(verboseBestBlock.PrevOuts);
1816+
}
1817+
1818+
// case 3: Get third block with information about transaction and prevOuts (verbosity 3).
1819+
{
1820+
await cli.GenerateToAddressAsync(1, addr);
1821+
1822+
var thirdBlockHash = await cli.GetBestBlockHashAsync();
1823+
GetBlockRPCResponse verboseBestBlock = await cli.GetBlockAsync(thirdBlockHash, GetBlockVerbosity.WithFullTxAndPrevouts);
1824+
1825+
var coinbaseTx = Assert.Single(verboseBestBlock.Block.Transactions);
1826+
Assert.True(coinbaseTx.IsCoinBase);
1827+
1828+
// There is only coinbase transaction.
1829+
Assert.NotNull(verboseBestBlock.PrevOuts);
1830+
var prevOutInfos = Assert.Single(verboseBestBlock.PrevOuts);
1831+
var prevOutInfo = Assert.Single(prevOutInfos);
1832+
Assert.Null(prevOutInfo);
1833+
}
1834+
1835+
// case 4: Send a transaction to ourselves and verify the prevout is populated (verbosity 3).
1836+
{
1837+
// Coinbase outputs need 100 confirmations before they're spendable.
1838+
await cli.GenerateToAddressAsync(98, addr);
1839+
var spendTxid = await cli.SendToAddressAsync(addr, Money.Coins(1m));
1840+
await cli.GenerateToAddressAsync(1, addr);
1841+
1842+
var latestBlockHash = await cli.GetBestBlockHashAsync();
1843+
GetBlockRPCResponse verboseBestBlock = await cli.GetBlockAsync(latestBlockHash, GetBlockVerbosity.WithFullTxAndPrevouts);
1844+
1845+
// There are just two transactions: coinbase and our transaction.
1846+
Assert.Equal(2, verboseBestBlock.Block.Transactions.Count);
1847+
1848+
var spendTxIndex = verboseBestBlock.Block.Transactions.FindIndex(tx => tx.GetHash() == spendTxid);
1849+
Assert.Equal(1, spendTxIndex);
1850+
Assert.NotNull(verboseBestBlock.PrevOuts);
1851+
1852+
// Assert prevOuts for the coinbase transaction.
1853+
{
1854+
var prevOutInfos = verboseBestBlock.PrevOuts[0];
1855+
var prevOutInfo = Assert.Single(prevOutInfos);
1856+
Assert.Null(prevOutInfo);
1857+
}
1858+
1859+
// Assert the prevOut for our transaction.
1860+
{
1861+
var ourTx = verboseBestBlock.Block.Transactions[spendTxIndex];
1862+
var txPrevOuts = verboseBestBlock.PrevOuts[spendTxIndex];
1863+
1864+
_ = Assert.Single(ourTx.Inputs);
1865+
PrevOutInfo prevOutInfo = Assert.Single(txPrevOuts);
1866+
1867+
// Spending coinbase coins.
1868+
Assert.True(prevOutInfo.Generated);
1869+
Assert.Equal(1, prevOutInfo.Height);
1870+
Assert.NotNull(prevOutInfo.ScriptPubKey);
1871+
Assert.Equal(addr.ScriptPubKey, prevOutInfo.ScriptPubKey);
1872+
Assert.Equal(Money.Coins(50m), prevOutInfo.Value);
1873+
}
1874+
}
18101875
}
18111876

18121877
static void AssertException<T>(Action act, Action<T> assert) where T : Exception

‎NBitcoin/RPC/GetBlockRPCResponse.cs‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
#nullable enable
12
using System;
23
using System.Linq;
34
using System.Collections.Generic;
45
using NBitcoin;
6+
57
namespace NBitcoin.RPC
68
{
79
/// <summary>
@@ -36,24 +38,45 @@ public class GetBlockRPCResponse
3638
public int Size { get; set; }
3739
public int Weight { get; set; }
3840
public int Height { get; set; }
41+
#nullable disable
3942
public string VersionHex { get; set; }
4043
public uint MedianTimeUnix { get; set; }
4144
public double Difficulty { get; set; }
4245
public uint256 ChainWork { get; set; }
43-
4446
#nullable enable
45-
/// <summary>
46-
/// This field exists only when the block is not on the tip.
47-
/// </summary>
47+
/// <summary>This field exists only when the block is not on the tip.</summary>
4848
public uint256? NextBlockHash { get; set; }
49-
/// <summary>
50-
/// This field exists only when you specified `WithFullTx` verbosity
51-
/// </summary>
49+
50+
/// <summary>This field exists only when you specified `WithFullTx` verbosity</summary>
5251
public Block? Block { get; set; }
53-
#nullable disable
5452

53+
#nullable disable
5554
public BlockHeader Header { get; set; }
5655
public List<uint256> TxIds { get; set; }
56+
#nullable enable
5757
public DateTimeOffset MedianTime => NBitcoin.Utils.UnixTimeToDateTime(MedianTimeUnix);
58+
59+
/// <summary>Prevout information for each input of each transaction. Only populated when verbosity 3 was requested.</summary>
60+
/// <remarks>
61+
/// Outer list is indexed by transaction, in the same order as <see cref="TxIds"/> / <see cref="Block"/>.Transactions.
62+
/// Inner list is indexed by input (vin) number within that transaction.
63+
/// An entry is <see langword="null"/> when no prevout data is available for that input (e.g. a coinbase input, or the node is pruned).
64+
/// </remarks>
65+
public List<List<PrevOutInfo?>>? PrevOuts { get; set; }
66+
}
67+
68+
/// <summary>
69+
/// Prevout information for a spent input as returned by <c>getblock</c> verbosity 3.
70+
/// </summary>
71+
/// <param name="Generated"><c>true</c> if the spent coin was a coinbase output.</param>
72+
/// <param name="Height">Height of the block that created the spent output.</param>
73+
/// <param name="Value">Value of the UTXO in bitcoin.</param>
74+
/// <param name="ScriptPubKey">ScriptPubKey of the spent output.</param>
75+
public class PrevOutInfo(bool Generated, int Height, Money Value, Script? ScriptPubKey)
76+
{
77+
public bool Generated { get; } = Generated;
78+
public int Height { get; } = Height;
79+
public Money Value { get; } = Value;
80+
public Script? ScriptPubKey { get; } = ScriptPubKey;
5881
}
5982
}

‎NBitcoin/RPC/RPCClient.cs‎

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,9 +1476,15 @@ public async Task<GetBlockRPCResponse> GetBlockAsync(uint256 blockHash, GetBlock
14761476
return ParseVerboseBlock(resp, (int)verbosity);
14771477
}
14781478

1479+
#nullable enable
14791480
private GetBlockRPCResponse ParseVerboseBlock(RPCResponse resp, int verbosity)
14801481
{
14811482
var json = (JObject)resp.Result;
1483+
if (json is null)
1484+
{
1485+
throw new ArgumentException("Expected 'result' to be non-null.");
1486+
}
1487+
14821488
var blockHeader = Network.Consensus.ConsensusFactory.CreateBlockHeader();
14831489
blockHeader.Bits = new Target(Encoders.Hex.DecodeData(json.Value<string>("bits")));
14841490
blockHeader.Version = json.Value<int>("version");
@@ -1495,35 +1501,79 @@ private GetBlockRPCResponse ParseVerboseBlock(RPCResponse resp, int verbosity)
14951501
{
14961502
blockHeader.HashPrevBlock = null;
14971503
}
1504+
14981505
// nextblockhash field does not exist for the chain tip.
1499-
uint256 nextBlockHash = null;
1506+
uint256? nextBlockHash = null;
15001507
if (json.TryGetValue("nextblockhash", StringComparison.Ordinal, out var nextBlockHashHex))
15011508
{
15021509
nextBlockHash = uint256.Parse(nextBlockHashHex.ToString());
15031510
}
15041511

1505-
Block block = null;
1512+
Block? block = null;
15061513
var txids = new List<uint256>();
1507-
if (verbosity == 2)
1514+
List<List<PrevOutInfo?>>? prevOuts = null;
1515+
1516+
if (verbosity == 2 || verbosity == 3)
15081517
{
15091518
var txs = new List<Transaction>();
1510-
foreach (var txInfo in json.Value<JArray>("tx"))
1519+
var txArray = json.Value<JArray>("tx") ?? throw new ArgumentNullException("'tx' must be an array. Null given.");
1520+
1521+
if (verbosity == 3)
1522+
prevOuts = new();
1523+
1524+
foreach (var txInfo in txArray)
15111525
{
15121526
var tx = Transaction.Parse(txInfo.Value<string>("hex"), Network);
15131527
txs.Add(tx);
15141528
txids.Add(tx.GetHash());
1529+
1530+
// Gather prevOut info for this transaction.
1531+
if (verbosity == 3)
1532+
{
1533+
var inputPrevOuts = new List<PrevOutInfo?>();
1534+
var vinArray = txInfo.Value<JArray>("vin");
1535+
1536+
if (vinArray is not null)
1537+
{
1538+
foreach (var vin in vinArray)
1539+
{
1540+
var prevOutJson = vin["prevout"] as JObject;
1541+
if (prevOutJson is null)
1542+
{
1543+
// Coinbase input, or a pruned node omitted this field.
1544+
inputPrevOuts.Add(null);
1545+
continue;
1546+
}
1547+
1548+
var scriptPubKeyJson = prevOutJson.Value<JObject>("scriptPubKey");
1549+
var scriptHex = scriptPubKeyJson?.Value<string>("hex");
1550+
1551+
var generated = prevOutJson.Value<bool>("height");
1552+
var height = prevOutJson.Value<int>("height");
1553+
var money = Money.Coins(prevOutJson.Value<decimal>("value"));
1554+
var scriptPubKey = scriptHex is null ? null : new Script(Encoders.Hex.DecodeData(scriptHex));
1555+
inputPrevOuts.Add(new PrevOutInfo(generated, height, money, scriptPubKey));
1556+
}
1557+
}
1558+
1559+
prevOuts!.Add(inputPrevOuts);
1560+
}
15151561
}
1562+
15161563
block = Network.Consensus.ConsensusFactory.CreateBlock();
15171564
block.Header = blockHeader;
15181565
block.Transactions = txs;
1566+
15191567
if (!block.GetMerkleRoot().Hash.Equals(blockHeader.HashMerkleRoot))
15201568
{
15211569
throw new FormatException($"Bogus GetBlockRPCResponse! merkle root mistmach (expected: {blockHeader.HashMerkleRoot}. actual: {block.GetMerkleRoot().Hash})");
15221570
}
15231571
}
15241572
else if (verbosity == 1)
15251573
{
1526-
foreach (var tx in json.Value<JArray>("tx"))
1574+
var txArray = json.Value<JArray>("tx") ?? throw new ArgumentNullException("'tx' must be an array. Null given.");
1575+
1576+
foreach (var tx in txArray)
15271577
{
15281578
txids.Add(uint256.Parse(tx.ToString()));
15291579
}
@@ -1553,8 +1603,10 @@ private GetBlockRPCResponse ParseVerboseBlock(RPCResponse resp, int verbosity)
15531603
Block = block,
15541604
Header = blockHeader,
15551605
TxIds = txids,
1606+
PrevOuts = prevOuts,
15561607
};
15571608
}
1609+
#nullable disable
15581610

15591611
public uint256 GetBlockHash(int height)
15601612
{

0 commit comments

Comments
 (0)