Skip to content

Commit

Permalink
[ColdStaking] Add retrieval endpoint for incorrectly sent coldstaking…
Browse files Browse the repository at this point in the history
… transactions (#678)

* Add retrieval endpoint for incorrectly sent coldstaking transactions
  • Loading branch information
zeptin authored and fassadlr committed Aug 31, 2021
1 parent 062acfd commit 7bac980
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,11 @@ private void Initialize([System.Runtime.CompilerServices.CallerMemberName] strin
this.loggerFactory, DateTimeProvider.Default, walletRepository);

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

var walletTransactionHandler = new WalletTransactionHandler(this.loggerFactory, this.coldStakingManager, new Mock<IWalletFeePolicy>().Object, this.Network, new StandardTransactionPolicy(this.Network), reserveUtxoService);

this.coldStakingController = new ColdStakingController(this.loggerFactory, this.coldStakingManager, walletTransactionHandler);
this.coldStakingController = new ColdStakingController(this.loggerFactory, this.coldStakingManager, walletTransactionHandler, walletFeePolicy, broadcasterManager);

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

Expand Down
46 changes: 46 additions & 0 deletions src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -692,5 +692,51 @@ public IEnumerable<UnspentOutputReference> GetSpendableTransactionsInColdWallet(
this.logger.LogTrace("(-):*.Count={0}", res.Count());
return res;
}

public List<Transaction> RetrieveFilteredUtxos(string walletName, string walletPassword, string transactionHex, FeeRate feeRate, string walletAccount = null)
{
var retrievalTransactions = new List<Transaction>();

Transaction transactionToReclaim = this.network.Consensus.ConsensusFactory.CreateTransaction(transactionHex);

foreach (TxOut output in transactionToReclaim.Outputs)
{
Wallet.Wallet wallet = this.GetWallet(walletName);

HdAddress address = wallet.GetAllAddresses(Wallet.Wallet.AllAccounts).FirstOrDefault(a => a.ScriptPubKey == output.ScriptPubKey);

// The address is not in the wallet so ignore this output.
if (address == null)
continue;

HdAccount destinationAccount = wallet.GetAccounts(Wallet.Wallet.NormalAccounts).First();

// This shouldn't really happen unless the user has no proper accounts in the wallet.
if (destinationAccount == null)
continue;

Script destination = destinationAccount.GetFirstUnusedReceivingAddress().ScriptPubKey;

ISecret extendedPrivateKey = wallet.GetExtendedPrivateKeyForAddress(walletPassword, address);

Key privateKey = extendedPrivateKey.PrivateKey;

var builder = new TransactionBuilder(this.network);

var coin = new Coin(transactionToReclaim, output);

builder.AddCoins(coin);
builder.AddKeys(privateKey);
builder.Send(destination, output.Value);
builder.SubtractFees();
builder.SendEstimatedFees(feeRate);

Transaction builtTransaction = builder.BuildTransaction(true);

retrievalTransactions.Add(builtTransaction);
}

return retrievalTransactions;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,33 @@ namespace Stratis.Bitcoin.Features.ColdStaking.Controllers
public class ColdStakingController : Controller
{
public ColdStakingManager ColdStakingManager { get; private set; }

private readonly IWalletTransactionHandler walletTransactionHandler;
private readonly IWalletFeePolicy walletFeePolicy;
private readonly IBroadcasterManager broadcasterManager;

/// <summary>Instance logger.</summary>
private readonly ILogger logger;

public ColdStakingController(
ILoggerFactory loggerFactory,
IWalletManager walletManager,
IWalletTransactionHandler walletTransactionHandler)
IWalletTransactionHandler walletTransactionHandler,
IWalletFeePolicy walletFeePolicy,
IBroadcasterManager broadcasterManager)
{
Guard.NotNull(loggerFactory, nameof(loggerFactory));
Guard.NotNull(walletManager, nameof(walletManager));
Guard.NotNull(walletTransactionHandler, nameof(walletTransactionHandler));
Guard.NotNull(walletFeePolicy, nameof(walletFeePolicy));
Guard.NotNull(broadcasterManager, nameof(broadcasterManager));

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

this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.walletTransactionHandler = walletTransactionHandler;
this.walletFeePolicy = walletFeePolicy;
this.broadcasterManager = broadcasterManager;
}

/// <summary>
Expand Down Expand Up @@ -589,5 +597,45 @@ public IActionResult EstimateColdStakingWithdrawalFee([FromBody] ColdStakingWith
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}

[Route("retrieve-filtered-utxos")]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
public IActionResult RetrieveFilteredUtxos([FromBody] RetrieveFilteredUtxosRequest request)
{
Guard.NotNull(request, nameof(request));

// Checks the request is valid.
if (!this.ModelState.IsValid)
{
this.logger.LogTrace("(-)[MODEL_STATE_INVALID]");
return ModelStateErrors.BuildErrorResponse(this.ModelState);
}

try
{
FeeRate feeRate = this.walletFeePolicy.GetFeeRate(FeeType.High.ToConfirmations());

List<Transaction> retrievalTransactions = this.ColdStakingManager.RetrieveFilteredUtxos(request.WalletName, request.WalletPassword, request.Hex, feeRate, request.WalletAccount);

if (request.Broadcast)
{
foreach (Transaction transaction in retrievalTransactions)
{
this.broadcasterManager.BroadcastTransactionAsync(transaction);
}
}

return this.Json(retrievalTransactions.Select(t => t.ToHex()));
}
catch (Exception e)
{
this.logger.LogError("Exception occurred: {0}", e.ToString());
this.logger.LogTrace("(-)[ERROR]");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,38 @@ public override string ToString()
return $"{nameof(this.TransactionHex)}={this.TransactionHex}";
}
}

public class RetrieveFilteredUtxosRequest
{
/// <summary>The wallet name.</summary>
[Required]
[JsonProperty(PropertyName = "walletName")]
public string WalletName { get; set; }

/// <summary>The wallet password.</summary>
[Required]
[JsonProperty(PropertyName = "walletPassword")]
public string WalletPassword { get; set; }

/// <summary>
/// The (optional) account for the retrieved UTXOs to be sent back to.
/// If this is not specified, the first available non-coldstaking account will be used.
/// </summary>
[JsonProperty(PropertyName = "walletAccount")]
public string WalletAccount { get; set; }

/// <summary>
/// The hex of the transaction to retrieve the UTXOs for.
/// Only UTXOs sent to addresses within the supplied wallet can be reclaimed.
/// </summary>
[Required]
[JsonProperty(PropertyName = "hex")]
public string Hex { get; set; }

/// <summary>
/// Indicate whether the built transactions should be sent to the network immediately after being built.
/// </summary>
[JsonProperty(PropertyName = "broadcast")]
public bool Broadcast { get; set; }
}
}
1 change: 0 additions & 1 deletion src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using NBitcoin.BuilderExtensions;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Features.Wallet.Models;
using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Utilities.Extensions;
using TracerAttributes;
Expand Down
52 changes: 52 additions & 0 deletions src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using Stratis.Bitcoin.Features.Api;
using Stratis.Bitcoin.Features.BlockStore;
using Stratis.Bitcoin.Features.ColdStaking;
using Stratis.Bitcoin.Features.ColdStaking.Controllers;
using Stratis.Bitcoin.Features.ColdStaking.Models;
using Stratis.Bitcoin.Features.Consensus;
using Stratis.Bitcoin.Features.MemoryPool;
using Stratis.Bitcoin.Features.Miner;
Expand Down Expand Up @@ -199,5 +201,55 @@ public async Task WalletCanMineWithColdWalletCoinsAsync()
}, cancellationToken: cancellationToken);
}
}

[Fact]
[Trait("Unstable", "True")]
public async Task CanRetrieveFilteredUtxosAsync()
{
using (var builder = NodeBuilder.Create(this))
{
var network = new StraxRegTest();

CoreNode stratisSender = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: false);
CoreNode stratisColdStake = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: true);

stratisSender.WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start();
stratisColdStake.WithWallet().Start();

var coldWalletManager = stratisColdStake.FullNode.WalletManager() as ColdStakingManager;

// Set up cold staking account on cold wallet.
coldWalletManager.GetOrCreateColdStakingAccount(WalletName, true, Password, null);
HdAddress coldWalletAddress = coldWalletManager.GetFirstUnusedColdStakingAddress(WalletName, true);

var walletAccountReference = new WalletAccountReference(WalletName, Account);
long total2 = stratisSender.FullNode.WalletManager().GetSpendableTransactionsInAccount(walletAccountReference, 1).Sum(s => s.Transaction.Amount);

// Sync nodes.
TestHelper.Connect(stratisSender, stratisColdStake);

// Send coins to cold address.
Money amountToSend = total2 - network.Consensus.ProofOfWorkReward;
Transaction transaction1 = stratisSender.FullNode.WalletTransactionHandler().BuildTransaction(CreateContext(stratisSender.FullNode.Network, new WalletAccountReference(WalletName, Account), Password, coldWalletAddress.ScriptPubKey, amountToSend, FeeType.Medium, 1));

// Broadcast to the other nodes.
await stratisSender.FullNode.NodeController<WalletController>().SendTransaction(new SendTransactionRequest(transaction1.ToHex()));

// Wait for the transaction to arrive.
TestBase.WaitLoop(() => stratisColdStake.CreateRPCClient().GetRawMempool().Length > 0);

// Despite the funds being sent to an address in the cold account, the wallet does not recognise the output as funds belonging to it.
Assert.True(stratisColdStake.FullNode.WalletManager().GetBalances(WalletName, Account).Sum(a => a.AmountUnconfirmed + a.AmountUnconfirmed) == 0);

uint256[] mempoolTransactionId = stratisColdStake.CreateRPCClient().GetRawMempool();

Transaction misspentTransaction = stratisColdStake.CreateRPCClient().GetRawTransaction(mempoolTransactionId[0]);

// Now retrieve the UTXO sent to the cold address. The funds will reappear in a normal account on the cold staking node.
stratisColdStake.FullNode.NodeController<ColdStakingController>().RetrieveFilteredUtxos(new RetrieveFilteredUtxosRequest() { WalletName = stratisColdStake.WalletName, WalletPassword = stratisColdStake.WalletPassword, Hex = misspentTransaction.ToHex(), WalletAccount = null, Broadcast = true});

TestBase.WaitLoop(() => stratisColdStake.FullNode.WalletManager().GetBalances(WalletName, Account).Sum(a => a.AmountUnconfirmed + a.AmountUnconfirmed) > 0);
}
}
}
}

0 comments on commit 7bac980

Please sign in to comment.