From 3ef7a89e5ef862ae941007b9fbf9d204d5eddec6 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Tue, 24 Aug 2021 16:15:43 +0200 Subject: [PATCH 1/3] Add retrieval endpoint for incorrectly sent coldstaking transactions --- .../ColdStakingManager.cs | 46 ++++++++++++++++ .../Controllers/ColdStakingController.cs | 52 ++++++++++++++++++- .../Models/ColdStakingModels.cs | 34 ++++++++++++ .../WalletManager.cs | 1 - .../Wallet/ColdWalletTests.cs | 52 +++++++++++++++++++ 5 files changed, 182 insertions(+), 3 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs b/src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs index 490f284f49..b82bbe1d67 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs +++ b/src/Stratis.Bitcoin.Features.ColdStaking/ColdStakingManager.cs @@ -692,5 +692,51 @@ public IEnumerable GetSpendableTransactionsInColdWallet( this.logger.LogTrace("(-):*.Count={0}", res.Count()); return res; } + + public List RetrieveFilteredUtxos(string walletName, string walletPassword, string transactionHex, FeeRate feeRate, string walletAccount = null) + { + var retrievalTransactions = new List(); + + 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; + } } } diff --git a/src/Stratis.Bitcoin.Features.ColdStaking/Controllers/ColdStakingController.cs b/src/Stratis.Bitcoin.Features.ColdStaking/Controllers/ColdStakingController.cs index 213fafeb07..c65c9565f4 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking/Controllers/ColdStakingController.cs +++ b/src/Stratis.Bitcoin.Features.ColdStaking/Controllers/ColdStakingController.cs @@ -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; - /// Instance logger. 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; } /// @@ -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 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()); + } + } } } diff --git a/src/Stratis.Bitcoin.Features.ColdStaking/Models/ColdStakingModels.cs b/src/Stratis.Bitcoin.Features.ColdStaking/Models/ColdStakingModels.cs index 40606b7d4f..9cf43418ed 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking/Models/ColdStakingModels.cs +++ b/src/Stratis.Bitcoin.Features.ColdStaking/Models/ColdStakingModels.cs @@ -315,4 +315,38 @@ public override string ToString() return $"{nameof(this.TransactionHex)}={this.TransactionHex}"; } } + + public class RetrieveFilteredUtxosRequest + { + /// The wallet name. + [Required] + [JsonProperty(PropertyName = "walletName")] + public string WalletName { get; set; } + + /// The wallet password. + [Required] + [JsonProperty(PropertyName = "walletPassword")] + public string WalletPassword { get; set; } + + /// + /// 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. + /// + [JsonProperty(PropertyName = "walletAccount")] + public string WalletAccount { get; set; } + + /// + /// The hex of the transaction to retrieve the UTXOs for. + /// Only UTXOs sent to addresses within the supplied wallet can be reclaimed. + /// + [Required] + [JsonProperty(PropertyName = "hex")] + public string Hex { get; set; } + + /// + /// Indicate whether the built transactions should be sent to the network immediately after being built. + /// + [JsonProperty(PropertyName = "broadcast")] + public bool Broadcast { get; set; } + } } diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs index 4c7e908e93..1bbf0362fa 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletManager.cs @@ -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; diff --git a/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs b/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs index a3e64cd763..10527fb596 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs @@ -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; @@ -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().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 funds sent to the cold address. They will reappear in a normal account on the cold staking node. + stratisColdStake.FullNode.NodeController().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); + } + } } } From bcb2222aac3209d6d42d62f0c9b43f7cd421da30 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Tue, 24 Aug 2021 16:36:43 +0200 Subject: [PATCH 2/3] Whoops --- .../ColdStakingControllerTest.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs b/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs index 308b6d73a0..3bb7994cc2 100644 --- a/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs +++ b/src/Stratis.Bitcoin.Features.ColdStaking.Tests/ColdStakingControllerTest.cs @@ -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().Object); + var walletFeePolicy = new Mock().Object; + var broadcasterManager = new Mock().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().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().Object); From a427d56553dcdb37abd6f63be95a1b7d3ea6574e Mon Sep 17 00:00:00 2001 From: zeptin Date: Wed, 25 Aug 2021 07:03:37 +0200 Subject: [PATCH 3/3] Update src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs Boot CI --- src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs b/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs index 10527fb596..ae6c9a6196 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/Wallet/ColdWalletTests.cs @@ -245,7 +245,7 @@ public async Task CanRetrieveFilteredUtxosAsync() Transaction misspentTransaction = stratisColdStake.CreateRPCClient().GetRawTransaction(mempoolTransactionId[0]); - // Now retrieve the funds sent to the cold address. They will reappear in a normal account on the cold staking node. + // 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().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);