diff --git a/protobuf/aelf/core.proto b/protobuf/aelf/core.proto index e45d7e581d..4fe3a5bcf4 100644 --- a/protobuf/aelf/core.proto +++ b/protobuf/aelf/core.proto @@ -22,6 +22,16 @@ message Transaction { bytes signature = 10000; } +message TransactionAndChainId { + Transaction transaction = 1; + int32 chain_id = 2; +} + +message MultiTransaction { + repeated TransactionAndChainId transactions = 1; + bytes signature = 10000; +} + message StatePath { // The partial path of the state path. repeated string parts = 1; diff --git a/src/AElf.Types/Types/MultiTransaction.cs b/src/AElf.Types/Types/MultiTransaction.cs new file mode 100644 index 0000000000..e45c087ae7 --- /dev/null +++ b/src/AElf.Types/Types/MultiTransaction.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using Google.Protobuf; + +namespace AElf.Types +{ + public partial class MultiTransaction + { + private Hash _transactionId; + + public Hash GetHash() + { + if (_transactionId == null) + _transactionId = HashHelper.ComputeFrom(GetSignatureData()); + + return _transactionId; + } + + public ValidationStatus VerifyFields() + { + if (Transactions.Count < 2) + return ValidationStatus.OnlyOneTransaction; + + if (!AllTransactionsHaveSameFrom()) + return ValidationStatus.MoreThanOneFrom; + + if (Transactions.Any(transaction => string.IsNullOrEmpty(transaction.Transaction.MethodName))) + return ValidationStatus.MethodNameIsEmpty; + + if (Transactions.Any(transaction => transaction.Transaction.Signature.IsEmpty)) + { + return ValidationStatus.UserSignatureIsEmpty; + } + + return ValidationStatus.Success; + } + + public enum ValidationStatus + { + Success, + OnlyOneTransaction, + MoreThanOneFrom, + MethodNameIsEmpty, + UserSignatureIsEmpty + } + + private bool AllTransactionsHaveSameFrom() + { + var firstFrom = Transactions[0].Transaction.From; + return Transactions.All(transaction => transaction.Transaction.From == firstFrom); + } + + private byte[] GetSignatureData() + { + var verifyResult = VerifyFields(); + if (verifyResult != ValidationStatus.Success) + throw new InvalidOperationException($"Invalid multi transaction, {verifyResult.ToString()}: {this}"); + + if (Signature.IsEmpty) + return this.ToByteArray(); + + var multiTransaction = Clone(); + multiTransaction.Signature = ByteString.Empty; + return multiTransaction.ToByteArray(); + } + } +} \ No newline at end of file diff --git a/src/AElf.WebApp.Application.Chain/ChainApplicationWebAppAElfModule.cs b/src/AElf.WebApp.Application.Chain/ChainApplicationWebAppAElfModule.cs index d5820e1828..7455e1a9ad 100644 --- a/src/AElf.WebApp.Application.Chain/ChainApplicationWebAppAElfModule.cs +++ b/src/AElf.WebApp.Application.Chain/ChainApplicationWebAppAElfModule.cs @@ -22,5 +22,8 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services .AddSingleton(); + + Configure(context.Services.GetConfiguration() + .GetSection("MultiTransaction")); } } \ No newline at end of file diff --git a/src/AElf.WebApp.Application.Chain/Dto/SendMultiTransactionInput.cs b/src/AElf.WebApp.Application.Chain/Dto/SendMultiTransactionInput.cs new file mode 100644 index 0000000000..4a0afff9e1 --- /dev/null +++ b/src/AElf.WebApp.Application.Chain/Dto/SendMultiTransactionInput.cs @@ -0,0 +1,6 @@ +namespace AElf.WebApp.Application.Chain.Dto; + +public class SendMultiTransactionInput : SendTransactionsInput +{ + +} \ No newline at end of file diff --git a/src/AElf.WebApp.Application.Chain/Dto/SendMultiTransactionOutput.cs b/src/AElf.WebApp.Application.Chain/Dto/SendMultiTransactionOutput.cs new file mode 100644 index 0000000000..3fcc80a61f --- /dev/null +++ b/src/AElf.WebApp.Application.Chain/Dto/SendMultiTransactionOutput.cs @@ -0,0 +1,6 @@ +namespace AElf.WebApp.Application.Chain.Dto; + +public class SendMultiTransactionOutput +{ + public string[] TransactionIds { get; set; } +} diff --git a/src/AElf.WebApp.Application.Chain/Error.cs b/src/AElf.WebApp.Application.Chain/Error.cs index e194dacbad..f1782f4f9b 100644 --- a/src/AElf.WebApp.Application.Chain/Error.cs +++ b/src/AElf.WebApp.Application.Chain/Error.cs @@ -11,10 +11,12 @@ public static class Error public const int InvalidOffset = 20006; public const int InvalidLimit = 20007; public const int InvalidTransaction = 20008; + public const int InvalidXTransaction = 20009; public const int InvalidContractAddress = 20010; public const int NoMatchMethodInContractAddress = 20011; public const int InvalidParams = 20012; public const int InvalidSignature = 20013; + public const int InvalidGatewaySignature = 20014; public const string NeedBasicAuth = "User name and password for basic auth should be set"; public static readonly Dictionary Message = new() @@ -26,9 +28,11 @@ public static class Error { InvalidOffset, "Offset must greater than or equal to 0" }, { InvalidLimit, "Limit must between 0 and 100" }, { InvalidTransaction, "Invalid transaction information" }, + { InvalidXTransaction, "Invalid multi-transaction information" }, { InvalidContractAddress, "Invalid contract address" }, { NoMatchMethodInContractAddress, "No match method in contract address" }, { InvalidParams, "Invalid params" }, - { InvalidSignature, "Invalid signature" } + { InvalidSignature, "Invalid signature" }, + { InvalidGatewaySignature, "Invalid gateway signature" } }; } \ No newline at end of file diff --git a/src/AElf.WebApp.Application.Chain/MultiTransactionOptions.cs b/src/AElf.WebApp.Application.Chain/MultiTransactionOptions.cs new file mode 100644 index 0000000000..076fe4502c --- /dev/null +++ b/src/AElf.WebApp.Application.Chain/MultiTransactionOptions.cs @@ -0,0 +1,7 @@ +namespace AElf.WebApp.Application.Chain; + +public class MultiTransactionOptions +{ + public string GatewayAddress { get; set; } + public string GatewayContractAddress { get; set; } +} \ No newline at end of file diff --git a/src/AElf.WebApp.Application.Chain/Services/TransactionAppService.cs b/src/AElf.WebApp.Application.Chain/Services/TransactionAppService.cs index f4836f8128..1fe31f00a6 100644 --- a/src/AElf.WebApp.Application.Chain/Services/TransactionAppService.cs +++ b/src/AElf.WebApp.Application.Chain/Services/TransactionAppService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using AElf.Cryptography; using AElf.Kernel; using AElf.Kernel.Blockchain.Application; using AElf.Kernel.FeeCalculation.Extensions; @@ -36,6 +37,8 @@ public interface ITransactionAppService Task SendTransactionAsync(SendTransactionInput input); + Task SendMultiTransactionAsync(SendMultiTransactionInput input); + Task SendTransactionsAsync(SendTransactionsInput input); Task CalculateTransactionFeeAsync(CalculateTransactionFeeInput input); @@ -49,19 +52,21 @@ public class TransactionAppService : AElfAppService, ITransactionAppService private readonly ITransactionResultStatusCacheProvider _transactionResultStatusCacheProvider; private readonly IPlainTransactionExecutingService _plainTransactionExecutingService; private readonly WebAppOptions _webAppOptions; - + private readonly MultiTransactionOptions _multiTransactionOptions; public TransactionAppService(ITransactionReadOnlyExecutionService transactionReadOnlyExecutionService, IBlockchainService blockchainService, IObjectMapper objectMapper, ITransactionResultStatusCacheProvider transactionResultStatusCacheProvider, IPlainTransactionExecutingService plainTransactionExecutingService, - IOptionsMonitor webAppOptions) + IOptionsMonitor webAppOptions, + IOptionsSnapshot multiTransactionSignerOptions) { _transactionReadOnlyExecutionService = transactionReadOnlyExecutionService; _blockchainService = blockchainService; _objectMapper = objectMapper; _transactionResultStatusCacheProvider = transactionResultStatusCacheProvider; _plainTransactionExecutingService = plainTransactionExecutingService; + _multiTransactionOptions = multiTransactionSignerOptions.Value; _webAppOptions = webAppOptions.CurrentValue; LocalEventBus = NullLocalEventBus.Instance; @@ -238,6 +243,64 @@ public async Task SendTransactionAsync(SendTransactionInp }; } + public async Task SendMultiTransactionAsync(SendMultiTransactionInput input) + { + var multiTxBytes = ByteArrayHelper.HexStringToByteArray(input.RawTransactions); + var multiTransaction = MultiTransaction.Parser.ParseFrom(multiTxBytes); + if (multiTransaction.VerifyFields() != MultiTransaction.ValidationStatus.Success) + { + throw new UserFriendlyException(Error.Message[Error.InvalidTransaction], + Error.InvalidTransaction.ToString()); + } + + CryptoHelper.RecoverPublicKey(multiTransaction.Signature.ToByteArray(), multiTransaction.GetHash().ToByteArray(), out var pubkey); + + if (!await IsGatewayAddress(Address.FromPublicKey(pubkey))) + { + throw new UserFriendlyException(Error.Message[Error.InvalidGatewaySignature], + Error.InvalidGatewaySignature.ToString()); + } + + var chain = await _blockchainService.GetChainAsync(); + var txListOfCurrentChain = multiTransaction.Transactions + .Where(t => t.ChainId == chain.Id) + .Select(t => t.Transaction.ToByteArray().ToHex()).ToArray(); + var txIds = await PublishTransactionsAsync(txListOfCurrentChain); + + return new SendMultiTransactionOutput + { + TransactionIds = txIds + }; + } + + private async Task IsGatewayAddress(Address address) + { + if (string.IsNullOrEmpty(_multiTransactionOptions.GatewayAddress) && + string.IsNullOrEmpty(_multiTransactionOptions.GatewayContractAddress)) + { + return true; + } + + if (!string.IsNullOrEmpty(_multiTransactionOptions.GatewayContractAddress)) + { + var chain = await _blockchainService.GetChainAsync(); + var isGatewayAddressBytes = await CallReadOnlyAsync(new Transaction + { + From = address, + To = Address.FromBase58(_multiTransactionOptions.GatewayContractAddress), + MethodName = "IsGatewayAddress", + Params = address.ToByteString(), + RefBlockNumber = chain.BestChainHeight, + RefBlockPrefix = BlockHelper.GetRefBlockPrefix(chain.BestChainHash) + }); + var isGatewayAddress = new BoolValue(); + isGatewayAddress.MergeFrom(isGatewayAddressBytes); + return isGatewayAddress.Value; + } + + return _multiTransactionOptions.GatewayAddress == address.ToBase58(); + } + /// /// Broadcast multiple transactions ///