Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -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)]
Expand Down Expand Up @@ -92,6 +96,36 @@ public IActionResult PushCurrentBlockTip([FromBody] BlockTipModel blockTip)
}
}

/// <summary>
/// Authorizes withdrawals.
/// </summary>
/// <param name="authRequest">A structure containing one or more transactions to authorize.</param>
/// <returns>An array containing one or more signed transaction or <c>null</c> for transaction that could not be authorized.</returns>
[Route(FederationGatewayRouteEndPoint.AuthorizeWithdrawals)]
[HttpPost]
public IActionResult AuthorizeWithdrawals([FromBody] AuthorizeWithdrawalsModel authRequest)
{
Guard.NotNull(authRequest, nameof(authRequest));

if (!this.ModelState.IsValid)
{
IEnumerable<string> 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());
}
}

/// <summary>
/// Retrieves blocks deposits.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand Down Expand Up @@ -267,6 +266,7 @@ public static IFullNodeBuilder AddFederationGateway(this IFullNodeBuilder fullNo
services.AddSingleton<IMaturedBlockReceiver, MaturedBlockReceiver>();
services.AddSingleton<IMaturedBlocksRequester, RestMaturedBlockRequester>();
services.AddSingleton<IMaturedBlocksProvider, MaturedBlocksProvider>();
services.AddSingleton<ISignatureProvider, SignatureProvider>();
services.AddSingleton<IFederationGatewaySettings, FederationGatewaySettings>();
services.AddSingleton<IOpReturnDataReader, OpReturnDataReader>();
services.AddSingleton<IDepositExtractor, DepositExtractor>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Stratis.FederatedPeg.Features.FederationGateway.Interfaces
{
public interface IAuthorizeWithdrawalsModel
{
string TransactionHex { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ public interface IFederationWalletManager
/// <returns><c>-1</c> if the <paramref name="outPoint1"/> occurs first and <c>1</c> otherwise.</returns>
int CompareOutpoints(OutPoint outPoint1, OutPoint outPoint2);

/// <summary>
/// Signs a transaction if it is valid.
/// </summary>
/// <param name="transaction">The transaction.</param>
/// <param name="isValid">A function that determines the validity of the withdrawal.</param>
/// <param name="key">The key to use.</param>
/// <returns><c>True</c> if the withdrawal is valid and <c>false</c> otherwise.</returns>
Transaction SignTransaction(Transaction transaction, Func<Transaction, IWithdrawal, bool> isValid, Key key);

/// <summary>
/// Determines if federation has been activated.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Stratis.FederatedPeg.Features.FederationGateway.Interfaces
{
public interface ISignatureProvider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please describe the purpose and behaviour of this class in details, its clear it provides signatures but from who and how are the keys provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keys are provided by the API call that activates federation. The purpose of the class is in the associated issue. I will add the detail to the class.

{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="transactionHex">The hexadecimal representations of transactions to sign.</param>
/// <returns>An array of signed transactions (in hex) or <c>null</c> for transactions that can't be signed.</returns>
string SignTransaction(string transactionHex);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// An instance of this class represents a particular block hash and associated height on the source chain.
/// </summary>
public class AuthorizeWithdrawalsModel : RequestModel, IAuthorizeWithdrawalsModel
{
[Required(ErrorMessage = "The transaction to authorize")]
public string TransactionHex { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
/// Determines if a withdrawal transaction can be authorized.
/// </summary>
/// <param name="transaction">The transaction to authorize.</param>
/// <param name="withdrawal">The withdrawal transaction already extracted from the transaction.</param>
/// <returns><c>True</c> if the withdrawal is valid and <c>false</c> otherwise.</returns>
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);
}

/// <inheritdoc />
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,64 @@ public bool ValidateTransaction(Transaction transaction, bool checkSignature = f
}
}

/// <inheritdoc />
public Transaction SignTransaction(Transaction externalTransaction, Func<Transaction, IWithdrawal, bool> 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<Coin>();
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;
}

/// <inheritdoc />
public bool IsFederationActive()
{
Expand Down
22 changes: 15 additions & 7 deletions src/Stratis.FederatedPeg.Tests/CrossChainTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ protected Script redeemScript
/// <summary>
/// Initializes the cross-chain transfer tests.
/// </summary>
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();
Expand Down Expand Up @@ -222,9 +226,6 @@ protected void AppendBlocks(int blocks)
/// <returns>The last chained header.</returns>
protected ChainedHeader AppendBlock(params Transaction[] transactions)
{
ChainedHeader last = null;
uint nonce = RandomUtils.GetUInt32();

Block block = this.network.CreateBlock();

// Create coinbase.
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class FederationGatewayControllerTests

private readonly ILeaderReceiver leaderReceiver;

private readonly ISignatureProvider signatureProvider;

public FederationGatewayControllerTests()
{
this.network = FederatedPegNetwork.NetworksSelector.Regtest();
Expand All @@ -49,6 +51,7 @@ public FederationGatewayControllerTests()
this.leaderProvider = Substitute.For<ILeaderProvider>();
this.depositExtractor = Substitute.For<IDepositExtractor>();
this.leaderReceiver = Substitute.For<ILeaderReceiver>();
this.signatureProvider = Substitute.For<ISignatureProvider>();
}

private FederationGatewayController CreateController()
Expand All @@ -58,7 +61,8 @@ private FederationGatewayController CreateController()
this.maturedBlockReceiver,
this.leaderProvider,
this.GetMaturedBlocksProvider(),
this.leaderReceiver);
this.leaderReceiver,
this.signatureProvider);

return controller;
}
Expand Down
Loading