diff --git a/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHCompatibleClientProvider.cs b/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHCompatibleClientProvider.cs index 8c2d68ab2c..6bf84be67a 100644 --- a/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHCompatibleClientProvider.cs +++ b/src/Stratis.Bitcoin.Features.Interop/ETHClient/ETHCompatibleClientProvider.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Stratis.Features.FederatedPeg.Conversion; +using Stratis.Bitcoin.Features.Wallet; namespace Stratis.Bitcoin.Features.Interop.ETHClient { diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs b/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs index 29adf12a13..a0777ba0da 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropPoller.cs @@ -10,9 +10,10 @@ using Nethereum.Web3; using Stratis.Bitcoin.AsyncWork; using Stratis.Bitcoin.Configuration; -using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Features.Interop.ETHClient; using Stratis.Bitcoin.Features.Interop.Payloads; +using Stratis.Bitcoin.Features.PoA; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Utilities; using Stratis.Features.Collateral.CounterChain; @@ -77,7 +78,7 @@ public InteropPoller(NodeSettings nodeSettings, InteropSettings interopSettings, this.interopTransactionManager = interopTransactionManager; this.counterChainNetwork = counterChainNetworkWrapper.CounterChainNetwork; this.logger = nodeSettings.LoggerFactory.CreateLogger(this.GetType().FullName); - + this.eventFilterCreationRequired = new Dictionary() { {DestinationChain.ETH, true} @@ -136,7 +137,7 @@ public void Initialize() { foreach (KeyValuePair supportedChain in this.clientProvider.GetAllSupportedChains()) await this.CheckForContractEventsAsync(supportedChain.Key, supportedChain.Value).ConfigureAwait(false); - + await this.ProcessConversionRequestsAsync().ConfigureAwait(false); } catch (Exception e) @@ -225,7 +226,7 @@ private async Task CheckForContractEventsAsync(DestinationChain targetChain, IET string destinationAddress = await chainClient.GetDestinationAddressAsync(transferEvent.Event.From).ConfigureAwait(false); this.logger.LogInformation("Conversion burn transaction {0} has destination address {1}.", transferEvent.Log.TransactionHash, destinationAddress); - + try { // Validate that it is a mainchain address here before bothering to add it to the repository. @@ -255,7 +256,7 @@ private async Task CheckForContractEventsAsync(DestinationChain targetChain, IET Amount = this.ConvertWeiToSatoshi(transferEvent.Event.Value), BlockHeight = (int)blockHeight, DestinationAddress = destinationAddress, - DestinationChain = targetChain + DestinationChain = targetChain }); } } @@ -383,147 +384,147 @@ private async Task ProcessConversionRequestsAsync() switch (request.RequestStatus) { case ((int)ConversionRequestStatus.Unprocessed): - { - if (originator) { - // If this node is the designated transaction originator, it must create and submit the transaction to the multisig. - this.logger.LogInformation("This node selected as originator for transaction {0}.", request.RequestId); + if (originator) + { + // If this node is the designated transaction originator, it must create and submit the transaction to the multisig. + this.logger.LogInformation("This node selected as originator for transaction {0}.", request.RequestId); - request.RequestStatus = ConversionRequestStatus.OriginatorNotSubmitted; - } - else - { - this.logger.LogInformation("This node was not selected as the originator for transaction {0}. The originator is: {1}.", request.RequestId, designatedMember.PubKey.ToHex()); + request.RequestStatus = ConversionRequestStatus.OriginatorNotSubmitted; + } + else + { + this.logger.LogInformation("This node was not selected as the originator for transaction {0}. The originator is: {1}.", request.RequestId, designatedMember.PubKey.ToHex()); - request.RequestStatus = ConversionRequestStatus.NotOriginator; - } + request.RequestStatus = ConversionRequestStatus.NotOriginator; + } - break; - } + break; + } case (ConversionRequestStatus.OriginatorNotSubmitted): - { - // First construct the necessary transfer() transaction data, utilising the ABI of the wrapped STRAX ERC20 contract. - // When this constructed transaction is actually executed, the transfer's source account will be the account executing the transaction i.e. the multisig contract address. - string abiData = clientForDestChain.EncodeTransferParams(request.DestinationAddress, amountInWei); + { + // First construct the necessary transfer() transaction data, utilising the ABI of the wrapped STRAX ERC20 contract. + // When this constructed transaction is actually executed, the transfer's source account will be the account executing the transaction i.e. the multisig contract address. + string abiData = clientForDestChain.EncodeTransferParams(request.DestinationAddress, amountInWei); - // Submit the unconfirmed transaction data to the multisig contract, returning a transactionId used to refer to it. - // Once sufficient multisig owners have confirmed the transaction the multisig contract will execute it. - // Note that by submitting the transaction to the multisig wallet contract, the originator is implicitly granting it one confirmation. - BigInteger transactionId = await clientForDestChain.SubmitTransactionAsync(this.interopSettings.ETHSettings.WrappedStraxContractAddress, 0, abiData).ConfigureAwait(false); + // Submit the unconfirmed transaction data to the multisig contract, returning a transactionId used to refer to it. + // Once sufficient multisig owners have confirmed the transaction the multisig contract will execute it. + // Note that by submitting the transaction to the multisig wallet contract, the originator is implicitly granting it one confirmation. + BigInteger transactionId = await clientForDestChain.SubmitTransactionAsync(this.interopSettings.ETHSettings.WrappedStraxContractAddress, 0, abiData).ConfigureAwait(false); - this.logger.LogInformation("Originator submitted transaction to multisig and was allocated transactionId {0}.", transactionId); + this.logger.LogInformation("Originator submitted transaction to multisig and was allocated transactionId {0}.", transactionId); - // TODO: Need to persist vote storage across node shutdowns - this.interopTransactionManager.AddVote(request.RequestId, transactionId, this.federationManager.CurrentFederationKey.PubKey); + // TODO: Need to persist vote storage across node shutdowns + this.interopTransactionManager.AddVote(request.RequestId, transactionId, this.federationManager.CurrentFederationKey.PubKey); - request.RequestStatus = ConversionRequestStatus.OriginatorSubmitted; + request.RequestStatus = ConversionRequestStatus.OriginatorSubmitted; - break; - } + break; + } case (ConversionRequestStatus.OriginatorSubmitted): - { - // It must then propagate the transactionId to the other nodes so that they know they should confirm it. - // The reason why each node doesn't simply maintain its own transaction counter, is that it can't be guaranteed - // that a transaction won't be submitted out-of-turn by a rogue or malfunctioning federation multisig node. - // The coordination mechanism safeguards against this, as any such spurious transaction will not receive acceptance votes. - // TODO: The transactionId should be accompanied by the hash of the submission transaction on the Ethereum chain so that it can be verified - - BigInteger transactionId2 = this.interopTransactionManager.GetCandidateTransactionId(request.RequestId); - - if (transactionId2 != BigInteger.MinusOne) { - await this.BroadcastCoordinationAsync(request.RequestId, transactionId2, request.DestinationChain).ConfigureAwait(false); + // It must then propagate the transactionId to the other nodes so that they know they should confirm it. + // The reason why each node doesn't simply maintain its own transaction counter, is that it can't be guaranteed + // that a transaction won't be submitted out-of-turn by a rogue or malfunctioning federation multisig node. + // The coordination mechanism safeguards against this, as any such spurious transaction will not receive acceptance votes. + // TODO: The transactionId should be accompanied by the hash of the submission transaction on the Ethereum chain so that it can be verified - BigInteger agreedTransactionId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); + BigInteger transactionId2 = this.interopTransactionManager.GetCandidateTransactionId(request.RequestId); - if (agreedTransactionId != BigInteger.MinusOne) + if (transactionId2 != BigInteger.MinusOne) { - this.logger.LogInformation("Transaction {0} has received sufficient votes, it should now start getting confirmed by each peer.", agreedTransactionId); + await this.BroadcastCoordinationAsync(request.RequestId, transactionId2, request.DestinationChain).ConfigureAwait(false); - request.RequestStatus = ConversionRequestStatus.VoteFinalised; + BigInteger agreedTransactionId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); + + if (agreedTransactionId != BigInteger.MinusOne) + { + this.logger.LogInformation("Transaction {0} has received sufficient votes, it should now start getting confirmed by each peer.", agreedTransactionId); + + request.RequestStatus = ConversionRequestStatus.VoteFinalised; + } } - } - break; - } + break; + } case (ConversionRequestStatus.VoteFinalised): - { - BigInteger transactionId3 = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); - - if (transactionId3 != BigInteger.MinusOne) { - // The originator isn't responsible for anything further at this point, except for periodically checking the confirmation count. - // The non-originators also need to monitor the confirmation count so that they know when to mark the transaction as processed locally. - BigInteger confirmationCount = await clientForDestChain.GetConfirmationCountAsync(transactionId3).ConfigureAwait(false); + BigInteger transactionId3 = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); - if (confirmationCount >= 6) + if (transactionId3 != BigInteger.MinusOne) { - this.logger.LogInformation("Transaction {0} has received at least 6 confirmations, it will be automatically executed by the multisig contract.", transactionId3); + // The originator isn't responsible for anything further at this point, except for periodically checking the confirmation count. + // The non-originators also need to monitor the confirmation count so that they know when to mark the transaction as processed locally. + BigInteger confirmationCount = await clientForDestChain.GetConfirmationCountAsync(transactionId3).ConfigureAwait(false); - request.RequestStatus = ConversionRequestStatus.Processed; - request.Processed = true; + if (confirmationCount >= 6) + { + this.logger.LogInformation("Transaction {0} has received at least 6 confirmations, it will be automatically executed by the multisig contract.", transactionId3); - // We no longer need to track votes for this transaction. - this.interopTransactionManager.RemoveTransaction(request.RequestId); - } - else - { - this.logger.LogInformation("Transaction {0} has finished voting but does not yet have 8 confirmations, re-broadcasting votes to peers.", transactionId3); + request.RequestStatus = ConversionRequestStatus.Processed; + request.Processed = true; + + // We no longer need to track votes for this transaction. + this.interopTransactionManager.RemoveTransaction(request.RequestId); + } + else + { + this.logger.LogInformation("Transaction {0} has finished voting but does not yet have 8 confirmations, re-broadcasting votes to peers.", transactionId3); - // There are not enough confirmations yet. - // Even though the vote is finalised, other nodes may come and go. So we re-broadcast the finalised votes to all federation peers. - // Nodes will simply ignore the messages if they are not relevant. + // There are not enough confirmations yet. + // Even though the vote is finalised, other nodes may come and go. So we re-broadcast the finalised votes to all federation peers. + // Nodes will simply ignore the messages if they are not relevant. - await this.BroadcastCoordinationAsync(request.RequestId, transactionId3, request.DestinationChain).ConfigureAwait(false); + await this.BroadcastCoordinationAsync(request.RequestId, transactionId3, request.DestinationChain).ConfigureAwait(false); - // No state transition here, we are waiting for sufficient confirmations. + // No state transition here, we are waiting for sufficient confirmations. + } } - } - break; - } + break; + } case (ConversionRequestStatus.NotOriginator): - { - // If not the originator, this node needs to determine what multisig wallet transactionId it should confirm. - // Initially there will not be a quorum of nodes that agree on the transactionId. - // So each node needs to satisfy itself that the transactionId sent by the originator exists in the multisig wallet. - // This is done within the InteropBehavior automatically, we just check each poll loop if a transaction has enough votes yet. - // Each node must only ever confirm a single transactionId for a given conversion transaction. - BigInteger agreedUponId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); - - if (agreedUponId != BigInteger.MinusOne) { - this.logger.LogInformation("Quorum reached for conversion transaction {0} with transactionId {1}, submitting confirmation to contract.", request.RequestId, agreedUponId); - - // Once a quorum is reached, each node confirms the agreed transactionId. - // If the originator or some other nodes renege on their vote, the current node will not re-confirm a different transactionId. - string confirmationHash = await clientForDestChain.ConfirmTransactionAsync(agreedUponId).ConfigureAwait(false); + // If not the originator, this node needs to determine what multisig wallet transactionId it should confirm. + // Initially there will not be a quorum of nodes that agree on the transactionId. + // So each node needs to satisfy itself that the transactionId sent by the originator exists in the multisig wallet. + // This is done within the InteropBehavior automatically, we just check each poll loop if a transaction has enough votes yet. + // Each node must only ever confirm a single transactionId for a given conversion transaction. + BigInteger agreedUponId = this.interopTransactionManager.GetAgreedTransactionId(request.RequestId, 6); + + if (agreedUponId != BigInteger.MinusOne) + { + this.logger.LogInformation("Quorum reached for conversion transaction {0} with transactionId {1}, submitting confirmation to contract.", request.RequestId, agreedUponId); - this.logger.LogInformation("The hash of the confirmation transaction for conversion transaction {0} was {1}.", request.RequestId, confirmationHash); + // Once a quorum is reached, each node confirms the agreed transactionId. + // If the originator or some other nodes renege on their vote, the current node will not re-confirm a different transactionId. + string confirmationHash = await clientForDestChain.ConfirmTransactionAsync(agreedUponId).ConfigureAwait(false); - request.RequestStatus = ConversionRequestStatus.VoteFinalised; - } - else - { - BigInteger transactionId4 = this.interopTransactionManager.GetCandidateTransactionId(request.RequestId); + this.logger.LogInformation("The hash of the confirmation transaction for conversion transaction {0} was {1}.", request.RequestId, confirmationHash); - if (transactionId4 != BigInteger.MinusOne) + request.RequestStatus = ConversionRequestStatus.VoteFinalised; + } + else { - this.logger.LogInformation("Broadcasting vote (transactionId {0}) for conversion transaction {1}.", transactionId4, request.RequestId); + BigInteger transactionId4 = this.interopTransactionManager.GetCandidateTransactionId(request.RequestId); + + if (transactionId4 != BigInteger.MinusOne) + { + this.logger.LogInformation("Broadcasting vote (transactionId {0}) for conversion transaction {1}.", transactionId4, request.RequestId); + + this.interopTransactionManager.AddVote(request.RequestId, transactionId4, this.federationManager.CurrentFederationKey.PubKey); - this.interopTransactionManager.AddVote(request.RequestId, transactionId4, this.federationManager.CurrentFederationKey.PubKey); + await this.BroadcastCoordinationAsync(request.RequestId, transactionId4, request.DestinationChain).ConfigureAwait(false); + } - await this.BroadcastCoordinationAsync(request.RequestId, transactionId4, request.DestinationChain).ConfigureAwait(false); + // No state transition here, as we are waiting for the candidate transactionId to progress to an agreed upon transactionId via a quorum. } - // No state transition here, as we are waiting for the candidate transactionId to progress to an agreed upon transactionId via a quorum. + break; } - - break; - } } // Make sure that any state transitions are persisted to storage. @@ -553,7 +554,7 @@ private BigInteger CoinsToWei(Money coins) return baseCurrencyUnits; } - + /// public void Dispose() { diff --git a/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs b/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs index b51a864ab8..c61d106193 100644 --- a/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs +++ b/src/Stratis.Bitcoin.Features.Interop/InteropSettings.cs @@ -1,6 +1,6 @@ using System; using Stratis.Bitcoin.Configuration; -using Stratis.Features.FederatedPeg.Conversion; +using Stratis.Bitcoin.Features.Wallet; namespace Stratis.Bitcoin.Features.Interop { @@ -10,7 +10,6 @@ public class InteropSettings public BNBInteropSettings BNBSettings { get; set; } - public InteropSettings(NodeSettings nodeSettings) { this.ETHSettings = new ETHInteropSettings(nodeSettings); @@ -22,13 +21,13 @@ public ETHInteropSettings GetSettingsByChain(DestinationChain chain) switch (chain) { case DestinationChain.ETH: - { - return this.ETHSettings; - } + { + return this.ETHSettings; + } case DestinationChain.BNB: - { - return this.BNBSettings; - } + { + return this.BNBSettings; + } } throw new NotImplementedException("Provided chain type not supported: " + chain); @@ -56,7 +55,7 @@ public class ETHInteropSettings /// Passphrase for the ethereum account. public string Passphrase { get; set; } - + /// The gas limit for Ethereum interoperability transactions. public int GasLimit { get; set; } @@ -67,10 +66,10 @@ public class ETHInteropSettings /// This is intended for future functionality and should therefore not be provided/set yet. public string InteropContractCirrusAddress { get; set; } - + /// This is intended for future functionality and should therefore not be provided/set yet. public string InteropContractAddress { get; set; } - + #endregion public ETHInteropSettings(NodeSettings nodeSettings) @@ -105,7 +104,7 @@ public ETHInteropSettings(NodeSettings nodeSettings) if (string.IsNullOrWhiteSpace(this.ClientUrl)) throw new Exception($"Cannot initialize interoperability feature without -{clientUrlKey} specified."); } - + protected virtual string GetSettingsPrefix() { return "eth_"; diff --git a/src/Stratis.Bitcoin.Features.Interop/Payloads/InteropCoordinationPayload.cs b/src/Stratis.Bitcoin.Features.Interop/Payloads/InteropCoordinationPayload.cs index aecd421283..fd4b42686c 100644 --- a/src/Stratis.Bitcoin.Features.Interop/Payloads/InteropCoordinationPayload.cs +++ b/src/Stratis.Bitcoin.Features.Interop/Payloads/InteropCoordinationPayload.cs @@ -1,6 +1,6 @@ using NBitcoin; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.P2P.Protocol.Payloads; -using Stratis.Features.FederatedPeg.Conversion; namespace Stratis.Bitcoin.Features.Interop.Payloads { @@ -13,9 +13,9 @@ public class InteropCoordinationPayload : Payload private int destinationChain; public string RequestId => this.requestId; - + public int TransactionId => this.transactionId; - + public string Signature => this.signature; public DestinationChain DestinationChain { get { return (DestinationChain)this.destinationChain; } set { this.destinationChain = (int)value; } } diff --git a/src/Stratis.Bitcoin.Features.Wallet.Tests/InterFluxOpReturnEncoderTest.cs b/src/Stratis.Bitcoin.Features.Wallet.Tests/InterFluxOpReturnEncoderTest.cs index 0188fabe9a..8691a5f8ff 100644 --- a/src/Stratis.Bitcoin.Features.Wallet.Tests/InterFluxOpReturnEncoderTest.cs +++ b/src/Stratis.Bitcoin.Features.Wallet.Tests/InterFluxOpReturnEncoderTest.cs @@ -11,7 +11,7 @@ public void CanEncodeAndDecode() int chain = 3; string address = "0x51EC92A3aB8cfcA412Ea43766A9259523fC81501"; - string encoded = InterFluxOpReturnEncoder.Encode(chain, address); + string encoded = InterFluxOpReturnEncoder.Encode((DestinationChain)chain, address); bool success = InterFluxOpReturnEncoder.TryDecode(encoded, out int resultChain, out string resultAddress); @@ -28,6 +28,18 @@ public void CanEncodeAndDecode() Assert.Equal(address, resultAddress2); } + [Fact] + public void EncodeAndDecodeETHAddress() + { + string address = "0xd2390da742872294BE05dc7359D7249d7C79460E"; + string encoded = InterFluxOpReturnEncoder.Encode(DestinationChain.ETH, address); + bool success = InterFluxOpReturnEncoder.TryDecode(encoded, out int resultChain, out string resultAddress); + + Assert.True(success); + Assert.Equal(DestinationChain.ETH, (DestinationChain)resultChain); + Assert.Equal(address, resultAddress); + } + [Fact] public void DecodeDoesntThrowWhenFormatIsIncorrect() { @@ -35,6 +47,7 @@ public void DecodeDoesntThrowWhenFormatIsIncorrect() Assert.False(InterFluxOpReturnEncoder.TryDecode("INTER3_", out int _, out string _)); Assert.False(InterFluxOpReturnEncoder.TryDecode("INTERefsdvsdvdsvsdv", out int _, out string _)); Assert.False(InterFluxOpReturnEncoder.TryDecode("xvev456545cwsdfFSXVB365", out int _, out string _)); + Assert.False(InterFluxOpReturnEncoder.TryDecode("INTER1_aaaa", out int _, out string _)); } } -} +} \ No newline at end of file diff --git a/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs b/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs index 4914a9e0c1..b86775d903 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/Controllers/WalletController.cs @@ -445,7 +445,7 @@ public async Task BuildTransaction([FromBody] BuildTransactionReq public async Task BuildInterFluxTransaction([FromBody] BuildInterFluxTransactionRequest request, CancellationToken cancellationToken = default(CancellationToken)) { - request.OpReturnData = InterFluxOpReturnEncoder.Encode(request.DestinationChain, request.DestinationAddress); + request.OpReturnData = InterFluxOpReturnEncoder.Encode((DestinationChain)request.DestinationChain, request.DestinationAddress); return await this.Execute(request, cancellationToken, async (req, token) => Json(await this.walletService.BuildTransaction(req, token))); diff --git a/src/Stratis.Bitcoin.Features.Wallet/DepositValidationHelper.cs b/src/Stratis.Bitcoin.Features.Wallet/DepositValidationHelper.cs index 8b7e481fdc..de57794448 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/DepositValidationHelper.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/DepositValidationHelper.cs @@ -40,7 +40,7 @@ public static bool TryGetDepositsToMultisig(Network network, Transaction transac depositsToMultisig = transaction.Outputs.Where(output => output.ScriptPubKey == depositScript && output.Value >= crossChainTransferMinimum).ToList(); - + return depositsToMultisig.Any(); } @@ -49,9 +49,10 @@ public static bool TryGetTarget(Transaction transaction, IOpReturnDataReader opR conversion = false; targetChain = 0 /* DestinationChain.STRAX */; - // Check the common case first. + // First check cross chain transfers from the STRAX to Cirrus network or vice versa. if (!opReturnDataReader.TryGetTargetAddress(transaction, out targetAddress)) { + // Else try and validate the destination adress by the destination chain. byte[] opReturnBytes = OpReturnDataReader.SelectBytesContentFromOpReturn(transaction).FirstOrDefault(); if (opReturnBytes != null && InterFluxOpReturnEncoder.TryDecode(opReturnBytes, out int destinationChain, out targetAddress)) @@ -61,7 +62,7 @@ public static bool TryGetTarget(Transaction transaction, IOpReturnDataReader opR else return false; - conversion = true; + conversion = true; } return true; diff --git a/src/Stratis.Bitcoin.Features.Wallet/InterFluxOpReturnEncoder.cs b/src/Stratis.Bitcoin.Features.Wallet/InterFluxOpReturnEncoder.cs index d1e16c7e99..6c04c97e80 100644 --- a/src/Stratis.Bitcoin.Features.Wallet/InterFluxOpReturnEncoder.cs +++ b/src/Stratis.Bitcoin.Features.Wallet/InterFluxOpReturnEncoder.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.RegularExpressions; namespace Stratis.Bitcoin.Features.Wallet { @@ -7,9 +8,9 @@ public static class InterFluxOpReturnEncoder { private static string InterFluxPrefix = "INTER"; - public static string Encode(int destinationChain, string address) + public static string Encode(DestinationChain destinationChain, string address) { - return InterFluxPrefix + destinationChain + "_" + address; + return InterFluxPrefix + (int)destinationChain + "_" + address; } public static bool TryDecode(string opReturnData, out int destinationChain, out string address) @@ -23,15 +24,35 @@ public static bool TryDecode(string opReturnData, out int destinationChain, out if (prefixIndex == -1 || separatorIndex == -1) return false; + // Try and extract the destination chain. if (!int.TryParse(opReturnData.Substring(InterFluxPrefix.Length, separatorIndex - InterFluxPrefix.Length), out destinationChain)) return false; address = opReturnData.Substring(separatorIndex + 1); - if (string.IsNullOrEmpty(address)) - return false; + // If the destination chain is Ethereum, try and validate. + // Once conversions to other chains are implemented, we can add validation here. + if (destinationChain == (int)DestinationChain.ETH) + { + if (!TryConvertValidOpReturnDataToETHAddress(address)) + return false; + } + + return !string.IsNullOrEmpty(address); + } - return true; + /// + /// Try and validate an address on the Ethereum chain. + /// + /// The address to validate. + /// true if valid, false otherwise. + private static bool TryConvertValidOpReturnDataToETHAddress(string potentialETHAddress) + { + // Attempt to parse the string. An Ethereum address is 42 characters: + // 0x - initial prefix + // <20 bytes> - rightmost 20 bytes of the Keccak hash of a public key, encoded as hex + Match match = Regex.Match(potentialETHAddress, @"^0x([A-Fa-f0-9]{40})$", RegexOptions.IgnoreCase); + return match.Success; } public static bool TryDecode(byte[] opReturnData, out int destinationChain, out string address) @@ -41,4 +62,16 @@ public static bool TryDecode(byte[] opReturnData, out int destinationChain, out return TryDecode(stringData, out destinationChain, out address); } } + + /// Chains supported by InterFlux integration. + public enum DestinationChain + { + STRAX = 0, // Stratis + ETH, // Ethereum + BNB, // Binance + + ETC, // Ethereum classic + AVAX, // Avalanche + ADA, // Cardano + } } diff --git a/src/Stratis.Bitcoin/OpReturnDataReader.cs b/src/Stratis.Bitcoin/OpReturnDataReader.cs index 4c9c4ab2bd..9f7f879c24 100644 --- a/src/Stratis.Bitcoin/OpReturnDataReader.cs +++ b/src/Stratis.Bitcoin/OpReturnDataReader.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using NBitcoin; using NLog; using TracerAttributes; @@ -26,8 +25,6 @@ public interface IOpReturnDataReader /// true if address was extracted; false otherwise. bool TryGetTargetAddress(Transaction transaction, out string address); - bool TryGetTargetETHAddress(Transaction transaction, out string address); - /// /// Tries to find a single OP_RETURN output that can be interpreted as a transaction id. /// @@ -68,24 +65,6 @@ public bool TryGetTargetAddress(Transaction transaction, out string address) return true; } - public bool TryGetTargetETHAddress(Transaction transaction, out string address) - { - var opReturnAddresses = SelectBytesContentFromOpReturn(transaction) - .Select(this.TryConvertValidOpReturnDataToETHAddress) - .Where(s => s != null) - .Distinct(StringComparer.InvariantCultureIgnoreCase).ToList(); - - // A standard OP_RETURN is not long enough to fit more than 1 Ethereum address, but a non-standard transaction could have multiple. - if (opReturnAddresses.Count != 1) - { - address = null; - return false; - } - - address = opReturnAddresses[0]; - return true; - } - /// public bool TryGetTransactionId(Transaction transaction, out string txId) { @@ -134,22 +113,6 @@ private string TryConvertValidOpReturnDataToAddress(byte[] data) } } - private string TryConvertValidOpReturnDataToETHAddress(byte[] data) - { - // After removing the RETURN operator, convert the remaining bytes to our candidate address. - string destination = Encoding.UTF8.GetString(data); - - // Attempt to parse the string. An Ethereum address is 42 characters: - // 0x - initial prefix - // <20 bytes> - rightmost 20 bytes of the Keccak hash of a public key, encoded as hex - Match match = Regex.Match(destination, @"^0x([A-Fa-f0-9]{40})$", RegexOptions.IgnoreCase); - - if (!match.Success) - return null; - - return destination; - } - private string TryConvertValidOpReturnDataToHash(byte[] data) { // Attempt to parse the hash. Validates the uint256 string. diff --git a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs index ed1f9a5de4..c71728b428 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/CrossChainTransferStoreTests.cs @@ -12,9 +12,9 @@ using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Controllers; using Stratis.Bitcoin.Features.MemoryPool; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Features.Wallet.Models; using Stratis.Bitcoin.Tests.Common; -using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Events; using Stratis.Features.FederatedPeg.Interfaces; using Stratis.Features.FederatedPeg.Models; diff --git a/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs b/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs index 9e2e7f7090..feefe4b9ed 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/DepositExtractorTests.cs @@ -7,7 +7,6 @@ using Stratis.Bitcoin; using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Networks; -using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Interfaces; using Stratis.Features.FederatedPeg.SourceChain; using Stratis.Features.FederatedPeg.Tests.Utils; @@ -271,23 +270,23 @@ public void ExtractLargeConversionDeposits_ReturnDeposits_AboveNormalThreshold() // Create the target address. BitcoinPubKeyAddress targetAddress = this.addressHelper.GetNewTargetChainPubKeyAddress(); byte[] opReturnBytes = Encoding.UTF8.GetBytes(targetAddress.ToString()); - + // Set amount to be less than deposit minimum CreateDepositTransaction(targetAddress, block, FederatedPegSettings.CrossChainTransferMinimum - 1, opReturnBytes); - + // Set amount to be less than the small threshold amount. CreateDepositTransaction(targetAddress, block, this.federationSettings.SmallDepositThresholdAmount - 1, opReturnBytes); - + // Set amount to be exactly the normal threshold amount. CreateDepositTransaction(targetAddress, block, this.federationSettings.NormalDepositThresholdAmount, opReturnBytes); - byte[] ethOpReturnBytes = Encoding.UTF8.GetBytes(InterFluxOpReturnEncoder.Encode((int)DestinationChain.ETH, TargetETHAddress)); + byte[] ethOpReturnBytes = Encoding.UTF8.GetBytes(InterFluxOpReturnEncoder.Encode(DestinationChain.ETH, TargetETHAddress)); // Set amount to be equal to the normal threshold amount. - CreateConversionTransaction(TargetETHAddress, block, this.federationSettings.NormalDepositThresholdAmount, ethOpReturnBytes); + CreateConversionTransaction(block, this.federationSettings.NormalDepositThresholdAmount, ethOpReturnBytes); // Set amount to be greater than the conversion deposit minimum amount. - CreateConversionTransaction(TargetETHAddress, block, Money.Coins(DepositExtractor.ConversionTransactionMinimum + 1), ethOpReturnBytes); + CreateConversionTransaction(block, Money.Coins(DepositExtractor.ConversionTransactionMinimum + 1), ethOpReturnBytes); int blockHeight = 12345; IReadOnlyList extractedDeposits = this.depositExtractor.ExtractDepositsFromBlock(block, blockHeight, new[] { DepositRetrievalType.ConversionLarge }); @@ -301,7 +300,7 @@ public void ExtractLargeConversionDeposits_ReturnDeposits_AboveNormalThreshold() } } - private Transaction CreateConversionTransaction(string targetETHAddress, Block block, Money depositAmount, byte[] opReturnBytes) + private Transaction CreateConversionTransaction(Block block, Money depositAmount, byte[] opReturnBytes) { // Create the conversion transaction. Transaction conversionTransaction = this.transactionBuilder.BuildOpReturnTransaction(this.addressHelper.SourceChainMultisigAddress, opReturnBytes, depositAmount); @@ -309,12 +308,6 @@ private Transaction CreateConversionTransaction(string targetETHAddress, Block b // Add the conversion transaction to the block. block.AddTransaction(conversionTransaction); - this.opReturnDataReader.TryGetTargetETHAddress(conversionTransaction, out string _).Returns(callInfo => - { - callInfo[1] = targetETHAddress; - return true; - }); - return conversionTransaction; } diff --git a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs index d4f257f260..014d3a1ab0 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksProviderTests.cs @@ -13,7 +13,6 @@ using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Tests.Common; using Stratis.Bitcoin.Utilities; -using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Interfaces; using Stratis.Features.FederatedPeg.Models; using Stratis.Features.FederatedPeg.SourceChain; @@ -54,7 +53,7 @@ public MaturedBlocksProviderTests() this.addressHelper = new MultisigAddressHelper(this.network, this.mainChainNetwork); this.targetAddress = this.addressHelper.GetNewTargetChainPubKeyAddress(); - this.opReturnBytes = Encoding.UTF8.GetBytes(InterFluxOpReturnEncoder.Encode((int)DestinationChain.STRAX, this.targetAddress.ToString())); + this.opReturnBytes = Encoding.UTF8.GetBytes(InterFluxOpReturnEncoder.Encode(DestinationChain.STRAX, this.targetAddress.ToString())); this.federatedPegSettings = Substitute.For(); this.federatedPegSettings.MultiSigRedeemScript.Returns(this.addressHelper.PayToMultiSig); diff --git a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs index 6ca5b31a34..79b7a7ccff 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/MaturedBlocksSyncManagerTests.cs @@ -134,4 +134,4 @@ public Task ExposedSyncBatchOfBlocksAsync() } } } -} +} \ No newline at end of file diff --git a/src/Stratis.Features.FederatedPeg.Tests/Utils/TestingValues.cs b/src/Stratis.Features.FederatedPeg.Tests/Utils/TestingValues.cs index 031261dd1b..82c90f2cb1 100644 --- a/src/Stratis.Features.FederatedPeg.Tests/Utils/TestingValues.cs +++ b/src/Stratis.Features.FederatedPeg.Tests/Utils/TestingValues.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; using NBitcoin; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Utilities; -using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.Interfaces; using Stratis.Features.FederatedPeg.Models; using Stratis.Features.FederatedPeg.SourceChain; diff --git a/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs b/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs index 37efa869ad..3f31df7793 100644 --- a/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs +++ b/src/Stratis.Features.FederatedPeg/Conversion/ConversionRequest.cs @@ -1,4 +1,5 @@ using NBitcoin; +using Stratis.Bitcoin.Features.Wallet; namespace Stratis.Features.FederatedPeg.Conversion { @@ -21,17 +22,6 @@ public enum ConversionRequestStatus NotOriginator } - /// Chains supported by InterFlux integration. - public enum DestinationChain - { - STRAX = 0, // Stratis - ETH, // Ethereum - BNB, // Binance - - ETC, // Ethereum classic - AVAX, // Avalanche - ADA, // Cardano - } /// Request to mint or burn wSTRAX. /// diff --git a/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs b/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs index 518097d980..1aecefea1c 100644 --- a/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs +++ b/src/Stratis.Features.FederatedPeg/Interfaces/IDeposit.cs @@ -1,7 +1,7 @@ using NBitcoin; using Newtonsoft.Json; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Bitcoin.Utilities.JsonConverters; -using Stratis.Features.FederatedPeg.Conversion; using Stratis.Features.FederatedPeg.SourceChain; namespace Stratis.Features.FederatedPeg.Interfaces diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs b/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs index 77111ba145..d2b4776e1d 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/Deposit.cs @@ -1,6 +1,6 @@ using NBitcoin; using Newtonsoft.Json; -using Stratis.Features.FederatedPeg.Conversion; +using Stratis.Bitcoin.Features.Wallet; using Stratis.Features.FederatedPeg.Interfaces; namespace Stratis.Features.FederatedPeg.SourceChain diff --git a/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs b/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs index 234c3d666d..05068afdec 100644 --- a/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs +++ b/src/Stratis.Features.FederatedPeg/SourceChain/DepositExtractor.cs @@ -92,9 +92,11 @@ public IReadOnlyList ExtractDepositsFromBlock(Block block, int blockHe /// public IDeposit ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash) { + // If there are no deposits to the multsig (i.e. cross chain transfers) do nothing. if (!DepositValidationHelper.TryGetDepositsToMultisig(this.network, transaction, FederatedPegSettings.CrossChainTransferMinimum, out List depositsToMultisig)) return null; + // If there are deposits to the multsig (i.e. cross chain transfers), try and extract and validate the address by the specfied destination chain. if (!DepositValidationHelper.TryGetTarget(transaction, this.opReturnDataReader, out bool conversionTransaction, out string targetAddress, out int targetChain)) return null;