Skip to content

Commit 3e78dc0

Browse files
authored
Port of stratx implementation of sweep (#236)
1 parent 5ad34b9 commit 3e78dc0

11 files changed

Lines changed: 366 additions & 4 deletions

File tree

src/Features/Blockcore.Features.BlockStore/Api/Controllers/BlockStoreController.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Net;
34
using Blockcore.Base;
45
using Blockcore.Consensus.BlockInfo;
56
using Blockcore.Consensus.Chain;
7+
using Blockcore.Consensus.TransactionInfo;
68
using Blockcore.Controllers.Models;
79
using Blockcore.Features.BlockStore.AddressIndexing;
810
using Blockcore.Features.BlockStore.Api.Models;
@@ -44,24 +46,30 @@ public class BlockStoreController : Controller
4446
/// <summary>Current network for the active controller instance.</summary>
4547
private readonly Network network;
4648

49+
/// <summary>UTXO indexer.</summary>
50+
private readonly IUtxoIndexer utxoIndexer;
51+
4752
public BlockStoreController(
4853
Network network,
4954
ILoggerFactory loggerFactory,
5055
IBlockStore blockStore,
5156
IChainState chainState,
5257
ChainIndexer chainIndexer,
53-
IAddressIndexer addressIndexer)
58+
IAddressIndexer addressIndexer,
59+
IUtxoIndexer utxoIndexer)
5460
{
5561
Guard.NotNull(network, nameof(network));
5662
Guard.NotNull(loggerFactory, nameof(loggerFactory));
5763
Guard.NotNull(chainState, nameof(chainState));
5864
Guard.NotNull(addressIndexer, nameof(addressIndexer));
65+
Guard.NotNull(utxoIndexer, nameof(utxoIndexer));
5966

6067
this.addressIndexer = addressIndexer;
6168
this.network = network;
6269
this.blockStore = blockStore;
6370
this.chainState = chainState;
6471
this.chainIndexer = chainIndexer;
72+
this.utxoIndexer = utxoIndexer;
6573
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
6674
}
6775

@@ -210,5 +218,39 @@ public IActionResult GetVerboseAddressesBalancesData(string addresses)
210218
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
211219
}
212220
}
221+
222+
/// <summary>Returns every UTXO as of a given block height. This may take some time for large chains.</summary>
223+
/// <param name="atBlockHeight">Only process blocks up to this height for the purposes of constructing the UTXO set.</param>
224+
/// <returns>A result object containing the UTXOs.</returns>
225+
/// <response code="200">Returns the UTXO set.</response>
226+
/// <response code="400">Unexpected exception occurred</response>
227+
[Route(BlockStoreRouteEndPoint.GetUtxoSet)]
228+
[HttpGet]
229+
[ProducesResponseType((int)HttpStatusCode.OK)]
230+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
231+
public IActionResult GetUtxoSet(int atBlockHeight)
232+
{
233+
try
234+
{
235+
ReconstructedCoinviewContext coinView = this.utxoIndexer.GetCoinviewAtHeight(atBlockHeight);
236+
237+
var outputs = new List<UtxoModel>();
238+
239+
foreach (OutPoint outPoint in coinView.UnspentOutputs)
240+
{
241+
TxOut txOut = coinView.Transactions[outPoint.Hash].Outputs[outPoint.N];
242+
var utxo = new UtxoModel() { TxId = outPoint.Hash, Index = outPoint.N, ScriptPubKey = txOut.ScriptPubKey, Value = txOut.Value };
243+
244+
outputs.Add(utxo);
245+
}
246+
247+
return this.Json(outputs);
248+
}
249+
catch (Exception e)
250+
{
251+
this.logger.LogError("Exception occurred: {0}", e.ToString());
252+
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
253+
}
254+
}
213255
}
214256
}

src/Features/Blockcore.Features.BlockStore/Api/Models/BlockStoreRouteEndPoint.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ public static class BlockStoreRouteEndPoint
77
public const string GetAddressIndexerTip = "addressindexertip";
88
public const string GetBlock = "block";
99
public const string GetBlockCount = "GetBlockCount";
10+
public const string GetUtxoSet = "getutxoset";
1011
}
1112
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Blockcore.Consensus.ScriptInfo;
2+
using NBitcoin;
3+
using Newtonsoft.Json;
4+
5+
namespace Blockcore.Features.BlockStore.Models
6+
{
7+
public class UtxoModel
8+
{
9+
[JsonProperty]
10+
public uint256 TxId { get; set; }
11+
12+
[JsonProperty]
13+
public uint Index { get; set; }
14+
15+
[JsonProperty]
16+
public Script ScriptPubKey { get; set; }
17+
18+
[JsonProperty]
19+
public Money Value { get; set; }
20+
}
21+
}

src/Features/Blockcore.Features.BlockStore/BlockStoreFeature.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public static IFullNodeBuilder UseBlockStore(this IFullNodeBuilder fullNodeBuild
209209
services.AddSingleton<StoreSettings>();
210210
services.AddSingleton<IBlockStoreQueueFlushCondition, BlockStoreQueueFlushCondition>();
211211
services.AddSingleton<IAddressIndexer, AddressIndexer>();
212+
services.AddSingleton<IUtxoIndexer, UtxoIndexer>();
212213

213214
services.AddSingleton<IPrunedBlockRepository, PrunedBlockRepository>();
214215
services.AddSingleton<IPruneBlockStoreService, PruneBlockStoreService>();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Collections.Generic;
2+
using Blockcore.Consensus.TransactionInfo;
3+
using NBitcoin;
4+
5+
namespace Blockcore.Features.BlockStore
6+
{
7+
/// <summary>
8+
/// This is so named to indicate that it is not really intended for use outside of the block store's controller, i.e. it is not part of consensus.
9+
/// </summary>
10+
public class ReconstructedCoinviewContext
11+
{
12+
/// <summary>
13+
/// All of the outputs that haven't been spent at this point in time.
14+
/// </summary>
15+
public HashSet<OutPoint> UnspentOutputs { get; }
16+
17+
/// <summary>
18+
/// Easy access to all of the loaded transactions.
19+
/// </summary>
20+
public Dictionary<uint256, Transaction> Transactions { get; }
21+
22+
public ReconstructedCoinviewContext()
23+
{
24+
this.UnspentOutputs = new HashSet<OutPoint>();
25+
this.Transactions = new Dictionary<uint256, Transaction>();
26+
}
27+
}
28+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.Extensions.Logging;
5+
using NBitcoin;
6+
using Blockcore.Interfaces;
7+
using Blockcore.Networks;
8+
using Blockcore.Consensus.Chain;
9+
using Blockcore.Consensus.BlockInfo;
10+
using Blockcore.Consensus.TransactionInfo;
11+
12+
namespace Blockcore.Features.BlockStore
13+
{
14+
public interface IUtxoIndexer
15+
{
16+
ReconstructedCoinviewContext GetCoinviewAtHeight(int blockHeight);
17+
}
18+
19+
/// <summary>
20+
/// This is a separate use case from the address indexer, as we need full details about the current items in the UTXO set in order to
21+
/// be able to potentially build transactions from them. The address indexer is also intended for long-running use cases, and does
22+
/// incur overhead in processing each block. This indexer, conversely, is designed for occasional usage without the requirement
23+
/// of persisting its output for rapid lookup.
24+
/// The actual coindb is not well suited to this kind of query either, as it is being accessed frequently and lacks a 'snapshot'
25+
/// facility. This indexer therefore trades query speed for implementation simplicity, as we only have to enumerate the entire chain
26+
/// without regard for significantly impacting other components.
27+
/// Allowing the height to be specified is an additional convenience that the coindb on its own would not give us without additional
28+
/// consistency safeguards, which may be invasive. For example, halting consensus at a specific height to build a snapshot.
29+
/// </summary>
30+
/// <remarks>This borrows heavily from the <see cref="Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor.Controllers.BalancesController"/>, but is less specialised.</remarks>
31+
public class UtxoIndexer : IUtxoIndexer
32+
{
33+
private readonly Network network;
34+
private readonly ChainIndexer chainIndexer;
35+
private readonly IBlockStore blockStore;
36+
private readonly ILogger logger;
37+
38+
public UtxoIndexer(Network network, ChainIndexer chainIndexer, IBlockStore blockStore, ILoggerFactory loggerFactory)
39+
{
40+
this.network = network;
41+
this.chainIndexer = chainIndexer;
42+
this.blockStore = blockStore;
43+
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
44+
}
45+
46+
public ReconstructedCoinviewContext GetCoinviewAtHeight(int blockHeight)
47+
{
48+
var coinView = new ReconstructedCoinviewContext();
49+
50+
// TODO: Make this a command line option
51+
const int batchSize = 1000;
52+
53+
IEnumerable<ChainedHeader> allBlockHeaders = this.chainIndexer.EnumerateToTip(this.chainIndexer.Genesis);
54+
55+
int totalBlocksCounted = 0;
56+
57+
int i = 0;
58+
59+
while (true)
60+
{
61+
List<ChainedHeader> headers = allBlockHeaders.Skip(i * batchSize).Take(batchSize).ToList();
62+
List<Block> blocks = this.blockStore.GetBlocks(headers.Select(x => x.HashBlock).ToList());
63+
64+
foreach (Block block in blocks)
65+
{
66+
this.AdjustCoinviewForBlock(block, coinView);
67+
totalBlocksCounted += 1;
68+
69+
// We have reached the block height asked for.
70+
if (totalBlocksCounted >= blockHeight)
71+
break;
72+
}
73+
74+
// We have seen every block up to the tip or the chosen block height.
75+
if (headers.Count < batchSize || totalBlocksCounted >= blockHeight)
76+
break;
77+
78+
// Give the user some feedback every 10k blocks
79+
if (i % 10 == 9)
80+
this.logger.LogInformation($"Processed {totalBlocksCounted} blocks for UTXO indexing.");
81+
82+
i++;
83+
}
84+
85+
return coinView;
86+
}
87+
88+
private void AdjustCoinviewForBlock(Block block, ReconstructedCoinviewContext coinView)
89+
{
90+
foreach (Transaction tx in block.Transactions)
91+
{
92+
// Add outputs
93+
for (int i = 0; i < tx.Outputs.Count; i++)
94+
{
95+
TxOut output = tx.Outputs[i];
96+
97+
if (output.Value <= 0 || output.ScriptPubKey.IsUnspendable)
98+
continue;
99+
100+
coinView.UnspentOutputs.Add(new OutPoint(tx, i));
101+
coinView.Transactions[tx.GetHash()] = tx;
102+
}
103+
104+
// Spend inputs. Coinbases are special in that they don't reference previous transactions for their inputs, so ignore them.
105+
if (tx.IsCoinBase)
106+
continue;
107+
108+
foreach (TxIn txIn in tx.Inputs)
109+
{
110+
// TODO: This is inefficient because the Transactions dictionary entry does not get deleted, so extra memory gets used
111+
if (!coinView.UnspentOutputs.Remove(txIn.PrevOut))
112+
{
113+
throw new Exception("Attempted to spend a prevOut that isn't in the coinview at this time.");
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}

src/Features/Blockcore.Features.Wallet/Api/Controllers/WalletController.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,27 @@ public IActionResult DistributeUtxos([FromBody] DistributeUtxosRequest request)
15691569
}
15701570
}
15711571

1572+
/// <summary>
1573+
/// Sweeps one or more private keys to another address.
1574+
/// </summary>
1575+
/// <param name="request">request</param>
1576+
/// <returns>List of transactions</returns>
1577+
[HttpPost]
1578+
[Route("sweep")]
1579+
public IActionResult Sweep([FromBody] SweepRequest request)
1580+
{
1581+
try
1582+
{
1583+
var responseModel = this.walletManager.Sweep(request.PrivateKeys, request.DestinationAddress, request.Broadcast);
1584+
return this.Json(responseModel);
1585+
}
1586+
catch(Exception e)
1587+
{
1588+
this.logger.LogError("Exception occurred: {0}", e.ToString());
1589+
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
1590+
}
1591+
}
1592+
15721593
private void SyncFromBestHeightForRecoveredWallets(DateTime walletCreationDate)
15731594
{
15741595
// After recovery the wallet needs to be synced.

src/Features/Blockcore.Features.Wallet/Api/Models/RequestModels.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,4 +920,15 @@ public class VerifyRequest : RequestModel
920920
[Required(ErrorMessage = "A message is required.")]
921921
public string Message { get; set; }
922922
}
923+
924+
public class SweepRequest : RequestModel
925+
{
926+
[Required(ErrorMessage = "One or more private keys is required.")]
927+
public List<string> PrivateKeys { get; set; }
928+
929+
[Required(ErrorMessage = "A destination address is required.")]
930+
public string DestinationAddress { get; set; }
931+
932+
public bool Broadcast { get; set; }
933+
}
923934
}

src/Features/Blockcore.Features.Wallet/Interfaces/IWalletManager.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,5 +420,14 @@ public interface IWalletManager
420420
/// <param name="fromDate">The date after which the transactions should be removed.</param>
421421
/// <returns>A list of objects made up of a transactions ID along with the time at which they were created.</returns>
422422
HashSet<(uint256, DateTimeOffset)> RemoveTransactionsFromDate(string walletName, DateTimeOffset fromDate);
423+
424+
/// <summary>
425+
/// Sweeps the funds from the private keys to the destination address.
426+
/// </summary>
427+
/// <param name="privateKeys">Private keys to sweep funds from in wif format.</param>
428+
/// <param name="destAddress">Destination address to sweep funds to.</param>
429+
/// <param name="broadcast">Broadcast the transaction to the network.</param>
430+
/// <returns>List of sweep transactions.</returns>
431+
IEnumerable<string> Sweep(IEnumerable<string> privateKeys, string destAddress, bool broadcast);
423432
}
424433
}

0 commit comments

Comments
 (0)