diff --git a/src/Stratis.Bitcoin.Features.SmartContracts.Tests/SmartContractTransactionServiceTests.cs b/src/Stratis.Bitcoin.Features.SmartContracts.Tests/SmartContractTransactionServiceTests.cs index d1828ae954..8a1d1c813d 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts.Tests/SmartContractTransactionServiceTests.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts.Tests/SmartContractTransactionServiceTests.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using CSharpFunctionalExtensions; using Microsoft.Extensions.Logging; using Moq; using NBitcoin; +using NBitcoin.DataEncoders; using Stratis.Bitcoin.Configuration.Logging; using Stratis.Bitcoin.Features.SmartContracts.Models; using Stratis.Bitcoin.Features.SmartContracts.Wallet; @@ -974,5 +976,107 @@ public void BuildFeeEstimationContext_Recipient_Is_Not_P2PKH() ))); } */ + + [Fact] + public void CanPassSignatures() + { + const int utxoIndex = 0; + uint256 utxoId = uint256.Zero; + uint256 utxoIdUnused = uint256.One; + string senderAddress = uint160.Zero.ToBase58Address(this.network); + string contractAddress = uint160.One.ToBase58Address(this.network); + + var key1 = new Key(); + var key2 = new Key(); + var msg = "This is a test challenge"; + var sig1 = key1.SignMessage(msg); + var sig2 = key2.SignMessage(msg); + + var request = new BuildCallContractTransactionRequest + { + Amount = "0", + AccountName = "account 0", + ContractAddress = contractAddress, + FeeAmount = "0.01", + GasLimit = 100_000, + GasPrice = 100, + MethodName = "TestMethod", + WalletName = "wallet", + Password = "password", + Sender = senderAddress, + Outpoints = new List + { + new OutpointRequest + { + Index = utxoIndex, + TransactionId = utxoId.ToString() + }, + }, + Parameters = new[] { "SIG#" }, + Signatures = new[] { sig1, sig2 } + }; + + this.walletManager.Setup(x => x.GetAddressBalance(request.Sender)) + .Returns(new AddressBalance + { + Address = senderAddress, + AmountConfirmed = new Money(100, MoneyUnit.BTC) + }); + + this.walletManager.Setup(x => x.GetSpendableTransactionsInWallet(It.IsAny(), 0)) + .Returns(new List + { + new UnspentOutputReference + { + Address = new HdAddress + { + Address = senderAddress + }, + Transaction = new TransactionData + { + Id = utxoId, + Index = utxoIndex, + } + }, new UnspentOutputReference + { + Address = new HdAddress + { + Address = senderAddress + }, + Transaction = new TransactionData + { + Id = utxoIdUnused, + Index = utxoIndex, + } + } + }); + + var wallet = new Features.Wallet.Wallet(); + wallet.AccountsRoot.Add(new AccountRoot(wallet)); + var account0 = new HdAccount(wallet.AccountsRoot.First().Accounts) { Name = request.AccountName }; + account0.ExternalAddresses.Add(new HdAddress() { Address = senderAddress }); + + this.walletManager.Setup(x => x.GetWallet(request.WalletName)) + .Returns(wallet); + + var reserveUtxoService = new ReserveUtxoService(this.loggerFactory, new Mock().Object); + + var service = new SmartContractTransactionService( + this.network, + this.walletManager.Object, + this.walletTransactionHandler.Object, + this.stringSerializer.Object, + this.callDataSerializer.Object, + this.addressGenerator.Object, + this.stateRepository.Object, + reserveUtxoService); + + BuildCallContractTransactionResponse result = service.BuildCallTx(request); + + byte[] buffer = Convert.FromBase64String(sig1).Concat(Convert.FromBase64String(sig2)).ToArray(); + string expected = $"{(int)MethodParameterDataType.ByteArray}#{Encoders.Hex.EncodeData(buffer)}"; + + this.stringSerializer.Verify(x => x.Deserialize(It.Is(x => x[0] == expected))); + } } } diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCallContractTransactionRequest.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCallContractTransactionRequest.cs index b3fa86da0a..a70661ccfa 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCallContractTransactionRequest.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCallContractTransactionRequest.cs @@ -104,6 +104,14 @@ public BuildCallContractTransactionRequest() /// public string[] Parameters { get; set; } + /// + /// An array of base 64 encoded strings containing the signatures to add. + /// + /// + /// The strings passed here are typically obtained by signatories via the "signmessage" API by signing the "challenge" string returned by a method. + /// + public string[] Signatures { get; set; } + public override string ToString() { var builder = new StringBuilder(); diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCreateContractTransactionRequest.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCreateContractTransactionRequest.cs index b92c9e9703..8914a5f15a 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCreateContractTransactionRequest.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Models/BuildCreateContractTransactionRequest.cs @@ -96,6 +96,15 @@ public BuildCreateContractTransactionRequest() /// public string[] Parameters { get; set; } + /// + /// An array of base 64 encoded strings containing the signatures to add. + /// + /// + /// The strings passed here are typically obtained by signatories via the "signmessage" API by signing the "challenge" string returned by a method. + /// + + public string[] Signatures { get; set; } + public override string ToString() { var builder = new StringBuilder(); diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs index 81b008a0c4..c655cbb564 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractTransactionService.cs @@ -3,6 +3,7 @@ using System.Linq; using CSharpFunctionalExtensions; using NBitcoin; +using NBitcoin.DataEncoders; using Stratis.Bitcoin.Features.SmartContracts.Models; using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Features.Wallet.Interfaces; @@ -198,6 +199,41 @@ public BuildContractTransactionResult BuildTx(BuildContractTransactionRequest re return BuildContractTransactionResult.Success(model); } + private string[] ReplaceSignatures(string[] parameters, string[] signatures) + { + const int signatureLength = 65; + const int minHeaderByte = 27; + const int maxHeaderByte = 34; + + if (parameters == null) + return null; + + // Replace SIG# with any included signatures. + string encodedSigs = null; + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].ToUpper() == "SIG#") + { + if (encodedSigs == null) + { + var sigs = (signatures ?? new string[0]).Select(s => Convert.FromBase64String(s)).ToArray(); + if (sigs.Any(s => s.Length != signatureLength || s[0] < minHeaderByte || s[0] > maxHeaderByte)) + throw new Exception("Invalid signature(s)."); + + var sigbuf = new byte[sigs.Length * signatureLength]; + for (int j = 0; j < sigs.Length; j++) + Array.Copy(sigs[j], 0, sigbuf, j * signatureLength, signatureLength); + + encodedSigs = $"{(int)MethodParameterDataType.ByteArray}#{Encoders.Hex.EncodeData(sigbuf)}"; + } + + parameters[i] = encodedSigs; + } + } + + return parameters; + } + public BuildCallContractTransactionResponse BuildCallTx(BuildCallContractTransactionRequest request) { if (!this.CheckBalance(request.Sender)) @@ -214,6 +250,9 @@ public BuildCallContractTransactionResponse BuildCallTx(BuildCallContractTransac { try { + // If signatures have been included then they replace the SIG# parameter. + request.Parameters = ReplaceSignatures(request.Parameters, request.Signatures); + object[] methodParameters = this.methodParameterStringSerializer.Deserialize(request.Parameters); txData = new ContractTxData(ReflectionVirtualMachine.VmVersion, (Stratis.SmartContracts.RuntimeObserver.Gas)request.GasPrice, (Stratis.SmartContracts.RuntimeObserver.Gas)request.GasLimit, addressNumeric, request.MethodName, methodParameters); } @@ -275,6 +314,9 @@ public BuildCreateContractTransactionResponse BuildCreateTx(BuildCreateContractT { try { + // If signatures have been included then they replace the SIG# parameter. + request.Parameters = ReplaceSignatures(request.Parameters, request.Signatures); + object[] methodParameters = this.methodParameterStringSerializer.Deserialize(request.Parameters); txData = new ContractTxData(ReflectionVirtualMachine.VmVersion, (Stratis.SmartContracts.RuntimeObserver.Gas)request.GasPrice, (Stratis.SmartContracts.RuntimeObserver.Gas)request.GasLimit, request.ContractCode.HexToByteArray(), methodParameters); } diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractWalletController.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractWalletController.cs index cf48bd5ad3..bcab46eff9 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractWalletController.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Wallet/SmartContractWalletController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NBitcoin; +using NBitcoin.DataEncoders; using Stratis.Bitcoin.Connection; using Stratis.Bitcoin.Features.SmartContracts.Models; using Stratis.Bitcoin.Features.Wallet;