diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs
index b6f50e36..883ab1c4 100644
--- a/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs
@@ -6,7 +6,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
-using NBitcoin;
using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Utilities.JsonErrors;
using Stratis.FederatedPeg.Features.FederationGateway.Interfaces;
@@ -19,6 +18,7 @@ public static class FederationGatewayRouteEndPoint
public const string PushMaturedBlocks = "push_matured_blocks";
public const string PushCurrentBlockTip = "push_current_block_tip";
public const string GetMaturedBlockDeposits = "get_matured_block_deposits";
+ public const string AuthorizeWithdrawals = "authorize_withdrawals";
// TODO commented out since those constants are unused. Remove them later or start using.
//public const string CreateSessionOnCounterChain = "create-session-oncounterchain";
@@ -42,18 +42,22 @@ public class FederationGatewayController : Controller
private readonly ILeaderReceiver leaderReceiver;
+ private readonly ISignatureProvider signatureProvider;
+
public FederationGatewayController(
ILoggerFactory loggerFactory,
IMaturedBlockReceiver maturedBlockReceiver,
ILeaderProvider leaderProvider,
IMaturedBlocksProvider maturedBlocksProvider,
- ILeaderReceiver leaderReceiver)
+ ILeaderReceiver leaderReceiver,
+ ISignatureProvider signatureProvider)
{
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.maturedBlockReceiver = maturedBlockReceiver;
this.leaderProvider = leaderProvider;
this.maturedBlocksProvider = maturedBlocksProvider;
this.leaderReceiver = leaderReceiver;
+ this.signatureProvider = signatureProvider;
}
[Route(FederationGatewayRouteEndPoint.PushMaturedBlocks)]
@@ -92,6 +96,36 @@ public IActionResult PushCurrentBlockTip([FromBody] BlockTipModel blockTip)
}
}
+ ///
+ /// Authorizes withdrawals.
+ ///
+ /// A structure containing one or more transactions to authorize.
+ /// An array containing one or more signed transaction or null for transaction that could not be authorized.
+ [Route(FederationGatewayRouteEndPoint.AuthorizeWithdrawals)]
+ [HttpPost]
+ public IActionResult AuthorizeWithdrawals([FromBody] AuthorizeWithdrawalsModel authRequest)
+ {
+ Guard.NotNull(authRequest, nameof(authRequest));
+
+ if (!this.ModelState.IsValid)
+ {
+ IEnumerable errors = this.ModelState.Values.SelectMany(e => e.Errors.Select(m => m.ErrorMessage));
+ return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Formatting error", string.Join(Environment.NewLine, errors));
+ }
+
+ try
+ {
+ string result = this.signatureProvider.SignTransaction(authRequest.TransactionHex);
+
+ return this.Json(result);
+ }
+ catch (Exception e)
+ {
+ this.logger.LogTrace("Exception thrown calling /api/FederationGateway/{0}: {1}.", FederationGatewayRouteEndPoint.AuthorizeWithdrawals, e.Message);
+ return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, $"Could not authorize withdrawals: {e.Message}", e.ToString());
+ }
+ }
+
///
/// Retrieves blocks deposits.
///
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/FederationGatewayFeature.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/FederationGatewayFeature.cs
index e7f5748a..0686c86e 100644
--- a/src/Stratis.FederatedPeg.Features.FederationGateway/FederationGatewayFeature.cs
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/FederationGatewayFeature.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -267,6 +266,7 @@ public static IFullNodeBuilder AddFederationGateway(this IFullNodeBuilder fullNo
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IAuthorizeWithdrawalsModel.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IAuthorizeWithdrawalsModel.cs
new file mode 100644
index 00000000..5a62758c
--- /dev/null
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IAuthorizeWithdrawalsModel.cs
@@ -0,0 +1,7 @@
+namespace Stratis.FederatedPeg.Features.FederationGateway.Interfaces
+{
+ public interface IAuthorizeWithdrawalsModel
+ {
+ string TransactionHex { get; }
+ }
+}
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IFederationWalletManager.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IFederationWalletManager.cs
index 613aa7a6..be2c7b44 100644
--- a/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IFederationWalletManager.cs
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/IFederationWalletManager.cs
@@ -131,6 +131,15 @@ public interface IFederationWalletManager
/// -1 if the occurs first and 1 otherwise.
int CompareOutpoints(OutPoint outPoint1, OutPoint outPoint2);
+ ///
+ /// Signs a transaction if it is valid.
+ ///
+ /// The transaction.
+ /// A function that determines the validity of the withdrawal.
+ /// The key to use.
+ /// True if the withdrawal is valid and false otherwise.
+ Transaction SignTransaction(Transaction transaction, Func isValid, Key key);
+
///
/// Determines if federation has been activated.
///
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/ISignatureProvider.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/ISignatureProvider.cs
new file mode 100644
index 00000000..448a0115
--- /dev/null
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Interfaces/ISignatureProvider.cs
@@ -0,0 +1,19 @@
+namespace Stratis.FederatedPeg.Features.FederationGateway.Interfaces
+{
+ public interface ISignatureProvider
+ {
+ ///
+ /// Signs an externally provided withdrawal transaction if it is deemed valid. This method
+ /// is used to sign a transaction in response to signature requests from the federation
+ /// leader.
+ ///
+ ///
+ /// This method requires federation to be active as the wallet password is supplied during
+ /// activation. Transaction's are validated to ensure that they are expected as per
+ /// the deposits received on the source chain.
+ ///
+ /// The hexadecimal representations of transactions to sign.
+ /// An array of signed transactions (in hex) or null for transactions that can't be signed.
+ string SignTransaction(string transactionHex);
+ }
+}
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Models/AuthorizeWithdrawalsModel.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Models/AuthorizeWithdrawalsModel.cs
new file mode 100644
index 00000000..94dcd485
--- /dev/null
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Models/AuthorizeWithdrawalsModel.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+using Newtonsoft.Json;
+using Stratis.Bitcoin.Features.Wallet.Models;
+using Stratis.FederatedPeg.Features.FederationGateway.Interfaces;
+
+namespace Stratis.FederatedPeg.Features.FederationGateway.Models
+{
+ ///
+ /// An instance of this class represents a particular block hash and associated height on the source chain.
+ ///
+ public class AuthorizeWithdrawalsModel : RequestModel, IAuthorizeWithdrawalsModel
+ {
+ [Required(ErrorMessage = "The transaction to authorize")]
+ public string TransactionHex { get; set; }
+ }
+}
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/TargetChain/SignatureProvider.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/TargetChain/SignatureProvider.cs
new file mode 100644
index 00000000..91817fdf
--- /dev/null
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/TargetChain/SignatureProvider.cs
@@ -0,0 +1,105 @@
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using NBitcoin;
+using Stratis.Bitcoin.Utilities;
+using Stratis.FederatedPeg.Features.FederationGateway.Interfaces;
+using Stratis.FederatedPeg.Features.FederationGateway.Wallet;
+
+namespace Stratis.FederatedPeg.Features.FederationGateway.TargetChain
+{
+ ///
+ /// The purpose of this class is to sign externally provided withdrawal transactions if they are
+ /// deemed valid. Transactions would typically be signed in response to signature requests from
+ /// the federation leader. The federation is required to be active as the wallet password is
+ /// supplied during activation. Transaction's are validated to ensure that they are expected as
+ /// per the deposits received on the source chain. Out-of-sequence transactions or transactions
+ /// that are not utilising the expected UTXOs will not be signed.
+ ///
+ public class SignatureProvider: ISignatureProvider
+ {
+ private readonly IFederationWalletManager federationWalletManager;
+ private readonly ICrossChainTransferStore crossChainTransferStore;
+ private readonly IFederationGatewaySettings federationGatewaySettings;
+ private readonly Network network;
+ private readonly ILogger logger;
+
+ public SignatureProvider(
+ IFederationWalletManager federationWalletManager,
+ ICrossChainTransferStore crossChainTransferStore,
+ IFederationGatewaySettings federationGatewaySettings,
+ Network network,
+ ILoggerFactory loggerFactory)
+ {
+ Guard.NotNull(federationWalletManager, nameof(federationWalletManager));
+ Guard.NotNull(crossChainTransferStore, nameof(crossChainTransferStore));
+ Guard.NotNull(federationGatewaySettings, nameof(federationGatewaySettings));
+ Guard.NotNull(network, nameof(network));
+ Guard.NotNull(loggerFactory, nameof(loggerFactory));
+
+ this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
+ this.federationWalletManager = federationWalletManager;
+ this.federationGatewaySettings = federationGatewaySettings;
+ this.crossChainTransferStore = crossChainTransferStore;
+ this.network = network;
+ }
+
+ ///
+ /// Determines if a withdrawal transaction can be authorized.
+ ///
+ /// The transaction to authorize.
+ /// The withdrawal transaction already extracted from the transaction.
+ /// True if the withdrawal is valid and false otherwise.
+ private bool IsAuthorized(Transaction transaction, IWithdrawal withdrawal)
+ {
+ // It must be a transfer that we know about.
+ ICrossChainTransfer crossChainTransfer = this.crossChainTransferStore.GetAsync(new[] { withdrawal.DepositId }).GetAwaiter().GetResult().FirstOrDefault();
+ if (crossChainTransfer == null)
+ return false;
+
+ // If its already been seen in a block then we probably should not authorize it.
+ if (crossChainTransfer.Status == CrossChainTransferStatus.SeenInBlock)
+ return false;
+
+ // The templates must match what we expect to see.
+ if (!CrossChainTransfer.TemplatesMatch(crossChainTransfer.PartialTransaction, transaction))
+ return false;
+
+ return true;
+ }
+
+ private Transaction SignTransaction(Transaction transaction, Key key)
+ {
+ return this.federationWalletManager.SignTransaction(transaction, IsAuthorized, key);
+ }
+
+ ///
+ public string SignTransaction(string transactionHex)
+ {
+ Guard.NotNull(transactionHex, nameof(transactionHex));
+
+ this.logger.LogTrace("():{0}", transactionHex);
+
+ FederationWallet wallet = this.federationWalletManager.GetWallet();
+ if (wallet == null || this.federationWalletManager.Secret == null)
+ {
+ this.logger.LogTrace("(-)[FEDERATION_INACTIVE]");
+ return null;
+ }
+
+ Key key = wallet.MultiSigAddress.GetPrivateKey(wallet.EncryptedSeed, this.federationWalletManager.Secret.WalletPassword, this.network);
+ if (key.PubKey.ToHex() != this.federationGatewaySettings.PublicKey)
+ {
+ this.logger.LogTrace("(-)[FEDERATION_KEY_INVALID]");
+ return null;
+ }
+
+ Transaction transaction = this.network.CreateTransaction(transactionHex);
+
+ transactionHex = SignTransaction(transaction, key)?.ToHex(this.network);
+
+ this.logger.LogTrace("(-):{0}", transactionHex);
+
+ return transactionHex;
+ }
+ }
+}
diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Wallet/FederationWalletManager.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Wallet/FederationWalletManager.cs
index b6502d6d..91b40eef 100644
--- a/src/Stratis.FederatedPeg.Features.FederationGateway/Wallet/FederationWalletManager.cs
+++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Wallet/FederationWalletManager.cs
@@ -910,6 +910,64 @@ public bool ValidateTransaction(Transaction transaction, bool checkSignature = f
}
}
+ ///
+ public Transaction SignTransaction(Transaction externalTransaction, Func isValid, Key key)
+ {
+ // TODO: Check that the transaction is spending exactly the expected UTXO(s).
+ // TODO: Check that the transaction is serving the next expected UTXO(s).
+
+ Guard.NotNull(externalTransaction, nameof(externalTransaction));
+ Guard.NotNull(isValid, nameof(isValid));
+
+ this.logger.LogTrace("({0}:'{1}')", nameof(externalTransaction), externalTransaction.ToHex(this.network));
+
+ IWithdrawal withdrawal = this.withdrawalExtractor.ExtractWithdrawalFromTransaction(externalTransaction, 0, 0);
+ if (withdrawal == null)
+ {
+ this.logger.LogTrace("(-)[NOT_WITHDRAWAL]");
+ return null;
+ }
+
+ // Checks that the deposit id in the transaction is associated with a valid transfer.
+ if (!isValid(externalTransaction, withdrawal))
+ {
+ this.logger.LogTrace("(-)[INVALID_WITHDRAWAL]");
+ return null;
+ }
+
+ var coins = new List();
+ foreach (TxIn input in externalTransaction.Inputs)
+ {
+ TransactionData transactionData = this.outpointLookup[input.PrevOut];
+ if (transactionData == null)
+ {
+ this.logger.LogTrace("(-)[INVALID_UTXOS]");
+ return null;
+ }
+ coins.Add(new Coin(transactionData.Id, (uint)transactionData.Index, transactionData.Amount, transactionData.ScriptPubKey));
+ }
+
+ Transaction signedTransaction = null;
+ try
+ {
+ var builder = new TransactionBuilder(this.network);
+ signedTransaction = builder
+ .AddKeys(key)
+ .AddCoins(coins)
+ .SignTransactionInPlace(externalTransaction);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogTrace("Exception occurred: {0}", ex.ToString());
+ this.logger.LogTrace("(-)[COULD_NOT_SIGN]");
+ return null;
+ }
+
+ this.logger.LogTrace("(-):{0}", signedTransaction?.ToHex(this.network));
+
+ return signedTransaction;
+ }
+
///
public bool IsFederationActive()
{
diff --git a/src/Stratis.FederatedPeg.Tests/CrossChainTestBase.cs b/src/Stratis.FederatedPeg.Tests/CrossChainTestBase.cs
index 6508f023..993514c9 100644
--- a/src/Stratis.FederatedPeg.Tests/CrossChainTestBase.cs
+++ b/src/Stratis.FederatedPeg.Tests/CrossChainTestBase.cs
@@ -62,9 +62,13 @@ protected Script redeemScript
///
/// Initializes the cross-chain transfer tests.
///
- public CrossChainTestBase()
+ public CrossChainTestBase() : this(FederatedPegNetwork.NetworksSelector.Regtest())
{
- this.network = FederatedPegNetwork.NetworksSelector.Regtest();
+ }
+
+ public CrossChainTestBase(Network network)
+ {
+ this.network = network;
NetworkRegistration.Register(this.network);
var serializer = new DBreezeSerializer();
@@ -222,9 +226,6 @@ protected void AppendBlocks(int blocks)
/// The last chained header.
protected ChainedHeader AppendBlock(params Transaction[] transactions)
{
- ChainedHeader last = null;
- uint nonce = RandomUtils.GetUInt32();
-
Block block = this.network.CreateBlock();
// Create coinbase.
@@ -238,9 +239,16 @@ protected ChainedHeader AppendBlock(params Transaction[] transactions)
block.UpdateMerkleRoot();
block.Header.HashPrevBlock = this.chain.Tip.HashBlock;
- block.Header.Nonce = nonce;
- if (!this.chain.TrySetTip(block.Header, out last))
+ block.Header.Nonce = RandomUtils.GetUInt32();
+
+ return AppendBlock(block);
+ }
+
+ protected ChainedHeader AppendBlock(Block block)
+ {
+ if (!this.chain.TrySetTip(block.Header, out ChainedHeader last))
throw new InvalidOperationException("Previous not existing");
+
this.blockDict[block.GetHash()] = block;
this.federationWalletSyncManager.ProcessBlock(block);
diff --git a/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs b/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs
index 9324cabf..4a253b4c 100644
--- a/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs
+++ b/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs
@@ -38,6 +38,8 @@ public class FederationGatewayControllerTests
private readonly ILeaderReceiver leaderReceiver;
+ private readonly ISignatureProvider signatureProvider;
+
public FederationGatewayControllerTests()
{
this.network = FederatedPegNetwork.NetworksSelector.Regtest();
@@ -49,6 +51,7 @@ public FederationGatewayControllerTests()
this.leaderProvider = Substitute.For();
this.depositExtractor = Substitute.For();
this.leaderReceiver = Substitute.For();
+ this.signatureProvider = Substitute.For();
}
private FederationGatewayController CreateController()
@@ -58,7 +61,8 @@ private FederationGatewayController CreateController()
this.maturedBlockReceiver,
this.leaderProvider,
this.GetMaturedBlocksProvider(),
- this.leaderReceiver);
+ this.leaderReceiver,
+ this.signatureProvider);
return controller;
}
diff --git a/src/Stratis.FederatedPeg.Tests/SignatureProviderTests.cs b/src/Stratis.FederatedPeg.Tests/SignatureProviderTests.cs
new file mode 100644
index 00000000..6a22b6e1
--- /dev/null
+++ b/src/Stratis.FederatedPeg.Tests/SignatureProviderTests.cs
@@ -0,0 +1,122 @@
+using System.Linq;
+using System.Reactive.Linq;
+using NBitcoin;
+using NBitcoin.Policy;
+using Stratis.Bitcoin.Configuration;
+using Stratis.Bitcoin.Networks;
+using Stratis.FederatedPeg.Features.FederationGateway.Interfaces;
+using Stratis.FederatedPeg.Features.FederationGateway.Models;
+using Stratis.FederatedPeg.Features.FederationGateway.SourceChain;
+using Stratis.FederatedPeg.Features.FederationGateway.TargetChain;
+using Xunit;
+
+namespace Stratis.FederatedPeg.Tests
+{
+ public class SignatureProviderTests : CrossChainTestBase
+ {
+ public SignatureProviderTests() : base(Networks.Stratis.Testnet())
+ {
+ }
+
+ [Fact]
+ public void OtherMembersCanAddSignaturesToMyTransaction()
+ {
+ var dataFolder = new DataFolder(CreateTestDir(this));
+
+ this.Init(dataFolder);
+ this.AddFunding();
+ this.AppendBlocks(this.federationGatewaySettings.MinCoinMaturity);
+
+ using (ICrossChainTransferStore crossChainTransferStore = this.CreateStore())
+ {
+ crossChainTransferStore.Initialize();
+ crossChainTransferStore.Start();
+
+ Assert.Equal(this.chain.Tip.HashBlock, crossChainTransferStore.TipHashAndHeight.HashBlock);
+ Assert.Equal(this.chain.Tip.Height, crossChainTransferStore.TipHashAndHeight.Height);
+
+ BitcoinAddress address = (new Key()).PubKey.Hash.GetAddress(this.network);
+
+ var deposit = new Deposit(0, new Money(160m, MoneyUnit.BTC), address.ToString(), crossChainTransferStore.NextMatureDepositHeight, 1);
+
+ IMaturedBlockDeposits[] blockDeposits = new[] { new MaturedBlockDepositsModel(
+ new MaturedBlockModel() {
+ BlockHash = 1,
+ BlockHeight = crossChainTransferStore.NextMatureDepositHeight },
+ new[] { deposit })
+ };
+
+ crossChainTransferStore.RecordLatestMatureDepositsAsync(blockDeposits).GetAwaiter().GetResult();
+
+ ICrossChainTransfer crossChainTransfer = crossChainTransferStore.GetAsync(new[] { deposit.Id }).GetAwaiter().GetResult().SingleOrDefault();
+
+ Assert.NotNull(crossChainTransfer);
+
+ Transaction transaction = crossChainTransfer.PartialTransaction;
+
+ Assert.True(crossChainTransferStore.ValidateTransaction(transaction));
+
+ // Create a separate instance to generate another transaction.
+ Transaction transaction2;
+ var newTest = new SignatureProviderTests();
+ var dataFolder2 = new DataFolder(CreateTestDir(this));
+
+ newTest.federationKeys = this.federationKeys;
+ newTest.SetExtendedKey(1);
+ newTest.Init(dataFolder2);
+
+ // Clone chain
+ for (int i = 1; i <= this.chain.Height; i++)
+ {
+ ChainedHeader header = this.chain.GetBlock(i);
+ Block block = this.blockDict[header.HashBlock];
+ newTest.AppendBlock(block);
+ }
+
+ using (ICrossChainTransferStore crossChainTransferStore2 = newTest.CreateStore())
+ {
+ crossChainTransferStore2.Initialize();
+ crossChainTransferStore2.Start();
+
+ Assert.Equal(newTest.chain.Tip.HashBlock, crossChainTransferStore2.TipHashAndHeight.HashBlock);
+ Assert.Equal(newTest.chain.Tip.Height, crossChainTransferStore2.TipHashAndHeight.Height);
+
+ crossChainTransferStore2.RecordLatestMatureDepositsAsync(blockDeposits).GetAwaiter().GetResult();
+
+ ICrossChainTransfer crossChainTransfer2 = crossChainTransferStore2.GetAsync(new[] { deposit.Id }).GetAwaiter().GetResult().SingleOrDefault();
+
+ Assert.NotNull(crossChainTransfer2);
+
+ transaction2 = crossChainTransfer2.PartialTransaction;
+
+ Assert.True(crossChainTransferStore2.ValidateTransaction(transaction2));
+
+ // The first instance acts as signatory for the transaction coming from the second instance.
+ ISignatureProvider signatureProvider = new SignatureProvider(
+ this.federationWalletManager,
+ crossChainTransferStore,
+ this.federationGatewaySettings,
+ this.network,
+ this.loggerFactory);
+
+ string signedTransactionHex = signatureProvider.SignTransaction(transaction2.ToHex(this.network));
+ Assert.NotNull(signedTransactionHex);
+
+ // The second instance parses the hex.
+ Transaction signedTransaction = newTest.network.CreateTransaction(signedTransactionHex);
+ Assert.NotNull(signedTransaction);
+
+ // The second instance validates the transaction and signature.
+ var outpointLookup = newTest.wallet.MultiSigAddress.Transactions.ToDictionary(t => new OutPoint(t.Id, t.Index));
+ Coin[] coins = signedTransaction.Inputs
+ .Select(input => outpointLookup[input.PrevOut])
+ .Select(td => new Coin(td.Id, (uint)td.Index, td.Amount, td.ScriptPubKey))
+ .ToArray();
+
+ TransactionBuilder builder = new TransactionBuilder(newTest.wallet.Network).AddCoins(coins);
+ Assert.True(builder.Verify(signedTransaction, this.federationGatewaySettings.TransactionFee, out TransactionPolicyError[] errors));
+ }
+ }
+ }
+ }
+}