Skip to content

Commit

Permalink
Add command to download a contract (#209)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Harry <[email protected]>
  • Loading branch information
3 people authored Apr 19, 2022
1 parent 53f2d02 commit cdd3a18
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 14 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 22 additions & 1 deletion src/neoxp/Commands/BatchCommand.BatchFileCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/neoxp/Commands/BatchCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Neo.BlockchainToolkit;
using NeoExpress.Node;

namespace NeoExpress.Commands
{
Expand Down Expand Up @@ -122,6 +123,29 @@ await txExec.ContractDeployAsync(
cmd.Model.Force).ConfigureAwait(false);
break;
}
case CommandLineApplication<BatchFileCommands.Contract.Download> 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<BatchFileCommands.Contract.Invoke> cmd:
{
var script = await txExec.LoadInvocationScriptAsync(
Expand Down
77 changes: 77 additions & 0 deletions src/neoxp/Commands/ContractCommand.Download.cs
Original file line number Diff line number Diff line change
@@ -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<int> OnExecuteAsync(CommandLineApplication app, IConsole console)
{
try
{
await ExecuteAsync(console.Out).ConfigureAwait(false);
return 0;
}
catch (Exception ex)
{
app.WriteException(ex);
return 1;
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/neoxp/Commands/ContractCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions src/neoxp/Extensions/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<byte> prefix,
ReadOnlySpan<byte> 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<IReadOnlyList<(string key, string value)>> ExpressFindStatesAsync(this RpcClient rpcClient, UInt256 rootHash,
UInt160 contractScriptHash, ReadOnlyMemory<byte> prefix, ReadOnlyMemory<byte> 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();
}
}
}
3 changes: 3 additions & 0 deletions src/neoxp/IExpressNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,6 +42,7 @@ enum CheckpointMode { Online, Offline }
Task<IReadOnlyList<ExpressStorage>> ListStoragesAsync(UInt160 scriptHash);
Task<IReadOnlyList<TokenContract>> ListTokenContractsAsync();

Task<int> PersistContractAsync(ContractState state, IReadOnlyList<(string key, string value)> storagePairs, ContractCommand.OverwriteForce force);
IAsyncEnumerable<(uint blockIndex, NotificationRecord notification)> EnumerateNotificationsAsync(IReadOnlySet<UInt160>? contractFilter, IReadOnlySet<string>? eventFilter);
}
}
20 changes: 19 additions & 1 deletion src/neoxp/Node/Modules/ExpressRpcMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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<ContractCommand.OverwriteForce>(@params[0]["force"].AsString());

return NodeUtility.PersistContract(neoSystem, state, storagePairs, force);
}

static readonly IReadOnlySet<string> nep11PropertyNames = new HashSet<string>
{
Expand Down
Loading

0 comments on commit cdd3a18

Please sign in to comment.