Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[StateService] get historical state #638

Merged
merged 23 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/StateService/MPT/Helper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace Neo.Plugins.MPT
{
public static class Helper
{
public static int CompareTo(this byte[] arr1, byte[] arr2)
{
if (arr1 is null || arr2 is null) throw new ArgumentNullException();
for (int i = 0; i < arr1.Length && i < arr2.Length; i++)
{
var r = arr1[i].CompareTo(arr2[i]);
if (r != 0) return r;
}
return arr2.Length < arr1.Length ? 1 : arr2.Length == arr1.Length ? 0 : -1;
}
}
}
5 changes: 4 additions & 1 deletion src/StateService/MPT/MPTTrie.Delete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ partial class MPTTrie<TKey, TValue>
public bool Delete(TKey key)
{
var path = ToNibbles(key.ToArray());
if (path.Length == 0) throw new ArgumentException("could not be empty", nameof(key));
if (path.Length == 0)
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > MPTNode.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
return TryDelete(ref root, path);
}

Expand Down
52 changes: 43 additions & 9 deletions src/StateService/MPT/MPTTrie.Find.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,35 @@ private ReadOnlySpan<byte> Seek(ref MPTNode node, ReadOnlySpan<byte> path, out M
return ReadOnlySpan<byte>.Empty;
}

public IEnumerable<(TKey Key, TValue Value)> Find(ReadOnlySpan<byte> prefix)
public IEnumerable<(TKey Key, TValue Value)> Find(ReadOnlySpan<byte> prefix, byte[] from = null)
devhawk marked this conversation as resolved.
Show resolved Hide resolved
{
var path = ToNibbles(prefix);
int offset = 0;
if (from is null) from = Array.Empty<byte>();
if (0 < from.Length)
{
if (!from.AsSpan().StartsWith(prefix))
throw new InvalidOperationException("invalid from key");
from = ToNibbles(from.AsSpan());
}
if (path.Length > MPTNode.MaxKeyLength || from.Length > MPTNode.MaxKeyLength)
throw new ArgumentException("exceeds limit");
path = Seek(ref root, path, out MPTNode start).ToArray();
return Travers(start, path)
offset = path.Length;
return Travers(start, path, from, offset)
.Select(p => (FromNibbles(p.Key).AsSerializable<TKey>(), p.Value.AsSerializable<TValue>()));
}

private IEnumerable<(byte[] Key, byte[] Value)> Travers(MPTNode node, byte[] path)
private IEnumerable<(byte[] Key, byte[] Value)> Travers(MPTNode node, byte[] path, byte[] from, int offset)
{
if (node is null) yield break;
if (offset < 0) throw new InvalidOperationException("invalid offset");
switch (node.Type)
{
case NodeType.LeafNode:
{
yield return (path, (byte[])node.Value.Clone());
if (from.Length <= offset && !path.SequenceEqual(from))
yield return (path, (byte[])node.Value.Clone());
break;
}
case NodeType.Empty:
Expand All @@ -87,23 +100,44 @@ private ReadOnlySpan<byte> Seek(ref MPTNode node, ReadOnlySpan<byte> path, out M
var newNode = cache.Resolve(node.Hash);
if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt find");
node = newNode;
foreach (var item in Travers(node, path))
foreach (var item in Travers(node, path, from, offset))
yield return item;
break;
}
case NodeType.BranchNode:
{
for (int i = 0; i < MPTNode.BranchChildCount; i++)
if (offset < from.Length)
{
for (int i = 0; i < MPTNode.BranchChildCount - 1; i++)
{
if (from[offset] < i)
foreach (var item in Travers(node.Children[i], Concat(path, new byte[] { (byte)i }), from, from.Length))
yield return item;
else if (i == from[offset])
foreach (var item in Travers(node.Children[i], Concat(path, new byte[] { (byte)i }), from, offset + 1))
yield return item;
}
}
else
{
foreach (var item in Travers(node.Children[i], i == MPTNode.BranchChildCount - 1 ? path : Concat(path, new byte[] { (byte)i })))
foreach (var item in Travers(node.Children[MPTNode.BranchChildCount - 1], path, from, offset))
yield return item;
for (int i = 0; i < MPTNode.BranchChildCount - 1; i++)
{
foreach (var item in Travers(node.Children[i], Concat(path, new byte[] { (byte)i }), from, offset))
yield return item;
}
}
break;
}
case NodeType.ExtensionNode:
{
foreach (var item in Travers(node.Next, Concat(path, node.Key)))
yield return item;
if (offset < from.Length && from.AsSpan()[offset..].StartsWith(node.Key))
foreach (var item in Travers(node.Next, Concat(path, node.Key), from, offset + node.Key.Length))
yield return item;
else if (from.Length <= offset || 0 < node.Key.CompareTo(from[offset..]))
foreach (var item in Travers(node.Next, Concat(path, node.Key), from, from.Length))
yield return item;
break;
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/StateService/MPT/MPTTrie.Get.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ public TValue this[TKey key]
get
{
var path = ToNibbles(key.ToArray());
if (path.Length == 0) throw new ArgumentException("could not be empty", nameof(key));
if (path.Length == 0)
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > MPTNode.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
var result = TryGet(ref root, path, out var value);
return result ? value.ToArray().AsSerializable<TValue>() : throw new KeyNotFoundException();
}
Expand All @@ -21,10 +24,13 @@ public bool TryGetValue(TKey key, out TValue value)
{
value = default;
var path = ToNibbles(key.ToArray());
if (path.Length == 0) throw new ArgumentException("could not be empty", nameof(key));
if (path.Length == 0)
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > MPTNode.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
var result = TryGet(ref root, path, out var val);
if (result)
val.ToArray().AsSerializable<TValue>();
value = val.ToArray().AsSerializable<TValue>();
return result;
}

Expand Down
5 changes: 4 additions & 1 deletion src/StateService/MPT/MPTTrie.Proof.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ partial class MPTTrie<TKey, TValue>
public bool TryGetProof(TKey key, out HashSet<byte[]> proof)
{
var path = ToNibbles(key.ToArray());
if (path.Length == 0) throw new ArgumentException("could not be empty", nameof(key));
if (path.Length == 0)
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > MPTNode.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
proof = new HashSet<byte[]>(ByteArrayEqualityComparer.Default);
return GetProof(ref root, path, proof);
}
Expand Down
70 changes: 70 additions & 0 deletions src/StateService/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# StateService

## RPC API

### GetStateRoot
#### Params
|Name|Type|Summary|Required|
|-|-|-|-|
|Index|uint|index|true|
#### Result
StateRoot Object
|Name|Type|Summary|
|-|-|-|
|version|number|version|
|index|number|index|
|roothash|string|version|
|witness|Object|witness from validators|

### GetProof
#### Params
|Name|Type|Summary|Required|
|-|-|-|-|
|RootHash|UInt256|state root|true|
|ScriptHash|UInt160|contract script hash|true|
|Key|base64 string|key|true|
#### Result
Proof in base64 string

### VerifyProof
#### Params
|Name|Type|Summary|
|-|-|-|
|RootHash|UInt256|state root|true|
|Proof|base64 string|proof|true|
#### Result
Value in base64 string

### GetStateheight
#### Result
|Name|Type|Summary|
|-|-|-|
|localrootindex|number|root hash index calculated locally|
|validatedrootindex|number|root hash index verified by validators|

### GetState
ZhangTao1596 marked this conversation as resolved.
Show resolved Hide resolved
#### Params
|Name|Type|Summary|Required|
|-|-|-|-|
|RootHash|UInt256|specify state|true|
|ScriptHash|UInt160|contract script hash|true|
|Key|base64 string|key|true|
#### Result
Value in base64 string or `null`

### FindStates
#### Params
|Name|Type|Summary|Required|
|-|-|-|-|
|RootHash|UInt256|specify state|true|
|ScriptHash|UInt160|contract script hash|true|
|Prefix|base64 string|key prefix|true|
|From|base64 string|start key, default `Empty`|optional|
|Count|number|count of results in one request, default `MaxFindResultItems`|optional|
#### Result
|Name|Type|Summary|
|-|-|-|
|firstProof|string|proof of first value in results|
|lastProof|string|proof of last value in results|
|truncated|bool|whether the results is truncated because of limitation|
|results|array|key-values found|
2 changes: 2 additions & 0 deletions src/StateService/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal class Settings
public bool FullState { get; }
public uint Network { get; }
public bool AutoVerify { get; }
public int MaxFindResultItems { get; }

public static Settings Default { get; private set; }

Expand All @@ -17,6 +18,7 @@ private Settings(IConfigurationSection section)
FullState = section.GetValue("FullState", false);
Network = section.GetValue("Network", 5195086u);
AutoVerify = section.GetValue("AutoVerify", false);
MaxFindResultItems = section.GetValue("MaxFindResultItems", 100);
}

public static void Load(IConfigurationSection section)
Expand Down
91 changes: 90 additions & 1 deletion src/StateService/StatePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ private string GetProof(UInt256 root_hash, UInt160 script_hash, byte[] key)
Id = contract.Id,
Key = key,
};
var result = StateStore.Singleton.TryGetProof(root_hash, skey, out var proof);

using ISnapshot store = StateStore.Singleton.GetStoreSnapshot();
var trie = new MPTTrie<StorageKey, StorageItem>(store, root_hash);
var result = trie.TryGetProof(skey, out var proof);
if (!result) throw new RpcException(-100, "Unknown value");

using MemoryStream ms = new MemoryStream();
Expand Down Expand Up @@ -239,5 +242,91 @@ public JObject GetStateHeight(JArray _params)
json["validatedrootindex"] = StateStore.Singleton.ValidatedRootIndex;
return json;
}

private ContractState GetHistoricalContractState(MPTTrie<StorageKey, StorageItem> trie, UInt160 script_hash)
{
const byte prefix = 8;
StorageKey skey = new KeyBuilder(NativeContract.ContractManagement.Id, prefix).Add(script_hash);
return trie.TryGetValue(skey, out var value) ? value.GetInteroperable<ContractState>() : null;
}

[RpcMethod]
public JObject FindStates(JArray _params)
{
var root_hash = UInt256.Parse(_params[0].AsString());
if (!Settings.Default.FullState && StateStore.Singleton.CurrentLocalRootHash != root_hash)
throw new RpcException(-100, "Old state not supported");
var script_hash = UInt160.Parse(_params[1].AsString());
var prefix = Convert.FromBase64String(_params[2].AsString());
shargon marked this conversation as resolved.
Show resolved Hide resolved
byte[] key = Array.Empty<byte>();
if (3 < _params.Count)
key = Convert.FromBase64String(_params[3].AsString());
int count = Settings.Default.MaxFindResultItems;
if (4 < _params.Count)
count = int.Parse(_params[4].AsString());
if (Settings.Default.MaxFindResultItems < count)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should be returned as an exception because it could mislead the interpretation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How? Can you describe this in detail? We have truncated in response and the results are ordered by key.

count = Settings.Default.MaxFindResultItems;
using var store = StateStore.Singleton.GetStoreSnapshot();
var trie = new MPTTrie<StorageKey, StorageItem>(store, root_hash);
var contract = GetHistoricalContractState(trie, script_hash);
if (contract is null) throw new RpcException(-100, "Unknown contract");
StorageKey pkey = new()
{
Id = contract.Id,
Key = prefix,
};
StorageKey fkey = new()
{
Id = pkey.Id,
Key = key,
};
JObject json = new();
JArray jarr = new();
int i = 0;
foreach (var (ikey, ivalue) in trie.Find(pkey.ToArray(), 0 < key.Length ? fkey.ToArray() : null))
{
if (count < i) break;
if (i < count)
{
JObject j = new();
j["key"] = Convert.ToBase64String(ikey.Key);
j["value"] = Convert.ToBase64String(ivalue.Value);
jarr.Add(j);
}
i++;
};
if (0 < jarr.Count)
{
json["firstProof"] = GetProof(root_hash, script_hash, Convert.FromBase64String(jarr.First()["key"].AsString()));
}
if (1 < jarr.Count)
{
json["lastProof"] = GetProof(root_hash, script_hash, Convert.FromBase64String(jarr.Last()["key"].AsString()));
}
json["truncated"] = count < i;
json["results"] = jarr;
return json;
}

[RpcMethod]
public JObject GetState(JArray _params)
{
var root_hash = UInt256.Parse(_params[0].AsString());
if (!Settings.Default.FullState && StateStore.Singleton.CurrentLocalRootHash != root_hash)
throw new RpcException(-100, "Old state not supported");
shargon marked this conversation as resolved.
Show resolved Hide resolved
var script_hash = UInt160.Parse(_params[1].AsString());
var key = Convert.FromBase64String(_params[2].AsString());
using var store = StateStore.Singleton.GetStoreSnapshot();
var trie = new MPTTrie<StorageKey, StorageItem>(store, root_hash);

var contract = GetHistoricalContractState(trie, script_hash);
if (contract is null) throw new RpcException(-100, "Unknown contract");
StorageKey skey = new()
{
Id = contract.Id,
Key = key,
};
return Convert.ToBase64String(trie[skey].Value);
}
}
}
3 changes: 2 additions & 1 deletion src/StateService/StateService/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"Path": "Data_MPT_{0}",
"FullState": false,
"Network": 860833102,
"AutoVerify": false
"AutoVerify": false,
"MaxFindResultItems": 100
shargon marked this conversation as resolved.
Show resolved Hide resolved
},
"Dependency": [
"RpcServer"
Expand Down
6 changes: 2 additions & 4 deletions src/StateService/Storage/StateStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ public StateSnapshot GetSnapshot()
return new StateSnapshot(store);
}

public bool TryGetProof(UInt256 root, StorageKey skey, out HashSet<byte[]> proof)
public ISnapshot GetStoreSnapshot()
{
using ISnapshot snapshot = store.GetSnapshot();
var trie = new MPTTrie<StorageKey, StorageItem>(snapshot, root);
return trie.TryGetProof(skey, out proof);
return store.GetSnapshot();
}

protected override void OnReceive(object message)
Expand Down
Loading