Skip to content

Commit

Permalink
Validate cross-chain deposit target (#561)
Browse files Browse the repository at this point in the history
* Validate cross-chain transaction target

* Add more changes

* Change validation network

* Refactor

* Add ValidateCrossChainTransferAddress method

* Rename method

* Add some comments

* Update tests

* Add validation to SmartContractTransactionService

* Add minimum

* Fix spelling

* Fix message

* Update CirrusAddressValidationNetwork

* Add comments

* Refactor

* Remove whitespace

* Test for federation

* Move OpReturnDataReader to Stratis.Bitcoin

* Add reference
  • Loading branch information
quantumagi authored and fassadlr committed Jul 1, 2021
1 parent de0debe commit 9ea75e5
Show file tree
Hide file tree
Showing 16 changed files with 193 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ public BuildContractTransactionResult BuildTx(BuildContractTransactionRequest re

Transaction transaction = this.walletTransactionHandler.BuildTransaction(context);

DepositValidationHelper.ValidateCrossChainDeposit(this.network, transaction);

var model = new WalletBuildTransactionModel
{
Hex = transaction.ToHex(),
Expand Down
158 changes: 158 additions & 0 deletions src/Stratis.Bitcoin.Features.Wallet/DepositValidationHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NBitcoin;
using NBitcoin.DataEncoders;
using Stratis.Bitcoin.Builder.Feature;
using Stratis.Bitcoin.Networks;

namespace Stratis.Bitcoin.Features.Wallet
{
public static class DepositValidationHelper
{
/// <summary>
/// This deposit extractor implementation only looks for a very specific deposit format.
/// Deposits will have 2 outputs when there is no change.
/// </summary>
private const int ExpectedNumberOfOutputsNoChange = 2;

/// <summary> Deposits will have 3 outputs when there is change.</summary>
private const int ExpectedNumberOfOutputsChange = 3;

public static bool TryGetDepositsToMultisig(Network network, Transaction transaction, Money crossChainTransferMinimum, out List<TxOut> depositsToMultisig)
{
depositsToMultisig = null;

// Coinbase transactions can't have deposits.
if (transaction.IsCoinBase)
return false;

// Deposits have a certain structure.
if (transaction.Outputs.Count != ExpectedNumberOfOutputsNoChange && transaction.Outputs.Count != ExpectedNumberOfOutputsChange)
return false;

IFederation federation = network.Federations?.GetOnlyFederation();
if (federation == null)
return false;

var depositScript = PayToFederationTemplate.Instance.GenerateScriptPubKey(federation.Id).PaymentScript;

depositsToMultisig = transaction.Outputs.Where(output =>
output.ScriptPubKey == depositScript &&
output.Value >= crossChainTransferMinimum).ToList();

return depositsToMultisig.Any();
}

public static bool TryGetTarget(Transaction transaction, IOpReturnDataReader opReturnDataReader, out bool conversion, out string targetAddress, out int targetChain)
{
conversion = false;
targetChain = 0 /* DestinationChain.STRAX */;

// Check the common case first.
if (!opReturnDataReader.TryGetTargetAddress(transaction, out targetAddress))
{
byte[] opReturnBytes = OpReturnDataReader.SelectBytesContentFromOpReturn(transaction).FirstOrDefault();

if (opReturnBytes != null && InterFluxOpReturnEncoder.TryDecode(opReturnBytes, out int destinationChain, out targetAddress))
{
targetChain = destinationChain;
}
else
return false;

conversion = true;
}

return true;
}

/// <summary>
/// Determines if this is a cross-chain transfer and then validates the target address as required.
/// </summary>
/// <param name="network">The source network.</param>
/// <param name="transaction">The transaction to validate.</param>
/// <returns><c>True</c> if its a cross-chain transfer and <c>false</c> otherwise.</returns>
/// <exception cref="FeatureException">If the address is invalid or inappropriate for the target network.</exception>
public static bool ValidateCrossChainDeposit(Network network, Transaction transaction)
{
if (!DepositValidationHelper.TryGetDepositsToMultisig(network, transaction, Money.Zero, out List<TxOut> depositsToMultisig))
return false;

if (depositsToMultisig.Any(d => d.Value < Money.COIN))
{
throw new FeatureException(HttpStatusCode.BadRequest, "Amount below minimum.",
$"The cross-chain transfer amount is less than the minimum of 1.");
}

Network targetNetwork = null;

if (network.Name.StartsWith("Cirrus"))
{
targetNetwork = StraxNetwork.MainChainNetworks[network.NetworkType]();
}
else if (network.Name.StartsWith("Strax"))
{
targetNetwork = new CirrusAddressValidationNetwork(network.Name.Replace("Strax", "Cirrus"));
}
else
{
return true;
}

IOpReturnDataReader opReturnDataReader = new OpReturnDataReader(targetNetwork);
if (!DepositValidationHelper.TryGetTarget(transaction, opReturnDataReader, out _, out _, out _))
{
throw new FeatureException(HttpStatusCode.BadRequest, "No valid target address.",
$"The cross-chain transfer transaction contains no valid target address for the target network.");
}

return true;
}
}


/// <summary>
/// When running on Strax its difficult to get the correct Cirrus network class due to circular references.
/// This is a bare-minimum network class for the sole purpose of address validation.
/// </summary>
public class CirrusAddressValidationNetwork : Network
{
public CirrusAddressValidationNetwork(string name) : base()
{
this.Name = name;
this.Base58Prefixes = new byte[12][];
switch (name)
{
case "CirrusMain":
this.Base58Prefixes[(int)Base58Type.PUBKEY_ADDRESS] = new byte[] { 28 }; // C
this.Base58Prefixes[(int)Base58Type.SCRIPT_ADDRESS] = new byte[] { 88 }; // c
break;
case "CirrusTest":
this.Base58Prefixes[(int)Base58Type.PUBKEY_ADDRESS] = new byte[] { 127 }; // t
this.Base58Prefixes[(int)Base58Type.SCRIPT_ADDRESS] = new byte[] { 137 }; // x
break;
case "CirrusRegTest":
this.Base58Prefixes[(int)Base58Type.PUBKEY_ADDRESS] = new byte[] { 55 }; // P
this.Base58Prefixes[(int)Base58Type.SCRIPT_ADDRESS] = new byte[] { 117 }; // p
break;
}

this.Base58Prefixes[(int)Base58Type.SECRET_KEY] = new byte[] { (239) };
this.Base58Prefixes[(int)Base58Type.ENCRYPTED_SECRET_KEY_NO_EC] = new byte[] { 0x01, 0x42 };
this.Base58Prefixes[(int)Base58Type.ENCRYPTED_SECRET_KEY_EC] = new byte[] { 0x01, 0x43 };
this.Base58Prefixes[(int)Base58Type.EXT_PUBLIC_KEY] = new byte[] { (0x04), (0x35), (0x87), (0xCF) };
this.Base58Prefixes[(int)Base58Type.EXT_SECRET_KEY] = new byte[] { (0x04), (0x35), (0x83), (0x94) };
this.Base58Prefixes[(int)Base58Type.PASSPHRASE_CODE] = new byte[] { 0x2C, 0xE9, 0xB3, 0xE1, 0xFF, 0x39, 0xE2 };
this.Base58Prefixes[(int)Base58Type.CONFIRMATION_CODE] = new byte[] { 0x64, 0x3B, 0xF6, 0xA8, 0x9A };
this.Base58Prefixes[(int)Base58Type.STEALTH_ADDRESS] = new byte[] { 0x2b };
this.Base58Prefixes[(int)Base58Type.ASSET_ID] = new byte[] { 115 };
this.Base58Prefixes[(int)Base58Type.COLORED_ADDRESS] = new byte[] { 0x13 };

Bech32Encoder encoder = Encoders.Bech32("tb");
this.Bech32Encoders = new Bech32Encoder[2];
this.Bech32Encoders[(int)Bech32Type.WITNESS_PUBKEY_ADDRESS] = encoder;
this.Bech32Encoders[(int)Bech32Type.WITNESS_SCRIPT_ADDRESS] = encoder;
}
}
}
4 changes: 4 additions & 0 deletions src/Stratis.Bitcoin.Features.Wallet/Services/WalletService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Policy;
using Stratis.Bitcoin.Builder.Feature;
using Stratis.Bitcoin.Configuration;
Expand All @@ -19,6 +20,7 @@
using Stratis.Bitcoin.Features.Wallet.Controllers;
using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Features.Wallet.Models;
using Stratis.Bitcoin.Networks;
using Stratis.Bitcoin.Utilities;

namespace Stratis.Bitcoin.Features.Wallet.Services
Expand Down Expand Up @@ -552,6 +554,8 @@ public async Task<AddressesModel> GetAllAddresses(GetAllAddressesModel request,
Transaction transactionResult = this.walletTransactionHandler.BuildTransaction(context);
DepositValidationHelper.ValidateCrossChainDeposit(this.network, transactionResult);
return new WalletBuildTransactionModel
{
Hex = transactionResult.ToHex(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<ProjectReference Include="..\Stratis.Bitcoin.Features.BlockStore\Stratis.Bitcoin.Features.BlockStore.csproj" />
<ProjectReference Include="..\Stratis.Bitcoin.Features.MemoryPool\Stratis.Bitcoin.Features.MemoryPool.csproj" />
<ProjectReference Include="..\Stratis.Bitcoin.Features.RPC\Stratis.Bitcoin.Features.RPC.csproj" />
<ProjectReference Include="..\Stratis.Bitcoin.Networks\Stratis.Bitcoin.Networks.csproj" />
<ProjectReference Include="..\Stratis.Bitcoin\Stratis.Bitcoin.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
using System.Text.RegularExpressions;
using NBitcoin;
using NLog;
using Stratis.Features.Collateral.CounterChain;
using TracerAttributes;

namespace Stratis.Features.FederatedPeg
namespace Stratis.Bitcoin
{
/// <summary>
/// OP_RETURN data can be a hash, an address or unknown.
Expand Down Expand Up @@ -45,10 +44,10 @@ public class OpReturnDataReader : IOpReturnDataReader

private readonly Network counterChainNetwork;

public OpReturnDataReader(CounterChainNetworkWrapper counterChainNetworkWrapper)
public OpReturnDataReader(Network network)
{
this.logger = LogManager.GetCurrentClassLogger();
this.counterChainNetwork = counterChainNetworkWrapper.CounterChainNetwork;
this.counterChainNetwork = network;
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public CrossChainTestBase(Network network = null, Network counterChainNetwork =
this.asyncProvider = new AsyncProvider(this.loggerFactory, this.signals);
this.loggerFactory.CreateLogger(null).ReturnsForAnyArgs(this.logger);
this.dateTimeProvider = DateTimeProvider.Default;
this.opReturnDataReader = new OpReturnDataReader(this.counterChainNetworkWrapper);
this.opReturnDataReader = new OpReturnDataReader(this.counterChainNetworkWrapper.CounterChainNetwork);
this.blockRepository = Substitute.For<IBlockRepository>();
this.fullNode = Substitute.For<IFullNode>();
this.withdrawalTransactionBuilder = Substitute.For<IWithdrawalTransactionBuilder>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
using NBitcoin;
using Newtonsoft.Json;
using NSubstitute;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Controllers;
using Stratis.Bitcoin.Features.MemoryPool;
using Stratis.Bitcoin.Features.Wallet.Models;
using Stratis.Bitcoin.Tests.Common;
using Stratis.Features.Collateral.CounterChain;
using Stratis.Features.FederatedPeg.Conversion;
using Stratis.Features.FederatedPeg.Events;
using Stratis.Features.FederatedPeg.Interfaces;
Expand Down Expand Up @@ -158,7 +158,7 @@ public async Task StoringDepositsWhenWalletBalanceSufficientSucceedsWithDetermin

// Transaction[0] output value - op_return.
Assert.Equal(new Money(1m, MoneyUnit.Satoshi), transactions[0].Outputs[2].Value);
new OpReturnDataReader(this.counterChainNetworkWrapper).TryGetTransactionId(transactions[0], out string actualDepositId);
new OpReturnDataReader(this.counterChainNetworkWrapper.CounterChainNetwork).TryGetTransactionId(transactions[0], out string actualDepositId);
Assert.Equal(deposit1.Id.ToString(), actualDepositId);

// Transactions[1] inputs.
Expand All @@ -179,7 +179,7 @@ public async Task StoringDepositsWhenWalletBalanceSufficientSucceedsWithDetermin

// Transaction[1] output value - op_return.
Assert.Equal(new Money(1m, MoneyUnit.Satoshi), transactions[1].Outputs[2].Value);
new OpReturnDataReader(this.counterChainNetworkWrapper).TryGetTransactionId(transactions[1], out string actualDepositId2);
new OpReturnDataReader(this.counterChainNetworkWrapper.CounterChainNetwork).TryGetTransactionId(transactions[1], out string actualDepositId2);
Assert.Equal(deposit2.Id.ToString(), actualDepositId2);

ICrossChainTransfer[] transfers = crossChainTransferStore.GetAsync(new uint256[] { 0, 1 }).GetAwaiter().GetResult().ToArray();
Expand Down Expand Up @@ -311,7 +311,7 @@ public async Task StoringDepositsWhenWalletBalanceInSufficientSucceedsWithSuspen

// Transaction[0] output value - op_return.
Assert.Equal(new Money(1m, MoneyUnit.Satoshi), transactions[0].Outputs[2].Value);
new OpReturnDataReader(this.counterChainNetworkWrapper).TryGetTransactionId(transactions[0], out string actualDepositId);
new OpReturnDataReader(this.counterChainNetworkWrapper.CounterChainNetwork).TryGetTransactionId(transactions[0], out string actualDepositId);
Assert.Equal(deposit1.Id.ToString(), actualDepositId);

Assert.Null(transactions[1]);
Expand Down Expand Up @@ -349,7 +349,7 @@ public async Task StoringDepositsWhenWalletBalanceInSufficientSucceedsWithSuspen

// Transaction[1] output value - op_return.
Assert.Equal(new Money(1m, MoneyUnit.Satoshi), transactions[1].Outputs[2].Value);
new OpReturnDataReader(this.counterChainNetworkWrapper).TryGetTransactionId(transactions[1], out string actualDepositId2);
new OpReturnDataReader(this.counterChainNetworkWrapper.CounterChainNetwork).TryGetTransactionId(transactions[1], out string actualDepositId2);
Assert.Equal(deposit2.Id.ToString(), actualDepositId2);

Assert.Equal(2, transfers.Length);
Expand Down Expand Up @@ -604,7 +604,7 @@ public void DoTest()

var transaction = new PosTransaction(model.Hex);

var reader = new OpReturnDataReader(new CounterChainNetworkWrapper(CirrusNetwork.NetworksSelector.Testnet()));
var reader = new OpReturnDataReader(CirrusNetwork.NetworksSelector.Testnet());
var extractor = new DepositExtractor(this.federatedPegSettings, this.network, this.opReturnDataReader);
IDeposit deposit = extractor.ExtractDepositFromTransaction(transaction, 2, 1);

Expand Down Expand Up @@ -958,7 +958,7 @@ public async Task ReorgDoesntLeaveBehindUnconfirmedTransactionsAsync()
// Get rid of the pre-existing transactions. It's easier to track with 10 of our own utxos.
this.fundingTransactions.Clear();

foreach (TransactionData tx in this.wallet.MultiSigAddress.Transactions.ToList())
foreach (FederatedPeg.Wallet.TransactionData tx in this.wallet.MultiSigAddress.Transactions.ToList())
{
this.wallet.MultiSigAddress.Transactions.Remove(tx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FluentAssertions;
using NBitcoin;
using NSubstitute;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Features.Wallet;
using Stratis.Bitcoin.Networks;
using Stratis.Features.FederatedPeg.Conversion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using NBitcoin;
using NSubstitute;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Features.Wallet.Interfaces;
Expand All @@ -12,7 +13,6 @@
using Stratis.Bitcoin.Signals;
using Stratis.Bitcoin.Tests.Common;
using Stratis.Bitcoin.Utilities;
using Stratis.Features.Collateral.CounterChain;
using Stratis.Features.FederatedPeg.Distribution;
using Stratis.Features.FederatedPeg.Interfaces;
using Stratis.Features.FederatedPeg.SourceChain;
Expand Down Expand Up @@ -57,7 +57,7 @@ public RewardClaimerTests()
this.initialBlockDownloadState = Substitute.For<IInitialBlockDownloadState>();
this.initialBlockDownloadState.IsInitialBlockDownload().Returns(false);

this.opReturnDataReader = new OpReturnDataReader(new CounterChainNetworkWrapper(new CirrusRegTest()));
this.opReturnDataReader = new OpReturnDataReader(new CirrusRegTest());

this.federatedPegSettings = Substitute.For<IFederatedPegSettings>();
this.federatedPegSettings.MultiSigRedeemScript.Returns(this.addressHelper.PayToMultiSig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using NBitcoin;
using NSubstitute;
using NSubstitute.Core;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Features.Wallet;
using Stratis.Bitcoin.Networks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
using Microsoft.Extensions.Logging;
using NBitcoin;
using NSubstitute;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Networks;
using Stratis.Features.Collateral.CounterChain;
using Stratis.Features.FederatedPeg.Tests.Utils;
using Stratis.Sidechains.Networks;
using Xunit;
Expand All @@ -25,7 +25,7 @@ public OpReturnDataReaderTests()
this.loggerFactory = Substitute.For<ILoggerFactory>();
this.network = CirrusNetwork.NetworksSelector.Regtest();
this.counterChainNetwork = Networks.Strax.Regtest();
this.opReturnDataReader = new OpReturnDataReader(new CounterChainNetworkWrapper(this.counterChainNetwork));
this.opReturnDataReader = new OpReturnDataReader(this.counterChainNetwork);

this.transactionBuilder = new TestTransactionBuilder();
this.addressHelper = new AddressHelper(this.network, this.counterChainNetwork);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Extensions.Logging;
using NBitcoin;
using NSubstitute;
using Stratis.Bitcoin;
using Stratis.Bitcoin.Networks;
using Stratis.Features.FederatedPeg.Conversion;
using Stratis.Features.FederatedPeg.Interfaces;
Expand Down
3 changes: 2 additions & 1 deletion src/Stratis.Features.FederatedPeg/FederatedPegFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Stratis.Bitcoin.Connection;
using Stratis.Bitcoin.Features.Notifications;
using Stratis.Bitcoin.Features.SmartContracts;
using Stratis.Bitcoin.Features.Wallet;
using Stratis.Bitcoin.P2P.Peer;
using Stratis.Bitcoin.P2P.Protocol.Payloads;
using Stratis.Bitcoin.Utilities;
Expand Down Expand Up @@ -258,7 +259,7 @@ public static IFullNodeBuilder AddFederatedPeg(this IFullNodeBuilder fullNodeBui
{
services.AddSingleton<IMaturedBlocksProvider, MaturedBlocksProvider>();
services.AddSingleton<IFederatedPegSettings, FederatedPegSettings>();
services.AddSingleton<IOpReturnDataReader, OpReturnDataReader>();
services.AddSingleton<IOpReturnDataReader>(provider => new OpReturnDataReader(provider.GetService<CounterChainNetworkWrapper>().CounterChainNetwork));
services.AddSingleton<IDepositExtractor, DepositExtractor>();
services.AddSingleton<IWithdrawalExtractor, WithdrawalExtractor>();
services.AddSingleton<IFederationWalletSyncManager, FederationWalletSyncManager>();
Expand Down
Loading

0 comments on commit 9ea75e5

Please sign in to comment.