Skip to content

Commit 7bac980

Browse files
zeptinfassadlr
authored andcommitted
[ColdStaking] Add retrieval endpoint for incorrectly sent coldstaking transactions (#678)
* Add retrieval endpoint for incorrectly sent coldstaking transactions
1 parent 062acfd commit 7bac980

File tree

6 files changed

+186
-6
lines changed

6 files changed

+186
-6
lines changed

src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,11 @@ private void Initialize([System.Runtime.CompilerServices.CallerMemberName] strin
234234
this.loggerFactory, DateTimeProvider.Default, walletRepository);
235235

236236
var reserveUtxoService = new ReserveUtxoService(this.loggerFactory, new Mock<ISignals>().Object);
237+
var walletFeePolicy = new Mock<IWalletFeePolicy>().Object;
238+
var broadcasterManager = new Mock<IBroadcasterManager>().Object;
239+
var walletTransactionHandler = new WalletTransactionHandler(this.loggerFactory, this.coldStakingManager, walletFeePolicy, this.Network, new StandardTransactionPolicy(this.Network), reserveUtxoService);
237240

238-
var walletTransactionHandler = new WalletTransactionHandler(this.loggerFactory, this.coldStakingManager, new Mock<IWalletFeePolicy>().Object, this.Network, new StandardTransactionPolicy(this.Network), reserveUtxoService);
239-
240-
this.coldStakingController = new ColdStakingController(this.loggerFactory, this.coldStakingManager, walletTransactionHandler);
241+
this.coldStakingController = new ColdStakingController(this.loggerFactory, this.coldStakingManager, walletTransactionHandler, walletFeePolicy, broadcasterManager);
241242

242243
this.asyncProvider = new AsyncProvider(this.loggerFactory, new Mock<ISignals>().Object);
243244

src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,5 +692,51 @@ public IEnumerable<UnspentOutputReference> GetSpendableTransactionsInColdWallet(
692692
this.logger.LogTrace("(-):*.Count={0}", res.Count());
693693
return res;
694694
}
695+
696+
public List<Transaction> RetrieveFilteredUtxos(string walletName, string walletPassword, string transactionHex, FeeRate feeRate, string walletAccount = null)
697+
{
698+
var retrievalTransactions = new List<Transaction>();
699+
700+
Transaction transactionToReclaim = this.network.Consensus.ConsensusFactory.CreateTransaction(transactionHex);
701+
702+
foreach (TxOut output in transactionToReclaim.Outputs)
703+
{
704+
Wallet.Wallet wallet = this.GetWallet(walletName);
705+
706+
HdAddress address = wallet.GetAllAddresses(Wallet.Wallet.AllAccounts).FirstOrDefault(a => a.ScriptPubKey == output.ScriptPubKey);
707+
708+
// The address is not in the wallet so ignore this output.
709+
if (address == null)
710+
continue;
711+
712+
HdAccount destinationAccount = wallet.GetAccounts(Wallet.Wallet.NormalAccounts).First();
713+
714+
// This shouldn't really happen unless the user has no proper accounts in the wallet.
715+
if (destinationAccount == null)
716+
continue;
717+
718+
Script destination = destinationAccount.GetFirstUnusedReceivingAddress().ScriptPubKey;
719+
720+
ISecret extendedPrivateKey = wallet.GetExtendedPrivateKeyForAddress(walletPassword, address);
721+
722+
Key privateKey = extendedPrivateKey.PrivateKey;
723+
724+
var builder = new TransactionBuilder(this.network);
725+
726+
var coin = new Coin(transactionToReclaim, output);
727+
728+
builder.AddCoins(coin);
729+
builder.AddKeys(privateKey);
730+
builder.Send(destination, output.Value);
731+
builder.SubtractFees();
732+
builder.SendEstimatedFees(feeRate);
733+
734+
Transaction builtTransaction = builder.BuildTransaction(true);
735+
736+
retrievalTransactions.Add(builtTransaction);
737+
}
738+
739+
return retrievalTransactions;
740+
}
695741
}
696742
}

src/Stratis.Bitcoin.Features.ColdStaking/Controllers/ColdStakingController.cs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,33 @@ namespace Stratis.Bitcoin.Features.ColdStaking.Controllers
2323
public class ColdStakingController : Controller
2424
{
2525
public ColdStakingManager ColdStakingManager { get; private set; }
26+
2627
private readonly IWalletTransactionHandler walletTransactionHandler;
28+
private readonly IWalletFeePolicy walletFeePolicy;
29+
private readonly IBroadcasterManager broadcasterManager;
2730

28-
/// <summary>Instance logger.</summary>
2931
private readonly ILogger logger;
3032

3133
public ColdStakingController(
3234
ILoggerFactory loggerFactory,
3335
IWalletManager walletManager,
34-
IWalletTransactionHandler walletTransactionHandler)
36+
IWalletTransactionHandler walletTransactionHandler,
37+
IWalletFeePolicy walletFeePolicy,
38+
IBroadcasterManager broadcasterManager)
3539
{
3640
Guard.NotNull(loggerFactory, nameof(loggerFactory));
3741
Guard.NotNull(walletManager, nameof(walletManager));
3842
Guard.NotNull(walletTransactionHandler, nameof(walletTransactionHandler));
43+
Guard.NotNull(walletFeePolicy, nameof(walletFeePolicy));
44+
Guard.NotNull(broadcasterManager, nameof(broadcasterManager));
3945

4046
this.ColdStakingManager = walletManager as ColdStakingManager;
4147
Guard.NotNull(this.ColdStakingManager, nameof(this.ColdStakingManager));
4248

4349
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
4450
this.walletTransactionHandler = walletTransactionHandler;
51+
this.walletFeePolicy = walletFeePolicy;
52+
this.broadcasterManager = broadcasterManager;
4553
}
4654

4755
/// <summary>
@@ -589,5 +597,45 @@ public IActionResult EstimateColdStakingWithdrawalFee([FromBody] ColdStakingWith
589597
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
590598
}
591599
}
600+
601+
[Route("retrieve-filtered-utxos")]
602+
[HttpPost]
603+
[ProducesResponseType((int)HttpStatusCode.OK)]
604+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
605+
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
606+
public IActionResult RetrieveFilteredUtxos([FromBody] RetrieveFilteredUtxosRequest request)
607+
{
608+
Guard.NotNull(request, nameof(request));
609+
610+
// Checks the request is valid.
611+
if (!this.ModelState.IsValid)
612+
{
613+
this.logger.LogTrace("(-)[MODEL_STATE_INVALID]");
614+
return ModelStateErrors.BuildErrorResponse(this.ModelState);
615+
}
616+
617+
try
618+
{
619+
FeeRate feeRate = this.walletFeePolicy.GetFeeRate(FeeType.High.ToConfirmations());
620+
621+
List<Transaction> retrievalTransactions = this.ColdStakingManager.RetrieveFilteredUtxos(request.WalletName, request.WalletPassword, request.Hex, feeRate, request.WalletAccount);
622+
623+
if (request.Broadcast)
624+
{
625+
foreach (Transaction transaction in retrievalTransactions)
626+
{
627+
this.broadcasterManager.BroadcastTransactionAsync(transaction);
628+
}
629+
}
630+
631+
return this.Json(retrievalTransactions.Select(t => t.ToHex()));
632+
}
633+
catch (Exception e)
634+
{
635+
this.logger.LogError("Exception occurred: {0}", e.ToString());
636+
this.logger.LogTrace("(-)[ERROR]");
637+
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
638+
}
639+
}
592640
}
593641
}

src/Stratis.Bitcoin.Features.ColdStaking/Models/ColdStakingModels.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,38 @@ public override string ToString()
315315
return $"{nameof(this.TransactionHex)}={this.TransactionHex}";
316316
}
317317
}
318+
319+
public class RetrieveFilteredUtxosRequest
320+
{
321+
/// <summary>The wallet name.</summary>
322+
[Required]
323+
[JsonProperty(PropertyName = "walletName")]
324+
public string WalletName { get; set; }
325+
326+
/// <summary>The wallet password.</summary>
327+
[Required]
328+
[JsonProperty(PropertyName = "walletPassword")]
329+
public string WalletPassword { get; set; }
330+
331+
/// <summary>
332+
/// The (optional) account for the retrieved UTXOs to be sent back to.
333+
/// If this is not specified, the first available non-coldstaking account will be used.
334+
/// </summary>
335+
[JsonProperty(PropertyName = "walletAccount")]
336+
public string WalletAccount { get; set; }
337+
338+
/// <summary>
339+
/// The hex of the transaction to retrieve the UTXOs for.
340+
/// Only UTXOs sent to addresses within the supplied wallet can be reclaimed.
341+
/// </summary>
342+
[Required]
343+
[JsonProperty(PropertyName = "hex")]
344+
public string Hex { get; set; }
345+
346+
/// <summary>
347+
/// Indicate whether the built transactions should be sent to the network immediately after being built.
348+
/// </summary>
349+
[JsonProperty(PropertyName = "broadcast")]
350+
public bool Broadcast { get; set; }
351+
}
318352
}

src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using NBitcoin.BuilderExtensions;
1111
using Stratis.Bitcoin.Configuration;
1212
using Stratis.Bitcoin.Features.Wallet.Interfaces;
13-
using Stratis.Bitcoin.Features.Wallet.Models;
1413
using Stratis.Bitcoin.Utilities;
1514
using Stratis.Bitcoin.Utilities.Extensions;
1615
using TracerAttributes;

src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using Stratis.Bitcoin.Features.Api;
99
using Stratis.Bitcoin.Features.BlockStore;
1010
using Stratis.Bitcoin.Features.ColdStaking;
11+
using Stratis.Bitcoin.Features.ColdStaking.Controllers;
12+
using Stratis.Bitcoin.Features.ColdStaking.Models;
1113
using Stratis.Bitcoin.Features.Consensus;
1214
using Stratis.Bitcoin.Features.MemoryPool;
1315
using Stratis.Bitcoin.Features.Miner;
@@ -199,5 +201,55 @@ public async Task WalletCanMineWithColdWalletCoinsAsync()
199201
}, cancellationToken: cancellationToken);
200202
}
201203
}
204+
205+
[Fact]
206+
[Trait("Unstable", "True")]
207+
public async Task CanRetrieveFilteredUtxosAsync()
208+
{
209+
using (var builder = NodeBuilder.Create(this))
210+
{
211+
var network = new StraxRegTest();
212+
213+
CoreNode stratisSender = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: false);
214+
CoreNode stratisColdStake = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: true);
215+
216+
stratisSender.WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start();
217+
stratisColdStake.WithWallet().Start();
218+
219+
var coldWalletManager = stratisColdStake.FullNode.WalletManager() as ColdStakingManager;
220+
221+
// Set up cold staking account on cold wallet.
222+
coldWalletManager.GetOrCreateColdStakingAccount(WalletName, true, Password, null);
223+
HdAddress coldWalletAddress = coldWalletManager.GetFirstUnusedColdStakingAddress(WalletName, true);
224+
225+
var walletAccountReference = new WalletAccountReference(WalletName, Account);
226+
long total2 = stratisSender.FullNode.WalletManager().GetSpendableTransactionsInAccount(walletAccountReference, 1).Sum(s => s.Transaction.Amount);
227+
228+
// Sync nodes.
229+
TestHelper.Connect(stratisSender, stratisColdStake);
230+
231+
// Send coins to cold address.
232+
Money amountToSend = total2 - network.Consensus.ProofOfWorkReward;
233+
Transaction transaction1 = stratisSender.FullNode.WalletTransactionHandler().BuildTransaction(CreateContext(stratisSender.FullNode.Network, new WalletAccountReference(WalletName, Account), Password, coldWalletAddress.ScriptPubKey, amountToSend, FeeType.Medium, 1));
234+
235+
// Broadcast to the other nodes.
236+
await stratisSender.FullNode.NodeController<WalletController>().SendTransaction(new SendTransactionRequest(transaction1.ToHex()));
237+
238+
// Wait for the transaction to arrive.
239+
TestBase.WaitLoop(() => stratisColdStake.CreateRPCClient().GetRawMempool().Length > 0);
240+
241+
// Despite the funds being sent to an address in the cold account, the wallet does not recognise the output as funds belonging to it.
242+
Assert.True(stratisColdStake.FullNode.WalletManager().GetBalances(WalletName, Account).Sum(a => a.AmountUnconfirmed + a.AmountUnconfirmed) == 0);
243+
244+
uint256[] mempoolTransactionId = stratisColdStake.CreateRPCClient().GetRawMempool();
245+
246+
Transaction misspentTransaction = stratisColdStake.CreateRPCClient().GetRawTransaction(mempoolTransactionId[0]);
247+
248+
// Now retrieve the UTXO sent to the cold address. The funds will reappear in a normal account on the cold staking node.
249+
stratisColdStake.FullNode.NodeController<ColdStakingController>().RetrieveFilteredUtxos(new RetrieveFilteredUtxosRequest() { WalletName = stratisColdStake.WalletName, WalletPassword = stratisColdStake.WalletPassword, Hex = misspentTransaction.ToHex(), WalletAccount = null, Broadcast = true});
250+
251+
TestBase.WaitLoop(() => stratisColdStake.FullNode.WalletManager().GetBalances(WalletName, Account).Sum(a => a.AmountUnconfirmed + a.AmountUnconfirmed) > 0);
252+
}
253+
}
202254
}
203255
}

0 commit comments

Comments
 (0)