From cdd3a18b3549bfcfa6e9e50d47dff78083307414 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 20 Apr 2022 01:12:13 +0200 Subject: [PATCH] Add command to download a contract (#209) * Add command to download a contract from a remote network into our local chain of choice * Process feedback * Add ExpressFindStatesAsync without key decoding and with paging support Add {} around exceptions Rename ExpressOracle * batch command * Resolve null warnings * handle duplicate contracts * process feedback * Update ExpressFindStatesAsync * pass storage pairs as IReadOnlyList instead of as an array * remove EncodedFoundStates * fix LastUsedContractId and relevant logic * - Limit contract download to single node consensus setup - rename DownloadParamsAsync -> DownloadContractStateAsync * check consensus node count before getting express node * get contract state via state service * fix storage persisting if local contract does not yet exist * Improve error message when state service plugin is not installed * Improve error message when contract already exists * Generic collections > object model collections * variable rename + format * simplify GetStateHeightAsync logic * minor fixed + format * reworked PersistContract * fix compile break * don't create empty array via new * reworked PersistContract * throw better exeption on missing contract + disallow native contract download * Remove unused code + touch up * very minor whitespace + cleanup * update changelog Co-authored-by: Harry Pierson Co-authored-by: Harry --- changelog.md | 1 + .../BatchCommand.BatchFileCommands.cs | 23 +- src/neoxp/Commands/BatchCommand.cs | 24 ++ .../Commands/ContractCommand.Download.cs | 77 +++++++ src/neoxp/Commands/ContractCommand.cs | 2 +- src/neoxp/Extensions/Extensions.cs | 43 ++++ src/neoxp/IExpressNode.cs | 3 + src/neoxp/Node/Modules/ExpressRpcMethods.cs | 20 +- .../Node/{ExpressOracle.cs => NodeUtility.cs} | 208 +++++++++++++++++- src/neoxp/Node/OfflineNode.cs | 23 +- src/neoxp/Node/OnlineNode.cs | 31 ++- 11 files changed, 441 insertions(+), 14 deletions(-) create mode 100644 src/neoxp/Commands/ContractCommand.Download.cs rename src/neoxp/Node/{ExpressOracle.cs => NodeUtility.cs} (53%) diff --git a/changelog.md b/changelog.md index 3a693a0a..7cde4998 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,7 @@ may not exactly match a publicly released version. #### Added * `show notifications` command +* `contract download` command (#209). Thank you to @ixje for contributing this code! * `--json` option to `wallet list` command (3ea29881d8be352cedaeebd8b8b16e49aee3aed6 and #216) * `--data` option to `contract deploy` command (#214) * `--timestamp-delta` option to `fastfwd` command (#224) diff --git a/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs b/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs index 93012aa0..7a5be682 100644 --- a/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs +++ b/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs @@ -29,7 +29,7 @@ internal class Create } [Command("contract")] - [Subcommand(typeof(Deploy), typeof(Invoke), typeof(Run))] + [Subcommand(typeof(Deploy), typeof(Download), typeof(Invoke), typeof(Run))] internal class Contract { [Command("deploy")] @@ -56,6 +56,27 @@ internal class Deploy [Option(Description = "Deploy contract regardless of name conflict")] internal bool Force { get; } } + + [Command("download")] + internal class Download + { + [Argument(0, Description = "Contract invocation hash")] + [Required] + internal string Contract { get; init; } = string.Empty; + + [Argument(1, Description = "URL of Neo JSON-RPC Node\nSpecify MainNet (default), TestNet or JSON-RPC URL")] + [Required] + internal string RpcUri { get; } = string.Empty; + + [Argument(2, Description = "Block height to get contract state for")] + [Required] + internal uint Height { get; } = 0; + + [Option(CommandOptionType.SingleOrNoValue, + Description = "Replace contract and storage if it already exists (Default: All)")] + [AllowedValues(StringComparison.OrdinalIgnoreCase, "All", "ContractOnly", "StorageOnly")] + internal ContractCommand.OverwriteForce Force { get; init; } = ContractCommand.OverwriteForce.None; + } [Command("invoke")] internal class Invoke diff --git a/src/neoxp/Commands/BatchCommand.cs b/src/neoxp/Commands/BatchCommand.cs index 1dba5f72..1c547542 100644 --- a/src/neoxp/Commands/BatchCommand.cs +++ b/src/neoxp/Commands/BatchCommand.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using McMaster.Extensions.CommandLineUtils; using Neo.BlockchainToolkit; +using NeoExpress.Node; namespace NeoExpress.Commands { @@ -122,6 +123,29 @@ await txExec.ContractDeployAsync( cmd.Model.Force).ConfigureAwait(false); break; } + case CommandLineApplication cmd: + { + if (cmd.Model.Height == 0) + { + throw new ArgumentException("Height cannot be 0. Please specify a height > 0"); + } + + if (chainManager.Chain.ConsensusNodes.Count != 1) + { + throw new ArgumentException("Contract download is only supported for single-node consensus"); + } + + var expressNode = txExec.ExpressNode; + var result = await NodeUtility.DownloadContractStateAsync( + cmd.Model.Contract, + cmd.Model.RpcUri, + cmd.Model.Height).ConfigureAwait(false); + await expressNode.PersistContractAsync( + result.contractState, + result.storagePairs, + cmd.Model.Force).ConfigureAwait(false); + break; + } case CommandLineApplication cmd: { var script = await txExec.LoadInvocationScriptAsync( diff --git a/src/neoxp/Commands/ContractCommand.Download.cs b/src/neoxp/Commands/ContractCommand.Download.cs new file mode 100644 index 00000000..0c4dbf1a --- /dev/null +++ b/src/neoxp/Commands/ContractCommand.Download.cs @@ -0,0 +1,77 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; +using McMaster.Extensions.CommandLineUtils; +using NeoExpress.Node; + +namespace NeoExpress.Commands +{ + partial class ContractCommand + { + internal enum OverwriteForce + { + None, + All, + ContractOnly, + StorageOnly + } + + [Command(Name = "download", Description = "Download contract with storage from remote chain into local chain")] + internal class Download + { + readonly ExpressChainManagerFactory chainManagerFactory; + + public Download(ExpressChainManagerFactory chainManagerFactory) + { + this.chainManagerFactory = chainManagerFactory; + } + + [Argument(0, Description = "Contract invocation hash")] + [Required] + internal string Contract { get; init; } = string.Empty; + + [Argument(1, Description = "URL of Neo JSON-RPC Node\nSpecify MainNet (default), TestNet or JSON-RPC URL")] + internal string RpcUri { get; } = string.Empty; + + [Option(Description = "Path to neo-express data file")] + internal string Input { get; init; } = string.Empty; + + [Option(Description = "Block height to get contract state for")] + internal uint Height { get; } = 0; + + [Option(CommandOptionType.SingleOrNoValue, + Description = "Replace contract and storage if it already exists (Default: All)")] + [AllowedValues(StringComparison.OrdinalIgnoreCase, "All", "ContractOnly", "StorageOnly")] + internal OverwriteForce Force { get; init; } = OverwriteForce.None; + + internal async Task ExecuteAsync(TextWriter writer) + { + var (chainManager, _) = chainManagerFactory.LoadChain(Input); + + if (chainManager.Chain.ConsensusNodes.Count != 1) + { + throw new ArgumentException("Contract download is only supported for single-node consensus"); + } + + var result = await NodeUtility.DownloadContractStateAsync(Contract, RpcUri, Height); + using var expressNode = chainManager.GetExpressNode(); + await expressNode.PersistContractAsync(result.contractState, result.storagePairs, Force); + } + + internal async Task OnExecuteAsync(CommandLineApplication app, IConsole console) + { + try + { + await ExecuteAsync(console.Out).ConfigureAwait(false); + return 0; + } + catch (Exception ex) + { + app.WriteException(ex); + return 1; + } + } + } + } +} \ No newline at end of file diff --git a/src/neoxp/Commands/ContractCommand.cs b/src/neoxp/Commands/ContractCommand.cs index c2fd9c66..4a625272 100644 --- a/src/neoxp/Commands/ContractCommand.cs +++ b/src/neoxp/Commands/ContractCommand.cs @@ -3,7 +3,7 @@ namespace NeoExpress.Commands { [Command("contract", Description = "Manage smart contracts")] - [Subcommand(typeof(Deploy), typeof(Get), typeof(Hash), typeof(Invoke), typeof(List), typeof(Run), typeof(Storage))] + [Subcommand(typeof(Deploy), typeof(Download), typeof(Get), typeof(Hash), typeof(Invoke), typeof(List), typeof(Run), typeof(Storage))] partial class ContractCommand { internal int OnExecute(CommandLineApplication app, IConsole console) diff --git a/src/neoxp/Extensions/Extensions.cs b/src/neoxp/Extensions/Extensions.cs index a09c1f44..d5d44a36 100644 --- a/src/neoxp/Extensions/Extensions.cs +++ b/src/neoxp/Extensions/Extensions.cs @@ -7,6 +7,7 @@ using McMaster.Extensions.CommandLineUtils; using Neo; using Neo.BlockchainToolkit; +using Neo.IO.Json; using Neo.Network.P2P.Payloads; using Neo.Network.RPC; using Neo.Network.RPC.Models; @@ -266,5 +267,47 @@ static bool TryGetNEP6Wallet(string path, string password, ProtocolSettings sett } } } + + // TODO: remove this copy of MakeFindStateParam https://github.com/neo-project/neo-express/issues/219 + private static JObject[] MakeFindStatesParams(UInt256 rootHash, UInt160 scriptHash, ReadOnlySpan prefix, + ReadOnlySpan from = default, int? count = null) + { + var @params = new JObject[count.HasValue ? 5 : 4]; + @params[0] = (JObject)rootHash.ToString(); + @params[1] = (JObject)scriptHash.ToString(); + @params[2] = (JObject)Convert.ToBase64String(prefix); + @params[3] = (JObject)Convert.ToBase64String(from); + if (count.HasValue) @params[4] = (JObject)(double)count.Value; + return @params; + } + + public static async Task> ExpressFindStatesAsync(this RpcClient rpcClient, UInt256 rootHash, + UInt160 contractScriptHash, ReadOnlyMemory prefix, ReadOnlyMemory from = default, int? pageSize = null) + { + var states = Enumerable.Empty<(string key, string value)>(); + var start = from; + + while (true) + { + var @params = MakeFindStatesParams(rootHash, contractScriptHash, prefix.Span, start.Span, pageSize); + var response = await rpcClient.RpcSendAsync("findstates", @params).ConfigureAwait(false); + + var jsonResults = (JArray)response["results"]; + if (jsonResults.Count == 0) break; + + var results = jsonResults + .Select(j => ( + j["key"].AsString(), + j["value"].AsString() + )); + states = states.Concat(results); + + var truncated = response["truncated"].AsBoolean(); + if (truncated) break; + start = Convert.FromBase64String(jsonResults[jsonResults.Count - 1]["key"].AsString()); + } + + return states.ToList(); + } } } diff --git a/src/neoxp/IExpressNode.cs b/src/neoxp/IExpressNode.cs index 69e8aa30..e091f546 100644 --- a/src/neoxp/IExpressNode.cs +++ b/src/neoxp/IExpressNode.cs @@ -6,10 +6,12 @@ using Neo.Cryptography.ECC; using Neo.Network.P2P.Payloads; using Neo.Network.RPC.Models; +using Neo.SmartContract; using Neo.SmartContract.Manifest; using Neo.SmartContract.Native; using Neo.VM; using Neo.Wallets; +using NeoExpress.Commands; using NeoExpress.Models; namespace NeoExpress @@ -40,6 +42,7 @@ enum CheckpointMode { Online, Offline } Task> ListStoragesAsync(UInt160 scriptHash); Task> ListTokenContractsAsync(); + Task PersistContractAsync(ContractState state, IReadOnlyList<(string key, string value)> storagePairs, ContractCommand.OverwriteForce force); IAsyncEnumerable<(uint blockIndex, NotificationRecord notification)> EnumerateNotificationsAsync(IReadOnlySet? contractFilter, IReadOnlySet? eventFilter); } } diff --git a/src/neoxp/Node/Modules/ExpressRpcMethods.cs b/src/neoxp/Node/Modules/ExpressRpcMethods.cs index 342d6f54..00630573 100644 --- a/src/neoxp/Node/Modules/ExpressRpcMethods.cs +++ b/src/neoxp/Node/Modules/ExpressRpcMethods.cs @@ -10,14 +10,18 @@ using Neo.IO; using Neo.IO.Json; using Neo.Network.P2P.Payloads; +using Neo.Network.RPC; using Neo.Persistence; using Neo.Plugins; using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; using Neo.Wallets; +using NeoExpress.Commands; using NeoExpress.Models; using ByteString = Neo.VM.Types.ByteString; +using RpcException = Neo.Plugins.RpcException; +using Utility = Neo.Utility; namespace NeoExpress.Node { @@ -236,7 +240,7 @@ public JObject ExpressListTokenContracts(JArray _) var height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; var oracleNodes = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height); var request = NativeContract.Oracle.GetRequest(snapshot, response.Id); - var tx = ExpressOracle.CreateResponseTx(snapshot, request, response, oracleNodes, neoSystem.Settings); + var tx = NodeUtility.CreateResponseTx(snapshot, request, response, oracleNodes, neoSystem.Settings); return tx == null ? null : Convert.ToBase64String(tx.ToArray()); } @@ -521,6 +525,20 @@ public JObject GetNep11Properties(JArray @params) } return json; } + + [RpcMethod] + public JObject ExpressPersistContract(JObject @params) + { + var state = RpcClient.ContractStateFromJson(@params[0]["state"]); + var storagePairs = ((JArray)@params[0]["storage"]) + .Select(s => ( + s["key"].AsString(), + s["value"].AsString()) + ).ToArray(); + var force = Enum.Parse(@params[0]["force"].AsString()); + + return NodeUtility.PersistContract(neoSystem, state, storagePairs, force); + } static readonly IReadOnlySet nep11PropertyNames = new HashSet { diff --git a/src/neoxp/Node/ExpressOracle.cs b/src/neoxp/Node/NodeUtility.cs similarity index 53% rename from src/neoxp/Node/ExpressOracle.cs rename to src/neoxp/Node/NodeUtility.cs index 62772b6f..1ace2e3c 100644 --- a/src/neoxp/Node/ExpressOracle.cs +++ b/src/neoxp/Node/NodeUtility.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using System.Threading.Tasks; using Neo; using Neo.BlockchainToolkit.Models; @@ -9,17 +10,19 @@ using Neo.Cryptography.ECC; using Neo.IO; using Neo.Network.P2P.Payloads; +using Neo.Network.RPC; using Neo.Persistence; using Neo.SmartContract; using Neo.SmartContract.Manifest; using Neo.SmartContract.Native; using Neo.VM; using Neo.Wallets; +using NeoExpress.Commands; using NeoExpress.Models; namespace NeoExpress.Node { - class ExpressOracle + class NodeUtility { public static Block CreateSignedBlock(Header prevHeader, IReadOnlyList keyPairs, uint network, Transaction[]? transactions = null, ulong timestamp = 0) { @@ -69,7 +72,7 @@ public static async Task FastForwardAsync(Header prevHeader, uint blockCount, Ti if (blockCount == 1) { - var block = ExpressOracle.CreateSignedBlock( + var block = CreateSignedBlock( prevHeader, keyPairs, network, timestamp: timestamp + delta); await submitBlockAsync(block).ConfigureAwait(false); } @@ -78,7 +81,7 @@ public static async Task FastForwardAsync(Header prevHeader, uint blockCount, Ti var period = delta / (blockCount - 1); for (int i = 0; i < blockCount; i++) { - var block = ExpressOracle.CreateSignedBlock( + var block = CreateSignedBlock( prevHeader, keyPairs, network, timestamp: timestamp); await submitBlockAsync(block).ConfigureAwait(false); prevHeader = block.Header; @@ -208,10 +211,207 @@ public static void SignOracleResponseTransaction(ProtocolSettings settings, Expr return tx; } + // constants from ContractManagement native contracts + const byte Prefix_Contract = 8; + const byte Prefix_NextAvailableId = 15; + + public static async Task<(ContractState contractState, IReadOnlyList<(string key, string value)> storagePairs)> DownloadContractStateAsync( + string contractHash, string rpcUri, uint stateHeight) + { + if (!UInt160.TryParse(contractHash, out var _contractHash)) + { + throw new ArgumentException($"Invalid contract hash: \"{contractHash}\""); + } + + if (!TransactionExecutor.TryParseRpcUri(rpcUri, out var uri)) + { + throw new ArgumentException($"Invalid RpcUri value \"{rpcUri}\""); + } + + using var rpcClient = new RpcClient(uri); + var stateAPI = new StateAPI(rpcClient); + + if (stateHeight == 0) + { + uint? localRootIndex; + try + { + (localRootIndex, _) = await stateAPI.GetStateHeightAsync(); + } + catch (RpcException e) + { + if (e.Message.Contains("Method not found")) + { + throw new Exception( + "Could not get state information. Make sure the remote RPC server has state service support"); + } + throw; + } + + stateHeight = localRootIndex.HasValue ? localRootIndex.Value + : throw new Exception($"Null \"{nameof(localRootIndex)}\" in state height response"); + } + + var stateRoot = await stateAPI.GetStateRootAsync(stateHeight); + + // rpcClient.GetContractStateAsync returns the current ContractState, but this method needs + // the ContractState as it was at stateHeight. ContractManagement stores ContractState by + // contractHash with the prefix 8. The following code uses stateAPI.GetStateAsync to retrieve + // the value with that key at the height state root and then deserializes it into a ContractState + // instance via GetInteroperable. + + var key = new byte[21]; + key[0] = Prefix_Contract; + _contractHash.ToArray().CopyTo(key, 1); + + ContractState contractState; + try + { + var contractStateBuffer = await stateAPI.GetStateAsync( + stateRoot.RootHash, NativeContract.ContractManagement.Hash, key); + contractState = new StorageItem(contractStateBuffer).GetInteroperable(); + } + catch (RpcException ex) + { + const int COR_E_KEYNOTFOUND = unchecked((int)0x80131577); + if (ex.HResult == COR_E_KEYNOTFOUND) throw new Exception($"Contract {contractHash} not found at height {stateHeight}"); + throw; + } + + if (contractState.Id < 0) throw new NotSupportedException("Contract download not supported for native contracts"); + + var states = await rpcClient.ExpressFindStatesAsync(stateRoot.RootHash, _contractHash, default); + + return (contractState, states); + } + + public static int PersistContract(NeoSystem neoSystem, ContractState state, + IReadOnlyList<(string key, string value)> storagePairs, ContractCommand.OverwriteForce force) + { + if (state.Id < 0) throw new ArgumentException("PersistContract not supported for native contracts", nameof(state)); + + using var snapshot = neoSystem.GetSnapshot(); + + StorageKey key = new KeyBuilder(NativeContract.ContractManagement.Id, Prefix_Contract).Add(state.Hash); + var localContract = snapshot.GetAndChange(key)?.GetInteroperable(); + if (localContract is null) + { + // if localContract is null, the downloaded contract does not exist in the local Express chain + // Save the downloaded state + storage directly to the local chain + + state.Id = GetNextAvailableId(snapshot); + snapshot.Add(key, new StorageItem(state)); + PersistStoragePairs(snapshot, state.Id, storagePairs); + + snapshot.Commit(); + return state.Id; + } + + // if localContract is not null, compare the current state + storage to the downloaded state + storage + // and overwrite changes if specified by user option + + var (overwriteContract, overwriteStorage) = force switch + { + ContractCommand.OverwriteForce.All => (true, true), + ContractCommand.OverwriteForce.ContractOnly => (true, false), + ContractCommand.OverwriteForce.None => (false, false), + ContractCommand.OverwriteForce.StorageOnly => (false, true), + _ => throw new NotSupportedException($"Invalid OverwriteForce value {force}"), + }; + + var dirty = false; + + if (!ContractStateEquals(state, localContract)) + { + if (overwriteContract) + { + // Note: a ManagementContract.Update() will not change the contract hash. Not even if the NEF changed. + localContract.Nef = state.Nef; + localContract.Manifest = state.Manifest; + localContract.UpdateCounter = state.UpdateCounter; + dirty = true; + } + else + { + throw new Exception("Downloaded contract already exists. Use --force to overwrite"); + } + } + + if (!ContractStorageEquals(localContract.Id, snapshot, storagePairs)) + { + if (overwriteStorage) + { + byte[] prefix_key = StorageKey.CreateSearchPrefix(localContract.Id, default); + foreach (var (k, v) in snapshot.Find(prefix_key)) + { + snapshot.Delete(k); + } + PersistStoragePairs(snapshot, localContract.Id, storagePairs); + dirty = true; + } + else + { + throw new Exception("Downloaded contract storage already exists. Use --force to overwrite"); + } + } + + if (dirty) snapshot.Commit(); + return localContract.Id; + + static int GetNextAvailableId(DataCache snapshot) + { + StorageKey key = new KeyBuilder(NativeContract.ContractManagement.Id, Prefix_NextAvailableId); + StorageItem item = snapshot.GetAndChange(key); + int value = (int)(BigInteger)item; + item.Add(1); + return value; + } + + static void PersistStoragePairs(DataCache snapshot, int contractId, IReadOnlyList<(string key, string value)> storagePairs) + { + for (int i = 0; i < storagePairs.Count; i++) + { + snapshot.Add( + new StorageKey { Id = contractId, Key = Convert.FromBase64String(storagePairs[i].key) }, + new StorageItem(Convert.FromBase64String(storagePairs[i].value))); + } + } + + static bool ContractStateEquals(ContractState a, ContractState b) + { + return a.Hash.Equals(b.Hash) + && a.UpdateCounter == b.UpdateCounter + && a.Nef.ToArray().SequenceEqual(b.Nef.ToArray()) + && a.Manifest.ToJson().ToByteArray(false).SequenceEqual(b.Manifest.ToJson().ToByteArray(false)); + } + + static bool ContractStorageEquals(int contractId, DataCache snapshot, IReadOnlyList<(string key, string value)> storagePairs) + { + IReadOnlyDictionary storagePairMap = storagePairs.ToDictionary(p => p.key, p => p.value); + var storageCount = 0; + + byte[] prefixKey = StorageKey.CreateSearchPrefix(contractId, default); + foreach (var (k, v) in snapshot.Find(prefixKey)) + { + var storageKey = Convert.ToBase64String(k.Key); + if (storagePairMap.TryGetValue(storageKey, out var storageValue) + && storageValue.Equals(Convert.ToBase64String(v.Value))) + { + storageCount++; + } + else + { + return false; + } + } + + return storageCount != storagePairs.Count; + } + } + // Need an IVerifiable.GetScriptHashesForVerifying implementation that doesn't // depend on the DataCache snapshot parameter in order to create a // ContractParametersContext without direct access to node data. - class BlockScriptHashes : IVerifiable { readonly UInt160[] hashes; diff --git a/src/neoxp/Node/OfflineNode.cs b/src/neoxp/Node/OfflineNode.cs index c6efe306..2b6b67c5 100644 --- a/src/neoxp/Node/OfflineNode.cs +++ b/src/neoxp/Node/OfflineNode.cs @@ -11,6 +11,7 @@ using Neo.Cryptography; using Neo.Cryptography.ECC; using Neo.IO; +using Neo.IO.Json; using Neo.Ledger; using Neo.Network.P2P.Payloads; using Neo.Network.RPC; @@ -21,6 +22,7 @@ using Neo.SmartContract.Native; using Neo.VM; using Neo.Wallets; +using NeoExpress.Commands; using NeoExpress.Models; using static Neo.Ledger.Blockchain; @@ -176,9 +178,9 @@ public async Task SubmitOracleResponseAsync(OracleResponse response, IR using var snapshot = neoSystem.GetSnapshot(); var height = NativeContract.Ledger.CurrentIndex(snapshot) + 1; var request = NativeContract.Oracle.GetRequest(snapshot, response.Id); - var tx = ExpressOracle.CreateResponseTx(snapshot, request, response, oracleNodes, ProtocolSettings); + var tx = NodeUtility.CreateResponseTx(snapshot, request, response, oracleNodes, ProtocolSettings); if (tx == null) throw new Exception("Failed to create Oracle Response Tx"); - ExpressOracle.SignOracleResponseTransaction(ProtocolSettings, chain, tx, oracleNodes); + NodeUtility.SignOracleResponseTransaction(ProtocolSettings, chain, tx, oracleNodes); var blockHash = await SubmitTransactionAsync(tx); return tx.Hash; @@ -191,7 +193,7 @@ public async Task FastForwardAsync(uint blockCount, TimeSpan timestampDelta) var prevHash = NativeContract.Ledger.CurrentHash(neoSystem.StoreView); var prevHeader = NativeContract.Ledger.GetHeader(neoSystem.StoreView, prevHash); - await ExpressOracle.FastForwardAsync(prevHeader, + await NodeUtility.FastForwardAsync(prevHeader, blockCount, timestampDelta, consensusNodesKeys.Value, @@ -218,7 +220,7 @@ async Task SubmitTransactionAsync(Transaction tx) var prevHash = NativeContract.Ledger.CurrentHash(neoSystem.StoreView); var prevHeader = NativeContract.Ledger.GetHeader(neoSystem.StoreView, prevHash); - var block = ExpressOracle.CreateSignedBlock(prevHeader, + var block = NodeUtility.CreateSignedBlock(prevHeader, consensusNodesKeys.Value, neoSystem.Settings.Network, transactions); @@ -369,9 +371,20 @@ IReadOnlyList ListStorages(UInt160 scriptHash) public Task> ListStoragesAsync(UInt160 scriptHash) => MakeAsync(() => ListStorages(scriptHash)); + public Task PersistContractAsync(ContractState state, IReadOnlyList<(string key, string value)> storagePairs, ContractCommand.OverwriteForce force) + => MakeAsync(() => + { + if (chain.ConsensusNodes.Count != 1) + { + throw new ArgumentException("Contract download is only supported for single-node consensus"); + } + + return NodeUtility.PersistContract(neoSystem, state, storagePairs, force); + }); + // warning CS1998: This async method lacks 'await' operators and will run synchronously. // EnumerateNotificationsAsync has to be async in order to be polymorphic with OnlineNode's implementation -#pragma warning disable 1998 +#pragma warning disable 1998 public async IAsyncEnumerable<(uint blockIndex, NotificationRecord notification)> EnumerateNotificationsAsync(IReadOnlySet? contractFilter, IReadOnlySet? eventFilter) { var notifications = PersistencePlugin.GetNotifications(this.rocksDbStorageProvider, SeekDirection.Backward, contractFilter, eventFilter); diff --git a/src/neoxp/Node/OnlineNode.cs b/src/neoxp/Node/OnlineNode.cs index 04c69ad3..7061b86d 100644 --- a/src/neoxp/Node/OnlineNode.cs +++ b/src/neoxp/Node/OnlineNode.cs @@ -16,6 +16,7 @@ using Neo.SmartContract.Native; using Neo.VM; using Neo.Wallets; +using NeoExpress.Commands; using NeoExpress.Models; namespace NeoExpress.Node @@ -60,7 +61,7 @@ public async Task FastForwardAsync(uint blockCount, TimeSpan timestampDelta) var prevHeaderHex = await rpcClient.GetBlockHeaderHexAsync($"{prevHash}").ConfigureAwait(false); var prevHeader = Convert.FromBase64String(prevHeaderHex).AsSerializable
(); - await ExpressOracle.FastForwardAsync(prevHeader, + await NodeUtility.FastForwardAsync(prevHeader, blockCount, timestampDelta, consensusNodesKeys.Value, @@ -110,7 +111,7 @@ public async Task SubmitOracleResponseAsync(OracleResponse response, IR { var jsonTx = await rpcClient.RpcSendAsync("expresscreateoracleresponsetx", response.ToJson()).ConfigureAwait(false); var tx = Convert.FromBase64String(jsonTx.AsString()).AsSerializable(); - ExpressOracle.SignOracleResponseTransaction(ProtocolSettings, chain, tx, oracleNodes); + NodeUtility.SignOracleResponseTransaction(ProtocolSettings, chain, tx, oracleNodes); return await rpcClient.SendRawTransactionAsync(tx).ConfigureAwait(false); } @@ -242,6 +243,32 @@ public async Task> ListStoragesAsync(UInt160 scrip return Array.Empty(); } + public async Task PersistContractAsync(ContractState state, IReadOnlyList<(string key, string value)> storagePairs, ContractCommand.OverwriteForce force) + { + if (chain.ConsensusNodes.Count != 1) + { + throw new ArgumentException("Contract download is only supported for single-node consensus"); + } + + JObject o = new JObject(); + o["state"] = state.ToJson(); + + JArray storage = new JArray(); + foreach (var pair in storagePairs) + { + JObject kv = new JObject(); + kv["key"] = pair.key; + kv["value"] = pair.value; + storage.Add(kv); + } + + o["storage"] = storage; + o["force"] = force; + + var response = await rpcClient.RpcSendAsync("expresspersistcontract", o).ConfigureAwait(false); + return (int)response.AsNumber(); + } + public async IAsyncEnumerable<(uint blockIndex, NotificationRecord notification)> EnumerateNotificationsAsync(IReadOnlySet? contractFilter, IReadOnlySet? eventFilter) { JObject contractsArg = (contractFilter ?? Enumerable.Empty())