diff --git a/MPTStates.UnitTests/MPTStates.UnitTests.csproj b/MPTStates.UnitTests/MPTStates.UnitTests.csproj new file mode 100644 index 000000000..2c768ea92 --- /dev/null +++ b/MPTStates.UnitTests/MPTStates.UnitTests.csproj @@ -0,0 +1,28 @@ + + + Exe + netcoreapp2.0 + MPTStates.UnitTests + MPTStates.UnitTests + true + + + + runtime; build; native; contentfiles; analyzers + all + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MPTStates.UnitTests/TestDataCache.cs b/MPTStates.UnitTests/TestDataCache.cs new file mode 100644 index 000000000..68f3b07da --- /dev/null +++ b/MPTStates.UnitTests/TestDataCache.cs @@ -0,0 +1,52 @@ +using Neo.IO; +using Neo.IO.Caching; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.UnitTests +{ + public class TestDataCache : DataCache + where TKey : IEquatable, ISerializable + where TValue : class, ICloneable, ISerializable, new() + { + private readonly TValue _defaultValue; + + public TestDataCache() + { + _defaultValue = null; + } + + public TestDataCache(TValue defaultValue) + { + this._defaultValue = defaultValue; + } + public override void DeleteInternal(TKey key) + { + } + + protected override void AddInternal(TKey key, TValue value) + { + } + + protected override IEnumerable> FindInternal(byte[] key_prefix) + { + return Enumerable.Empty>(); + } + + protected override TValue GetInternal(TKey key) + { + if (_defaultValue == null) throw new NotImplementedException(); + return _defaultValue; + } + + protected override TValue TryGetInternal(TKey key) + { + return _defaultValue; + } + + protected override void UpdateInternal(TKey key, TValue value) + { + } + } +} diff --git a/MPTStates.UnitTests/TestDataCacheWithInternal.cs b/MPTStates.UnitTests/TestDataCacheWithInternal.cs new file mode 100644 index 000000000..b0de3bff2 --- /dev/null +++ b/MPTStates.UnitTests/TestDataCacheWithInternal.cs @@ -0,0 +1,33 @@ +using Neo.IO; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.UnitTests +{ + public class TestDataCacheWithInternal : TestDataCache + where TKey : IEquatable, ISerializable + where TValue : class, ICloneable, ISerializable, new() + { + private readonly Dictionary _internal = new Dictionary(); + + public override void DeleteInternal(TKey key) => _internal.Remove(key); + + protected override void AddInternal(TKey key, TValue value) => _internal.Add(key, value); + + protected override IEnumerable> FindInternal(byte[] key_prefix) + { + return Enumerable.Empty>(); + } + + protected override TValue GetInternal(TKey key) => _internal[key]; + + protected override TValue TryGetInternal(TKey key) + { + _internal.TryGetValue(key, out var value); + return value; + } + + protected override void UpdateInternal(TKey key, TValue value) => _internal[key] = value; + } +} \ No newline at end of file diff --git a/MPTStates.UnitTests/UT_MerklePatricia.cs b/MPTStates.UnitTests/UT_MerklePatricia.cs new file mode 100644 index 000000000..49815bdd7 --- /dev/null +++ b/MPTStates.UnitTests/UT_MerklePatricia.cs @@ -0,0 +1,521 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Plugins.MPT; + +namespace Neo.UnitTests.Ledger +{ + [TestClass] + public class UT_MerklePatricia + { + private static MerklePatriciaTree NewTree() => new MerklePatriciaTree(); + + private static void EqualsAfterRemove(List values, int selected = 0, int? max = null) + { + values = new List(new HashSet(values)); + for (max = max ?? 1 << values.Count; selected < max; selected++) + { + var inserted = NewTree(); + var insertedRemoved = NewTree(); + var insertedRemovedBackwards = NewTree(); + + for (var i = 0; i < values.Count; i++) + { + if (selected.GetBit(i)) + { + inserted[values[i]] = values[i]; + } + + insertedRemoved[values[i]] = values[i]; + insertedRemovedBackwards[values[i]] = values[i]; + } + + for (var i = 0; i < values.Count; i++) + { + if (selected.GetBit(i)) continue; + insertedRemoved.Remove(values[i]); + } + + for (var i = values.Count - 1; i >= 0; i--) + { + if (selected.GetBit(i)) continue; + insertedRemovedBackwards.Remove(values[i]); + } + + var testMessage = $"Test {selected}/{max}"; + var testBackwardsMessage = $"Test backwards {selected}/{max}"; + for (var i = 0; i < values.Count; i++) + { + if (selected.GetBit(i)) + { + Assert.AreEqual(values[i], inserted[values[i]], testMessage); + Assert.AreEqual(values[i], insertedRemoved[values[i]], testMessage); + Assert.AreEqual(values[i], insertedRemovedBackwards[values[i]], testBackwardsMessage); + } + else + { + Assert.IsFalse(inserted.ContainsKey(values[i]), testMessage); + Assert.IsFalse(insertedRemoved.ContainsKey(values[i]), testMessage); + Assert.IsFalse(insertedRemovedBackwards.ContainsKey(values[i]), testBackwardsMessage); + } + } + + Assert.AreEqual(inserted, insertedRemoved, testMessage); + Assert.AreEqual(inserted, insertedRemovedBackwards, testBackwardsMessage); + // At this point insertedRemoved should be equals to insertedRemovedBackwards so it must be + Assert.AreEqual(insertedRemoved, insertedRemovedBackwards, testBackwardsMessage); + } + } + + [TestMethod] + public void EqualsAfterRemove() + { + EqualsAfterRemove(new List {"a", "ab", "ba", "bb", "zba"}, 3); + + EqualsAfterRemove(new List {"a"}); + EqualsAfterRemove(new List {"a", "ab"}); + EqualsAfterRemove(new List {"a", "ab", "ba"}); + EqualsAfterRemove(new List {"a", "ab", "ba", "bb"}); + EqualsAfterRemove(new List {"a", "ab", "ba", "bb", "zba"}); + EqualsAfterRemove(new List {"a", "ab", "ba", "bb", "zba", "cba"}); + EqualsAfterRemove(new List {"a", "b", "aaaa", "baaa", "aaab", "baab"}); + + EqualsAfterRemove(new List {"a", "b", "aaaa", "baaa", "aaab", "baab"}); + } + + private static void CheckCopy(MerklePatriciaTree mp) + { + var cloned = NewTree(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mp.Serialize(bw); + using (var br = new BinaryReader(bw.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(cloned.Validate()); + Assert.AreEqual(mp, cloned); + + cloned = mp.Clone(); + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(cloned.Validate()); + Assert.AreEqual(mp, cloned); + + cloned = NewTree(); + cloned.FromReplica(mp); + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(cloned.Validate()); + Assert.AreEqual(mp, cloned); + } + + [TestMethod] + public void DistinctRoot() + { + var mp = NewTree(); + Assert.IsFalse(mp == null); + mp[new byte[] {0x0, 0x0, 0x1}] = new byte[] {0x0, 0x0, 0x1}; + Assert.IsTrue(new byte[] {0x0, 0x0, 0x1}.SequenceEqual(mp[new byte[] {0x0, 0x0, 0x1}])); + + mp[new byte[] {0x11, 0x0, 0x2}] = new byte[] {0x11, 0x0, 0x2}; + Assert.IsTrue(new byte[] {0x11, 0x0, 0x2}.SequenceEqual(mp[new byte[] {0x11, 0x0, 0x2}])); + Assert.IsNull(mp[new byte[] {0x20, 0x0, 0x2}]); + + CheckCopy(mp); + } + + [TestMethod] + public void SameRoot() + { + var mp = NewTree(); + Assert.IsFalse(mp == null); + mp[new byte[] {0x0, 0x0, 0x1}] = new byte[] {0x0, 0x0, 0x1}; + Assert.IsTrue(new byte[] {0x0, 0x0, 0x1}.SequenceEqual(mp[new byte[] {0x0, 0x0, 0x1}])); + + mp[new byte[] {0x0, 0x0, 0x1}] = new byte[] {0x0, 0x0, 0x2}; + Assert.IsTrue(new byte[] {0x0, 0x0, 0x2}.SequenceEqual(mp[new byte[] {0x0, 0x0, 0x1}])); + + CheckCopy(mp); + } + + [TestMethod] + public void EndsOnExtension() + { + var mp = NewTree(); + mp["bola"] = "bola"; + mp["bolo"] = "bolo"; + + Assert.AreEqual("bola", mp["bola"]); + Assert.AreEqual("bolo", mp["bolo"]); + Assert.AreEqual(2, mp.Count()); + mp["bol"] = "bol"; + Assert.AreEqual("bol", mp["bol"]); + mp["aol"] = "aol"; + Assert.AreEqual("aol", mp["aol"]); + mp["zol"] = "zol"; + Assert.AreEqual("zol", mp["zol"]); + mp["bala"] = "bala"; + Assert.AreEqual("bala", mp["bala"]); + mp["bo"] = "bo"; + Assert.AreEqual("bo", mp["bo"]); + Assert.AreEqual(7, mp.Count()); + + Assert.AreEqual("aol", mp["aol"]); + Assert.AreEqual("zol", mp["zol"]); + Assert.AreEqual("bol", mp["bol"]); + Assert.AreEqual("bola", mp["bola"]); + Assert.AreEqual("bolo", mp["bolo"]); + + CheckCopy(mp); + } + + [TestMethod] + public void ColidKeys() + { + var mp = NewTree(); + mp["oi"] = "batata"; + mp["oi"] = "batatatinha"; + Assert.IsTrue(mp.ContainsKey("oi")); + Assert.AreEqual("batatatinha", mp["oi"]); + Assert.IsTrue(mp.Validate()); + + mp["orelha"] = "batatatinha"; + Assert.AreEqual("batatatinha", mp["orelha"]); + Assert.IsTrue(mp.Validate()); + + mp["orfão"] = "criança"; + Assert.AreEqual("criança", mp["orfão"]); + Assert.IsTrue(mp.Validate()); + + mp["orfanato"] = "crianças"; + Assert.AreEqual("crianças", mp["orfanato"]); + Assert.IsTrue(mp.Validate()); + + Assert.IsTrue(mp.Remove("orfanato")); + Assert.AreEqual("criança", mp["orfão"]); + Assert.IsFalse(mp.ContainsKey("orfanato")); + Assert.IsTrue(mp.Validate()); + + mp["orfã"] = "menina"; + Assert.AreEqual("menina", mp["orfã"]); + Assert.IsTrue(mp.Validate()); + + CheckCopy(mp); + } + + [TestMethod] + public void StepByStep() + { + var mp = NewTree(); + Assert.AreEqual(0, mp.Count()); + var a0001 = new byte[] {0x0, 0x01}; + mp[a0001] = a0001; + Assert.AreEqual(1, mp.Count()); + Assert.AreEqual(a0001, mp[a0001]); + + var a0101 = new byte[] {0x01, 0x01}; + mp[a0101] = a0101; + Assert.AreEqual(2, mp.Count()); + Assert.AreEqual(a0001, mp[a0001]); + Assert.AreEqual(a0101, mp[a0101]); + + Assert.IsTrue(mp.Remove(a0101)); + Assert.IsFalse(mp.ContainsKey(a0101)); + Assert.AreEqual(a0001, mp[a0001]); + + var merklePatricia = NewTree(); + merklePatricia[a0001] = a0001; + Assert.AreEqual(merklePatricia, mp); + + CheckCopy(mp); + } + + [TestMethod] + public void ContainsValue() + { + var mp = NewTree(); + mp["aoi"] = "oi"; + mp["boi2"] = "oi2"; + mp["coi1"] = "oi3"; + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(mp.ContainsValue("oi")); + Assert.IsTrue(mp.ContainsValue("oi2")); + Assert.IsTrue(mp.ContainsValue("oi3")); + + Assert.IsFalse(mp.ContainsValue("aoi")); + Assert.IsFalse(mp.ContainsValue("boi2")); + Assert.IsFalse(mp.ContainsValue("coi3")); + Assert.IsTrue(mp.Validate()); + + CheckCopy(mp); + } + + [TestMethod] + public void PatriciaCount() + { + var mp = NewTree(); + Assert.AreEqual(0, mp.Count()); + Assert.IsTrue(mp.Validate()); + + mp["oi"] = "oi"; + Assert.AreEqual(1, mp.Count()); + Assert.IsTrue(mp.Validate()); + + mp["oi"] = "oi"; + Assert.AreEqual(1, mp.Count()); + Assert.IsTrue(mp.Validate()); + + mp["oi"] = "oi1"; + Assert.AreEqual(1, mp.Count()); + + mp["oi1"] = "oi1"; + mp["oi2"] = "oi2"; + Assert.AreEqual(3, mp.Count()); + + mp["bala"] = "bala2"; + Assert.AreEqual(4, mp.Count()); + Assert.IsTrue(mp.Validate()); + + CheckCopy(mp); + } + + [TestMethod] + public void Dictionary() + { + var exemplo = new Dictionary + { + ["oi"] = "bala", + ["oi1"] = "bala1", + ["oi2"] = "bala2", + ["oi12"] = "bala12", + ["bola"] = "oi", + ["birosca123"] = "bruca123", + ["ca123"] = "que123", + ["oi123"] = "bala123" + }; + + var merklePatricia = NewTree(); + foreach (var keyValue in exemplo) + { + merklePatricia[keyValue.Key] = keyValue.Value; + } + + Assert.IsTrue(merklePatricia.Validate()); + + foreach (var keyValue in exemplo) + { + Assert.AreEqual(keyValue.Value, merklePatricia[keyValue.Key]); + } + + Assert.IsTrue(merklePatricia.Validate()); + + CheckCopy(merklePatricia); + } + + [TestMethod] + public void ListOfValues() + { + var lista = new[] {"oi", "oi1", "oi2", "oi12", "bola", "birosca123", "ca123", "oi123"}; + var mp = NewTree(); + foreach (var it in lista) + { + mp[it] = it; + Console.WriteLine($"Linha: '{it}:{Encoding.UTF8.GetBytes(it).ByteToHexString(false, false)}':\n{mp}"); + Assert.AreEqual(it, mp[it]); + Assert.IsTrue(mp.Validate()); + } + + CheckCopy(mp); + } + + [TestMethod] + public void One() + { + var merklePatricia = NewTree(); + Assert.IsTrue(merklePatricia.Validate()); +// Assert.AreEqual(0, merklePatricia.Height()); + + void InserirTestar(string x, string y) + { + merklePatricia[x] = y; + Assert.IsTrue(merklePatricia.Validate()); + Assert.IsTrue(merklePatricia.ContainsKey(x)); + Assert.IsFalse(merklePatricia.ContainsKey(x + "123")); + Assert.AreEqual(y, merklePatricia[x]); + Assert.IsNull(merklePatricia[x + "123k"]); + } + + InserirTestar("01a2", "valor1"); +// Assert.AreEqual(1, merklePatricia.Height()); + Assert.IsTrue(merklePatricia.Validate()); + + InserirTestar("11a2", "valor2"); + Assert.IsTrue(merklePatricia.Validate()); + + InserirTestar("0212", "valor3"); +// Assert.Equal(3, merklePatricia.Height()); + Assert.IsTrue(merklePatricia.Validate()); + + merklePatricia["0"] = "valor4"; + Assert.IsTrue(merklePatricia.Validate()); + + CheckCopy(merklePatricia); + } + + [TestMethod] + public void Remove() + { + var mp = NewTree(); + Assert.IsTrue(mp.Validate()); + + void RemoverTestar(string x, string y) + { + mp[x] = y; + Assert.IsTrue(mp.ContainsKey(x)); + Assert.IsFalse(mp.ContainsKey(x + "123")); + Assert.AreEqual(y, mp[x]); + Assert.IsNull(mp[x + "123k"]); + + Assert.IsTrue(mp.Remove(x)); + Assert.IsFalse(mp.Remove(x)); + } + + RemoverTestar("oi", "bala"); + Assert.IsTrue(mp.Validate()); + mp.Remove("oi"); + Assert.IsFalse(mp.ContainsKey("oi")); + Assert.IsTrue(mp.Validate()); + + mp["123"] = "abc"; + mp["a123"] = "1abc"; + Assert.AreEqual(2, mp.Count()); + Assert.IsTrue(mp.Validate()); + + Assert.IsFalse(mp.Remove("b123")); + Assert.AreEqual(2, mp.Count()); + Assert.IsTrue(mp.Remove("a123")); + Assert.IsTrue(mp.Validate()); + Assert.AreEqual(1, mp.Count()); + Assert.IsFalse(mp.ContainsKey("a123")); + Assert.IsTrue(mp.ContainsKey("123")); + Assert.IsTrue(mp.ContainsKey("123")); + Assert.IsTrue(mp.Validate()); + + var mp2 = NewTree(); + mp2["123"] = "abc"; + Assert.AreEqual(mp2, mp); + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(mp2.Validate()); + + CheckCopy(mp); + CheckCopy(mp2); + } + + [TestMethod] + public void EqualsThree() + { + Assert.AreNotEqual(null, NewTree()); + var mpOi = NewTree(); + mpOi["oi"] = "oi"; + Assert.AreNotEqual(NewTree(), mpOi); + + var mpA = NewTree(); + mpA["oi"] = "bola"; + mpA["oi1"] = "1bola"; + mpA["oi2"] = "b2ola"; + mpA["oi1"] = "bola1"; + Assert.IsTrue(mpA.Validate()); + + var mpB = NewTree(); + mpB["oi"] = "bola"; + mpB["oi1"] = "1bola"; + mpB["oi2"] = "b2ola"; + mpB["oi1"] = "bola1"; + Assert.IsTrue(mpB.Validate()); + Assert.AreEqual(mpA, mpB); + + mpA["oi"] = "escola"; + Assert.AreNotEqual(mpA, mpB); + Assert.IsTrue(mpA.Validate()); + + mpB["oi"] = "escola"; + Assert.AreEqual(mpA, mpB); + Assert.IsTrue(mpB.Validate()); + + mpA["oi123"] = "escola"; + mpA["oi12"] = "escola1"; + mpA["bola"] = "escola2"; + mpA["dog"] = "escola2"; + Assert.IsTrue(mpA.Validate()); + + mpB["bola"] = "escola2"; + mpB["dog"] = "escola2"; + mpB["oi12"] = "escola1"; + mpB["oi123"] = "escola"; + Assert.AreEqual(mpA, mpB); + Assert.IsTrue(mpB.Validate()); + + mpA.Remove("oi"); + mpA.Remove("oi"); + Assert.AreNotEqual(mpA, mpB); + Assert.IsTrue(mpA.Validate()); + + mpB.Remove("oi"); + Assert.AreEqual(mpA, mpB); + Assert.IsTrue(mpB.Validate()); + + Assert.IsNull(mpA["Meg"]); + + Assert.ThrowsException(() => mpA[(string) null]); + Assert.ThrowsException(() => mpA[(string) null] = null); + Assert.ThrowsException(() => mpA[(byte[]) null]); + Assert.ThrowsException(() => mpA[(byte[]) null] = null); + Assert.ThrowsException(() => mpA[new byte[] {0, 1}] = null); + Assert.ThrowsException(() => mpA["Meg"] = null); + + CheckCopy(mpA); + CheckCopy(mpB); + } + + [TestMethod] + public void ToStringTesting() + { + var mp = NewTree(); + Assert.AreEqual("{}", mp.ToString()); + mp["a"] = "a"; + var converted = Encoding.UTF8.GetBytes("a").ByteToHexString(); + Assert.AreEqual($"[\"{converted}\",\"{converted}\",\"{converted}\"]", mp.ToString()); + + CheckCopy(mp); + } + + [TestMethod] + public void Equals() + { + var mpNode = NewTree(); + Assert.AreNotEqual(mpNode, null); + Assert.IsFalse(mpNode == null); + Assert.IsFalse(mpNode.Equals(null)); + + Assert.AreEqual(mpNode, mpNode); + Assert.IsTrue(mpNode == mpNode); + Assert.IsTrue(mpNode.Equals(mpNode)); + + var mpA = NewTree(); + mpA["a"] = "a"; + Assert.AreNotEqual(mpNode, mpA); + Assert.IsFalse(mpNode == mpA); + Assert.IsFalse(mpNode.Equals(mpA)); + + CheckCopy(mpNode); + CheckCopy(mpA); + } + } +} \ No newline at end of file diff --git a/MPTStates.UnitTests/UT_MerklePatriciaNode.cs b/MPTStates.UnitTests/UT_MerklePatriciaNode.cs new file mode 100644 index 000000000..7eb09fa2c --- /dev/null +++ b/MPTStates.UnitTests/UT_MerklePatriciaNode.cs @@ -0,0 +1,361 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Cryptography; +using Neo.Ledger; +using Neo.Plugins.MPT; + +namespace Neo.UnitTests.Ledger +{ + [TestClass] + public class UT_MerklePatriciaNode + { + [TestMethod] + public void CloneLeaf() + { + var mptItem = MerklePatriciaNode.LeafNode(); + mptItem.Path = Encoding.UTF8.GetBytes("2"); + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + + var cloned = mptItem.Clone(); + Assert.IsTrue(cloned.IsLeaf); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = Encoding.UTF8.GetBytes("23f"); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + mptItem.Value = Encoding.UTF8.GetBytes("abc1"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + } + + [TestMethod] + public void CloneExtension() + { + var mptItem = MerklePatriciaNode.ExtensionNode(); + mptItem.Path = Encoding.UTF8.GetBytes("2"); + mptItem.Next = Encoding.UTF8.GetBytes("oi").Sha256(); + + var cloned = mptItem.Clone(); + Assert.IsTrue(cloned.IsExtension); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Next)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = Encoding.UTF8.GetBytes("23"); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + mptItem.Next = Encoding.UTF8.GetBytes("oi4").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Next)); + } + + [TestMethod] + public void CloneBranch() + { + var mptItem = MerklePatriciaNode.BranchNode(); + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + mptItem[0] = Encoding.UTF8.GetBytes("turma").Sha256(); + mptItem[7] = Encoding.UTF8.GetBytes("turma7").Sha256(); + mptItem[10] = Encoding.UTF8.GetBytes("turma10").Sha256(); + + var cloned = mptItem.Clone(); + Assert.IsTrue(cloned.IsBranch); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma").Sha256().SequenceEqual(cloned[0])); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma7").Sha256().SequenceEqual(cloned[7])); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma10").Sha256().SequenceEqual(cloned[10])); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Key = Encoding.UTF8.GetBytes("oi11").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + mptItem.Value = Encoding.UTF8.GetBytes("abc45"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + mptItem[0] = Encoding.UTF8.GetBytes("turma0").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma").Sha256().SequenceEqual(cloned[0])); + } + + [TestMethod] + public void FromReplicaLeaf() + { + var mptItem = MerklePatriciaNode.LeafNode(); + mptItem.Path = Encoding.UTF8.GetBytes("2"); + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + + var cloned = MerklePatriciaNode.BranchNode(); + cloned.FromReplica(mptItem); + Assert.IsTrue(cloned.IsLeaf); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = Encoding.UTF8.GetBytes("23f"); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + mptItem.Value = Encoding.UTF8.GetBytes("abc1"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + } + + [TestMethod] + public void FromReplicaExtension() + { + var mptItem = MerklePatriciaNode.ExtensionNode(); + mptItem.Path = Encoding.UTF8.GetBytes("2"); + mptItem.Next = Encoding.UTF8.GetBytes("oi").Sha256(); + + var cloned = MerklePatriciaNode.BranchNode(); + cloned.FromReplica(mptItem); + Assert.IsTrue(cloned.IsExtension); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Next)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = Encoding.UTF8.GetBytes("23"); + Assert.IsTrue(Encoding.UTF8.GetBytes("2").SequenceEqual(cloned.Path)); + mptItem.Next = Encoding.UTF8.GetBytes("oi4").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Next)); + } + + [TestMethod] + public void FromReplicaBranch() + { + var mptItem = MerklePatriciaNode.BranchNode(); + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + mptItem[0] = Encoding.UTF8.GetBytes("turma").Sha256(); + mptItem[7] = Encoding.UTF8.GetBytes("turma7").Sha256(); + mptItem[10] = Encoding.UTF8.GetBytes("turma10").Sha256(); + + var cloned = MerklePatriciaNode.LeafNode(); + cloned.FromReplica(mptItem); + Assert.IsTrue(cloned.IsBranch); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma").Sha256().SequenceEqual(cloned[0])); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma7").Sha256().SequenceEqual(cloned[7])); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma10").Sha256().SequenceEqual(cloned[10])); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Key = Encoding.UTF8.GetBytes("oi11").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + mptItem.Value = Encoding.UTF8.GetBytes("abc45"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + mptItem[0] = Encoding.UTF8.GetBytes("turma0").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma").Sha256().SequenceEqual(cloned[0])); + } + + [TestMethod] + public void SerializeLeaf() + { + var mptItem = MerklePatriciaNode.LeafNode(); + mptItem.Path = new byte[] {0, 1, 3, 4}; + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + + var cloned = MerklePatriciaNode.BranchNode(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mptItem.Serialize(bw); + using (var br = new BinaryReader(bw.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + + Assert.IsTrue(cloned.IsLeaf); + Assert.IsTrue(new byte[] {0, 1, 3, 4}.SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = Encoding.UTF8.GetBytes("23f"); + Assert.IsTrue(new byte[] {0, 1, 3, 4}.SequenceEqual(cloned.Path)); + mptItem.Value = Encoding.UTF8.GetBytes("abc1"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + } + + [TestMethod] + public void SerializeLeafEmptyPath() + { + var mptItem = MerklePatriciaNode.LeafNode(); + mptItem.Path = new byte[0]; + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + + var cloned = MerklePatriciaNode.BranchNode(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mptItem.Serialize(bw); + using (var br = new BinaryReader(bw.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + + Assert.IsTrue(cloned.IsLeaf); + Assert.IsTrue(new byte[0].SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = Encoding.UTF8.GetBytes("23f"); + Assert.IsTrue(new byte[0].SequenceEqual(cloned.Path)); + mptItem.Value = Encoding.UTF8.GetBytes("abc1"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + } + + [TestMethod] + public void SerializeExtension() + { + var mptItem = MerklePatriciaNode.ExtensionNode(); + mptItem.Path = new byte[] {0, 1}; + mptItem.Next = Encoding.UTF8.GetBytes("oi").Sha256(); + + var cloned = MerklePatriciaNode.BranchNode(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mptItem.Serialize(bw); + using (var br = new BinaryReader(bw.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + + Assert.IsTrue(cloned.IsExtension); + Assert.IsTrue(new byte[] {0, 1}.SequenceEqual(cloned.Path)); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Next)); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Path = new byte[] {2, 3, 7, 8}; + Assert.IsTrue(new byte[] {0, 1}.SequenceEqual(cloned.Path)); + mptItem.Next = Encoding.UTF8.GetBytes("oi4").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Next)); + } + + [TestMethod] + public void SerializeBranch() + { + var mptItem = MerklePatriciaNode.BranchNode(); + mptItem.Key = Encoding.UTF8.GetBytes("oi").Sha256(); + mptItem.Value = Encoding.UTF8.GetBytes("abc"); + mptItem[0] = Encoding.UTF8.GetBytes("turma").Sha256(); + mptItem[7] = Encoding.UTF8.GetBytes("turma7").Sha256(); + mptItem[10] = Encoding.UTF8.GetBytes("turma10").Sha256(); + + var cloned = MerklePatriciaNode.LeafNode(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mptItem.Serialize(bw); + using (var br = new BinaryReader(bw.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + + Assert.IsTrue(cloned.IsBranch); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma").Sha256().SequenceEqual(cloned[0])); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma7").Sha256().SequenceEqual(cloned[7])); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma10").Sha256().SequenceEqual(cloned[10])); + + Assert.AreEqual(mptItem, cloned); + + mptItem.Key = Encoding.UTF8.GetBytes("oi11").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("oi").Sha256().SequenceEqual(cloned.Key)); + mptItem.Value = Encoding.UTF8.GetBytes("abc45"); + Assert.IsTrue(Encoding.UTF8.GetBytes("abc").SequenceEqual(cloned.Value)); + mptItem[0] = Encoding.UTF8.GetBytes("turma0").Sha256(); + Assert.IsTrue(Encoding.UTF8.GetBytes("turma").Sha256().SequenceEqual(cloned[0])); + } + + [TestMethod] + public void EqualsBranch() + { + var mpNode = MerklePatriciaNode.BranchNode(); + Assert.AreNotEqual(mpNode, null); + Assert.AreEqual(mpNode, mpNode); + Assert.AreNotEqual(mpNode, MerklePatriciaNode.ExtensionNode()); + } + + [TestMethod] + public void EqualsLeaf() + { + var mpNode = MerklePatriciaNode.LeafNode(); + Assert.AreNotEqual(mpNode, null); + Assert.AreEqual(mpNode, mpNode); + Assert.AreNotEqual(mpNode, MerklePatriciaNode.ExtensionNode()); + } + + [TestMethod] + public void EqualsExtension() + { + var mpNode = MerklePatriciaNode.ExtensionNode(); + Assert.AreNotEqual(mpNode, null); + Assert.AreEqual(mpNode, mpNode); + Assert.AreNotEqual(mpNode, MerklePatriciaNode.LeafNode()); + } + + [TestMethod] + public void ToStringBranch() + { + var mpNode = MerklePatriciaNode.BranchNode(); + Assert.AreEqual("{}", $"{mpNode}"); + } + + [TestMethod] + public void UsingOnSet() + { + var setObj = new HashSet {MerklePatriciaNode.BranchNode()}; + Assert.AreEqual(1, setObj.Count); + setObj.Add(MerklePatriciaNode.ExtensionNode()); + Assert.AreEqual(2, setObj.Count); + setObj.Add(MerklePatriciaNode.LeafNode()); + Assert.AreEqual(3, setObj.Count); + setObj.Add(MerklePatriciaNode.ExtensionNode()); + Assert.AreEqual(3, setObj.Count); + + var node = MerklePatriciaNode.LeafNode(); + node.Key = new byte[] {0, 1}; + node.Value = new byte[] {0, 1}; + setObj.Add(node); + Assert.AreEqual(4, setObj.Count); + } + + [TestMethod] + public void ToStringExtension() + { + var mpNode = MerklePatriciaNode.ExtensionNode(); + Assert.AreEqual("[null,null]", $"{mpNode}"); + } + + [TestMethod] + public void ToStringLeaf() + { + var mpNode = MerklePatriciaNode.LeafNode(); + Assert.AreEqual("[null,null,null]", $"{mpNode}"); + } + } +} \ No newline at end of file diff --git a/MPTStates.UnitTests/UT_MerklePatriciaPersistence.cs b/MPTStates.UnitTests/UT_MerklePatriciaPersistence.cs new file mode 100644 index 000000000..b671be7a0 --- /dev/null +++ b/MPTStates.UnitTests/UT_MerklePatriciaPersistence.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Ledger; +using Neo.Plugins.MPT; + +namespace Neo.UnitTests.Ledger +{ + [TestClass] + public class UT_MerklePatriciaPersistence + { + private static MerklePatriciaTree NewTree() => new MerklePatriciaTree(); + + [TestMethod] + public void Serialize() + { + var mp = NewTree(); + mp["oi"] = "bola"; + + var cloned = NewTree(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mp.Serialize(bw); + using (var br = new BinaryReader(bw.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(cloned.Validate()); + Assert.AreEqual("bola", cloned["oi"]); + Assert.AreEqual(mp, cloned); + + mp["cachorro"] = "cachorro"; + Assert.AreNotEqual(mp, cloned); + } + + [TestMethod] + public void SerializeFakingData() + { + var mp = NewTree(); + mp["oi"] = "bola"; + + var cloned = NewTree(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mp.Serialize(bw); + var data = ms.ToArray(); + data[data.Length - 2] += 1; + using (var memoryStream = new MemoryStream()) + using (var binWriter = new BinaryWriter(memoryStream)) + { + binWriter.Write(data); + using (var br = new BinaryReader(binWriter.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + } + + Assert.IsTrue(mp.Validate()); + Assert.IsFalse(cloned.Validate()); + Assert.AreNotEqual(mp, cloned); + + Assert.AreEqual("bola", mp["oi"]); + Assert.AreNotEqual("bola", cloned["oi"]); + } + + [TestMethod] + public void SerializeFakingDataBranch() + { + var mp = NewTree(); + mp["oi"] = "bola"; + mp["ola"] = "ola"; + + var cloned = NewTree(); + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + mp.Serialize(bw); + var data = ms.ToArray(); + data[data.Length - 2] += 1; + using (var memoryStream = new MemoryStream()) + using (var binWriter = new BinaryWriter(memoryStream)) + { + binWriter.Write(data); + using (var br = new BinaryReader(binWriter.BaseStream)) + { + br.BaseStream.Position = 0; + cloned.Deserialize(br); + } + } + } + + Assert.IsTrue(mp.Validate()); + Assert.IsFalse(cloned.Validate()); + Assert.AreNotEqual(mp, cloned); + } + + [TestMethod] + public void Clone() + { + var mp = NewTree(); + mp["oi"] = "bola"; + + var cloned = mp.Clone(); + + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(cloned.Validate()); + Assert.AreEqual("bola", cloned["oi"]); + Assert.AreEqual(mp, cloned); + + mp["cachorro"] = "cachorro"; + Assert.AreNotEqual(mp, cloned); + } + + [TestMethod] + public void FromReplica() + { + var mp = NewTree(); + mp["oi"] = "bola"; + + var cloned = NewTree(); + cloned.FromReplica(mp); + + Assert.IsTrue(mp.Validate()); + Assert.IsTrue(cloned.Validate()); + Assert.AreEqual("bola", cloned["oi"]); + Assert.AreEqual(mp, cloned); + + mp["cachorro"] = "cachorro"; + Assert.AreNotEqual(mp, cloned); + } + + [TestMethod] + public void UsingOnSet() + { + var setObj = new HashSet(); + Assert.AreEqual(0, setObj.Count); + + var mpA = NewTree(); + mpA["a"] = "a"; + setObj.Add(mpA); + Assert.AreEqual(1, setObj.Count); + + var mpB = NewTree(); + mpB["b"] = "b"; + setObj.Add(mpB); + Assert.AreEqual(2, setObj.Count); + + var mpC = NewTree(); + mpC["b"] = "b"; + setObj.Add(mpC); + Assert.AreEqual(2, setObj.Count); + + Assert.IsTrue(setObj.Remove(mpA)); + Assert.AreEqual(1, setObj.Count); + + var mpD = NewTree(); + mpD["d"] = "d"; + Assert.IsFalse(setObj.Remove(mpD)); + setObj.Add(mpD); + Assert.AreEqual(2, setObj.Count); + + setObj.Add(NewTree()); + } + } +} \ No newline at end of file diff --git a/MPTStates.UnitTests/UT_MerklePatriciaTools.cs b/MPTStates.UnitTests/UT_MerklePatriciaTools.cs new file mode 100644 index 000000000..6637dc290 --- /dev/null +++ b/MPTStates.UnitTests/UT_MerklePatriciaTools.cs @@ -0,0 +1,66 @@ +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Plugins.MPT; + +namespace Neo.UnitTests.Ledger +{ + [TestClass] + public class UT_MerklePatriciaTools + { + [TestMethod] + public void DistinctRoot() + { + Assert.AreEqual("faf01", new byte[] {0xfa, 0xf0, 0x1}.ByteToHexString(false, false)); + Assert.AreEqual("fa f0 1", new byte[] {0xfa, 0xf0, 0x1}.ByteToHexString(true, false)); + Assert.AreEqual("faf001", new byte[] {0xfa, 0xf0, 0x1}.ByteToHexString(false)); + Assert.AreEqual("fa f0 01", new byte[] {0xfa, 0xf0, 0x1}.ByteToHexString()); + } + + [TestMethod] + public void Inc() + { + Assert.AreEqual(true, 1.GetBit(0)); + Assert.AreEqual(false, 2.GetBit(0)); + } + + [TestMethod] + public void CompactEncode() + { + Assert.IsTrue(new byte[] {1, 2, 3, 4, 5}.CompactEncode().SequenceEqual(new byte[] {0x11, 0x23, 0x45})); + Assert.IsTrue(new byte[] {0, 1, 2, 3, 4, 5}.CompactEncode() + .SequenceEqual(new byte[] {0, 0x01, 0x23, 0x45})); + Assert.IsTrue(new byte[] {0, 0xf, 1, 0xc, 0xb, 8, 10}.CompactEncode() + .SequenceEqual(new byte[] {0x10, 0xf1, 0xcb, 0x8a})); + Assert.IsTrue(new byte[] {0xf, 1, 0xc, 0xb, 8, 10}.CompactEncode() + .SequenceEqual(new byte[] {0x00, 0xf1, 0xcb, 0x8a})); + + Assert.IsTrue(new byte[] {1, 2, 3, 4, 5}.CompactEncode(true).SequenceEqual(new byte[] {0x31, 0x23, 0x45})); + Assert.IsTrue(new byte[] {0, 1, 2, 3, 4, 5}.CompactEncode(true) + .SequenceEqual(new byte[] {0x20, 0x01, 0x23, 0x45})); + Assert.IsTrue(new byte[] {0, 0xf, 1, 0xc, 0xb, 8, 10}.CompactEncode(true) + .SequenceEqual(new byte[] {0x30, 0xf1, 0xcb, 0x8a})); + Assert.IsTrue(new byte[] {0xf, 1, 0xc, 0xb, 8, 10}.CompactEncode(true) + .SequenceEqual(new byte[] {0x20, 0xf1, 0xcb, 0x8a})); + } + + [TestMethod] + public void CompactDecode() + { + Assert.IsTrue(new byte[] {0x11, 0x23, 0x45}.CompactDecode().SequenceEqual(new byte[] {1, 2, 3, 4, 5})); + Assert.IsTrue(new byte[] {0, 0x01, 0x23, 0x45}.CompactDecode() + .SequenceEqual(new byte[] {0, 1, 2, 3, 4, 5})); + Assert.IsTrue(new byte[] {0x10, 0xf1, 0xcb, 0x8a}.CompactDecode() + .SequenceEqual(new byte[] {0, 0xf, 1, 0xc, 0xb, 8, 10})); + Assert.IsTrue(new byte[] {0x00, 0xf1, 0xcb, 0x8a}.CompactDecode() + .SequenceEqual(new byte[] {0xf, 1, 0xc, 0xb, 8, 10})); + + Assert.IsTrue(new byte[] {0x31, 0x23, 0x45}.CompactDecode().SequenceEqual(new byte[] {1, 2, 3, 4, 5})); + Assert.IsTrue(new byte[] {0x20, 0x01, 0x23, 0x45}.CompactDecode() + .SequenceEqual(new byte[] {0, 1, 2, 3, 4, 5})); + Assert.IsTrue(new byte[] {0x30, 0xf1, 0xcb, 0x8a}.CompactDecode() + .SequenceEqual(new byte[] {0, 0xf, 1, 0xc, 0xb, 8, 10})); + Assert.IsTrue(new byte[] {0x20, 0xf1, 0xcb, 0x8a}.CompactDecode() + .SequenceEqual(new byte[] {0xf, 1, 0xc, 0xb, 8, 10})); + } + } +} \ No newline at end of file diff --git a/MPTStates/ByteArrayComparer.cs b/MPTStates/ByteArrayComparer.cs new file mode 100644 index 000000000..c397cc14e --- /dev/null +++ b/MPTStates/ByteArrayComparer.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Neo.Cryptography; + +namespace Neo +{ + /// + /// + /// Byte array comparer, useful to have a byte array on a set or as key of a dictionary. + /// + public class ByteArrayComparer : IEqualityComparer + { + /// + public bool Equals(byte[] left, byte[] right) + { + if (left == null || right == null) + { + return left == null && right == null; + } + + return left.SequenceEqual(right); + } + + /// + public int GetHashCode(byte[] key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return (int) key.Murmur32(0); + } + } +} \ No newline at end of file diff --git a/MPTStates/MPT/MPTKey.cs b/MPTStates/MPT/MPTKey.cs new file mode 100644 index 000000000..3fa4e295f --- /dev/null +++ b/MPTStates/MPT/MPTKey.cs @@ -0,0 +1,56 @@ +using Neo.IO; +using System; +using System.IO; + +namespace Neo.Plugins.MPT +{ + /// + /// + /// MPTKey for the DataCache. + /// + public class MPTKey : IEquatable, ISerializable + { + public UInt160 ScriptHash; + public UInt256 HashKey; + + /// + int ISerializable.Size => ScriptHash.Size + HashKey.Size; + + /// + public bool Equals(MPTKey other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return ScriptHash.Equals(other.ScriptHash) && HashKey.Equals(other.HashKey); + } + + /// + public override bool Equals(object obj) + { + if (obj is null) return false; + return obj is MPTKey key && Equals(key); + } + + /// + public override int GetHashCode() => ScriptHash.GetHashCode() + HashKey.GetHashCode(); + + /// + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(ScriptHash); + writer.Write(HashKey); + } + + /// + void ISerializable.Deserialize(BinaryReader reader) + { + ScriptHash = reader.ReadSerializable(); + HashKey = reader.ReadSerializable(); + } + + /// + public override string ToString() => $"{{'ScriptHash':{ScriptHash}, 'HashKey':{HashKey}}}"; + } +} \ No newline at end of file diff --git a/MPTStates/MPT/MerklePatricia.cs b/MPTStates/MPT/MerklePatricia.cs new file mode 100644 index 000000000..35dcf6a13 --- /dev/null +++ b/MPTStates/MPT/MerklePatricia.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Neo.Cryptography; +using Neo.Ledger; +using Neo.Plugins.MPT.Operation; + +namespace Neo.Plugins.MPT +{ + /// + /// + /// Modified Merkel Patricia Tree. + /// Note: It is not a thread safe implementation. + /// + public abstract class MerklePatricia : StateBase, IEquatable + { + private readonly MPTSet mptSet; + private readonly MPTGet mptGet; + private readonly MPTRemove mptRemove; + private readonly MPTValidate mptValidate; + + /// + /// Get data from the database. + /// + protected abstract MerklePatriciaNode GetDb(byte[] key); + + /// + /// Removes data from the database. + /// + protected abstract bool RemoveDb(byte[] key); + + /// + /// Set data on the database. + /// + protected abstract MerklePatriciaNode SetDb(byte[] kye, MerklePatriciaNode node); + + /// + /// Check if the database contains the key. + /// + protected abstract bool ContainsKeyDb(byte[] key); + + /// + /// Get the root hash. + /// + protected abstract byte[] GetRoot(); + + /// + /// Change the root hash. + /// + protected abstract void SetRoot(byte[] root); + + internal MerklePatricia() + { + mptSet = new MPTSet(GetDb, RemoveDb, SetDb, GetRoot, SetRoot); + mptGet = new MPTGet(GetDb, GetRoot); + mptRemove = new MPTRemove(GetDb, RemoveDb, SetDb, GetRoot, SetRoot); + mptValidate = new MPTValidate(GetDb, ContainsKeyDb); + } + + /// + /// Get and set the key and value pairs of the tree. + /// + /// The string key that indicates the reference. + public string this[string key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var resp = this[Encoding.UTF8.GetBytes(key)]; + return resp == null ? null : Encoding.UTF8.GetString(resp); + } + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + this[Encoding.UTF8.GetBytes(key)] = Encoding.UTF8.GetBytes(value); + } + } + + + /// + /// Get and set the key and value pairs of the tree. + /// + /// The key that indicates the reference. + public byte[] this[byte[] key] + { + get => mptGet.Get(key); + set => mptSet.Set(key, value); + } + + /// + /// Test if contains a specific key. + /// + /// Key to be tested. + /// true in the case the tree contains the key. + public bool ContainsKey(byte[] key) => this[key] != null; + + public bool ContainsKey(string key) => ContainsKey(Encoding.UTF8.GetBytes(key)); + + /// + /// Removes a value for a specific key. + /// + /// Remove this key from the tree. + /// true is the key was present and sucessifully removed. + public bool Remove(string key) => Remove(Encoding.UTF8.GetBytes(key)); + + /// + /// Removes a value for a specific key. + /// + /// Remove this key from the tree. + /// true is the key was present and sucessifully removed. + public bool Remove(byte[] key) => mptRemove.Remove(key); + + /// + /// Checks if the hashes correspond to their nodes. + /// + /// In the case the validation is Ok. + public bool Validate() => mptValidate.Validate(GetRoot()); + + /// + public override string ToString() => GetRoot() == null ? "{}" : ToString(GetDb(GetRoot())); + + /// + private string ToString(MerklePatriciaNode node) + { + if (node.IsExtension) + { + return $"{{\"{node.Path.ByteToHexString(false, false)}\": {ToString(GetDb(node.Next))}}}"; + } + + if (node.IsLeaf) + { + return node.ToString(); + } + + var resp = new StringBuilder("{"); + var virgula = false; + for (var i = 0; i < node.Length; i++) + { + if (node[i] == null) continue; + resp.Append(virgula ? "," : "") + .Append(i < node.Length - 2 + ? $"\"{i:x}\":{ToString(GetDb(node[i]))}" + : $"\"{i:x}\":\"{node[i].ByteToHexString(false, false)}\""); + + virgula = true; + } + + return resp.Append("}").ToString(); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((MerklePatricia) obj); + } + + /// + public bool Equals(MerklePatricia other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + if (GetRoot() == null && other.GetRoot() == null) return true; + if ((GetRoot() == null && other.GetRoot() != null) || + (GetRoot() != null && other.GetRoot() == null)) + { + return false; + } + + if (!GetRoot().SequenceEqual(other.GetRoot())) + { + return false; + } + + var hashesToCheck = new Stack(); + hashesToCheck.Push(GetRoot()); + while (hashesToCheck.Count > 0) + { + var it = hashesToCheck.Pop(); + if (it == null) continue; + var itNode = GetDb(it); + var otherNode = other.GetDb(it); + if (otherNode == null || !otherNode.Equals(itNode)) + { + return false; + } + + foreach (var hash in itNode) + { + if (hash != null) + { + hashesToCheck.Push(hash); + } + } + } + + return true; + } + + /// + public override int GetHashCode() => GetRoot() != null ? (int) GetRoot().Murmur32(0) : 0; + } +} \ No newline at end of file diff --git a/MPTStates/MPT/MerklePatriciaDataCache.cs b/MPTStates/MPT/MerklePatriciaDataCache.cs new file mode 100644 index 000000000..07534869a --- /dev/null +++ b/MPTStates/MPT/MerklePatriciaDataCache.cs @@ -0,0 +1,55 @@ +using Neo.IO; +using Neo.IO.Caching; + +namespace Neo.Plugins.MPT +{ + /// + /// + /// The MerklePatriciaDataCache data structure delegates the calls to the DataCache, + /// so there is no need to retrieve all the MPT from the database. + /// + public class MerklePatriciaDataCache : MerklePatricia + { + private readonly DataCache db; + private byte[] _rootHash; + + public MerklePatriciaDataCache(DataCache db, byte[] rootHash) + { + this.db = db; + _rootHash = rootHash; + if (_rootHash != null && _rootHash.Length == 0) + { + _rootHash = null; + } + } + + /// + protected override MerklePatriciaNode GetDb(byte[] hash) => db.TryGet(hash.AsSerializable()); + + /// + protected override bool RemoveDb(byte[] hash) + { + db.Delete(hash.AsSerializable()); + return true; + } + + /// + protected override MerklePatriciaNode SetDb(byte[] hash, MerklePatriciaNode node) + { + var hashAsSerializable = hash.AsSerializable(); + // This next line is necessary due to the caching mechanism + db.Delete(hashAsSerializable); + db.Add(hashAsSerializable, node); + return node; + } + + /// + protected override bool ContainsKeyDb(byte[] key) => db.TryGet(key.AsSerializable()) != null; + + /// + protected override byte[] GetRoot() => _rootHash; + + /// + protected override void SetRoot(byte[] root) => _rootHash = root; + } +} \ No newline at end of file diff --git a/MPTStates/MPT/MerklePatriciaNode.cs b/MPTStates/MPT/MerklePatriciaNode.cs new file mode 100644 index 000000000..c72fa5e50 --- /dev/null +++ b/MPTStates/MPT/MerklePatriciaNode.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Neo.Cryptography; +using Neo.IO; +using Neo.Ledger; + +namespace Neo.Plugins.MPT +{ + /// + /// + /// Modified Merkel Patricia Node. + /// Note: It is not a thread safe implementation. + /// + public class MerklePatriciaNode : StateBase, + ICloneable, + IEquatable, + IEnumerable + { + private const int BranchSize = 18; + private const int ExtensionSize = 2; + private const int LeafSize = 3; + + private byte[][] _hashes; + + public MerklePatriciaNode() : this(0) + { + } + + public MerklePatriciaNode(int size) => _hashes = new byte[size][]; + + /// + /// Indicates if the node is a branch. + /// + public bool IsBranch => _hashes.Length == BranchSize; + + /// + /// Indicates if the node is an extension. + /// + public bool IsExtension => _hashes.Length == ExtensionSize; + + /// + /// Indicates if the node is a leaf. + /// + public bool IsLeaf => _hashes.Length == LeafSize; + + /// + /// Get and set the hashes by index. + /// + /// Index of the hash to get or set. + public byte[] this[int index] + { + get => _hashes[index]; + set => _hashes[index] = value; + } + + /// + /// Get and set the path of the node. + /// Used for leaf and extension nodes. + /// + public byte[] Path + { + get => _hashes[0]; + set => _hashes[0] = value; + } + + /// + /// Get and set the key of the node. + /// Used for leaf and branch nodes. + /// + public byte[] Key + { + get => _hashes[_hashes.Length - 2]; + set => _hashes[_hashes.Length - 2] = value; + } + + /// + /// Get and set the value of the node. + /// Used for leaf and branch nodes. + /// + public byte[] Value + { + get => _hashes[_hashes.Length - 1]; + set => _hashes[_hashes.Length - 1] = value; + } + + /// + /// Get and set the hash of the next node. + /// Only for extension node. + /// + public byte[] Next + { + get => _hashes[_hashes.Length - 1]; + set => _hashes[_hashes.Length - 1] = value; + } + + /// + /// Calculates the node hash. + /// + /// The calculated hash. + public byte[] Hash() + { + var bytes = new List(); + for (var i = 0; i < _hashes.Length; i++) + { + bytes.Add((byte) i); + if (_hashes[i] != null) + { + bytes.AddRange(_hashes[i]); + } + } + + return new Crypto().Hash256(bytes.ToArray()); + } + + /// + public override string ToString() + { + var resp = new StringBuilder(IsBranch ? "{" : "["); + var virgula = false; + for (var i = 0; i < _hashes.Length; i++) + { + if (IsBranch && _hashes[i] == null) continue; + resp.Append(virgula ? "," : "") + .Append(IsBranch ? $"\"{i:x}\":" : "") + .Append(_hashes[i] != null ? $"\"{_hashes[i].ByteToHexString(false, false)}\"" : "null"); + virgula = true; + } + + return resp.Append(IsBranch ? "}" : "]").ToString(); + } + + /// + /// + /// Iterates only on the hashes. + /// + /// + public IEnumerator GetEnumerator() + { + if (IsLeaf) + { + yield break; + } + + if (IsExtension) + { + yield return Next; + } + else + { + for (var i = 0; i < Length - 2; ++i) + { + yield return _hashes[i]; + } + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// The number of hashes on the node. + /// + public int Length => _hashes.Length; + + /// + /// Creates a branch node. + /// + /// The node created. + public static MerklePatriciaNode BranchNode() => new MerklePatriciaNode(BranchSize); + + /// + /// Creates an extension node. + /// + /// The node created. + public static MerklePatriciaNode ExtensionNode() => new MerklePatriciaNode(ExtensionSize); + + /// + /// Creates a leaf node. + /// + /// The node created. + public static MerklePatriciaNode LeafNode() => new MerklePatriciaNode(LeafSize); + + /// + public MerklePatriciaNode Clone() + { + var resp = new MerklePatriciaNode(Length); + for (var i = 0; i < Length; i++) + { + resp._hashes[i] = _hashes[i] != null ? _hashes[i].ToArray() : null; + } + + return resp; + } + + /// + public void FromReplica(MerklePatriciaNode replica) + { + _hashes = new byte[replica.Length][]; + for (var i = 0; i < Length; i++) + { + _hashes[i] = replica._hashes[i] != null ? replica._hashes[i].ToArray() : null; + } + } + + /// + public bool Equals(MerklePatriciaNode other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (_hashes.Length != other.Length) + { + return false; + } + + return !_hashes.Where((t, i) => + (t != null || other._hashes[i] != null) && + ((t == null && other._hashes[i] != null) || (t != null && other._hashes[i] == null) || + !t.SequenceEqual(other._hashes[i]))).Any(); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((MerklePatriciaNode) obj); + } + + /// + public override int GetHashCode() => (int) Hash().Murmur32(0); + + /// + public override void Deserialize(BinaryReader reader) + { + base.Deserialize(reader); + _hashes = new byte[reader.ReadByte()][]; + for (var i = 0; i < _hashes.Length; i++) + { + if (i == 0 && !IsBranch) + { + _hashes[i] = reader.ReadVarBytes().CompactDecode(); + } + else + { + _hashes[i] = reader.ReadVarBytes(); + _hashes[i] = IsBranch && _hashes[i].Length == 0 ? null : _hashes[i]; + } + } + } + + /// + public override void Serialize(BinaryWriter writer) + { + base.Serialize(writer); + writer.Write((byte) _hashes.Length); + for (var i = 0; i < _hashes.Length; i++) + { + if (i == 0 && !IsBranch) + { + writer.WriteVarBytes(_hashes[i] != null ? _hashes[i].CompactEncode() : new byte[0]); + } + else + { + writer.WriteVarBytes(_hashes[i] ?? new byte[0]); + } + } + } + + /// + /// Returns the number of the first not null hash and the number of hashes. + /// + /// Max of hashes to check. + /// The index and the number of not null hashes. + public (int, int) IndexAndCountNotNullHashes(int max = 2) + { + var i = 0; + var cont = 0; + var index = -1; + foreach (var it in this) + { + if (it != null) + { + cont++; + index = i; + } + + ++i; + if (cont >= max) + { + break; + } + } + + return (index, cont); + } + } +} \ No newline at end of file diff --git a/MPTStates/MPT/MerklePatriciaTools.cs b/MPTStates/MPT/MerklePatriciaTools.cs new file mode 100644 index 000000000..f497a8520 --- /dev/null +++ b/MPTStates/MPT/MerklePatriciaTools.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Neo.Ledger; + +namespace Neo.Plugins.MPT +{ + public static class MerklePatriciaTools + { + /// + /// Converts a byte array to an hexadecimal string. + /// + /// Byte array to be converted. + /// The converted string. + public static string ByteToHexString(this byte[] hexchar, bool useSpace = true, bool forceTwoChars = true) => + string.Join(useSpace ? " " : "", + hexchar.Select(x => x.ToString("x" + (forceTwoChars ? "2" : ""))).ToList()); + + /// + /// Get the index-th bit of a number. + /// + /// Number from where to get the bit. + /// Index of the bit. + /// The bit + public static bool GetBit(this int number, int index) => (number >> index) % 2 == 1; + + /// + /// Encodes nibbles into bytes. + /// + /// Nibbles to be encoded. + /// Indicates if is a leaf or an extension node. + /// An array of bytes + public static byte[] CompactEncode(this byte[] hexarray, bool isLeaf = false) + { + var first = (byte) ((isLeaf ? 2 : 0) + hexarray.Length % 2); + var hexarrayList = new List(hexarray); + if (first % 2 == 0) + { + hexarrayList.Insert(0, first); + hexarrayList.Insert(1, 0); + } + else + { + hexarrayList.Insert(0, first); + } + + var resp = new List(); + var hexarrayListCount = hexarrayList.Count; + for (var i = 0; i < hexarrayListCount; i += 2) + { + resp.Add((byte) (16 * hexarrayList[i] + hexarrayList[i + 1])); + } + + return resp.ToArray(); + } + + /// + /// Decodes an array of bytes to a array of nibbles. + /// + /// The array to be decoded. + /// An array of nibbles. + public static byte[] CompactDecode(this byte[] hexarray) + { + var resp = new List(hexarray.Length * 2); + if (hexarray[0] / 16 % 2 == 1) + { + resp.Add((byte) (hexarray[0] % 16)); + } + + for (var i = 1; i < hexarray.Length; i++) + { + resp.Add((byte) (hexarray[i] / 16)); + resp.Add((byte) (hexarray[i] % 16)); + } + + return resp.ToArray(); + } + + /// + /// Converts byte array to StorageItem + /// + /// The byte array. + /// The generated StorageItem. + public static StorageItem ToStorageItem(this byte[] data) + { + using (var memoryStream = new MemoryStream()) + using (var binWriter = new BinaryWriter(memoryStream)) + { + binWriter.Write(data); + using (var br = new BinaryReader(binWriter.BaseStream)) + { + br.BaseStream.Position = 0; + var item = new StorageItem(); + item.Deserialize(br); + return item; + } + } + } + + /// + /// Convert a byte array in a nibble representation. + /// + /// The byte array to be converted. + /// The byte array with the nibbles. + public static byte[] ConvertToNibble(this byte[] key) + { + var resp = new byte[key.Length * 2]; + for (var i = 0; i < key.Length; i++) + { + resp[2 * i] = (byte) (key[i] / 16); + resp[2 * i + 1] = (byte) (key[i] % 16); + } + + return resp; + } + } +} \ No newline at end of file diff --git a/MPTStates/MPT/MerklePatriciaTree.cs b/MPTStates/MPT/MerklePatriciaTree.cs new file mode 100644 index 000000000..7c7b4ab36 --- /dev/null +++ b/MPTStates/MPT/MerklePatriciaTree.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Neo.IO; + +namespace Neo.Plugins.MPT +{ + /// + /// + public class MerklePatriciaTree : MerklePatricia, + ICloneable + { + private readonly MerklePatricia _merklePatricia; + private byte[] _rootHash; + + private readonly Dictionary db = new + Dictionary(new ByteArrayComparer()); + + /// + protected override MerklePatriciaNode GetDb(byte[] hash) => db[hash]; + + /// + protected override bool RemoveDb(byte[] hash) => db.Remove(hash); + + /// + protected override MerklePatriciaNode SetDb(byte[] hash, MerklePatriciaNode node) => db[hash] = node; + + /// + protected override bool ContainsKeyDb(byte[] key) => db.ContainsKey(key); + + /// + protected override byte[] GetRoot() => _rootHash; + + /// + protected override void SetRoot(byte[] root) => _rootHash = root; + + private bool ContainsValueDb(byte[] value) => + db.Any(x => x.Value.Value != null && x.Value.Value.SequenceEqual(value)); + + private void ClearDb() => db.Clear(); + private int CountAllDb() => db.Count(); + private int CountValuesDb() => db.Count(x => x.Value.IsLeaf || (x.Value.IsBranch && x.Value.Value != null)); + private IEnumerable> GetEnumeratorDb() => db; + private MerklePatriciaTree NewTree() => new MerklePatriciaTree(); + + /// + public MerklePatriciaTree Clone() + { + var resp = NewTree(); + resp.SetRoot(GetRoot()?.ToArray()); + foreach (var entry in GetEnumeratorDb()) + { + resp.SetDb(entry.Key.ToArray(), entry.Value.Clone()); + } + + return resp; + } + + /// + public void FromReplica(MerklePatriciaTree replica) + { + ClearDb(); + SetRoot(replica.GetRoot()?.ToArray()); + foreach (var entry in replica.GetEnumeratorDb()) + { + SetDb(entry.Key.ToArray(), entry.Value.Clone()); + } + } + + /// + public override void Deserialize(BinaryReader reader) + { + base.Deserialize(reader); + ClearDb(); + SetRoot(reader.ReadVarBytes()); + if (GetRoot().Length == 0) + { + SetRoot(null); + } + + var size = reader.ReadVarInt(); + for (var i = 0ul; i < size; i++) + { + var key = reader.ReadVarBytes(); + var value = MerklePatriciaNode.ExtensionNode(); + value.Deserialize(reader); + SetDb(key, value); + } + } + + /// + public override void Serialize(BinaryWriter writer) + { + base.Serialize(writer); + writer.WriteVarBytes(GetRoot() ?? new byte[0]); + writer.WriteVarInt(CountAllDb()); + foreach (var it in GetEnumeratorDb()) + { + writer.WriteVarBytes(it.Key); + writer.Write(it.Value); + } + } + + /// + /// Count the number o values in the MPT not all the nodes, just the ones whith associated values. + /// + /// The number of values. + public int Count() => CountValuesDb(); + + /// + /// Verify if the MPT contains the specific value. + /// + /// The value to be checked. + /// true if the value is present. + public bool ContainsValue(string value) => ContainsValue(Encoding.UTF8.GetBytes(value)); + + /// + /// Test if the tree contains a specific value. + /// Takes O(n) operations. + /// + /// Value to look for. + /// true if the value is present. + public bool ContainsValue(byte[] value) => ContainsValueDb(value); + } +} \ No newline at end of file diff --git a/MPTStates/MPT/Operation/MPTGet.cs b/MPTStates/MPT/Operation/MPTGet.cs new file mode 100644 index 000000000..fd027b88e --- /dev/null +++ b/MPTStates/MPT/Operation/MPTGet.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; + +namespace Neo.Plugins.MPT.Operation +{ + /// + /// MPT get operation. + /// + internal class MPTGet + { + /// + /// Delegate to get data from the database. + /// + private readonly Func _getDb; + + /// + /// Delegate to get the root hash. + /// + private readonly Func _getRoot; + + /// + /// MPT get operation. + /// + /// Delegate to get data from the database. + /// Delegate to get the root hash. + internal MPTGet(Func _getDb, Func _getRoot) + { + this._getDb = _getDb; + this._getRoot = _getRoot; + } + + /// + /// Get a value from the database. + /// + /// Specific key. + /// The value attached to the key. + /// In the case a null key is received. + public byte[] Get(byte[] key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var root = _getRoot(); + return root == null ? null : Get(_getDb(root), key.ConvertToNibble()); + } + + private byte[] Get(MerklePatriciaNode node, byte[] path) + { + while (true) + { + if (node == null) + { + return null; + } + + if (node.IsLeaf) + { + return node.Path.SequenceEqual(path) ? node.Value : null; + } + + if (path.Length == 0 && !node.IsExtension) + { + return node.Value; + } + + if (node.IsExtension) + { + if (path.SequenceEqual(node.Path)) + { + node = _getDb(node.Next); + path = new byte[0]; + continue; + } + + if (node.Path.Length < path.Length && + path.Take(node.Path.Length).ToArray().SequenceEqual(node.Path)) + { + var node1 = node; + node = _getDb(node.Next); + path = path.Skip(node1.Path.Length).ToArray(); + continue; + } + + return null; + } + + // Branch node + if (node[path[0]] != null) + { + node = _getDb(node[path[0]]); + path = path.Skip(1).ToArray(); + continue; + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/MPTStates/MPT/Operation/MPTRemove.cs b/MPTStates/MPT/Operation/MPTRemove.cs new file mode 100644 index 000000000..bf73c8738 --- /dev/null +++ b/MPTStates/MPT/Operation/MPTRemove.cs @@ -0,0 +1,217 @@ +using System; +using System.Linq; + +namespace Neo.Plugins.MPT.Operation +{ + /// + /// MPT remove operation. + /// + internal class MPTRemove + { + /// + /// Delegate to get data from the database. + /// + private readonly Func _getDb; + + /// + /// Delegate to remove data from the database. + /// + private readonly Func _removeDb; + + /// + /// Delegate to set data on the database. + /// + private readonly Func _setDb; + + /// + /// Delegate to get the root hash. + /// + private readonly Func _getRoot; + + /// + /// Delegate to change the root hash. + /// + private readonly Action _setRoot; + + /// + /// MPT remove operation. + /// + /// Delegate to get data from the database. + /// Delegate to remove data from the database. + /// Delegate to set data on the database. + /// Delegate to get the root hash. + /// Delegate to change the root hash. + internal MPTRemove(Func _getDb, Func _removeDb, + Func _setDb, Func _getRoot, Action _setRoot) + { + this._getDb = _getDb; + this._removeDb = _removeDb; + this._setDb = _setDb; + this._getRoot = _getRoot; + this._setRoot = _setRoot; + } + + /// + /// Removes the key-value pair from the database. + /// + /// The key to be removed. + /// true in the case the value is found. + public bool Remove(byte[] key) + { + var root = _getRoot(); + if (root == null) + { + return false; + } + + var removido = Remove(root, key.ConvertToNibble()); + var resp = removido == null || !root.SequenceEqual(removido); + if (resp) + { + _removeDb(root); + } + + _setRoot(removido); + return resp; + } + + private byte[] Remove(byte[] nodeHash, byte[] path) + { + if (nodeHash == null) + { + return null; + } + + var node = _getDb(nodeHash); + byte[] respHash = null; + if (node.IsLeaf) + { + respHash = RemoveLeaf(nodeHash, path, node); + } + + if (node.IsExtension) + { + respHash = RemoveExtension(nodeHash, path, node); + } + + if (node.IsBranch) + { + respHash = RemoveBranch(nodeHash, path, node); + } + + return respHash; + } + + private byte[] RemoveLeaf(byte[] nodeHash, byte[] path, MerklePatriciaNode node) + { + if (node.Path.SequenceEqual(path)) + { + _removeDb(nodeHash); + return null; + } + + _setDb(nodeHash, node); + return nodeHash; + } + + private byte[] RemoveExtension(byte[] nodeHash, byte[] path, MerklePatriciaNode node) + { + if (path.Length >= node.Path.Length && + path.Take(node.Path.Length).ToArray().SequenceEqual(node.Path)) + { + node.Next = Remove(node.Next, path.Skip(node.Path.Length).ToArray()); + var nodeNext = _getDb(node.Next); + if (nodeNext.IsLeaf || nodeNext.IsExtension) + { + _removeDb(node.Next); + nodeNext.Path = node.Path.Concat(nodeNext.Path).ToArray(); + node = nodeNext; + } + else + { + var (index, cont) = nodeNext.IndexAndCountNotNullHashes(); + + if (cont == 1 && nodeNext.Value == null) + { + _removeDb(node.Next); + node.Path = node.Path.Concat(new[] {(byte) index}).ToArray(); + node.Next = nodeNext[index]; + + nodeNext = _getDb(node.Next); + if (nodeNext.IsExtension) + { + _removeDb(node.Next); + node.Path = node.Path.Concat(nodeNext.Path).ToArray(); + node.Next = nodeNext.Next; + } + } + } + } + else + { + _setDb(nodeHash, node); + return nodeHash; + } + + _removeDb(nodeHash); + nodeHash = node.Hash(); + _setDb(nodeHash, node); + return nodeHash; + } + + private byte[] RemoveBranch(byte[] nodeHash, byte[] path, MerklePatriciaNode node) + { + if (path.Length == 0) + { + node.Key = null; + node.Value = null; + } + else if (node[path[0]] != null) + { + node[path[0]] = Remove(node[path[0]], path.Skip(1).ToArray()); + } + + var (indexInnerNode, contar) = node.IndexAndCountNotNullHashes(); + + if (contar == 0) + { + var newNode = MerklePatriciaNode.LeafNode(); + newNode.Path = new byte[0]; + newNode.Key = node.Key; + newNode.Value = node.Value; + node = newNode; + } + else if (contar == 1 && node.Value == null) + { + var innerNodeHash = node[indexInnerNode]; + var innerNode = _getDb(innerNodeHash); + if (innerNode.IsLeaf) + { + _removeDb(innerNodeHash); + node = MerklePatriciaNode.LeafNode(); + node.Path = new[] {(byte) indexInnerNode}.Concat(innerNode.Path).ToArray(); + node.Key = innerNode.Key; + node.Value = innerNode.Value; + } + else if (innerNode.IsExtension) + { + _removeDb(innerNodeHash); + node = MerklePatriciaNode.ExtensionNode(); + node.Path = new[] {(byte) indexInnerNode}.Concat(innerNode.Path).ToArray(); + node.Next = innerNode.Next; + } + else if (innerNode.IsBranch) + { + node = MerklePatriciaNode.ExtensionNode(); + node.Path = new[] {(byte) indexInnerNode}; + node.Next = innerNodeHash; + } + } + + _removeDb(nodeHash); + nodeHash = node.Hash(); + _setDb(nodeHash, node); + return nodeHash; + } + } +} \ No newline at end of file diff --git a/MPTStates/MPT/Operation/MPTSet.cs b/MPTStates/MPT/Operation/MPTSet.cs new file mode 100644 index 000000000..21ff9455a --- /dev/null +++ b/MPTStates/MPT/Operation/MPTSet.cs @@ -0,0 +1,293 @@ +using System; +using System.Linq; + +namespace Neo.Plugins.MPT.Operation +{ + /// + /// MPT set operation. + /// + internal class MPTSet + { + /// + /// Delegate to get data from the database. + /// + private readonly Func _getDb; + + /// + /// Delegate to remove data from the database. + /// + private readonly Func _removeDb; + + /// + /// Delegate to set data on the database. + /// + private readonly Func _setDb; + + /// + /// Delegate to get the root hash. + /// + private readonly Func _getRoot; + + /// + /// Delegate to change the root hash. + /// + private readonly Action _setRoot; + + /// + /// MPT set operation. + /// + /// Delegate to get data from the database. + /// Delegate to remove data from the database. + /// Delegate to set data on the database. + /// Delegate to get the root hash. + /// Delegate to change the root hash. + internal MPTSet(Func _getDb, Func _removeDb, + Func _setDb, Func _getRoot, Action _setRoot) + { + this._getDb = _getDb; + this._removeDb = _removeDb; + this._setDb = _setDb; + this._getRoot = _getRoot; + this._setRoot = _setRoot; + } + + /// + /// Set a key-value pair in the MPT. + /// + /// The key to be inserted. + /// The value to be inserted. + /// The last value. + /// In the case that a null key is used. + public byte[] Set(byte[] key, byte[] value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var root = _getRoot(); + var node = root == null ? null : _getDb(root); + if (root != null) + { + _removeDb(root); + } + + var resp = Set(node, key.ConvertToNibble(), key, value); + _setRoot(resp); + return resp; + } + + private byte[] Set(MerklePatriciaNode node, byte[] path, byte[] key, byte[] value) + { + if (node == null) + { + node = MerklePatriciaNode.LeafNode(); + node.Path = path; + node.Key = key; + node.Value = value; + } + else if (node.IsLeaf) + { + return SetLeaf(node, path, key, value); + } + else if (node.IsExtension) + { + return SetExtension(node, path, key, value); + } + else + { + return SetBranch(node, path, key, value); + } + + var hashNode = node.Hash(); + _setDb(hashNode, node); + return hashNode; + } + + private byte[] SetLeaf(MerklePatriciaNode node, byte[] path, byte[] key, byte[] value) + { + if (path.Length == 0 || path.SequenceEqual(node.Path)) + { + node.Key = key; + node.Value = value; + } + else if (node.Path.Length == 0 || path[0] != node.Path[0]) + { + var innerHash = SetBranch(MerklePatriciaNode.BranchNode(), node.Path, node.Key, node.Value); + node = _getDb(innerHash); + _removeDb(innerHash); + innerHash = Set(node, path, key, value); + node = _getDb(innerHash); + _removeDb(innerHash); + } + else + { + for (var pos = 0;; pos++) + { + if (pos + 1 == path.Length || pos + 1 == node.Path.Length || + path[pos + 1] != node.Path[pos + 1]) + { + var innerNode = MerklePatriciaNode.ExtensionNode(); + innerNode.Path = path.Take(pos + 1).ToArray(); + innerNode.Next = SetBranch(MerklePatriciaNode.BranchNode(), node.Path.Skip(pos + 1).ToArray(), + node.Key, + node.Value); + node = innerNode; + innerNode = _getDb(node.Next); + _removeDb(node.Next); + node.Next = Set(innerNode, path.Skip(pos + 1).ToArray(), key, value); + break; + } + } + } + + var hashNode = node.Hash(); + _setDb(hashNode, node); + return hashNode; + } + + private byte[] SetExtension(MerklePatriciaNode node, byte[] path, byte[] key, byte[] value) + { + void ProcessOldExtension(MerklePatriciaNode nodeValue, MerklePatriciaNode oldExtension) + { + if (oldExtension.Path.Length == 1) + { + nodeValue[oldExtension.Path[0]] = oldExtension.Next; + } + else + { + var position = oldExtension.Path[0]; + oldExtension.Path = oldExtension.Path.Skip(1).ToArray(); + nodeValue[position] = oldExtension.Hash(); + _setDb(nodeValue[position], oldExtension); + } + } + + if (path.Length == 0) + { + var oldExtension = node; + _removeDb(node.Hash()); + node = MerklePatriciaNode.BranchNode(); + ProcessOldExtension(node, oldExtension); + + SetBranch(node, path, key, value); + } + else if (path.SequenceEqual(node.Path)) + { + var innerHash = node.Next; + var innerNode = _getDb(innerHash); + _removeDb(innerHash); + node.Next = Set(innerNode, new byte[0], key, value); + } + else if (node.Path.Length == 0 || path[0] != node.Path[0]) + { + var oldExtension = node; + _removeDb(node.Hash()); + node = MerklePatriciaNode.BranchNode(); + ProcessOldExtension(node, oldExtension); + + SetBranch(node, path, key, value); + } + else + { + for (var pos = 0;; pos++) + { + if (pos + 1 == node.Path.Length) + { + var innerHash = node.Next; + _removeDb(node.Hash()); + node.Next = Set(_getDb(innerHash), path.Skip(pos + 1).ToArray(), key, value); + _removeDb(innerHash); + break; + } + + if (pos + 1 == path.Length) + { + var oldExtension = node; + _removeDb(node.Hash()); + node = MerklePatriciaNode.ExtensionNode(); + node.Path = oldExtension.Path.Take(pos + 1).ToArray(); + + var branchNode = MerklePatriciaNode.BranchNode(); + oldExtension.Path = oldExtension.Path.Skip(pos + 1).ToArray(); + if (oldExtension.Path.Length == 1) + { + branchNode[oldExtension.Path[0]] = oldExtension.Next; + } + else + { + var position = oldExtension.Path[0]; + oldExtension.Path = oldExtension.Path.Skip(1).ToArray(); + branchNode[position] = oldExtension.Hash(); + _setDb(branchNode[position], oldExtension); + } + + node.Next = SetBranch(branchNode, new byte[0], key, value); + break; + } + + if (path[pos + 1] != node.Path[pos + 1]) + { + var oldExtension = node; + node = MerklePatriciaNode.ExtensionNode(); + node.Path = oldExtension.Path.Take(pos + 1).ToArray(); + node.Next = SetBranch(MerklePatriciaNode.BranchNode(), path.Skip(pos + 1).ToArray(), key, + value); + + var nodeNext = _getDb(node.Next); + _removeDb(node.Next); + var oldExtensionPath = oldExtension.Path; + oldExtension.Path = oldExtension.Path.Skip(pos + 2).ToArray(); + if (oldExtension.Path.Length > 0) + { + nodeNext[oldExtensionPath[pos + 1]] = oldExtension.Hash(); + _setDb(nodeNext[oldExtensionPath[pos + 1]], oldExtension); + } + else + { + nodeNext[oldExtensionPath[pos + 1]] = oldExtension.Next; + } + + node.Next = nodeNext.Hash(); + _setDb(node.Next, nodeNext); + + break; + } + } + } + + var hashNode = node.Hash(); + _setDb(hashNode, node); + return hashNode; + } + + private byte[] SetBranch(MerklePatriciaNode node, byte[] path, byte[] key, byte[] value) + { + if (path.Length == 0) + { + node.Key = key; + node.Value = value; + } + else + { + var innerHash = node[path[0]]; + var innerNode = innerHash != null ? _getDb(innerHash) : null; + if (innerHash != null) + { + _removeDb(innerHash); + } + + node[path[0]] = Set(innerNode, path.Skip(1).ToArray(), key, value); + } + + var hashNode = node.Hash(); + _setDb(hashNode, node); + return hashNode; + } + } +} \ No newline at end of file diff --git a/MPTStates/MPT/Operation/MPTValidate.cs b/MPTStates/MPT/Operation/MPTValidate.cs new file mode 100644 index 000000000..b2380409d --- /dev/null +++ b/MPTStates/MPT/Operation/MPTValidate.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; + +namespace Neo.Plugins.MPT.Operation +{ + /// + /// MPT validate operation. + /// + internal class MPTValidate + { + /// + /// Delegate to get data from the database. + /// + private readonly Func _getDb; + + /// + /// Delegate to check if the database contains the key. + /// + private readonly Func _containsKeyDb; + + /// + /// MPT validate operation. + /// + /// Delegate to get data from the database. + /// Delegate to check if the database contains the key. + internal MPTValidate(Func _getDb, Func _containsKeyDb) + { + this._getDb = _getDb; + this._containsKeyDb = _containsKeyDb; + } + + /// + /// Check if the MPT is valid. + /// + /// Root key. + /// true case the MPT is valid. + public bool Validate(byte[] key) => key == null + || (_containsKeyDb(key) && Validate(key, _getDb(key))); + + private bool Validate(byte[] nodeHash, MerklePatriciaNode node) + { + while (true) + { + if (!nodeHash.SequenceEqual(node.Hash())) + { + return false; + } + + if (node.IsExtension) + { + nodeHash = node.Next; + node = _getDb(node.Next); + continue; + } + + if (node.IsLeaf) + { + return true; + } + + foreach (var subNodeHash in node) + { + if (subNodeHash != null && !Validate(subNodeHash, _getDb(subNodeHash))) + { + return false; + } + } + + return true; + } + } + } +} \ No newline at end of file diff --git a/MPTStates/MPTStates.csproj b/MPTStates/MPTStates.csproj new file mode 100644 index 000000000..a8212489c --- /dev/null +++ b/MPTStates/MPTStates.csproj @@ -0,0 +1,23 @@ + + + 2.10.2 + netstandard2.0 + Neo.Plugins + + + + PreserveNewest + PreserveNewest + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4b5a3170f..bead2aa45 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,8 @@ Enables policies for Consensus or Seed Nodes. In particular, policies for accept ### StatesDumper Exports NEO-CLI status data \(useful for debugging\). + +### MPTStates +An experimental plugin for persisting neo blockchain states into Merkle Patricia Tries. +It is related to the implementation on the [PR-528](https://github.com/neo-project/neo/pull/528) of the main project. + diff --git a/neo-plugins.sln b/neo-plugins.sln index 4c1f919f6..a9709f8c2 100644 --- a/neo-plugins.sln +++ b/neo-plugins.sln @@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreMetrics", "CoreMetrics\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RpcSystemAssetTracker", "RpcSystemAssetTracker\RpcSystemAssetTracker.csproj", "{D3C183C1-8C23-4566-8909-EBF468DAD67A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MPTStates", "MPTStates\MPTStates.csproj", "{4704220C-75F4-4A8C-B55C-7E809D6AAAA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MPTStates.UnitTests", "MPTStates.UnitTests\MPTStates.UnitTests.csproj", "{A4517E03-B2E8-47EF-92C9-987C7E6508E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +72,14 @@ Global {D3C183C1-8C23-4566-8909-EBF468DAD67A}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3C183C1-8C23-4566-8909-EBF468DAD67A}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3C183C1-8C23-4566-8909-EBF468DAD67A}.Release|Any CPU.Build.0 = Release|Any CPU + {4704220C-75F4-4A8C-B55C-7E809D6AAAA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4704220C-75F4-4A8C-B55C-7E809D6AAAA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4704220C-75F4-4A8C-B55C-7E809D6AAAA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4704220C-75F4-4A8C-B55C-7E809D6AAAA6}.Release|Any CPU.Build.0 = Release|Any CPU + {A4517E03-B2E8-47EF-92C9-987C7E6508E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4517E03-B2E8-47EF-92C9-987C7E6508E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4517E03-B2E8-47EF-92C9-987C7E6508E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4517E03-B2E8-47EF-92C9-987C7E6508E7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE