From 0d95241eef29591e7836e44bb954ce6e6feebf67 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Thu, 29 Dec 2022 12:46:28 +0200 Subject: [PATCH 01/11] Add createrawtransaction RPC method --- .../CreateRawTransactionModelBinder.cs | 41 +++ .../CreateRawTransactionOutputBinder.cs | 52 ++++ .../Models/CreateRawTransactionModels.cs | 42 +++ .../RPCClient.Wallet.cs | 41 ++- src/Stratis.Bitcoin.Features.RPC/RPCClient.cs | 2 +- .../RPCOperations.cs | 1 + .../RPCRequest.cs | 68 +++-- .../WebHostExtensions.cs | 2 + .../Models/RequestModels.cs | 1 + .../RPC/RawTransactionTests.cs | 288 +++++++++++++++++- .../Utilities/JsonConverters/Serializer.cs | 19 +- 11 files changed, 529 insertions(+), 28 deletions(-) create mode 100644 src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs create mode 100644 src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs create mode 100644 src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs diff --git a/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs new file mode 100644 index 0000000000..b5f5b515e9 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionModelBinder.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Stratis.Bitcoin.Features.RPC.Models; +using Stratis.Bitcoin.Utilities.JsonConverters; + +namespace Stratis.Bitcoin.Features.RPC.ModelBinders +{ + public class CreateRawTransactionInputBinder : IModelBinder, IModelBinderProvider + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext.ModelType != typeof(CreateRawTransactionInput[])) + { + return Task.CompletedTask; + } + + ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + + string key = val.FirstValue; + + if (key == null) + { + return Task.CompletedTask; + } + + CreateRawTransactionInput[] inputs = Serializer.ToObject(key); + + bindingContext.Result = ModelBindingResult.Success(inputs); + + return Task.CompletedTask; + } + + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context.Metadata.ModelType == typeof(CreateRawTransactionInput[])) + return this; + + return null; + } + } +} diff --git a/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs new file mode 100644 index 0000000000..a6c357e2cf --- /dev/null +++ b/src/Stratis.Bitcoin.Features.RPC/ModelBinders/CreateRawTransactionOutputBinder.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json.Linq; +using Stratis.Bitcoin.Features.RPC.Models; + +public class CreateRawTransactionOutputBinder : IModelBinder, IModelBinderProvider +{ + public Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext.ModelType != typeof(CreateRawTransactionOutput[])) + { + return Task.CompletedTask; + } + + ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + + string raw = val.FirstValue; + + if (raw == null) + { + return Task.CompletedTask; + } + + JArray outerArray = JArray.Parse(raw); + + var model = new List(); + + foreach (var item in outerArray.Children()) + { + foreach (JProperty property in item.Properties()) + { + string addressOrData = property.Name; + string value = property.Value.ToString(); + + model.Add(new CreateRawTransactionOutput() { Key = addressOrData, Value = value }); + } + } + + bindingContext.Result = ModelBindingResult.Success(model.ToArray()); + + return Task.CompletedTask; + } + + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context.Metadata.ModelType == typeof(CreateRawTransactionOutput[])) + return this; + + return null; + } +} diff --git a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs new file mode 100644 index 0000000000..af33a9918c --- /dev/null +++ b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs @@ -0,0 +1,42 @@ +using NBitcoin; +using Newtonsoft.Json; + +namespace Stratis.Bitcoin.Features.RPC.Models +{ + /// + /// Used for storing the data that represents desired transaction inputs for the createrawtransaction RPC call. + /// + public class CreateRawTransactionInput + { + [JsonProperty(PropertyName = "txid")] + public uint256 TxId { get; set; } + + [JsonProperty(PropertyName = "vout")] + public int VOut { get; set; } + + [JsonProperty(PropertyName = "sequence")] + public int Sequence { get; set; } + } + + /// + /// Used for storing the key-value pairs that represent desired transaction outputs for the createrawtransaction RPC call. + /// + /// The key and value properties are populated by a custom model binder and therefore never appear with those names in the raw JSON. + public class CreateRawTransactionOutput + { + [JsonProperty] + public string Key { get; set; } + + [JsonProperty] + public string Value { get; set; } + } + + public class CreateRawTransactionResponse + { + [JsonProperty(PropertyName = "hex")] + public Transaction Transaction + { + get; set; + } + } +} diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs b/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs index 506f626eec..5bb95d1d4d 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs @@ -6,6 +6,7 @@ using NBitcoin.DataEncoders; using NBitcoin.Protocol; using Newtonsoft.Json.Linq; +using Stratis.Bitcoin.Features.RPC.Models; namespace Stratis.Bitcoin.Features.RPC { @@ -52,7 +53,7 @@ wallet settxfee wallet signmessage wallet walletlock wallet walletpassphrasechange - wallet walletpassphrase yes + wallet walletpassphrase Yes */ public partial class RPCClient { @@ -136,6 +137,44 @@ public IEnumerable GetAddressesByAccount(string account) return response.Result.Select(t => this.Network.Parse((string)t)); } + public CreateRawTransactionResponse CreateRawTransaction(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) + { + return CreateRawTransactionAsync(inputs, outputs, locktime, replaceable).GetAwaiter().GetResult(); + } + + public async Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) + { + var jOutputs = new JArray(); + + /* Need the layout of the output array to look like the following, per bitcoind documentation: + + [ + { (json object) + "address": amount, (numeric or string, required) A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC + }, + { (json object) + "data": "hex", (string, required) A key-value pair. The key must be "data", the value is hex-encoded data + }, + ... + ] + */ + foreach (KeyValuePair kv in outputs) + { + var temp = new JObject(); + temp.Add(new JProperty(kv.Key, kv.Value)); + jOutputs.Add(temp); + } + + RPCResponse response = await SendCommandAsync(RPCOperations.createrawtransaction, inputs, jOutputs, locktime, replaceable).ConfigureAwait(false); + + var r = (JObject)response.Result; + + return new CreateRawTransactionResponse() + { + Transaction = this.network.CreateTransaction(r["hex"].Value()) + }; + } + public FundRawTransactionResponse FundRawTransaction(Transaction transaction, FundRawTransactionOptions options = null, bool? isWitness = null) { return FundRawTransactionAsync(transaction, options, isWitness).GetAwaiter().GetResult(); diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs b/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs index edf94b760c..5fca236564 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCClient.cs @@ -104,7 +104,7 @@ generating setgenerate generating generate ------------------ Raw transactions - rawtransactions createrawtransaction + rawtransactions createrawtransaction Yes rawtransactions decoderawtransaction Yes rawtransactions decodescript rawtransactions getrawtransaction Yes diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs b/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs index 5a831638ba..a6d6da0fca 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCOperations.cs @@ -16,6 +16,7 @@ public enum RPCOperations importpubkey, dumpwallet, importwallet, + setwallet, getgenerate, setgenerate, diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs b/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs index eba7970889..f7d549cbbd 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCRequest.cs @@ -2,22 +2,22 @@ using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Stratis.Bitcoin.Utilities.JsonConverters; namespace Stratis.Bitcoin.Features.RPC { public class RPCRequest { - public RPCRequest(RPCOperations method, object[] parameters) - : this(method.ToString(), parameters) + public RPCRequest(RPCOperations method, object[] parameters) : this(method.ToString(), parameters) { - } - public RPCRequest(string method, object[] parameters) - : this() + + public RPCRequest(string method, object[] parameters) : this() { this.Method = method; this.Params = parameters; } + public RPCRequest() { this.JsonRpc = "1.0"; @@ -33,7 +33,7 @@ public RPCRequest() public void WriteJSON(TextWriter writer) { - using (JsonTextWriter jsonWriter = new JsonTextWriter(writer)) + using (var jsonWriter = new JsonTextWriter(writer)) { jsonWriter.CloseOutput = false; WriteJSON(jsonWriter); @@ -51,34 +51,54 @@ internal void WriteJSON(JsonTextWriter writer) writer.WritePropertyName("params"); writer.WriteStartArray(); - if(this.Params != null) + if (this.Params == null) + { + writer.WriteEndArray(); + writer.WriteEndObject(); + return; + } + + for (int i = 0; i < this.Params.Length; i++) { - for(int i = 0; i < this.Params.Length; i++) + if (this.Params[i] is JToken) { - if(this.Params[i] is JToken) - { - ((JToken) this.Params[i]).WriteTo(writer); - } - else if(this.Params[i] is Array) - { - writer.WriteStartArray(); - foreach(object x in (Array) this.Params[i]) - { - writer.WriteValue(x); - } - writer.WriteEndArray(); - } - else + ((JToken) this.Params[i]).WriteTo(writer); + } + else if (this.Params[i] is Array) + { + writer.WriteStartArray(); + + foreach (object x in (Array) this.Params[i]) { - writer.WriteValue(this.Params[i]); + // Primitive types are handled well by the writer's WriteValue method, but classes need to be serialised using the same converter set as the rest of the codebase. + WriteValueOrSerializeAndWrite(writer, x); } + + writer.WriteEndArray(); + } + else + { + WriteValueOrSerializeAndWrite(writer, this.Params[i]); } } - + writer.WriteEndArray(); writer.WriteEndObject(); } + private void WriteValueOrSerializeAndWrite(JsonTextWriter writer, object valueToWrite) + { + if (valueToWrite == null || valueToWrite.GetType().IsValueType) + { + writer.WriteValue(valueToWrite); + return; + } + + // TODO: It did not appear that the RPC subsystem was automatically handling complex class parameters in requests. So we will need to start passing the network into the RPCRequest constructor to properly handle every possible type + JToken token = Serializer.ToToken(valueToWrite); + token.WriteTo(writer); + } + private void WriteProperty(JsonTextWriter writer, string property, TValue value) { writer.WritePropertyName(property); diff --git a/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs b/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs index abd36f5fad..c5e38022dc 100644 --- a/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs +++ b/src/Stratis.Bitcoin.Features.RPC/WebHostExtensions.cs @@ -18,6 +18,8 @@ public static IWebHostBuilder ForFullNode(this IWebHostBuilder hostBuilder, Full o.ModelBinderProviders.Insert(0, new DestinationModelBinder()); o.ModelBinderProviders.Insert(0, new MoneyModelBinder()); o.ModelBinderProviders.Insert(0, new FundRawTransactionOptionsBinder()); + o.ModelBinderProviders.Insert(0, new CreateRawTransactionInputBinder()); + o.ModelBinderProviders.Insert(0, new CreateRawTransactionOutputBinder()); }); // Include all feature assemblies for action discovery otherwise RPC actions will not execute diff --git a/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs b/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs index 721f27ead7..fcc44801be 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Models/RequestModels.cs @@ -445,6 +445,7 @@ public class BuildTransactionRequest : TxFeeEstimateRequest, IValidatableObject /// /// Whether to send the change to a P2WPKH (segwit bech32) addresses, or a regular P2PKH address /// + [DefaultValue(false)] public bool SegwitChangeAddress { get; set; } /// diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs index 6b26b87db4..a16f0f7bdf 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs @@ -1,6 +1,10 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using NBitcoin; +using NBitcoin.DataEncoders; +using Stratis.Bitcoin.Base; using Stratis.Bitcoin.Features.RPC; +using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.IntegrationTests.Common; using Stratis.Bitcoin.IntegrationTests.Common.EnvironmentMockUpHelpers; @@ -54,6 +58,221 @@ private Money CheckFunding(CoreNode node, Transaction fundedTransaction) return totalInputs - totalOutputs; } + [Fact] + public void CanCreateRawTransaction() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + }); + + Assert.NotNull(response.Transaction); + + Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + + Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Transaction.Outputs[0].Value); + } + } + + [Fact] + public void CanCreateRawTransactionWithDataOutput() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response.Transaction); + + Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + + Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Transaction.Outputs[0].Value); + + Assert.True(response.Transaction.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Transaction.Outputs[1].Value); + + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Transaction.Outputs[1].ScriptPubKey); + byte[] opReturn = extracted[0]; + + string opReturnHexString = Encoders.Hex.EncodeData(opReturn); + + Assert.Equal("0011223344", opReturnHexString); + } + } + + [Fact] + public void CanCreateRawTransactionWithDataOutputOnly() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response.Transaction); + + Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + + Assert.True(response.Transaction.Outputs[0].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Transaction.Outputs[0].Value); + + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Transaction.Outputs[0].ScriptPubKey); + byte[] opReturn = extracted[0]; + + string opReturnHexString = Encoders.Hex.EncodeData(opReturn); + + Assert.Equal("0011223344", opReturnHexString); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutInputs() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response.Transaction); + + Assert.Empty(response.Transaction.Inputs); + + Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Transaction.Outputs[0].Value); + + Assert.True(response.Transaction.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Transaction.Outputs[1].Value); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutOutputs() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0 + } + }, + new List>() + { + }); + + Assert.NotNull(response.Transaction); + + Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + + Assert.Empty(response.Transaction.Outputs); + } + } + + [Fact] + public void CanCreateRawTransactionWithoutInputsOrOutputs() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + }, + new List>() + { + }); + + Assert.NotNull(response.Transaction); + + Assert.Empty(response.Transaction.Inputs); + Assert.Empty(response.Transaction.Outputs); + } + } + [Fact] public void CanFundRawTransactionWithoutOptions() { @@ -213,5 +432,72 @@ public void CannotSignRawTransactionWithUnownedUtxo() Assert.Throws(() => node.CreateRPCClient().SignRawTransaction(funded.Transaction)); } } + + [Fact] + public void CanCreateFundAndSignRawTransaction() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + new KeyValuePair("data", "0011223344") + }); + + Assert.NotNull(response.Transaction); + + Assert.Empty(response.Transaction.Inputs); + + Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Transaction.Outputs[0].Value); + + Assert.True(response.Transaction.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Transaction.Outputs[1].Value); + + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Transaction.Outputs[1].ScriptPubKey); + byte[] opReturn = extracted[0]; + + string opReturnHexString = Encoders.Hex.EncodeData(opReturn); + + Assert.Equal("0011223344", opReturnHexString); + + FundRawTransactionResponse funded = node.CreateRPCClient().FundRawTransaction(response.Transaction); + + Money fee = CheckFunding(node, funded.Transaction); + + Assert.Equal(new Money(this.network.MinRelayTxFee), fee); + Assert.True(funded.ChangePos > -1); + + node.CreateRPCClient().WalletPassphrase("password", 600); + + Transaction signed = node.CreateRPCClient().SignRawTransaction(funded.Transaction); + + Assert.NotNull(signed); + Assert.NotEmpty(signed.Inputs); + + foreach (var input in signed.Inputs) + { + Assert.NotNull(input.ScriptSig); + + // Basic sanity check that the transaction has actually been signed. + // A segwit transaction would fail this check but we aren't checking that here. + // In any case, the mempool count test shows definitively if the transaction passes validation. + Assert.NotEqual(input.ScriptSig, Script.Empty); + } + + node.CreateRPCClient().SendRawTransaction(signed); + + TestBase.WaitLoop(() => node.CreateRPCClient().GetRawMempool().Length == 1); + } + } } } diff --git a/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs b/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs index f92aec0a2b..2ccec368a6 100644 --- a/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs +++ b/src/Stratis.Bitcoin/Utilities/JsonConverters/Serializer.cs @@ -1,5 +1,6 @@ using NBitcoin; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Stratis.Bitcoin.Utilities.JsonConverters @@ -37,7 +38,9 @@ public static T ToObject(string data, Network network = null) { Formatting = Formatting.Indented }; + RegisterFrontConverters(settings, network); + return JsonConvert.DeserializeObject(data, settings); } @@ -47,8 +50,22 @@ public static string ToString(T response, Network network = null) { Formatting = Formatting.Indented }; + RegisterFrontConverters(settings, network); + return JsonConvert.SerializeObject(response, settings); } + + public static JToken ToToken(T response, Network network = null) + { + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + + RegisterFrontConverters(settings, network); + + return JToken.FromObject(response, JsonSerializer.Create(settings)); + } } -} \ No newline at end of file +} From 718b3127ee0482e6edd53fed1df0dd3b81c94aef Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Thu, 29 Dec 2022 13:17:12 +0200 Subject: [PATCH 02/11] Better handling of Sequence field, and safer defaults --- .../Models/CreateRawTransactionModels.cs | 7 +- .../WalletRPCController.cs | 96 +++++++++++++++++-- .../RPC/RawTransactionTests.cs | 42 ++++++++ 3 files changed, 137 insertions(+), 8 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs index af33a9918c..b9b68e3ef4 100644 --- a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs +++ b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs @@ -12,10 +12,13 @@ public class CreateRawTransactionInput public uint256 TxId { get; set; } [JsonProperty(PropertyName = "vout")] - public int VOut { get; set; } + public uint VOut { get; set; } + /// + /// If not provided, the sequence in the created transaction will be set to uint.MaxValue by default. + /// [JsonProperty(PropertyName = "sequence")] - public int Sequence { get; set; } + public uint? Sequence { get; set; } } /// diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index 20896c7ebc..20a41bbe47 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -16,6 +16,7 @@ using Stratis.Bitcoin.Features.BlockStore; using Stratis.Bitcoin.Features.RPC; using Stratis.Bitcoin.Features.RPC.Exceptions; +using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Features.Wallet.Models; using Stratis.Bitcoin.Features.Wallet.Services; @@ -170,6 +171,73 @@ public object DumpPrivKey(string address) } } + [ActionName("createrawtransaction")] + [ActionDescription("Create a transaction spending the given inputs and creating new outputs.")] + public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) + { + try + { + if (locktime != 0) + throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Setting input locktime is currently not supported"); + + if (replaceable) + throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Replaceable transactions are currently not supported"); + + Transaction rawTx = this.Network.CreateTransaction(); + + foreach (CreateRawTransactionInput input in inputs) + { + rawTx.AddInput(new TxIn() + { + PrevOut = new OutPoint(input.TxId, input.VOut), + Sequence = input.Sequence ?? uint.MaxValue + // Since this is a raw unsigned transaction, ScriptSig and WitScript must not be populated. + }); + } + + bool dataSeen = false; + + foreach (CreateRawTransactionOutput output in outputs) + { + if (output.Key == "data") + { + if (dataSeen) + throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Only one data output can be specified"); + + dataSeen = true; + + byte[] data = Encoders.Hex.DecodeData(output.Value); + + rawTx.AddOutput(new TxOut + { + ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(new []{ data }), + Value = 0 + }); + + continue; + } + + var address = BitcoinAddress.Create(output.Key, this.Network); + var amount = Money.Parse(output.Value); + + rawTx.AddOutput(new TxOut + { + ScriptPubKey = address.ScriptPubKey, + Value = amount + }); + } + + return Task.FromResult(new CreateRawTransactionResponse() + { + Transaction = rawTx + }); + } + catch (WalletException exception) + { + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); + } + } + [ActionName("fundrawtransaction")] [ActionDescription("Add inputs to a transaction until it has enough in value to meet its out value. Note that signing is performed separately.")] public Task FundRawTransactionAsync(string rawHex, FundRawTransactionOptions options = null, bool? isWitness = null) @@ -219,14 +287,30 @@ public Task FundRawTransactionAsync(string rawHex, F Sign = false }; - context.Recipients.AddRange(rawTx.Outputs - .Select(s => new Recipient + foreach (TxOut output in rawTx.Outputs) + { + // We can't add OP_RETURN outputs as recipients, they need to be added explicitly to the context's provided fields. + if (output.ScriptPubKey.IsUnspendable) { - ScriptPubKey = s.ScriptPubKey, - Amount = s.Value, - SubtractFeeFromAmount = false // TODO: Do we properly support only subtracting the fee from particular recipients? - })); + byte[][] data = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(output.ScriptPubKey); + + // This encoder uses 8-bit ASCII, so it is safe to use it on arbitrary bytes. + string opReturn = Encoders.ASCII.EncodeData(data[0]); + + context.OpReturnData = opReturn; + context.OpReturnAmount = output.Value; + + continue; + } + context.Recipients.Add(new Recipient + { + ScriptPubKey = output.ScriptPubKey, + Amount = output.Value, + SubtractFeeFromAmount = false // TODO: Do we properly support only subtracting the fee from particular recipients? + }); + } + context.AllowOtherInputs = true; foreach (TxIn transactionInput in rawTx.Inputs) diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs index a16f0f7bdf..e1a8608a3d 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs @@ -90,6 +90,48 @@ public void CanCreateRawTransaction() Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + Assert.Equal((Sequence)uint.MaxValue, response.Transaction.Inputs[0].Sequence); + + Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Transaction.Outputs[0].Value); + } + } + + [Fact] + public void CanCreateRawTransactionWithNonDefaultSequence() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); + + // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): + uint256 txHash = node.GetTip().HashBlock; + + BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); + var amount = new Money(0.00012345m, MoneyUnit.BTC); + + CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = txHash, + VOut = 0, + Sequence = 5 + } + }, + new List>() + { + new KeyValuePair(recipient.ToString(), amount.ToString()), + }); + + Assert.NotNull(response.Transaction); + + Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + + Assert.Equal((Sequence)5, response.Transaction.Inputs[0].Sequence); + Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); Assert.Equal(amount, response.Transaction.Outputs[0].Value); } From 615ddd28f7747caeec04efe867cac3c02013ba35 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Fri, 6 Jan 2023 10:03:25 +0200 Subject: [PATCH 03/11] Fix case where fundrawtransaction is called for the watch-only account --- .../Models/CreateRawTransactionModels.cs | 5 +- .../WalletRPCController.cs | 29 +++++- .../API/ApiSteps.cs | 2 +- .../RPC/RawTransactionTests.cs | 91 ++++++++++++++++++- 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs index b9b68e3ef4..8e5254af98 100644 --- a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs +++ b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs @@ -37,9 +37,6 @@ public class CreateRawTransactionOutput public class CreateRawTransactionResponse { [JsonProperty(PropertyName = "hex")] - public Transaction Transaction - { - get; set; - } + public Transaction Transaction { get; set; } } } diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index 20a41bbe47..66d6422611 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -252,7 +252,10 @@ public Task FundRawTransactionAsync(string rawHex, F // If this was not done the transaction deserialisation would attempt to use witness deserialisation and the transaction data would get mangled. rawTx.FromBytes(Encoders.Hex.DecodeData(rawHex), this.Network.Consensus.ConsensusFactory, ProtocolVersion.WITNESS_VERSION - 1); - WalletAccountReference account = this.GetWalletAccountReference(); + // It is difficult to combine multiple accounts as a source of funds given the existing transaction building logic. + // We would need to essentially run the process multiple times and combine the results if the non-watchonly account fails to provide sufficient funds. + // With the class of user expected to use this functionality it makes more sense in the interim to use the watchonly account exclusively if told to do so. + WalletAccountReference account = (options?.IncludeWatching ?? false) ? this.GetWatchOnlyWalletAccountReference() : this.GetWalletAccountReference(); HdAddress changeAddress = null; @@ -262,10 +265,31 @@ public Task FundRawTransactionAsync(string rawHex, F if (options?.ChangeAddress != null) { - changeAddress = this.walletManager.GetAllAccounts().SelectMany(a => a.GetCombinedAddresses()).FirstOrDefault(a => a.Address == options?.ChangeAddress); + if (options?.IncludeWatching ?? false) + { + Script changeAddressScriptPubKey = BitcoinAddress.Create(options.ChangeAddress).ScriptPubKey; + bool segwit = changeAddressScriptPubKey.IsScriptType(ScriptType.Witness); + + // For the watch-only account we need to construct a dummy HdAddress, as the wallet manager may not be able to find the address otherwise. + changeAddress = new HdAddress() + { + Address = !segwit ? options.ChangeAddress : null, + ScriptPubKey = changeAddressScriptPubKey, + Bech32Address = segwit ? options.ChangeAddress : null + }; + } + else + { + changeAddress = this.walletManager.GetAllAccounts().SelectMany(a => a.GetCombinedAddresses()).FirstOrDefault(a => a.Address == options.ChangeAddress); + } } else { + if (options?.IncludeWatching ?? false) + { + throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, "A change address needs to be specified when using watch-only funds"); + } + changeAddress = this.walletManager.GetUnusedChangeAddress(account); } @@ -284,6 +308,7 @@ public Task FundRawTransactionAsync(string rawHex, F Shuffle = false, UseSegwitChangeAddress = changeAddress != null && (options?.ChangeAddress == changeAddress.Bech32Address), + // Signing is deferred until the signrawtransaction RPC is called. Sign = false }; diff --git a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs index 7fc33e1d6c..8b207bebb9 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/API/ApiSteps.cs @@ -419,7 +419,7 @@ private void a_full_list_of_available_commands_is_returned() { var commands = JsonDataSerializer.Instance.Deserialize>(this.responseText); - commands.Count.Should().Be(38); + commands.Count.Should().Be(39); } private void status_information_is_returned() diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs index e1a8608a3d..e12d5cea6e 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NBitcoin; using NBitcoin.DataEncoders; -using Stratis.Bitcoin.Base; using Stratis.Bitcoin.Features.RPC; using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.Features.Wallet; @@ -412,6 +412,93 @@ public void CanFundRawTransactionWithChangePositionSpecified() } } + [Fact] + public void CanFundRawTransactionWithIncludeWatchingSpecified() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode nodeWithWallet = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); + + UnspentCoin[] unspent = nodeWithWallet.CreateRPCClient().ListUnspent(10, Int32.MaxValue); + + string pubKey = nodeWithWallet.FullNode.WalletManager().GetPubKey("mywallet", unspent[0].Address.ToString()); + + // Watch-only wallet node. + // Need a wallet to exist for importpubkey to work, or alternatively a default wallet needs to be configured. + var configParams = new NodeConfigParameters + { + { "-defaultwalletname", "test" }, + { "-defaultwalletpassword", "testpassword" }, + { "-unlockdefaultwallet", "1" } + }; + + CoreNode nodeWithWatchOnly = builder.CreateStratisPosNode(this.network, configParameters: configParams).Start(); + + nodeWithWatchOnly.CreateRPCClient().ImportPubKey(pubKey); + + TestHelper.ConnectAndSync(nodeWithWallet, nodeWithWatchOnly); + + var tx = this.network.CreateTransaction(); + var dest = new Key().ScriptPubKey; + tx.Outputs.Add(new TxOut(Money.Coins(1.0m), dest)); + + string changeAddress = new Key().PubKey.GetAddress(this.network).ToString(); + + var options = new FundRawTransactionOptions() + { + ChangeAddress = BitcoinAddress.Create(changeAddress, this.network).ToString(), + IncludeWatching = true + }; + + FundRawTransactionResponse funded = nodeWithWatchOnly.CreateRPCClient().FundRawTransaction(tx, options); + + Money fee = CheckFunding(nodeWithWatchOnly, funded.Transaction); + + Assert.Equal(new Money(this.network.MinRelayTxFee), fee); + Assert.True(funded.ChangePos > -1); + } + } + + [Fact] + public void CannotFundRawTransactionWithIncludeWatchingSpecifiedAndNoChangeAddress() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode nodeWithWallet = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); + + UnspentCoin[] unspent = nodeWithWallet.CreateRPCClient().ListUnspent(10, Int32.MaxValue); + + string pubKey = nodeWithWallet.FullNode.WalletManager().GetPubKey("mywallet", unspent[0].Address.ToString()); + + // Watch-only wallet node. + // Need a wallet to exist for importpubkey to work, or alternatively a default wallet needs to be configured. + var configParams = new NodeConfigParameters + { + { "-defaultwalletname", "test" }, + { "-defaultwalletpassword", "testpassword" }, + { "-unlockdefaultwallet", "1" } + }; + + CoreNode nodeWithWatchOnly = builder.CreateStratisPosNode(this.network, configParameters: configParams).Start(); + + nodeWithWatchOnly.CreateRPCClient().ImportPubKey(pubKey); + + TestHelper.ConnectAndSync(nodeWithWallet, nodeWithWatchOnly); + + var tx = this.network.CreateTransaction(); + var dest = new Key().ScriptPubKey; + tx.Outputs.Add(new TxOut(Money.Coins(1.0m), dest)); + + var options = new FundRawTransactionOptions() + { + ChangeAddress = null, + IncludeWatching = true + }; + + Assert.Throws(() => nodeWithWatchOnly.CreateRPCClient().FundRawTransaction(tx, options)); + } + } + [Fact] public void CanSignRawTransaction() { From ec0876dc553a43fe6c7b89926f60a4449d96eddc Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Mon, 16 Jan 2023 22:21:47 +0200 Subject: [PATCH 04/11] Add protocol version overloads for CreateTransaction --- src/NBitcoin/BlockStake.cs | 8 ++++++++ src/NBitcoin/ConsensusFactory.cs | 12 ++++++++++++ src/NBitcoin/Network.cs | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/src/NBitcoin/BlockStake.cs b/src/NBitcoin/BlockStake.cs index cb56134e95..902cbcbf73 100644 --- a/src/NBitcoin/BlockStake.cs +++ b/src/NBitcoin/BlockStake.cs @@ -263,6 +263,14 @@ public override Transaction CreateTransaction(string hex) return new PosTransaction(hex); } + /// + public virtual Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + { + var transaction = new PosTransaction(); + transaction.FromBytes(Encoders.Hex.DecodeData(hex), protocolVersion); + return transaction; + } + /// public override Transaction CreateTransaction(byte[] bytes) { diff --git a/src/NBitcoin/ConsensusFactory.cs b/src/NBitcoin/ConsensusFactory.cs index a2048d1a54..b9cd6d384a 100644 --- a/src/NBitcoin/ConsensusFactory.cs +++ b/src/NBitcoin/ConsensusFactory.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Reflection; using NBitcoin.DataEncoders; +using NBitcoin.Protocol; namespace NBitcoin { @@ -178,6 +179,17 @@ public virtual Transaction CreateTransaction(string hex) return transaction; } + /// + /// Create a instance from a hex string representation. + /// Allows the protocol version to be overridden from the node's default. + /// + public virtual Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + { + var transaction = new Transaction(); + transaction.FromBytes(Encoders.Hex.DecodeData(hex), protocolVersion); + return transaction; + } + /// /// Create a instance from a byte array representation. /// diff --git a/src/NBitcoin/Network.cs b/src/NBitcoin/Network.cs index eb69a200d1..17603173e8 100644 --- a/src/NBitcoin/Network.cs +++ b/src/NBitcoin/Network.cs @@ -984,6 +984,11 @@ public Transaction CreateTransaction(string hex) return this.Consensus.ConsensusFactory.CreateTransaction(hex); } + public Transaction CreateTransaction(string hex, ProtocolVersion protocolVersion) + { + return this.Consensus.ConsensusFactory.CreateTransaction(hex, protocolVersion); + } + public Transaction CreateTransaction(byte[] bytes) { return this.Consensus.ConsensusFactory.CreateTransaction(bytes); From d67f770186bcd2c7c73aaff0979f4af7f809045f Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Mon, 16 Jan 2023 22:22:50 +0200 Subject: [PATCH 05/11] Check that createrawtransaction RPC client works for SFN/bitcoind --- .../RPC/RpcBitcoinMutableTests.cs | 75 +++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs index 4f19c12bd1..79f0ec61e8 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs @@ -7,12 +7,10 @@ using NBitcoin; using Newtonsoft.Json.Linq; using Stratis.Bitcoin.Features.RPC; +using Stratis.Bitcoin.Features.RPC.Models; using Stratis.Bitcoin.IntegrationTests.Common; using Stratis.Bitcoin.IntegrationTests.Common.EnvironmentMockUpHelpers; -using Stratis.Bitcoin.Networks; -using Stratis.Bitcoin.Networks.Deployments; using Stratis.Bitcoin.Tests.Common; -using Stratis.Bitcoin.Utilities.Extensions; using Xunit; namespace Stratis.Bitcoin.IntegrationTests.RPC @@ -160,6 +158,70 @@ public void CanGetGenesisFromRPC() } } + [Fact] + public void CanCreateRawTransactionWithInput() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateBitcoinCoreNode(version: "0.18.0", useNewConfigStyle: true).Start(); + + CoreNode sfn = builder.CreateStratisPowNode(this.regTest).WithWallet().Start(); + + TestHelper.ConnectAndSync(node, sfn); + + RPCClient rpcClient = node.CreateRPCClient(); + RPCClient sfnRpc = sfn.CreateRPCClient(); + + // Need one block per node so they can each fund a transaction. + rpcClient.Generate(1); + + TestHelper.ConnectAndSync(node, sfn); + + sfnRpc.Generate(1); + + TestHelper.ConnectAndSync(node, sfn); + + // And then enough blocks mined on top for the coinbases to mature. + rpcClient.Generate(101); + + TestHelper.ConnectAndSync(node, sfn); + + Key dest = new Key(); + + var tx = rpcClient.CreateRawTransaction(new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = uint256.One, + VOut = 2 + } + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx); + + var tx2 = sfnRpc.CreateRawTransaction(new CreateRawTransactionInput[] + { + new CreateRawTransactionInput() + { + TxId = uint256.One, + VOut = 2 + } + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx2); + + Assert.True(tx.GetHash() == tx2.GetHash()); + } + } + [Fact] public void CanSignRawTransaction() { @@ -188,8 +250,11 @@ public void CanSignRawTransaction() TestHelper.ConnectAndSync(node, sfn); - var tx = new Transaction(); - tx.Outputs.Add(new TxOut(Money.Coins(1.0m), new Key())); + var tx = rpcClient.CreateRawTransaction(new CreateRawTransactionInput[] {}, new List>() + { + new KeyValuePair((new Key()).PubKey.GetAddress(this.regTest).ToString(), "1") + }); + FundRawTransactionResponse funded = rpcClient.FundRawTransaction(tx); // signrawtransaction was removed in 0.18. So just use its equivalent so that we can test SFN's ability to call signrawtransaction. From f77a9f53dfe60596f1c53eb8fc31bf7f9cd7525f Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Mon, 16 Jan 2023 22:24:37 +0200 Subject: [PATCH 06/11] Modify createrawtransaction RPC to return string response --- .../Models/CreateRawTransactionModels.cs | 6 - .../RPCClient.Wallet.cs | 11 +- .../WalletRPCController.cs | 11 +- .../RPC/RawTransactionTests.cs | 110 +++++++++--------- 4 files changed, 62 insertions(+), 76 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs index 8e5254af98..d0366e7828 100644 --- a/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs +++ b/src/Stratis.Bitcoin.Features.RPC/Models/CreateRawTransactionModels.cs @@ -33,10 +33,4 @@ public class CreateRawTransactionOutput [JsonProperty] public string Value { get; set; } } - - public class CreateRawTransactionResponse - { - [JsonProperty(PropertyName = "hex")] - public Transaction Transaction { get; set; } - } } diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs b/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs index 5bb95d1d4d..4f9bd73b78 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCClient.Wallet.cs @@ -137,12 +137,12 @@ public IEnumerable GetAddressesByAccount(string account) return response.Result.Select(t => this.Network.Parse((string)t)); } - public CreateRawTransactionResponse CreateRawTransaction(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) + public Transaction CreateRawTransaction(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) { return CreateRawTransactionAsync(inputs, outputs, locktime, replaceable).GetAwaiter().GetResult(); } - public async Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) + public async Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, List> outputs, int locktime = 0, bool replaceable = false) { var jOutputs = new JArray(); @@ -167,12 +167,7 @@ public async Task CreateRawTransactionAsync(Create RPCResponse response = await SendCommandAsync(RPCOperations.createrawtransaction, inputs, jOutputs, locktime, replaceable).ConfigureAwait(false); - var r = (JObject)response.Result; - - return new CreateRawTransactionResponse() - { - Transaction = this.network.CreateTransaction(r["hex"].Value()) - }; + return this.network.CreateTransaction(response.ResultString, ProtocolVersion.WITNESS_VERSION - 1); } public FundRawTransactionResponse FundRawTransaction(Transaction transaction, FundRawTransactionOptions options = null, bool? isWitness = null) diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index 66d6422611..9442a3dc59 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -173,7 +173,7 @@ public object DumpPrivKey(string address) [ActionName("createrawtransaction")] [ActionDescription("Create a transaction spending the given inputs and creating new outputs.")] - public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) + public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) { try { @@ -182,7 +182,7 @@ public Task CreateRawTransactionAsync(CreateRawTra if (replaceable) throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Replaceable transactions are currently not supported"); - + Transaction rawTx = this.Network.CreateTransaction(); foreach (CreateRawTransactionInput input in inputs) @@ -210,7 +210,7 @@ public Task CreateRawTransactionAsync(CreateRawTra rawTx.AddOutput(new TxOut { - ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(new []{ data }), + ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(new[] { data }), Value = 0 }); @@ -227,10 +227,7 @@ public Task CreateRawTransactionAsync(CreateRawTra }); } - return Task.FromResult(new CreateRawTransactionResponse() - { - Transaction = rawTx - }); + return Task.FromResult(rawTx.ToHex(ProtocolVersion.WITNESS_VERSION - 1)); } catch (WalletException exception) { diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs index e12d5cea6e..f6f0edf568 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RawTransactionTests.cs @@ -71,7 +71,7 @@ public void CanCreateRawTransaction() BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); var amount = new Money(0.00012345m, MoneyUnit.BTC); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { new CreateRawTransactionInput() @@ -85,15 +85,15 @@ public void CanCreateRawTransaction() new KeyValuePair(recipient.ToString(), amount.ToString()), }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); - Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); - Assert.Equal((Sequence)uint.MaxValue, response.Transaction.Inputs[0].Sequence); + Assert.Equal((Sequence)uint.MaxValue, response.Inputs[0].Sequence); - Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); - Assert.Equal(amount, response.Transaction.Outputs[0].Value); + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); } } @@ -110,7 +110,7 @@ public void CanCreateRawTransactionWithNonDefaultSequence() BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); var amount = new Money(0.00012345m, MoneyUnit.BTC); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { new CreateRawTransactionInput() @@ -125,15 +125,15 @@ public void CanCreateRawTransactionWithNonDefaultSequence() new KeyValuePair(recipient.ToString(), amount.ToString()), }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); - Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); - Assert.Equal((Sequence)5, response.Transaction.Inputs[0].Sequence); + Assert.Equal((Sequence)5, response.Inputs[0].Sequence); - Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); - Assert.Equal(amount, response.Transaction.Outputs[0].Value); + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); } } @@ -150,7 +150,7 @@ public void CanCreateRawTransactionWithDataOutput() BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); var amount = new Money(0.00012345m, MoneyUnit.BTC); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { new CreateRawTransactionInput() @@ -165,18 +165,18 @@ public void CanCreateRawTransactionWithDataOutput() new KeyValuePair("data", "0011223344") }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); - Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); - Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); - Assert.Equal(amount, response.Transaction.Outputs[0].Value); + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); - Assert.True(response.Transaction.Outputs[1].ScriptPubKey.IsUnspendable); - Assert.Equal(0, response.Transaction.Outputs[1].Value); + Assert.True(response.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[1].Value); - byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Transaction.Outputs[1].ScriptPubKey); + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Outputs[1].ScriptPubKey); byte[] opReturn = extracted[0]; string opReturnHexString = Encoders.Hex.EncodeData(opReturn); @@ -198,7 +198,7 @@ public void CanCreateRawTransactionWithDataOutputOnly() BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); var amount = new Money(0.00012345m, MoneyUnit.BTC); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { new CreateRawTransactionInput() @@ -212,15 +212,15 @@ public void CanCreateRawTransactionWithDataOutputOnly() new KeyValuePair("data", "0011223344") }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); - Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); - Assert.True(response.Transaction.Outputs[0].ScriptPubKey.IsUnspendable); - Assert.Equal(0, response.Transaction.Outputs[0].Value); + Assert.True(response.Outputs[0].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[0].Value); - byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Transaction.Outputs[0].ScriptPubKey); + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Outputs[0].ScriptPubKey); byte[] opReturn = extracted[0]; string opReturnHexString = Encoders.Hex.EncodeData(opReturn); @@ -239,7 +239,7 @@ public void CanCreateRawTransactionWithoutInputs() BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); var amount = new Money(0.00012345m, MoneyUnit.BTC); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { }, @@ -249,15 +249,15 @@ public void CanCreateRawTransactionWithoutInputs() new KeyValuePair("data", "0011223344") }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Empty(response.Transaction.Inputs); + Assert.Empty(response.Inputs); - Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); - Assert.Equal(amount, response.Transaction.Outputs[0].Value); + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); - Assert.True(response.Transaction.Outputs[1].ScriptPubKey.IsUnspendable); - Assert.Equal(0, response.Transaction.Outputs[1].Value); + Assert.True(response.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[1].Value); } } @@ -271,7 +271,7 @@ public void CanCreateRawTransactionWithoutOutputs() // Obtain an arbitrary uint256 to use as a 'transaction' hash (this transaction never needs to exist): uint256 txHash = node.GetTip().HashBlock; - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { new CreateRawTransactionInput() @@ -284,12 +284,12 @@ public void CanCreateRawTransactionWithoutOutputs() { }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Equal(txHash, response.Transaction.Inputs[0].PrevOut.Hash); - Assert.Equal(0U, response.Transaction.Inputs[0].PrevOut.N); + Assert.Equal(txHash, response.Inputs[0].PrevOut.Hash); + Assert.Equal(0U, response.Inputs[0].PrevOut.N); - Assert.Empty(response.Transaction.Outputs); + Assert.Empty(response.Outputs); } } @@ -300,7 +300,7 @@ public void CanCreateRawTransactionWithoutInputsOrOutputs() { CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest10Miner).Start(); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { }, @@ -308,10 +308,10 @@ public void CanCreateRawTransactionWithoutInputsOrOutputs() { }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Empty(response.Transaction.Inputs); - Assert.Empty(response.Transaction.Outputs); + Assert.Empty(response.Inputs); + Assert.Empty(response.Outputs); } } @@ -572,7 +572,7 @@ public void CanCreateFundAndSignRawTransaction() BitcoinAddress recipient = new Key().PubKey.Hash.GetAddress(node.FullNode.Network); var amount = new Money(0.00012345m, MoneyUnit.BTC); - CreateRawTransactionResponse response = node.CreateRPCClient().CreateRawTransaction( + Transaction response = node.CreateRPCClient().CreateRawTransaction( new CreateRawTransactionInput[] { }, @@ -582,24 +582,24 @@ public void CanCreateFundAndSignRawTransaction() new KeyValuePair("data", "0011223344") }); - Assert.NotNull(response.Transaction); + Assert.NotNull(response); - Assert.Empty(response.Transaction.Inputs); + Assert.Empty(response.Inputs); - Assert.Equal(recipient.ScriptPubKey, response.Transaction.Outputs[0].ScriptPubKey); - Assert.Equal(amount, response.Transaction.Outputs[0].Value); + Assert.Equal(recipient.ScriptPubKey, response.Outputs[0].ScriptPubKey); + Assert.Equal(amount, response.Outputs[0].Value); - Assert.True(response.Transaction.Outputs[1].ScriptPubKey.IsUnspendable); - Assert.Equal(0, response.Transaction.Outputs[1].Value); + Assert.True(response.Outputs[1].ScriptPubKey.IsUnspendable); + Assert.Equal(0, response.Outputs[1].Value); - byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Transaction.Outputs[1].ScriptPubKey); + byte[][] extracted = TxNullDataTemplate.Instance.ExtractScriptPubKeyParameters(response.Outputs[1].ScriptPubKey); byte[] opReturn = extracted[0]; string opReturnHexString = Encoders.Hex.EncodeData(opReturn); Assert.Equal("0011223344", opReturnHexString); - FundRawTransactionResponse funded = node.CreateRPCClient().FundRawTransaction(response.Transaction); + FundRawTransactionResponse funded = node.CreateRPCClient().FundRawTransaction(response); Money fee = CheckFunding(node, funded.Transaction); From 88c7c08198132c34c442576c711323a6549668e3 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Mon, 16 Jan 2023 22:28:33 +0200 Subject: [PATCH 07/11] Fix RPC middleware to correctly handle non-JSON RPC responses --- .../RPCMiddleware.cs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs b/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs index 97be3070fd..c975ae35d1 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; +using NBitcoin; using NBitcoin.DataEncoders; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -72,12 +73,12 @@ public async Task InvokeAsync(HttpContext httpContext) if (token is JArray) { // Batch request, invoke each request and accumulate responses into JArray. - response = await this.InvokeBatchAsync(httpContext, token as JArray); + response = await this.InvokeBatchAsync(httpContext, token as JArray).ConfigureAwait(false); } else if (token is JObject) { // Single request, invoke single request and return single response object. - response = await this.InvokeSingleAsync(httpContext, token as JObject); + response = await this.InvokeSingleAsync(httpContext, token as JObject).ConfigureAwait(false); if (response == null) response = JValue.CreateNull(); @@ -248,11 +249,29 @@ private async Task InvokeSingleAsync(HttpContext httpContext, JObject r responseMemoryStream.Position = 0; using (var streamReader = new StreamReader(responseMemoryStream)) - using (var textReader = new JsonTextReader(streamReader)) { - // Ensure floats are parsed as decimals and not as doubles. - textReader.FloatParseHandling = FloatParseHandling.Decimal; - response = await JObject.LoadAsync(textReader); + string jsonString = await streamReader.ReadToEndAsync().ConfigureAwait(false); + + // This is an extremely ugly hack, but not every RPC response body is actually valid JSON, so we are forced to check. + if (!jsonString.Contains("{")) + { + response = new JObject(); + response["result"] = jsonString; + + if (requestObj.ContainsKey("id")) + response["id"] = requestObj["id"]; + + return response; + } + + responseMemoryStream.Position = 0; + + using (var textReader = new JsonTextReader(streamReader)) + { + // Ensure floats are parsed as decimals and not as doubles. + textReader.FloatParseHandling = FloatParseHandling.Decimal; + response = await JObject.LoadAsync(textReader).ConfigureAwait(false); + } } } catch (Exception ex) @@ -266,9 +285,9 @@ private async Task InvokeSingleAsync(HttpContext httpContext, JObject r // Ensure floats are parsed as decimals and not as doubles. textReader.FloatParseHandling = FloatParseHandling.Decimal; - string val = streamReader.ReadToEnd(); + string val = await streamReader.ReadToEndAsync().ConfigureAwait(false); context.Response.Body.Position = 0; - response = await JObject.LoadAsync(textReader); + response = await JObject.LoadAsync(textReader).ConfigureAwait(false); } } From 433d88a18f2106ba01fe6ef3ddd63823591dc8cb Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Mon, 16 Jan 2023 23:01:55 +0200 Subject: [PATCH 08/11] Remove accidental duplication --- .../WalletRPCController.cs | 67 ------------------- 1 file changed, 67 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index aa8f31ee52..9442a3dc59 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -235,73 +235,6 @@ public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs } } - [ActionName("createrawtransaction")] - [ActionDescription("Create a transaction spending the given inputs and creating new outputs.")] - public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) - { - try - { - if (locktime != 0) - throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Setting input locktime is currently not supported"); - - if (replaceable) - throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Replaceable transactions are currently not supported"); - - Transaction rawTx = this.Network.CreateTransaction(); - - foreach (CreateRawTransactionInput input in inputs) - { - rawTx.AddInput(new TxIn() - { - PrevOut = new OutPoint(input.TxId, input.VOut), - Sequence = input.Sequence ?? uint.MaxValue - // Since this is a raw unsigned transaction, ScriptSig and WitScript must not be populated. - }); - } - - bool dataSeen = false; - - foreach (CreateRawTransactionOutput output in outputs) - { - if (output.Key == "data") - { - if (dataSeen) - throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "Only one data output can be specified"); - - dataSeen = true; - - byte[] data = Encoders.Hex.DecodeData(output.Value); - - rawTx.AddOutput(new TxOut - { - ScriptPubKey = TxNullDataTemplate.Instance.GenerateScriptPubKey(new []{ data }), - Value = 0 - }); - - continue; - } - - var address = BitcoinAddress.Create(output.Key, this.Network); - var amount = Money.Parse(output.Value); - - rawTx.AddOutput(new TxOut - { - ScriptPubKey = address.ScriptPubKey, - Value = amount - }); - } - - return Task.FromResult(new CreateRawTransactionResponse() - { - Transaction = rawTx - }); - } - catch (WalletException exception) - { - throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); - } - } - [ActionName("fundrawtransaction")] [ActionDescription("Add inputs to a transaction until it has enough in value to meet its out value. Note that signing is performed separately.")] public Task FundRawTransactionAsync(string rawHex, FundRawTransactionOptions options = null, bool? isWitness = null) From 26fa0a0ccd95e7d4b65e56a07464a00f6541e25e Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Tue, 17 Jan 2023 09:01:26 +0200 Subject: [PATCH 09/11] Fix test --- .../RPC/RpcBitcoinMutableTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs index 79f0ec61e8..b00619abb6 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs @@ -218,6 +218,9 @@ public void CanCreateRawTransactionWithInput() Assert.NotNull(tx2); + // TODO: Need to verify our adherence to BIP68 (https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki#specification). But in the meantime the raw transaction we produce is identical to bitcoind except for the version field. + tx2.Version = 2; + Assert.True(tx.GetHash() == tx2.GetHash()); } } From cf77153d858107b452e1112c925af27512fa6e94 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Wed, 18 Jan 2023 08:36:27 +0200 Subject: [PATCH 10/11] Revert middleware change and add additional test --- .../RPCMiddleware.cs | 16 ------- .../WalletRPCController.cs | 4 +- .../RPC/RpcBitcoinMutableTests.cs | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs b/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs index c975ae35d1..e974e00c56 100644 --- a/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs +++ b/src/Stratis.Bitcoin.Features.RPC/RPCMiddleware.cs @@ -250,22 +250,6 @@ private async Task InvokeSingleAsync(HttpContext httpContext, JObject r responseMemoryStream.Position = 0; using (var streamReader = new StreamReader(responseMemoryStream)) { - string jsonString = await streamReader.ReadToEndAsync().ConfigureAwait(false); - - // This is an extremely ugly hack, but not every RPC response body is actually valid JSON, so we are forced to check. - if (!jsonString.Contains("{")) - { - response = new JObject(); - response["result"] = jsonString; - - if (requestObj.ContainsKey("id")) - response["id"] = requestObj["id"]; - - return response; - } - - responseMemoryStream.Position = 0; - using (var textReader = new JsonTextReader(streamReader)) { // Ensure floats are parsed as decimals and not as doubles. diff --git a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs index 9442a3dc59..5b3301760f 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/WalletRPCController.cs @@ -173,7 +173,7 @@ public object DumpPrivKey(string address) [ActionName("createrawtransaction")] [ActionDescription("Create a transaction spending the given inputs and creating new outputs.")] - public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) + public async Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs, CreateRawTransactionOutput[] outputs, int locktime = 0, bool replaceable = false) { try { @@ -227,7 +227,7 @@ public Task CreateRawTransactionAsync(CreateRawTransactionInput[] inputs }); } - return Task.FromResult(rawTx.ToHex(ProtocolVersion.WITNESS_VERSION - 1)); + return new TransactionBriefModel(rawTx); } catch (WalletException exception) { diff --git a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs index b00619abb6..84bb33d83a 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/RPC/RpcBitcoinMutableTests.cs @@ -225,6 +225,51 @@ public void CanCreateRawTransactionWithInput() } } + [Fact] + public void CanCreateRawTransactionWithoutInput() + { + using (NodeBuilder builder = NodeBuilder.Create(this)) + { + CoreNode node = builder.CreateBitcoinCoreNode(version: "0.18.0", useNewConfigStyle: true).Start(); + + CoreNode sfn = builder.CreateStratisPowNode(this.regTest).WithWallet().Start(); + + TestHelper.ConnectAndSync(node, sfn); + + RPCClient rpcClient = node.CreateRPCClient(); + RPCClient sfnRpc = sfn.CreateRPCClient(); + + TestHelper.ConnectAndSync(node, sfn); + + Key dest = new Key(); + + var tx = rpcClient.CreateRawTransaction(new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx); + + var tx2 = sfnRpc.CreateRawTransaction(new CreateRawTransactionInput[] + { + }, + new List>() + { + new KeyValuePair(dest.PubKey.GetAddress(this.regTest).ToString(), "1") + }); + + Assert.NotNull(tx2); + + // TODO: Need to verify our adherence to BIP68 (https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki#specification). But in the meantime the raw transaction we produce is identical to bitcoind except for the version field. + tx2.Version = 2; + + Assert.True(tx.GetHash() == tx2.GetHash()); + } + } + [Fact] public void CanSignRawTransaction() { From aa343ae2b81b9ebc8b275400c7bd2739f7caefd8 Mon Sep 17 00:00:00 2001 From: Kevin Loubser Date: Fri, 20 Jan 2023 17:52:42 +0200 Subject: [PATCH 11/11] Add P2WSH handling to wallet database --- .../External/ITransactionsToLists.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs b/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs index 4213121851..00a324264b 100644 --- a/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs +++ b/src/Stratis.Features.SQLiteWalletRepository/External/ITransactionsToLists.cs @@ -55,7 +55,19 @@ internal IEnumerable GetDestinations(Script redeemScript) case TxOutType.TX_SEGWIT: TxDestination txDestination = PayToWitTemplate.Instance.ExtractScriptPubKeyParameters(this.network, redeemScript); if (txDestination != null) - yield return new KeyId(txDestination.ToBytes()); + { + if (txDestination.ToBytes().Length == 20) + { + yield return PayToWitPubKeyHashTemplate.Instance.ExtractScriptPubKeyParameters(this.network, redeemScript); + } + else if (txDestination.ToBytes().Length == 32) + { + yield return PayToWitScriptHashTemplate.Instance.ExtractScriptPubKeyParameters(this.network, redeemScript); + } + + // This should not happen, segwit scripts should generally only have one of the two valid lengths. + yield return txDestination; + } break; default: if (this.scriptAddressReader is ScriptDestinationReader scriptDestinationReader)