diff --git a/CHANGES.md b/CHANGES.md index 5ee168ce85d..9dab7398ac3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,22 +8,62 @@ To be released. ### Deprecated APIs + - `ValidatorSet.GetProposer()` has been removed. [[#3895]] + ### Backward-incompatible API changes + - `BlockMetadata.CurrentProtocolVersion` has been changed from 9 to 10. + [[#3895]] + ### Backward-incompatible network protocol changes ### Backward-incompatible storage format changes ### Added APIs + - Added `IConsensusCryptoBackend` interface, which contains VRF + functionalities which is used on the consensus as a pseudo-random function. + [[#3895]] + - Added `DefaultConsensusCryptoBackend` class as a default implementation of + `IConsensusCryptoBackend`. [[#3895]] + - Added `CryptoConfig.ConsensusCryptoBackend` property as a VRF backend used + on the consensus. [[#3895]] + - Added `PrivateKey.Prove()` method as a proof generation. [[#3895]] + - Added `PublicKey.VerifyProof()` method as a proof verification. [[#3895]] + - Added `Proof` struct as a wrapper structure of proof(pi-bytes) generated by + ECVRF. [[#3895]] + - Added `ConsensusInformation` struct as a base payload to be proved + with private key. [[#3895]] + - Added `IBlockMetadata.Proof` property. [[#3895]] + - Added `Lot` class as a content of message that submits a `Proof` + to be a proposer candidate during `ConsensusStep.Sortition`. [[#3895]] + - Added `DominantLot` class as a content of message that submits a vote for + dominant `Lot` during `ConsensusStep.Sortition`. [[#3895]]` + - Added `ConsensusStep.Sortition`. [[#3895]] + - Added `LotGatherSecond`, `SortitionSecondBase`, `SortitionMultiplier` + to `ContestTimeoutOption`. [[#3895]] + - Added `ConsensusLotMsg` class as a `ConsensusMsg` broadcasted during + `ConsensusStep.Sortition`. [[#3895]] + - Added `ConsensusDominantLotMsg` class as a `ConsensusMsg` broadcasted during + `ConsensusStep.Sortition`. after lot gathering delay. [[#3895]] + - Added `LotSet` class as a `Lot` and `DominantLot` selector. [[#3895]] + ### Behavioral changes + - `ActionEvaluator.EvaluateActions()` use `Proof.Seed` as a initial + random seed instead of `PreEvaluationHash`, `signature` combined seed. + [[#3895]] + - Proposer selection is now based on the VRF result. [[#3895]] + - Consensus now starts with `ConsensusStep.Sortition`. [[#3895]]` + ### Bug fixes ### Dependencies ### CLI tools +[#3895]: https://github.com/planetarium/libplanet/pull/3895 + Version 5.2.0 ------------- diff --git a/src/Libplanet.Action/ActionEvaluator.cs b/src/Libplanet.Action/ActionEvaluator.cs index 8d6eda0bbb7..ee7d1380d22 100644 --- a/src/Libplanet.Action/ActionEvaluator.cs +++ b/src/Libplanet.Action/ActionEvaluator.cs @@ -59,7 +59,7 @@ private delegate (ITrie, int) StateCommitter( public IActionLoader ActionLoader => _actionLoader; /// - /// Creates a random seed. + /// Creates a legacy random seed. /// /// The pre-evaluation hash as bytes. /// @@ -72,7 +72,7 @@ private delegate (ITrie, int) StateCommitter( /// Thrown when /// is empty. [Pure] - public static int GenerateRandomSeed( + public static int GenerateLegacyRandomSeed( byte[] preEvaluationHashBytes, byte[] signature, int actionOffset) @@ -228,7 +228,9 @@ IActionContext CreateActionContext( byte[] preEvaluationHashBytes = block.PreEvaluationHash.ToByteArray(); byte[] signature = tx?.Signature ?? Array.Empty(); - int seed = GenerateRandomSeed(preEvaluationHashBytes, signature, 0); + int seed = block.Proof is { } proof + ? proof.Seed + : GenerateLegacyRandomSeed(preEvaluationHashBytes, signature, 0); IWorld state = previousState; foreach (IAction action in actions) diff --git a/src/Libplanet.Crypto/CryptoConfig.cs b/src/Libplanet.Crypto/CryptoConfig.cs index dbe14798343..406f1878cbf 100644 --- a/src/Libplanet.Crypto/CryptoConfig.cs +++ b/src/Libplanet.Crypto/CryptoConfig.cs @@ -8,6 +8,7 @@ namespace Libplanet.Crypto public static class CryptoConfig { private static ICryptoBackend? _cryptoBackend; + private static IConsensusCryptoBackend? _consensusCryptoBackend; /// /// Global cryptography backend to sign and verify messages. @@ -17,5 +18,14 @@ public static ICryptoBackend CryptoBackend get => _cryptoBackend ??= new DefaultCryptoBackend(); set => _cryptoBackend = value; } + + /// + /// Global consensus cryptography backend to prove and verify messages. + /// + public static IConsensusCryptoBackend ConsensusCryptoBackend + { + get => _consensusCryptoBackend ??= new DefaultConsensusCryptoBackend(); + set => _consensusCryptoBackend = value; + } } } diff --git a/src/Libplanet.Crypto/DefaultConsensusCryptoBackend.cs b/src/Libplanet.Crypto/DefaultConsensusCryptoBackend.cs new file mode 100644 index 00000000000..ead5730f375 --- /dev/null +++ b/src/Libplanet.Crypto/DefaultConsensusCryptoBackend.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using Libplanet.Common; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; +using ECPoint = Org.BouncyCastle.Math.EC.ECPoint; + +namespace Libplanet.Crypto +{ + /// + /// Default consensus cryptography backend. + /// It implements ECVRF(Elliptic Curve Verifiable Random Function), + /// using RFC9381 as a reference. + /// + public class DefaultConsensusCryptoBackend : IConsensusCryptoBackend + { + private ECDomainParameters _eCParams; + private int _eCFieldBytesSize; + + public DefaultConsensusCryptoBackend() + { + X9ECParameters ps = ECNamedCurveTable.GetByName("secp256k1"); + _eCParams = new ECDomainParameters(ps.Curve, ps.G, ps.N, ps.H); + _eCFieldBytesSize = (_eCParams.Curve.FieldSize + 7) >> 3; + SuiteBytes = new byte[] { 0 }.ToImmutableArray(); + } + + /// + /// Suite bytes that specifying the ECVRF ciphersuite. + /// Since RFC9381 does not include ECVRF under secp256k1, + /// it cannot be between 0x01 ~ 0x04. + /// To express this implementation does not belong to any of those on + /// RFC9381, left it as 0x00. + /// + public ImmutableArray SuiteBytes { get; } + + /// + public byte[] Prove(byte[] alphaBytes, PrivateKey privateKey) + { + // n : modulus + BigInteger n = _eCParams.N; + + // d : private key + BigInteger d = privateKey.KeyParam.D; + var digest = new Sha256Digest(); + + // k(nonce) generator is deterministic + var kCalculator = new HMacDsaKCalculator(digest); + kCalculator.Init(n, d, alphaBytes); + + // k : nonce + BigInteger k = kCalculator.NextK(); + + // H : message hash point + ECPoint pointH = HashToCurveTai(alphaBytes, privateKey.PublicKey); + + // dH : Gamma + ECPoint dPointH = pointH.Multiply(d); + + // kG : r + ECPoint kPointG = _eCParams.G.Multiply(k); + + // kH + ECPoint kPointH = pointH.Multiply(k); + + ECPoint[] points = new ECPoint[] { pointH, dPointH, kPointG, kPointH }; + + // c = checksum(payload) + BigInteger c = HashPoints(points); + + // s = (k + c * d) mod N + BigInteger s = k.Add(c.Multiply(d)).Mod(n); + + byte[] gammaBytes = dPointH.GetEncoded(true); + byte[] cBytes = c.ToByteArrayUnsigned(); + byte[] sBytes = s.ToByteArrayUnsigned(); + + byte[] leftPadCBytes = new byte[_eCFieldBytesSize]; + byte[] leftPadSBytes = new byte[_eCFieldBytesSize]; + + Array.Copy(cBytes, 0, leftPadCBytes, _eCFieldBytesSize - cBytes.Length, cBytes.Length); + Array.Copy(sBytes, 0, leftPadSBytes, _eCFieldBytesSize - sBytes.Length, sBytes.Length); + + byte[] piBytes = gammaBytes.Concat(leftPadCBytes).Concat(leftPadSBytes).ToArray(); + + return piBytes; + } + + /// + public bool VerifyProof( + byte[] alphaBytes, byte[] piBytes, PublicKey publicKey) + { + ECPublicKeyParameters pubKeyParam = publicKey.KeyParam; + ECDomainParameters eCParam = pubKeyParam.Parameters; + ECPoint dPointH; + BigInteger c; + BigInteger s; + + try + { + (dPointH, c, s) = DecodeProof(piBytes); + } + catch (Exception) + { + return false; + } + + // sG - cdG = (s-cd)G = kG + ECPoint sPointG = eCParam.G.Multiply(s); + ECPoint cdPointG = publicKey.KeyParam.Q.Multiply(c); + ECPoint scdPointG = sPointG.Subtract(cdPointG); + + // sH - cdH = (s-cd)H = kH + ECPoint pointH = HashToCurveTai(alphaBytes, publicKey); + ECPoint sPointH = pointH.Multiply(s); + ECPoint cdPointH = dPointH.Multiply(c); + ECPoint scdPointH = sPointH.Subtract(cdPointH); + + ECPoint[] points = new ECPoint[] { pointH, dPointH, scdPointG, scdPointH }; + + // check if checksum of payload is same + if (!c.Equals(HashPoints(points))) + { + return false; + } + + return true; + } + + /// + public byte[] ProofToHash(byte[] piBytes) + { + (ECPoint gamma, _, _) = DecodeProof(piBytes); + + ECPoint gammaMul = _eCParams.H.Equals(BigInteger.One) + ? gamma + : gamma.Multiply(_eCParams.H); + + byte[] payload + = SuiteBytes + .Concat(new byte[] { 3 }) + .Concat(gammaMul.GetEncoded(true)) + .Concat(new byte[] { 0 }).ToArray(); + + HashDigest betaHash = HashDigest.DeriveFrom(payload); + byte[] betaBytes = betaHash.ToByteArray(); + + return betaBytes; + } + + /// + /// Maps a to Elliptic curve point + /// with try-and-increment method. + /// + /// A message bytearray. + /// used for salt. + /// + /// generated from . + /// + private ECPoint HashToCurveTai(byte[] alphaBytes, PublicKey publicKey) + { + int ctr = 0; + while (true) + { + byte[] ctrBytes + = BitConverter.IsLittleEndian + ? BitConverter.GetBytes(ctr).Reverse().ToArray() + : BitConverter.GetBytes(ctr); + ctr += 1; + byte[] payload + = SuiteBytes + .Concat(new byte[] { 1 }) + .Concat(publicKey.KeyParam.Q.GetEncoded(true)) + .Concat(alphaBytes) + .Concat(ctrBytes) + .Concat(new byte[] { 0 }).ToArray(); + HashDigest hashed = HashDigest.DeriveFrom(payload); + byte[] encoded = new byte[] { 2 }.Concat(hashed.ToByteArray() + .Take((_eCParams.Curve.FieldSize + 7) >> 3)).ToArray(); + try + { + return _eCParams.Curve.DecodePoint(encoded); + } + catch (ArgumentException) + { + } + } + + throw new ArgumentException(); + } + + /// + /// Maps to + /// with SHA-512 hashing. + /// + /// A message bytearray. + /// + /// generated from . + /// + private BigInteger HashPoints(ECPoint[] points) + { + byte[] payload = new byte[] { 2 }; + foreach (ECPoint point in points) + { + payload = payload.Concat(point.GetEncoded(true)).ToArray(); + } + + payload = payload.Concat(new byte[] { 0 }).ToArray(); + HashDigest cHash = HashDigest.DeriveFrom(payload); + byte[] truncatedCBytes = cHash.ToByteArray() + .Take((_eCParams.Curve.FieldSize + 7) >> 3).ToArray(); + BigInteger c = new BigInteger(1, truncatedCBytes); + + return c; + } + + /// + /// Decomposes a to original compositions. + /// + /// Proof to verify + /// and alphaBytes. + /// + /// + /// A tuple of gamma, c and s. + /// which is used to verify . + /// + private (ECPoint, BigInteger, BigInteger) DecodeProof(byte[] piBytes) + { + int gammaBytesLen = _eCFieldBytesSize + 1; + int cBytesLen = _eCFieldBytesSize; + int sBytesLen = _eCFieldBytesSize; + if (piBytes.Length != gammaBytesLen + cBytesLen + sBytesLen) + { + throw new ArgumentException( + $"Length of piBytes are expected to be " + + $"{gammaBytesLen + cBytesLen + sBytesLen}, " + + $"but found {piBytes.Length}"); + } + + byte[] gammaBytes = piBytes.Take(gammaBytesLen).ToArray(); + byte[] cBytes = piBytes.Skip(gammaBytesLen).Take(cBytesLen).ToArray(); + byte[] sBytes = piBytes.Skip(gammaBytesLen + cBytesLen).Take(sBytesLen).ToArray(); + + BigInteger c = new BigInteger(1, cBytes); + BigInteger s = new BigInteger(1, sBytes); + + if (s.CompareTo(_eCParams.N) == 1) + { + throw new ArgumentException("s cannot be bigger than EC parameter N"); + } + + // dH : Gamma + ECPoint gamma; + try + { + gamma = _eCParams.Curve.DecodePoint(gammaBytes); + } + catch (FormatException) + { + throw new ArgumentException("Given piBytes does not contain valid gammaBytes"); + } + + return (gamma, c, s); + } + } +} diff --git a/src/Libplanet.Crypto/IConsensusCryptoBackend.cs b/src/Libplanet.Crypto/IConsensusCryptoBackend.cs new file mode 100644 index 00000000000..d8b17f87448 --- /dev/null +++ b/src/Libplanet.Crypto/IConsensusCryptoBackend.cs @@ -0,0 +1,53 @@ +using System.Security.Cryptography; + +namespace Libplanet.Crypto +{ + /// + /// Cryptography backend interface. + /// + /// A which corresponds to a digest. + /// + public interface IConsensusCryptoBackend + { + /// + /// Creates a piBytes(proof) from + /// with the corresponding . + /// + /// A message bytearray to generate piBytes. + /// + /// to prove + /// . + /// + /// + /// piBytes that is created from the + /// with the corresponding , + /// which is called as proof. + /// + byte[] Prove(byte[] alphaBytes, PrivateKey privateKey); + + /// + /// Verifies whether a was created from + /// a with the corresponding . + /// + /// A message bytearray. + /// A proof that was created from the + /// . + /// used for verification. + /// true if the was created + /// from the with the corresponding + /// , otherwise false. + /// + bool VerifyProof(byte[] alphaBytes, byte[] piBytes, PublicKey publicKey); + + /// + /// Generate betaBytes(proof hash) from a . + /// + /// Proof to generate hash. + /// + /// + /// betaBytes generated from the , + /// which is called as proof hash. + /// + byte[] ProofToHash(byte[] piBytes); + } +} diff --git a/src/Libplanet.Crypto/InvalidProofException.cs b/src/Libplanet.Crypto/InvalidProofException.cs new file mode 100644 index 00000000000..870ce2ee2c8 --- /dev/null +++ b/src/Libplanet.Crypto/InvalidProofException.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.Serialization; + +namespace Libplanet.Crypto +{ + [Serializable] + public class InvalidProofException : Exception + { + public InvalidProofException() + { + } + + public InvalidProofException(string message) + : base(message) + { + } + + public InvalidProofException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected InvalidProofException( + SerializationInfo info, StreamingContext context + ) + : base(info, context) + { + } + } +} diff --git a/src/Libplanet.Crypto/PrivateKey.cs b/src/Libplanet.Crypto/PrivateKey.cs index 94644818f45..69537f750c3 100644 --- a/src/Libplanet.Crypto/PrivateKey.cs +++ b/src/Libplanet.Crypto/PrivateKey.cs @@ -230,6 +230,46 @@ public byte[] Sign(byte[] message) return CryptoConfig.CryptoBackend.Sign(hashed, this); } + /// + /// Creates a from the given . + /// + /// A created can be verified by the corresponding + /// . + /// + /// + /// A created can generate unique pseudorandom byte. + /// + /// + /// can be created by the + /// and only can be verified with corresponding . + /// + /// + /// To sum up, a is used to guarantee: + /// + /// + /// that the was created + /// by someone possessing the corresponding , + /// + /// that the possessor cannot deny having sent the + /// , + /// that the was not + /// forged in the middle of transit, and + /// that the generated pseudorandom byte was created + /// properly by someone possessing the corresponding + /// , and + /// that the generated pseudorandom byte was not + /// forged in the middle of transit. + /// + /// + /// A message s to sign. + /// A that proves the authenticity of the + /// . + /// It can be verified using method. + /// + /// + public Proof Prove(IEnumerable message) + => new Proof(CryptoConfig.ConsensusCryptoBackend.Prove(message.ToArray(), this)); + /// /// Creates a signature from the given . /// A created signature can be verified by the corresponding . diff --git a/src/Libplanet.Crypto/Proof.cs b/src/Libplanet.Crypto/Proof.cs new file mode 100644 index 00000000000..58b43175ef5 --- /dev/null +++ b/src/Libplanet.Crypto/Proof.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Numerics; +using Bencodex; +using Bencodex.Misc; +using Bencodex.Types; +using Libplanet.Common; + +namespace Libplanet.Crypto +{ + /// + /// Represents a proof that validators submits to be a proposer. + /// Once decided, it can be a source of random seed. + /// + public class Proof : IBencodable, IEquatable, IComparable, IComparable + { + private readonly ImmutableArray _piBytes; + private readonly ImmutableArray _hash; + + /// + /// Instantiates a new with given . + /// + /// Byte array represents proof. + /// Thrown when given + /// is invalid. + public Proof(IReadOnlyList piBytes) + { + try + { + _hash = CryptoConfig.ConsensusCryptoBackend + .ProofToHash(piBytes.ToArray()).ToImmutableArray(); + } + catch (ArgumentException e) + { + throw new InvalidProofException( + $"Bytes of Proof is invalid", e); + } + + _piBytes = piBytes.ToImmutableArray(); + } + + /// + /// Creates a new instance of with given + /// proof value. + /// + /// Bencodex-encoded proof value. + /// Thrown when format of given + /// is not . + public Proof(IValue bencoded) + : this(bencoded is Binary piBytes + ? piBytes + : throw new ArgumentException( + $"Given {nameof(bencoded)} must be of type " + + $"{typeof(Binary)}: {bencoded.GetType()}", + nameof(bencoded))) + { + } + + /// + /// Creates a new instance of with given + /// proof value. + /// + /// Bencodex-encoded proof value. + public Proof(Binary bencoded) + : this(bencoded.ByteArray) + { + } + + /// + public IValue Bencoded => new Binary(ByteArray); + + /// + /// An immutable byte array that represent this . + /// + /// This is immutable. + /// For a mutable array, call method. + /// + /// + public ImmutableArray ByteArray => _piBytes; + + /// + /// Hash of the . + /// It can be used as a random hash that can be verified. + /// + public ImmutableArray Hash => _hash; + + /// + /// Integer form of . + /// It can represent the random integer that can be verified. + /// Maximum value of it follows the space of . + /// + public BigInteger HashInt => HashToInt(Hash); + + /// + /// Random seed that can be derived by . + /// It's calculated by taking 4 bytes of , + /// and converting it into int. + /// + public int Seed + { + get + { + byte[] seed = Hash.ToArray().Take(4).ToArray(); + return BitConverter.IsLittleEndian + ? BitConverter.ToInt32(seed.Reverse().ToArray(), 0) + : BitConverter.ToInt32(seed, 0); + } + } + + /// + /// Tests if two values are equal. + /// + /// A value. + /// Another value. + /// if two values are equal. + /// Otherwise . + [Pure] + public static bool operator ==(Proof obj, Proof other) => + obj.Equals(other); + + /// + /// Tests if two values are unequal. + /// + /// A value. + /// Another value. + /// if two values are equal. + /// Otherwise . + [Pure] + public static bool operator !=(Proof obj, Proof other) => + !(obj == other); + + /// + /// Tests if the left operand () is less than the right operand + /// (). + /// + /// The left operand to compare. + /// The right operand to compare. + /// + /// if the left operand () is less than the right + /// operand (). Otherwise (even if two operands are equal) + /// . + [Pure] + public static bool operator <(Proof obj, Proof other) => + obj.CompareTo(other) < 0; + + /// + /// Tests if the left operand () is less than or equal to the right + /// operand (). + /// + /// The left operand to compare. + /// The right operand to compare. + /// + /// if the left operand () is less than or equal + /// to the right operand (). Otherwise . + /// + [Pure] + public static bool operator <=(Proof obj, Proof other) => + obj.CompareTo(other) <= 0; + + /// + /// Tests if the left operand () is greater than the right operand + /// (). + /// + /// The left operand to compare. + /// The right operand to compare. + /// + /// if the left operand () is greater than + /// the right operand (). Otherwise (even if two operands are + /// equal) . + [Pure] + public static bool operator >(Proof obj, Proof other) => + other < obj; + + /// + /// Tests if the left operand () is greater than or equal to the right + /// operand (). + /// + /// The left operand to compare. + /// The right operand to compare. + /// + /// if the left operand () is greater than or + /// equal to the right operand (). + /// Otherwise . + [Pure] + public static bool operator >=(Proof obj, Proof other) => + other <= obj; + + /// + /// Gets a mutable byte array that represent this . + /// + /// A new mutable array which represents this + /// . + /// Since it is created every time the method is called, + /// any mutation on that does not affect internal states of + /// this . + /// + public byte[] ToByteArray() => ByteArray.ToArray(); + + /// + /// Verifies with given + /// and . + /// + /// + /// corresponding to the + /// that has been used when generating the + /// by . + /// + /// + /// Payload that has been used when generating the + /// by . + /// + /// true if the proves authenticity of + /// the with the . + /// Otherwise false. + /// + /// + public bool Verify(PublicKey publicKey, byte[] payload) + => publicKey.VerifyProof(payload, this); + + /// + /// Draws expected number under given power portion. + /// It represents result of quantile function of binomial distribution + /// where quantile is portion of , n is , + /// and p is divided by . + /// + /// + /// Expected size of winnings. + /// + /// + /// Power that can be interpreted as the number of lots owns. + /// + /// + /// Total power that can be interpreted as the number of total lots. + /// + /// + /// Expected number of drawn lots under given condition. + /// + public BigInteger Draw(int expectedSize, BigInteger power, BigInteger totalPower) + { + double targetProb + = (double)HashInt + / (double)HashToInt(Enumerable.Repeat(byte.MaxValue, 64).ToImmutableArray()); + + return BinomialQuantileFunction(targetProb, expectedSize / (double)totalPower, power); + } + + /// + public bool Equals(Proof? other) + => other is Proof proof + && ByteArray.SequenceEqual(proof.ByteArray); + + /// + public override bool Equals(object? obj) + => obj is Proof otherProof && Equals(otherProof); + + /// + public int CompareTo(Proof? other) + => other is Proof proof + ? (HashInt - proof.HashInt).Sign + : throw new ArgumentException( + $"Argument {nameof(other)} cannot be null.", nameof(other)); + + /// + public int CompareTo(object? obj) + => obj is Proof otherProof + ? CompareTo(otherProof) + : throw new ArgumentException( + $"Argument {nameof(obj)} is not an ${nameof(Proof)}.", nameof(obj)); + + /// + public override int GetHashCode() + => ByteUtil.CalculateHashCode(ToByteArray()); + + /// + public override string ToString() + => ByteArray.Hex(); + + private static BigInteger HashToInt(ImmutableArray hash) + => new BigInteger( + BitConverter.IsLittleEndian + ? hash.Reverse().Concat(new byte[] { 0 }).ToArray() + : hash.Concat(new byte[] { 0 }).ToArray()); + + private static BigInteger BinomialQuantileFunction( + double targetProb, double prob, BigInteger nSample) + { + // Cumulative binomial distribution + double cumulativePositiveProb = 0; + for (BigInteger nPositive = 0; nPositive < nSample; nPositive++) + { + // Binomial distribution + cumulativePositiveProb += BinomialProb((double)nSample, (double)nPositive, prob); + + if (targetProb <= cumulativePositiveProb) + { + return nPositive; + } + } + + return nSample; + } + + private static double BinomialProb(double nSample, double nPositive, double prob) + { + return Combination(nSample, nPositive) + * Math.Pow(prob, nPositive) + * Math.Pow(1d - prob, nSample - nPositive); + } + + private static double Combination(double n, double r) + { + double nCr = 1; + for (double i = n; i > n - r; i--) + { + nCr *= i; + } + + for (double i = 1; i <= r; i++) + { + nCr /= i; + } + + return nCr; + } + } +} diff --git a/src/Libplanet.Crypto/PublicKey.cs b/src/Libplanet.Crypto/PublicKey.cs index 65a3fca8239..90115c27bc3 100644 --- a/src/Libplanet.Crypto/PublicKey.cs +++ b/src/Libplanet.Crypto/PublicKey.cs @@ -216,6 +216,26 @@ public bool Verify(IReadOnlyList message, IReadOnlyList signature) } } + /// + /// + /// Verifies whether a proves authenticity of + /// with the corresponding . + /// + /// + /// A original plaintext message that the + /// tries to prove its authenticity. I.e., an argument data passed to + /// method. + /// A which tries to authenticity of + /// . + /// I.e., a data that method returned. + /// + /// true if the proves authenticity of + /// the with the corresponding . + /// Otherwise false. + public bool VerifyProof(IReadOnlyList message, Proof proof) + => CryptoConfig.ConsensusCryptoBackend.VerifyProof( + message.ToArray(), proof.ToByteArray(), this); + /// /// Gets the public key's hexadecimal representation in compressed form. /// diff --git a/src/Libplanet.Net/Consensus/ConsensusStep.cs b/src/Libplanet.Net/Consensus/ConsensusStep.cs index ca00ec0de1f..116dd720eb7 100644 --- a/src/Libplanet.Net/Consensus/ConsensusStep.cs +++ b/src/Libplanet.Net/Consensus/ConsensusStep.cs @@ -7,6 +7,11 @@ public enum ConsensusStep /// Default, + /// + /// Sortition step. + /// + Sortition, + /// /// Proposing Step. /// diff --git a/src/Libplanet.Net/Consensus/Context.Async.cs b/src/Libplanet.Net/Consensus/Context.Async.cs index 790339c3355..569569131f0 100644 --- a/src/Libplanet.Net/Consensus/Context.Async.cs +++ b/src/Libplanet.Net/Consensus/Context.Async.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Libplanet.Consensus; using Libplanet.Net.Messages; using Libplanet.Types.Blocks; using Libplanet.Types.Evidence; @@ -139,6 +140,7 @@ private async Task ConsumeMutation(CancellationToken cancellationToken) System.Action mutation = await _mutationRequests.Reader.ReadAsync(cancellationToken); var prevState = new ContextState( _heightVoteSet.Count, + _lotSet.DominantLots.Count, Height, Round, Step, @@ -146,6 +148,7 @@ private async Task ConsumeMutation(CancellationToken cancellationToken) mutation(); var nextState = new ContextState( _heightVoteSet.Count, + _lotSet.DominantLots.Count, Height, Round, Step, @@ -168,6 +171,7 @@ private async Task ConsumeMutation(CancellationToken cancellationToken) StateChanged?.Invoke(this, nextState); prevState = new ContextState( _heightVoteSet.Count, + _lotSet.DominantLots.Count, Height, Round, Step, @@ -175,6 +179,7 @@ private async Task ConsumeMutation(CancellationToken cancellationToken) ProcessGenericUponRules(); nextState = new ContextState( _heightVoteSet.Count, + _lotSet.DominantLots.Count, Height, Round, Step, @@ -189,6 +194,40 @@ private void AppendBlock(Block block) _ = Task.Run(() => _blockChain.Append(block, GetBlockCommit())); } + /// + /// Schedule to vote for dominant lot after + /// amount of time. + /// + private async Task VoteLotAfterGathering() + { + TimeSpan delay = DelayLotGather(); + await Task.Delay(delay, _cancellationTokenSource.Token); + if (_lotSet.DominantLot is { } lot) + { + DominantLot dominantLot = new DominantLotMetadata( + lot, + DateTimeOffset.UtcNow, + _privateKey.PublicKey).Sign(_privateKey); + PublishMessage(new ConsensusDominantLotMsg(dominantLot)); + } + } + + /// + /// Schedule to be queued after + /// amount of time. + /// + /// A round that the timeout task is scheduled for. + private async Task OnTimeoutSortition(int round) + { + TimeSpan timeout = TimeoutSortition(round); + await Task.Delay(timeout, _cancellationTokenSource.Token); + _logger.Information( + "TimeoutSortition has occurred in {Timeout}. {Info}", + timeout, + ToString()); + ProduceMutation(() => ProcessTimeoutSortition(round)); + } + /// /// Schedules to be queued after /// amount of time. @@ -241,12 +280,14 @@ internal struct ContextState : IEquatable { public ContextState( int voteCount, + int dominantLotCount, long height, int round, ConsensusStep step, BlockHash? proposal) { VoteCount = voteCount; + DominantLotCount = dominantLotCount; Height = height; Round = round; Step = step; @@ -255,6 +296,8 @@ public ContextState( public int VoteCount { get; } + public int DominantLotCount { get; } + public long Height { get; } public int Round { get; } @@ -266,6 +309,7 @@ public ContextState( public bool Equals(ContextState other) { return VoteCount == other.VoteCount && + DominantLotCount == other.DominantLotCount && Round == other.Round && Step == other.Step && Proposal.Equals(other.Proposal); @@ -278,7 +322,12 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(VoteCount, Round, (int)Step, Proposal.GetHashCode()); + return HashCode.Combine( + VoteCount, + DominantLotCount, + Round, + (int)Step, + Proposal.GetHashCode()); } } } diff --git a/src/Libplanet.Net/Consensus/Context.Event.cs b/src/Libplanet.Net/Consensus/Context.Event.cs index 0ef7176515f..00fa27bd29e 100644 --- a/src/Libplanet.Net/Consensus/Context.Event.cs +++ b/src/Libplanet.Net/Consensus/Context.Event.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using Libplanet.Consensus; +using Libplanet.Crypto; using Libplanet.Net.Messages; using Libplanet.Types.Consensus; @@ -62,5 +65,13 @@ public partial class Context /// internal event EventHandler<(int Round, VoteFlag Flag, IEnumerable Votes)>? VoteSetModified; + + /// + /// An event that is invoked when the is modified. + /// + internal event EventHandler<( + ImmutableDictionary Lots, + ImmutableDictionary DominantLots)>? + LotSetModified; } } diff --git a/src/Libplanet.Net/Consensus/Context.Mutate.cs b/src/Libplanet.Net/Consensus/Context.Mutate.cs index 8b71cfee34b..d4e867e941e 100644 --- a/src/Libplanet.Net/Consensus/Context.Mutate.cs +++ b/src/Libplanet.Net/Consensus/Context.Mutate.cs @@ -27,43 +27,20 @@ private void StartRound(int round) Round = round; RoundStarted?.Invoke(this, Round); + + // TODO: Update last proof by peers. + // Last proof is not a parameter of StartRound() + // for its update by peers developed in future. + // It's crucial, to prevent network partition. + _lotSet.SetRound(round, _lastProof); _heightVoteSet.SetRound(round); Proposal = null; - Step = ConsensusStep.Propose; - if (_validatorSet.GetProposer(Height, Round).PublicKey == _privateKey.PublicKey) - { - _logger.Information( - "Starting round {NewRound} and is a proposer.", - round, - ToString()); - if ((_validValue ?? GetValue()) is Block proposalValue) - { - Proposal proposal = new ProposalMetadata( - Height, - Round, - DateTimeOffset.UtcNow, - _privateKey.PublicKey, - _codec.Encode(proposalValue.MarshalBlock()), - _validRound).Sign(_privateKey); + Step = ConsensusStep.Sortition; - PublishMessage(new ConsensusProposalMsg(proposal)); - } - else - { - _logger.Information( - "Failed to propose a block for round {Round}.", - round); - _ = OnTimeoutPropose(Round); - } - } - else - { - _logger.Information( - "Starting round {NewRound} and is not a proposer.", - round); - _ = OnTimeoutPropose(Round); - } + PublishMessage(new ConsensusLotMsg(_lotSet.GenerateLot(_privateKey))); + _ = OnTimeoutSortition(Round); + _ = VoteLotAfterGathering(); } /// @@ -75,9 +52,13 @@ private void StartRound(int round) /// If an invalid is given, this method throws /// an and handles it internally /// while invoking event. - /// An can be thrown when + /// An can be thrown when /// the internal does not accept it, i.e. /// returns . + /// An or + /// can be thrown when the internal does not accept it, i.e. + /// or + /// returns . /// /// private bool AddMessage(ConsensusMsg message) @@ -98,9 +79,46 @@ private bool AddMessage(ConsensusMsg message) message); } + if (message is ConsensusLotMsg lot) + { + _lotSet.AddLot(lot.Lot); + LotSetModified?.Invoke(this, (_lotSet.Lots, _lotSet.DominantLots)); + _logger.Debug( + "{FName}: Message: {Message} => Height: {Height}, Round: {Round}, " + + "Public key: {PublicKey}, " + + "Lot: {Lot}. (context: {Context})", + nameof(AddMessage), + lot, + lot.Height, + lot.Round, + lot.ValidatorPublicKey, + lot.Lot, + ToString()); + return true; + } + + if (message is ConsensusDominantLotMsg dominantLot) + { + _lotSet.AddDominantLot(dominantLot.DominantLot); + LotSetModified?.Invoke(this, (_lotSet.Lots, _lotSet.DominantLots)); + _logger.Debug( + "{FName}: Message: {Message} => Height: {Height}, Round: {Round}, " + + "Public key: {PublicKey}, " + + "Dominant lot: {Lot}. (context: {Context})", + nameof(AddMessage), + dominantLot, + dominantLot.Height, + dominantLot.Round, + dominantLot.ValidatorPublicKey, + dominantLot.DominantLot.Lot, + ToString()); + return true; + } + if (message is ConsensusProposalMsg proposal) { AddProposal(proposal.Proposal); + return true; } if (message is ConsensusVoteMsg voteMsg) @@ -142,6 +160,28 @@ private bool AddMessage(ConsensusMsg message) return false; } + catch (InvalidLotException ile) + { + var icme = new InvalidConsensusMessageException( + ile.Message, + message); + var msg = $"Failed to add invalid message {message} to the " + + $"{nameof(LotSet)}"; + _logger.Error(icme, msg); + ExceptionOccurred?.Invoke(this, icme); + return false; + } + catch (InvalidDominantLotException idle) + { + var icme = new InvalidConsensusMessageException( + idle.Message, + message); + var msg = $"Failed to add invalid message {message} to the " + + $"{nameof(LotSet)}"; + _logger.Error(icme, msg); + ExceptionOccurred?.Invoke(this, icme); + return false; + } catch (InvalidProposalException ipe) { var icme = new InvalidConsensusMessageException( @@ -176,8 +216,7 @@ private bool AddMessage(ConsensusMsg message) private void AddProposal(Proposal proposal) { - if (!_validatorSet.GetProposer(Height, Round) - .PublicKey.Equals(proposal.ValidatorPublicKey)) + if (!Proposer!.Equals(proposal.ValidatorPublicKey)) { throw new InvalidProposalException( $"Given proposal's proposer {proposal.ValidatorPublicKey} is not the " + @@ -236,7 +275,47 @@ private void ProcessGenericUponRules() return; } + if (Proposer is { } proposer && + Step == ConsensusStep.Sortition) + { + Step = ConsensusStep.Propose; + if (proposer == _privateKey.PublicKey) + { + _logger.Information( + "Entering propose step for round {NewRound} and is a proposer.", + Round, + ToString()); + if ((_validValue ?? GetValue()) is Block proposalValue) + { + Proposal proposal = new ProposalMetadata( + Height, + Round, + DateTimeOffset.UtcNow, + _privateKey.PublicKey, + _codec.Encode(proposalValue.MarshalBlock()), + _validRound).Sign(_privateKey); + + PublishMessage(new ConsensusProposalMsg(proposal)); + } + else + { + _logger.Information( + "Failed to propose a block for round {Round}.", + Round); + _ = OnTimeoutPropose(Round); + } + } + else + { + _logger.Information( + "Entering propose step for round {NewRound} and is not a proposer.", + Round); + _ = OnTimeoutPropose(Round); + } + } + (Block Block, int ValidRound)? propose = GetProposal(); + if (propose is { } p1 && p1.ValidRound == -1 && Step == ConsensusStep.Propose) @@ -456,11 +535,35 @@ private void ProcessHeightOrRoundUponRules(ConsensusMsg message) round, Round, ToString()); + + // TODO: This have to be modified with + // Update by peers + if (_lotSet.Maj23 is { } lotMaj23) + { + _lastProof = lotMaj23.Proof; + } + StartRound(round); return; } } + /// + /// A timeout mutation to run if +2/3 s were + /// not gathered in and is still in + /// step. + /// + /// A round that the timeout task is scheduled for. + private void ProcessTimeoutSortition(int round) + { + if (round == Round && Step == ConsensusStep.Sortition) + { + Step = ConsensusStep.Propose; + TimeoutProcessed?.Invoke(this, (round, ConsensusStep.Sortition)); + ProcessTimeoutPropose(round); + } + } + /// /// A timeout mutation to run if no is received in /// and is still in step. @@ -510,6 +613,11 @@ private void ProcessTimeoutPreCommit(int round) if (round == Round) { + if (_lotSet.Maj23 is { } lotMaj23) + { + _lastProof = lotMaj23.Proof; + } + StartRound(Round + 1); TimeoutProcessed?.Invoke(this, (round, ConsensusStep.PreCommit)); } diff --git a/src/Libplanet.Net/Consensus/Context.cs b/src/Libplanet.Net/Consensus/Context.cs index 69453b615fb..04a30a3f701 100644 --- a/src/Libplanet.Net/Consensus/Context.cs +++ b/src/Libplanet.Net/Consensus/Context.cs @@ -85,6 +85,7 @@ public partial class Context : IDisposable private readonly ValidatorSet _validatorSet; private readonly Channel _messageRequests; private readonly Channel _mutationRequests; + private readonly LotSet _lotSet; private readonly HeightVoteSet _heightVoteSet; private readonly PrivateKey _privateKey; private readonly HashSet _preVoteTimeoutFlags; @@ -105,6 +106,7 @@ private readonly EvidenceExceptionCollector _evidenceCollector private Block? _decision; private int _committedRound; private BlockCommit? _lastCommit; + private Proof? _lastProof; /// /// Initializes a new instance of the class. @@ -139,6 +141,7 @@ public Context( validators, ConsensusStep.Default, -1, + 20, 128, contextTimeoutOptions) { @@ -152,6 +155,7 @@ private Context( ValidatorSet validators, ConsensusStep consensusStep, int round = -1, + int drawSize = 20, int cacheSize = 128, ContextTimeoutOption? contextTimeoutOptions = null) { @@ -172,6 +176,7 @@ private Context( Round = round; Step = consensusStep; _lastCommit = lastCommit; + _lastProof = blockChain[height - 1].Proof; _lockedValue = null; _lockedRound = -1; _validValue = null; @@ -182,6 +187,7 @@ private Context( _codec = new Codec(); _messageRequests = Channel.CreateUnbounded(); _mutationRequests = Channel.CreateUnbounded(); + _lotSet = new LotSet(height, round, _lastProof, validators, drawSize); _heightVoteSet = new HeightVoteSet(height, validators); _preVoteTimeoutFlags = new HashSet(); _hasTwoThirdsPreVoteFlags = new HashSet(); @@ -214,8 +220,17 @@ private Context( /// public ConsensusStep Step { get; private set; } + /// + /// A proposal block for this round. If the block is proposed, it will be stored here. + /// public Proposal? Proposal { get; private set; } + /// + /// The proposer of this round. Determined by the . + /// + public PublicKey? Proposer + => _lotSet.Maj23?.PublicKey; + /// public void Dispose() { @@ -410,6 +425,28 @@ private TimeSpan TimeoutPropose(long round) round * _contextTimeoutOption.ProposeMultiplier); } + /// + /// Gets the timeout of with the given + /// round. + /// + /// A round to get the timeout. + /// A duration in . + private TimeSpan TimeoutSortition(long round) + { + return TimeSpan.FromSeconds( + _contextTimeoutOption.SortitionSecondBase + + round * _contextTimeoutOption.SortitionMultiplier); + } + + /// + /// Gets the delay to gather s to determine . + /// + /// A duration in . + private TimeSpan DelayLotGather() + { + return TimeSpan.FromSeconds(_contextTimeoutOption.LotGatherSecond); + } + /// /// Creates a new to propose. /// @@ -419,8 +456,11 @@ private TimeSpan TimeoutPropose(long round) { try { - var evidence = _blockChain.GetPendingEvidence(); - Block block = _blockChain.ProposeBlock(_privateKey, _lastCommit, evidence); + Block block = _blockChain.ProposeBlock( + _privateKey, + _lastCommit, + _lotSet.GenerateProof(_privateKey), + _blockChain.GetPendingEvidence()); _blockChain.Store.PutBlock(block); return block; } @@ -470,6 +510,16 @@ private bool IsValid(Block block) try { + if (!(block.Proof is Proof proof + && proof.Equals(_lotSet.Maj23?.Proof))) + { + throw new InvalidBlockProofException( + $"Proof if given block is different from " + + $"majority proof of consensus. " + + $"Expected: {_lotSet.Maj23?.Proof}" + + $"Actual: {block.Proof}"); + } + _blockChain.ValidateBlock(block); _blockChain.ValidateBlockNonces( block.Transactions diff --git a/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs b/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs index 78a5515ffd2..c742aea9f28 100644 --- a/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs +++ b/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs @@ -9,13 +9,41 @@ namespace Libplanet.Net.Consensus public class ContextTimeoutOption { public ContextTimeoutOption( - int proposeSecondBase = 8, + int lotGatherSecond = 1, + int sortitionSecondBase = 4, + int proposeSecondBase = 4, int preVoteSecondBase = 4, int preCommitSecondBase = 4, - int proposeMultiplier = 4, + int sortitionMultiplier = 2, + int proposeMultiplier = 2, int preVoteMultiplier = 2, int preCommitMultiplier = 2) { + if (lotGatherSecond <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(lotGatherSecond), + "LotGatherSecond must be greater than 0."); + } + + LotGatherSecond = lotGatherSecond; + + if (sortitionSecondBase <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(sortitionSecondBase), + "SortitionSecondBase must be greater than 0."); + } + + SortitionSecondBase = sortitionSecondBase; + + if (SortitionSecondBase < LotGatherSecond) + { + throw new ArgumentOutOfRangeException( + nameof(sortitionSecondBase), + "SortitionSecondBase must be greater than LotGatherSecond."); + } + if (proposeSecondBase <= 0) { throw new ArgumentOutOfRangeException( @@ -43,6 +71,15 @@ public ContextTimeoutOption( PreCommitSecondBase = preCommitSecondBase; + if (sortitionMultiplier <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(sortitionMultiplier), + "SortitionMultiplier must be greater than 0."); + } + + SortitionMultiplier = sortitionMultiplier; + if (proposeMultiplier <= 0) { throw new ArgumentOutOfRangeException( @@ -71,12 +108,18 @@ public ContextTimeoutOption( PreCommitMultiplier = preCommitMultiplier; } + public int LotGatherSecond { get; } + + public int SortitionSecondBase { get; } + public int ProposeSecondBase { get; } public int PreVoteSecondBase { get; } public int PreCommitSecondBase { get; } + public int SortitionMultiplier { get; } + public int ProposeMultiplier { get; } public int PreVoteMultiplier { get; } diff --git a/src/Libplanet.Net/Consensus/HeightVoteSet.cs b/src/Libplanet.Net/Consensus/HeightVoteSet.cs index a6eef27c7b5..e32434cb9ad 100644 --- a/src/Libplanet.Net/Consensus/HeightVoteSet.cs +++ b/src/Libplanet.Net/Consensus/HeightVoteSet.cs @@ -120,11 +120,6 @@ public void AddVote(Vote vote) PublicKey validatorKey = vote.ValidatorPublicKey; - if (validatorKey is null) - { - throw new InvalidVoteException("ValidatorKey of the vote cannot be null", vote); - } - if (!_validatorSet.ContainsPublicKey(validatorKey)) { throw new InvalidVoteException( diff --git a/src/Libplanet.Net/Consensus/InvalidDominantLotException.cs b/src/Libplanet.Net/Consensus/InvalidDominantLotException.cs new file mode 100644 index 00000000000..412b562a324 --- /dev/null +++ b/src/Libplanet.Net/Consensus/InvalidDominantLotException.cs @@ -0,0 +1,73 @@ +using System; +using System.Runtime.Serialization; +using Libplanet.Consensus; + +namespace Libplanet.Net.Consensus +{ + /// + /// An exception thrown when a received is invalid. In particular, + /// this is thrown pre-emptively before a is processed, i.e. + /// does not change the state of a in a meaningful way. + /// + [Serializable] + public class InvalidDominantLotException : Exception + { + /// + /// Initializes a new instance of class. + /// + /// The error message that explains the reason for the exception. + /// + /// The that caused this exception. + /// + /// The exception that is the cause of the current exception. + /// + public InvalidDominantLotException( + string message, + DominantLot dominantLot, + Exception innerException) + : base(message, innerException) + { + DominantLot = dominantLot; + } + + /// + /// Initializes a new instance of class. + /// + /// The error message that explains the reason for the exception. + /// + /// The that caused this exception. + /// + public InvalidDominantLotException(string message, DominantLot dominantLot) + : base(message) + { + DominantLot = dominantLot; + } + + /// + /// Initializes a new instance of the + /// class with serialized data. + /// + /// The + /// that holds the serialized object data about the exception being thrown. + /// + /// The + /// that contains contextual information about the source or destination. + /// + protected InvalidDominantLotException(SerializationInfo info, StreamingContext context) + { + DominantLot = + info.GetValue(nameof(DominantLot), typeof(DominantLot)) as DominantLot ?? + throw new SerializationException( + $"{nameof(DominantLot)} is expected to be a non-null {nameof(DominantLot)}."); + } + + public DominantLot DominantLot { get; } + + public override void GetObjectData( + SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(DominantLot), DominantLot); + } + } +} diff --git a/src/Libplanet.Net/Consensus/InvalidLotException.cs b/src/Libplanet.Net/Consensus/InvalidLotException.cs new file mode 100644 index 00000000000..febe749e4c9 --- /dev/null +++ b/src/Libplanet.Net/Consensus/InvalidLotException.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.Serialization; +using Libplanet.Consensus; + +namespace Libplanet.Net.Consensus +{ + /// + /// An exception thrown when a received is invalid. In particular, + /// this is thrown pre-emptively before a is processed, i.e. + /// does not change the state of a in a meaningful way. + /// + [Serializable] + public class InvalidLotException : Exception + { + /// + /// Initializes a new instance of class. + /// + /// The error message that explains the reason for the exception. + /// + /// The that caused this exception. + /// + /// The exception that is the cause of the current exception. + /// + public InvalidLotException( + string message, + Lot lot, + Exception innerException) + : base(message, innerException) + { + Lot = lot; + } + + /// + /// Initializes a new instance of class. + /// + /// The error message that explains the reason for the exception. + /// + /// The that caused this exception. + /// + public InvalidLotException(string message, Lot lot) + : base(message) + { + Lot = lot; + } + + /// + /// Initializes a new instance of the + /// class with serialized data. + /// + /// The + /// that holds the serialized object data about the exception being thrown. + /// + /// The + /// that contains contextual information about the source or destination. + /// + protected InvalidLotException(SerializationInfo info, StreamingContext context) + { + Lot = info.GetValue(nameof(Lot), typeof(Lot)) as Lot ?? + throw new SerializationException( + $"{nameof(Lot)} is expected to be a non-null {nameof(Lot)}."); + } + + public Lot Lot { get; } + + public override void GetObjectData( + SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(Lot), Lot); + } + } +} diff --git a/src/Libplanet.Net/Consensus/InvalidProposalException.cs b/src/Libplanet.Net/Consensus/InvalidProposalException.cs index 16b8b300f12..7158a22b8c3 100644 --- a/src/Libplanet.Net/Consensus/InvalidProposalException.cs +++ b/src/Libplanet.Net/Consensus/InvalidProposalException.cs @@ -13,7 +13,7 @@ namespace Libplanet.Net.Consensus public class InvalidProposalException : Exception { /// - /// Initializes a new instance of class. + /// Initializes a new instance of class. /// /// The error message that explains the reason for the exception. /// @@ -31,7 +31,7 @@ public InvalidProposalException( } /// - /// Initializes a new instance of class. + /// Initializes a new instance of class. /// /// The error message that explains the reason for the exception. /// @@ -44,7 +44,7 @@ public InvalidProposalException(string message, Proposal proposal) } /// - /// Initializes a new instance of the + /// Initializes a new instance of the /// class with serialized data. /// /// The diff --git a/src/Libplanet.Net/Consensus/InvalidVoteException.cs b/src/Libplanet.Net/Consensus/InvalidVoteException.cs index 5d3c9201e8b..a5caf91e513 100644 --- a/src/Libplanet.Net/Consensus/InvalidVoteException.cs +++ b/src/Libplanet.Net/Consensus/InvalidVoteException.cs @@ -13,7 +13,7 @@ namespace Libplanet.Net.Consensus public class InvalidVoteException : Exception { /// - /// Initializes a new instance of class. + /// Initializes a new instance of class. /// /// The error message that explains the reason for the exception. /// @@ -31,7 +31,7 @@ public InvalidVoteException( } /// - /// Initializes a new instance of class. + /// Initializes a new instance of class. /// /// The error message that explains the reason for the exception. /// @@ -44,7 +44,7 @@ public InvalidVoteException(string message, Vote vote) } /// - /// Initializes a new instance of the + /// Initializes a new instance of the /// class with serialized data. /// /// The diff --git a/src/Libplanet.Net/Consensus/LotSet.cs b/src/Libplanet.Net/Consensus/LotSet.cs new file mode 100644 index 00000000000..2ba7d70927e --- /dev/null +++ b/src/Libplanet.Net/Consensus/LotSet.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Numerics; +using Bencodex; +using Libplanet.Consensus; +using Libplanet.Crypto; +using Libplanet.Types.Consensus; +using Serilog; + +namespace Libplanet.Net.Consensus +{ + public class LotSet + { + private static readonly Codec _codec = new Codec(); + private readonly ILogger _logger; + private ConsensusInformation _consensusInformation; + private ValidatorSet _validatorSet; + private ConcurrentDictionary _lots; + private int _drawSize; + private (Lot, BigInteger)? _dominantLot; + private ConcurrentDictionary _dominantLots; + private ConcurrentDictionary _lotsPower; + + /// + /// Instantiates a new . + /// + /// The height of the consensus. + /// The round of the consensus. + /// The last + /// that has been decided on last round. + /// if last round is zero, it represents of the last block. + /// The of consensus. + /// The size of draw for selecting dominant lot. + /// if this value is too small, probability of selection can be close to even, + /// and influence of validator power can be reduced. + public LotSet( + long height, int round, Proof? lastProof, ValidatorSet validatorSet, int drawSize) + : this( + new ConsensusInformation(height, round, lastProof), + validatorSet, + drawSize) + { + } + + /// + /// Instantiates a new . + /// + /// The information of the consensus. + /// The of consensus. + /// The size of draw for selecting dominant lot. + /// if this value is too small, probability of selection can be close to even, + /// and influence of validator power can be reduced. + public LotSet( + ConsensusInformation consensusInformation, ValidatorSet validatorSet, int drawSize) + { + _logger = Log + .ForContext("Tag", "Consensus") + .ForContext("SubTag", "Context") + .ForContext() + .ForContext("Source", nameof(LotSet)); + _consensusInformation = consensusInformation; + _validatorSet = validatorSet; + _lots = new ConcurrentDictionary(); + _drawSize = drawSize; + _dominantLot = null; + _dominantLots = new ConcurrentDictionary(); + _lotsPower = new ConcurrentDictionary(); + Maj23 = null; + } + + /// + /// The height of the consensus. + /// + public long Height => _consensusInformation.Height; + + /// + /// The round of the consensus. + /// + public int Round => _consensusInformation.Round; + + /// + /// The last that has been decided on last round. + /// if last round is zero, it represents of the last block. + /// + public Proof? LastProof => _consensusInformation.LastProof; + + /// + /// The information of the consensus. + /// + public ConsensusInformation ConsensusInformation => _consensusInformation; + + /// + /// The that has gain the majority +2/3 of the power of the validator set + /// by . + /// + public Lot? Maj23 { get; private set; } + + /// + /// The that is dominant among the gathered. + /// + public Lot? DominantLot + => _dominantLot?.Item1; + + /// + /// The s that have been gathered. + /// + public ImmutableDictionary Lots + => _lots.ToImmutableDictionary(); + + /// + /// The s that have been gathered. + /// + public ImmutableDictionary DominantLots + => _dominantLots.ToImmutableDictionary(); + + /// + /// Sets the round of the . + /// + /// The round of the consensus. + /// The last proof that has been decided on the last round. + public void SetRound(int round, Proof? lastProof) + { + if (round <= Round) + { + throw new ArgumentException( + $"Parameter round must be greater than the current round {Round}.", + nameof(round)); + } + + _consensusInformation = new ConsensusInformation(Height, round, lastProof); + _lots.Clear(); + _dominantLot = null; + _dominantLots.Clear(); + _lotsPower.Clear(); + Maj23 = null; + } + + /// + /// Generate a for the . + /// + /// The prover of the . + /// A that has been proved by , + /// with the . + public Proof GenerateProof(PrivateKey privateKey) + => _consensusInformation.Prove(privateKey); + + /// + /// Generate a for the . + /// + /// The prover of the . + /// A that has been proved by , + /// with the . + public Lot GenerateLot(PrivateKey privateKey) + => _consensusInformation.ToLot(privateKey); + + /// + /// Add a to the . + /// + /// A to be added. + /// Thrown when + /// is duplicated, or does not match with , + /// or prover does not belongs to the validator set. + public void AddLot(Lot lot) + { + if (_lots.ContainsKey(lot.PublicKey)) + { + throw new InvalidLotException( + $"Found duplicated {nameof(lot)} of public key {lot.PublicKey}", + lot); + } + + ValidateLot(lot); + _lots.TryAdd(lot.PublicKey, lot); + CompeteLot(lot); + } + + /// + /// Add a to the . + /// + /// A + /// to be added. + /// Thrown when + /// is duplicated, or does not belongs to the + /// validator set. + /// Thrown when of + /// does not match with + /// , or prover does not belongs to the validator set. + /// + public void AddDominantLot(DominantLot dominantLot) + { + if (_dominantLots.ContainsKey(dominantLot.ValidatorPublicKey)) + { + throw new InvalidDominantLotException( + $"Found duplicated {nameof(dominantLot)} " + + $"of public key {dominantLot.ValidatorPublicKey}", + dominantLot); + } + + if (!_validatorSet.ContainsPublicKey(dominantLot.ValidatorPublicKey)) + { + throw new InvalidDominantLotException( + $"Public key of {nameof(dominantLot)} {dominantLot.ValidatorPublicKey} " + + $"is not in the validator set.", + dominantLot); + } + + ValidateLot(dominantLot.Lot); + _dominantLots.TryAdd(dominantLot.ValidatorPublicKey, dominantLot); + UpdateMaj23(dominantLot); + } + + private void UpdateMaj23(DominantLot dominantLot) + { + BigInteger validatorPower = _validatorSet.GetValidatorsPower( + new List() { dominantLot.ValidatorPublicKey }); + _lotsPower.AddOrUpdate( + dominantLot.Lot, validatorPower, (lot, power) => power + validatorPower); + + if (!_lotsPower.TryGetValue(dominantLot.Lot, out BigInteger lotPower)) + { + throw new NullReferenceException( + "Lot is expected to exist in the dictionary, but it does not."); + } + + if (lotPower > _validatorSet.TwoThirdsPower) + { + Maj23 = dominantLot.Lot; + } + } + + private void ValidateLot(Lot lot) + { + if (!lot.ConsensusInformation.Equals(_consensusInformation)) + { + throw new InvalidLotException( + $"{nameof(lot)} has different consensus information with " + + $"{nameof(LotSet)}. " + + $"Expected : height {Height}, round {Round}, last proof {LastProof}" + + $"Actual : height {lot.ConsensusInformation.Height}, " + + $"round {lot.ConsensusInformation.Round}, " + + $"last proof {lot.ConsensusInformation.LastProof}", + lot); + } + + if (!_validatorSet.ContainsPublicKey(lot.PublicKey)) + { + throw new InvalidLotException( + $"Public key of {nameof(lot)} {lot.PublicKey} is not in the validator set.", + lot); + } + } + + private void CompeteLot(Lot lot) + { + BigInteger drawn = lot.Proof.Draw( + _drawSize, + _validatorSet.GetValidator(lot.PublicKey).Power, + _validatorSet.TotalPower); + + if (!(_dominantLot is { } dominantLot + && (drawn < dominantLot.Item2 + || (drawn == dominantLot.Item2 && lot.Proof <= dominantLot.Item1.Proof)))) + { + _dominantLot = (lot, drawn); + } + } + } +} diff --git a/src/Libplanet.Net/Messages/ConsensusDominantLotMsg.cs b/src/Libplanet.Net/Messages/ConsensusDominantLotMsg.cs new file mode 100644 index 00000000000..3ed5b360cb5 --- /dev/null +++ b/src/Libplanet.Net/Messages/ConsensusDominantLotMsg.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using Libplanet.Consensus; +using Libplanet.Net.Consensus; + +namespace Libplanet.Net.Messages +{ + /// + /// A message class for . + /// + public class ConsensusDominantLotMsg : ConsensusMsg + { + /// + /// Initializes a new instance of the class. + /// + /// A + /// that represents dominant of given height and round. + public ConsensusDominantLotMsg(DominantLot dominantLot) + : base( + dominantLot.ValidatorPublicKey, + dominantLot.Lot.ConsensusInformation.Height, + dominantLot.Lot.ConsensusInformation.Round) + { + DominantLot = dominantLot; + } + + /// + /// Initializes a new instance of the class + /// with marshalled message. + /// + /// A marshalled message. + public ConsensusDominantLotMsg(byte[][] dataframes) + : this(dominantLot: new DominantLot(dataframes[0])) + { + } + + /// + /// A of the message. + /// + public DominantLot DominantLot { get; } + + /// + public override IEnumerable DataFrames => + new List { DominantLot.ToByteArray() }; + + /// + public override MessageType Type => MessageType.ConsensusDominantLot; + + /// + public override bool Equals(ConsensusMsg? other) + { + return other is ConsensusDominantLotMsg message + && message.DominantLot.Equals(DominantLot); + } + + /// + public override bool Equals(object? obj) + { + return obj is ConsensusMsg other && Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Type, DominantLot); + } + } +} diff --git a/src/Libplanet.Net/Messages/ConsensusLotMsg.cs b/src/Libplanet.Net/Messages/ConsensusLotMsg.cs new file mode 100644 index 00000000000..6ca3b11122a --- /dev/null +++ b/src/Libplanet.Net/Messages/ConsensusLotMsg.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using Libplanet.Consensus; +using Libplanet.Net.Consensus; + +namespace Libplanet.Net.Messages +{ + /// + /// A message class for . + /// + public class ConsensusLotMsg : ConsensusMsg + { + /// + /// Initializes a new instance of the class. + /// + /// A + /// of given height and round. + public ConsensusLotMsg(Lot lot) + : base(lot.PublicKey, lot.ConsensusInformation.Height, lot.ConsensusInformation.Round) + { + Lot = lot; + } + + /// + /// Initializes a new instance of the class + /// with marshalled message. + /// + /// A marshalled message. + public ConsensusLotMsg(byte[][] dataframes) + : this(lot: new Lot(dataframes[0])) + { + } + + /// + /// A of the message. + /// + public Lot Lot { get; } + + /// + public override IEnumerable DataFrames => + new List { Lot.ToByteArray() }; + + /// + public override MessageType Type => MessageType.ConsensusLot; + + /// + public override bool Equals(ConsensusMsg? other) + { + return other is ConsensusLotMsg message && message.Lot.Equals(Lot); + } + + /// + public override bool Equals(object? obj) + { + return obj is ConsensusMsg other && Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Type, Lot); + } + } +} diff --git a/src/Libplanet.Net/Messages/MessageContent.cs b/src/Libplanet.Net/Messages/MessageContent.cs index dce4c52d744..10745d741c2 100644 --- a/src/Libplanet.Net/Messages/MessageContent.cs +++ b/src/Libplanet.Net/Messages/MessageContent.cs @@ -136,20 +136,30 @@ public enum MessageType : byte /// ConsensusProposalClaimMsg = 0x55, + /// + /// Consensus message that sends a lot to be drawn. + /// + ConsensusLot = 0x56, + + /// + /// Consensus message that sends a dominant lot to vote for proposer. + /// + ConsensusDominantLot = 0x57, + /// /// Inventory to transfer evidence. /// - EvidenceIds = 0x56, + EvidenceIds = 0x58, /// /// Request to query evidence. /// - GetEvidence = 0x57, + GetEvidence = 0x59, /// /// Message containing serialized evidence. /// - Evidence = 0x58, + Evidence = 0x60, } /// diff --git a/src/Libplanet.Net/Messages/NetMQMessageCodec.cs b/src/Libplanet.Net/Messages/NetMQMessageCodec.cs index 5fa79fc817f..9b7d0fab05d 100644 --- a/src/Libplanet.Net/Messages/NetMQMessageCodec.cs +++ b/src/Libplanet.Net/Messages/NetMQMessageCodec.cs @@ -229,6 +229,10 @@ internal static MessageContent CreateMessage( return new ConsensusVoteSetBitsMsg(dataframes); case MessageContent.MessageType.ConsensusProposalClaimMsg: return new ConsensusProposalClaimMsg(dataframes); + case MessageContent.MessageType.ConsensusLot: + return new ConsensusLotMsg(dataframes); + case MessageContent.MessageType.ConsensusDominantLot: + return new ConsensusDominantLotMsg(dataframes); default: throw new InvalidCastException($"Given type {type} is not a valid message."); } diff --git a/src/Libplanet.Store/BlockDigest.cs b/src/Libplanet.Store/BlockDigest.cs index e2871df7b67..872c3096578 100644 --- a/src/Libplanet.Store/BlockDigest.cs +++ b/src/Libplanet.Store/BlockDigest.cs @@ -98,6 +98,9 @@ public BlockDigest(Bencodex.Types.Dictionary dict) /// public BlockCommit? LastCommit => _metadata.LastCommit; + /// + public Proof? Proof => _metadata.Proof; + /// public HashDigest? EvidenceHash => _metadata.EvidenceHash; diff --git a/src/Libplanet.Types/Blocks/Block.cs b/src/Libplanet.Types/Blocks/Block.cs index f2d5b19d1f7..a477fb1b8d6 100644 --- a/src/Libplanet.Types/Blocks/Block.cs +++ b/src/Libplanet.Types/Blocks/Block.cs @@ -128,6 +128,9 @@ BlockHash Hash /// public BlockCommit? LastCommit => _preEvaluationBlock.LastCommit; + /// + public Proof? Proof => _preEvaluationBlock.Proof; + /// public HashDigest? EvidenceHash => _preEvaluationBlock.EvidenceHash; diff --git a/src/Libplanet.Types/Blocks/BlockContent.cs b/src/Libplanet.Types/Blocks/BlockContent.cs index 128d8dd4e8d..7149f3656af 100644 --- a/src/Libplanet.Types/Blocks/BlockContent.cs +++ b/src/Libplanet.Types/Blocks/BlockContent.cs @@ -157,6 +157,9 @@ public BlockContent( /// public BlockCommit? LastCommit => _blockMetadata.LastCommit; + /// + public Proof? Proof => _blockMetadata.Proof; + /// public HashDigest? EvidenceHash => _blockMetadata.EvidenceHash; diff --git a/src/Libplanet.Types/Blocks/BlockHeader.cs b/src/Libplanet.Types/Blocks/BlockHeader.cs index 6053d424baa..bb5133cf7f0 100644 --- a/src/Libplanet.Types/Blocks/BlockHeader.cs +++ b/src/Libplanet.Types/Blocks/BlockHeader.cs @@ -160,6 +160,9 @@ BlockHash Hash /// public BlockCommit? LastCommit => _preEvaluationBlockHeader.LastCommit; + /// + public Proof? Proof => _preEvaluationBlockHeader.Proof; + /// public HashDigest? EvidenceHash => _preEvaluationBlockHeader.EvidenceHash; diff --git a/src/Libplanet.Types/Blocks/BlockMarshaler.cs b/src/Libplanet.Types/Blocks/BlockMarshaler.cs index 1ca963c4cd0..6e8efb0628c 100644 --- a/src/Libplanet.Types/Blocks/BlockMarshaler.cs +++ b/src/Libplanet.Types/Blocks/BlockMarshaler.cs @@ -21,6 +21,7 @@ public static class BlockMarshaler // Block fields: private static readonly Binary HeaderKey = new Binary(new byte[] { 0x48 }); // 'H' + private static readonly Binary PreEvalHeaderKey = new Binary(new byte[] { 0x45 }); // 'E' private static readonly Binary TransactionsKey = new Binary(new byte[] { 0x54 }); // 'T' private static readonly Binary EvidenceKey = new Binary(new byte[] { 0x56 }); // 'V' @@ -41,6 +42,9 @@ public static class BlockMarshaler private static readonly Binary PreEvaluationHashKey = new Binary(0x63); // 'c' private static readonly Binary LastCommitKey = new Binary(0x43); // 'C' + private static readonly Binary ProofKey = + new Binary(new byte[] { 0x52 }); // 'R' + private static readonly Binary EvidenceHashKey = new Binary(new byte[] { 0x76 }); // 'v' @@ -76,6 +80,11 @@ public static Dictionary MarshalBlockMetadata(IBlockMetadata metadata) dict = dict.Add(LastCommitKey, commit.Bencoded); } + if (metadata.Proof is { } proof) + { + dict = dict.Add(ProofKey, proof.Bencoded); + } + if (metadata.EvidenceHash is { } evidenceHash) { dict = dict.Add(EvidenceHashKey, evidenceHash.ByteArray); @@ -166,6 +175,27 @@ public static Dictionary MarshalBlock(this Block block) => MarshalTransactions(block.Transactions), MarshalEvidence(block.Evidence)); + public static Dictionary MarshalPreEvaluationBlock( + Dictionary marshaledPreEvaluationBlockHeader, + List marshaledTransactions + ) + { + Dictionary dict = Dictionary.Empty + .Add(PreEvalHeaderKey, marshaledPreEvaluationBlockHeader); + if (marshaledTransactions.Any()) + { + dict = dict.Add(TransactionsKey, marshaledTransactions); + } + + return dict; + } + + public static Dictionary MarshalPreEvaluationBlock( + this PreEvaluationBlock preEvaluationBlock) => + MarshalPreEvaluationBlock( + MarshalPreEvaluationBlockHeader(preEvaluationBlock.Header), + MarshalTransactions(preEvaluationBlock.Transactions)); + public static long UnmarshalBlockMetadataIndex(Dictionary marshaledMetadata) => (Integer)marshaledMetadata[IndexKey]; @@ -204,6 +234,7 @@ public static BlockMetadata UnmarshalBlockMetadata(Dictionary marshaled) lastCommit: marshaled.ContainsKey(LastCommitKey) ? new BlockCommit(marshaled[LastCommitKey]) : (BlockCommit?)null, + proof: UnmarshalProof(marshaled), evidenceHash: marshaled.TryGetValue(EvidenceHashKey, out IValue ehv) ? new HashDigest(ehv) : (HashDigest?)null); @@ -280,5 +311,10 @@ public static Block UnmarshalBlock(Dictionary marshaled) IReadOnlyList evidence = UnmarshalBlockEvidence(marshaled); return new Block(header, txs, evidence); } + + public static Proof? UnmarshalProof(Dictionary marshaled) => + marshaled.ContainsKey(ProofKey) + ? new Proof(marshaled[ProofKey]) + : (Proof?)null; } } diff --git a/src/Libplanet.Types/Blocks/BlockMetadata.cs b/src/Libplanet.Types/Blocks/BlockMetadata.cs index 23d239b0a99..b9c5a0edccf 100644 --- a/src/Libplanet.Types/Blocks/BlockMetadata.cs +++ b/src/Libplanet.Types/Blocks/BlockMetadata.cs @@ -23,7 +23,7 @@ public class BlockMetadata : IBlockMetadata /// /// The latest protocol version. /// - public const int CurrentProtocolVersion = 9; + public const int CurrentProtocolVersion = 10; /// /// @@ -100,6 +100,15 @@ public class BlockMetadata : IBlockMetadata /// public const int EvidenceProtocolVersion = 9; + /// + /// The starting protocol version where has been added + /// for verifiable random function. This is used for proposer selection, + /// and app random seed. Prior to this version, + /// proposer of block was selected with round robin, and + /// was used for app random seed. + /// + public const int VRFProtocolVersion = 10; + private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; private static readonly Codec Codec = new Codec(); @@ -138,6 +147,7 @@ public BlockMetadata(IBlockMetadata metadata) previousHash: metadata.PreviousHash, txHash: metadata.TxHash, lastCommit: metadata.LastCommit, + proof: metadata.Proof, evidenceHash: metadata.EvidenceHash) { } @@ -153,9 +163,10 @@ public BlockMetadata(IBlockMetadata metadata) /// Goes to . /// Goes to . /// Goes to . + /// Goes to . /// Goes to . - /// + /// public BlockMetadata( long index, DateTimeOffset timestamp, @@ -163,6 +174,7 @@ public BlockMetadata( BlockHash? previousHash, HashDigest? txHash, BlockCommit? lastCommit, + Proof? proof, HashDigest? evidenceHash) : this( protocolVersion: CurrentProtocolVersion, @@ -173,6 +185,7 @@ public BlockMetadata( previousHash: previousHash, txHash: txHash, lastCommit: lastCommit, + proof: proof, evidenceHash: evidenceHash) { } @@ -193,6 +206,7 @@ public BlockMetadata( /// Goes to . /// Goes to . /// Goes to . + /// Goes to . /// Goes to . /// Thrown when /// is less than zero or greater than @@ -222,6 +236,7 @@ public BlockMetadata( BlockHash? previousHash, HashDigest? txHash, BlockCommit? lastCommit, + Proof? proof, HashDigest? evidenceHash) { // Protocol version validity check. @@ -309,6 +324,7 @@ public BlockMetadata( TxHash = txHash; LastCommit = lastCommit; + Proof = proof; EvidenceHash = evidenceHash; } @@ -331,9 +347,13 @@ public BlockMetadata( public BlockHash? PreviousHash { get; } /// - public HashDigest? TxHash { get; private set; } + public HashDigest? TxHash { get; } - public BlockCommit? LastCommit { get; set; } + /// + public BlockCommit? LastCommit { get; } + + /// + public Proof? Proof { get; } public HashDigest? EvidenceHash { get; private set; } @@ -368,6 +388,11 @@ public Bencodex.Types.Dictionary MakeCandidateData() dict = dict.Add("last_commit", lastCommit.ToHash().ByteArray); } + if (Proof is { } proof) + { + dict = dict.Add("proof", proof.ByteArray); + } + if (EvidenceHash is { } evidenceHash) { dict = dict.Add("evidence_hash", evidenceHash.ByteArray); diff --git a/src/Libplanet.Types/Blocks/IBlockMetadata.cs b/src/Libplanet.Types/Blocks/IBlockMetadata.cs index 99e9ff60587..f8227e76494 100644 --- a/src/Libplanet.Types/Blocks/IBlockMetadata.cs +++ b/src/Libplanet.Types/Blocks/IBlockMetadata.cs @@ -91,6 +91,15 @@ public interface IBlockMetadata /// BlockCommit? LastCommit { get; } + /// + /// The from the proposer candidate. This can be verified with + /// proposer candidate's , and can be used as a source of + /// proposer candidate dependent random variable. With above property, + /// It is used for proposer sortition, and once proposer is decided, can be interpreted + /// as source of proposer dependent random variable. + /// + Proof? Proof { get; } + /// /// Committing s of vote infraction /// that has been made on previous blocks. diff --git a/src/Libplanet.Types/Blocks/InvalidBlockProofException.cs b/src/Libplanet.Types/Blocks/InvalidBlockProofException.cs new file mode 100644 index 00000000000..bb65bf74a91 --- /dev/null +++ b/src/Libplanet.Types/Blocks/InvalidBlockProofException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Libplanet.Types.Blocks +{ + [Serializable] + public class InvalidBlockProofException : InvalidBlockException + { + public InvalidBlockProofException(string message) + : base(message) + { + } + } +} diff --git a/src/Libplanet.Types/Blocks/PreEvaluationBlock.cs b/src/Libplanet.Types/Blocks/PreEvaluationBlock.cs index 1298955730e..77e3a9e5bba 100644 --- a/src/Libplanet.Types/Blocks/PreEvaluationBlock.cs +++ b/src/Libplanet.Types/Blocks/PreEvaluationBlock.cs @@ -83,6 +83,9 @@ internal PreEvaluationBlock( /// public BlockCommit? LastCommit => _header.LastCommit; + /// + public Proof? Proof => _header.Proof; + /// public HashDigest? EvidenceHash => _header.EvidenceHash; diff --git a/src/Libplanet.Types/Blocks/PreEvaluationBlockHeader.cs b/src/Libplanet.Types/Blocks/PreEvaluationBlockHeader.cs index bd12e6eb3c5..8c0995100d3 100644 --- a/src/Libplanet.Types/Blocks/PreEvaluationBlockHeader.cs +++ b/src/Libplanet.Types/Blocks/PreEvaluationBlockHeader.cs @@ -97,6 +97,9 @@ public PreEvaluationBlockHeader( /// public BlockCommit? LastCommit => Metadata.LastCommit; + /// + public Proof? Proof => Metadata.Proof; + /// public HashDigest? EvidenceHash => Metadata.EvidenceHash; diff --git a/src/Libplanet.Types/Consensus/ValidatorSet.cs b/src/Libplanet.Types/Consensus/ValidatorSet.cs index 849571ad7a2..53817e8cf17 100644 --- a/src/Libplanet.Types/Consensus/ValidatorSet.cs +++ b/src/Libplanet.Types/Consensus/ValidatorSet.cs @@ -236,26 +236,6 @@ public override int GetHashCode() return hashCode; } - /// - /// Gets the proposer for a given context. - /// - /// The height of the context under consideration. - /// The round of the context under consideration. - /// A deterministically chosen from - /// , , and . - /// - /// Thrown when - /// is empty. - public Validator GetProposer(long height, int round) - { - // FIXME: Empty Validators should not be allowed. Preventing during construction - // would require heavier refactoring of BlockPolicy. - return Validators.IsEmpty - ? throw new InvalidOperationException( - "Cannot select a proposer from an empty list of validators.") - : Validators[(int)((height + round) % Validators.Count)]; - } - /// /// Checks whether is ordered /// by of each , diff --git a/src/Libplanet/Blockchain/BlockChain.ProposeBlock.cs b/src/Libplanet/Blockchain/BlockChain.ProposeBlock.cs index f2e98380a79..779ba6db9c9 100644 --- a/src/Libplanet/Blockchain/BlockChain.ProposeBlock.cs +++ b/src/Libplanet/Blockchain/BlockChain.ProposeBlock.cs @@ -7,6 +7,7 @@ using Bencodex.Types; using Libplanet.Blockchain.Policies; using Libplanet.Common; +using Libplanet.Consensus; using Libplanet.Crypto; using Libplanet.Store.Trie; using Libplanet.Types.Blocks; @@ -62,6 +63,7 @@ public static Block ProposeGenesisBlock( previousHash: null, txHash: BlockContent.DeriveTxHash(transactions), lastCommit: null, + proof: new ConsensusInformation(0L, 0, null).Prove(privateKey), evidenceHash: null), transactions: transactions, evidence: Array.Empty()); @@ -84,6 +86,7 @@ public static Block ProposeGenesisBlock( /// /// The evidence of the previous /// . + /// The proved from proposer candidate. /// The pending s to be committed on the /// . /// An optional comparer for give certain transactions to @@ -94,6 +97,7 @@ public static Block ProposeGenesisBlock( public Block ProposeBlock( PrivateKey proposer, BlockCommit lastCommit = null, + Proof proof = null, ImmutableArray? evidence = null, IComparer txPriority = null) { @@ -116,6 +120,7 @@ public Block ProposeBlock( proposer, transactions, lastCommit, + proof, evidence ?? ImmutableArray.Empty); _logger.Debug( "Proposed block #{Index} {Hash} with previous hash {PreviousHash}", @@ -132,8 +137,8 @@ public Block ProposeBlock( /// list of s. /// /// - /// Unlike , + /// Unlike , /// this may result in a that does not conform to the /// . /// @@ -143,12 +148,14 @@ public Block ProposeBlock( /// The list of s to include. /// The evidence of the previous /// . + /// The proved from proposer candidate. /// The s to be committed. /// A that is proposed. internal Block ProposeBlock( PrivateKey proposer, ImmutableList transactions, BlockCommit lastCommit, + Proof proof, ImmutableArray evidence) { long index = Count; @@ -174,6 +181,7 @@ internal Block ProposeBlock( previousHash: prevHash, txHash: BlockContent.DeriveTxHash(orderedTransactions), lastCommit: lastCommit, + proof: proof, evidenceHash: BlockContent.DeriveEvidenceHash(orderedEvidence)), transactions: orderedTransactions, evidence: orderedEvidence); diff --git a/src/Libplanet/Blockchain/BlockChain.Validate.cs b/src/Libplanet/Blockchain/BlockChain.Validate.cs index c471fb5661b..6ee875f6ba6 100644 --- a/src/Libplanet/Blockchain/BlockChain.Validate.cs +++ b/src/Libplanet/Blockchain/BlockChain.Validate.cs @@ -314,6 +314,20 @@ internal void ValidateBlock(Block block) } } + if (block.Proof is { } + && block.ProtocolVersion < BlockMetadata.VRFProtocolVersion) + { + throw new InvalidBlockProofException( + "Block of protocol version lower than 9 does not support proof."); + } + + if (block.Proof is null + && block.ProtocolVersion >= BlockMetadata.VRFProtocolVersion) + { + throw new InvalidBlockProofException( + "Block of protocol version higher than 9 must contain proof."); + } + foreach (var ev in block.Evidence) { var stateRootHash = GetNextStateRootHash(ev.Height); diff --git a/src/Libplanet/Consensus/ConsensusInformation.cs b/src/Libplanet/Consensus/ConsensusInformation.cs new file mode 100644 index 00000000000..b9373424137 --- /dev/null +++ b/src/Libplanet/Consensus/ConsensusInformation.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Libplanet.Consensus +{ + /// + /// Consensus round information used as payload of the + /// in the + /// . + /// + public class ConsensusInformation : IEquatable + { + private static readonly Binary HeightKey = + new Binary(new byte[] { 0x48 }); // 'H' + + private static readonly Binary RoundKey = + new Binary(new byte[] { 0x52 }); // 'R' + + private static readonly Binary LastProofKey = + new Binary(new byte[] { 0x4c }); // 'L' + + private static readonly Codec _codec = new Codec(); + + /// + /// Instantiates . + /// + /// Height of the consensus where + /// participate in the draw of the + /// . + /// Round of the consensus where + /// participate in the draw of the . + /// that has been decided on the previous round. + /// if deciding is 0, it indicates + /// from the previous . + /// + /// Thrown when or + /// is negative. + public ConsensusInformation( + long height, + int round, + Proof? lastProof) + { + if (height < 0) + { + throw new ArgumentException( + $"Given {nameof(height)} cannot be negative: {height}"); + } + else if (round < -1) + { + throw new ArgumentException( + $"Given {nameof(round)} cannot be less than -1: {round}"); + } + + Height = height; + Round = round; + LastProof = lastProof; + Encoded = Encode(height, round, lastProof); + } + + /// + /// Instantiates with byte array encoded form + /// . + /// + /// Byte array encoded . + public ConsensusInformation(IReadOnlyList encoded) + : this(_codec.Decode(encoded.ToArray())) + { + } + + private ConsensusInformation(IValue bencoded) + : this(bencoded is Dictionary dict + ? dict + : throw new ArgumentException( + $"Given {nameof(bencoded)} must be of type " + + $"{typeof(Dictionary)}: {bencoded.GetType()}", + nameof(bencoded))) + { + } + +#pragma warning disable SA1118 // Parameter should not span multiple lines + private ConsensusInformation(Dictionary bencoded) + : this( + (Integer)bencoded[HeightKey], + (Integer)bencoded[RoundKey], + bencoded.ContainsKey(LastProofKey) + ? new Proof(bencoded[LastProofKey]) + : null) + { + } +#pragma warning restore SA1118 // Parameter should not span multiple lines + + /// + /// Height of the consensus where + /// participate in the draw of the + /// . + /// + public long Height { get; } + + /// + /// Round of the consensus where + /// participate in the draw of the . + /// + public int Round { get; } + + /// + /// that has been decided on the previous round. + /// + public Proof? LastProof { get; } + + /// + /// Byte array encoded form of , + /// used as a payload of during prepropose consensus step. + /// + public ImmutableArray Encoded { get; } + + /// + /// Generate a with and + /// . + /// + /// + /// to prove with. + /// + /// + /// that contains generated by + /// , of + /// and . + public Lot ToLot(PrivateKey prover) + => new Lot(Prove(prover), prover.PublicKey, this); + + /// + /// Generate a with and + /// . + /// + /// + /// to prove with. + /// + /// + /// that has been proved by . + public Proof Prove(PrivateKey prover) + { + if (Round < 0) + { + throw new InvalidOperationException( + $"Cannot prove {nameof(ConsensusInformation)} " + + $"with negative {nameof(Round)}: {Round}"); + } + + return prover.Prove(Encoded); + } + + /// + /// Verify the with and + /// . + /// + /// + /// to verify with and + /// . + /// + /// to verify with. + /// + /// + /// if verified properly , otherwise . + /// + public bool Verify(Proof proof, PublicKey verifier) + { + if (Round < 0) + { + throw new InvalidOperationException( + $"Cannot verify {nameof(ConsensusInformation)} " + + $"with negative {nameof(Round)}: {Round}"); + } + + return verifier.VerifyProof(Encoded, proof); + } + + /// + public bool Equals(ConsensusInformation? other) + => other is ConsensusInformation ci + && Height == ci.Height + && Round == ci.Round + && (LastProof?.Equals(ci.LastProof) ?? ci.LastProof is null); + + /// + public override bool Equals(object? obj) => + obj is ConsensusInformation other && Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine( + Height, + Round, + LastProof); + + /// + public override string ToString() + { + var dict = new Dictionary + { + { "height", Height }, + { "round", Round }, + { "lastProof", LastProof?.ToString() ?? "Empty" }, + }; + return JsonSerializer.Serialize(dict); + } + + private static IValue Bencode(long height, int round, Proof? lastProof) + { + Dictionary bencoded = Dictionary.Empty + .Add(HeightKey, height) + .Add(RoundKey, round); + + if (lastProof is Proof lastProofNonNull) + { + bencoded = bencoded.Add(LastProofKey, lastProofNonNull.ByteArray); + } + + return bencoded; + } + + private static ImmutableArray Encode(long height, int round, Proof? lastProof) + => new Codec().Encode(Bencode(height, round, lastProof)).ToImmutableArray(); + } +} diff --git a/src/Libplanet/Consensus/DominantLot.cs b/src/Libplanet/Consensus/DominantLot.cs new file mode 100644 index 00000000000..26e3298554e --- /dev/null +++ b/src/Libplanet/Consensus/DominantLot.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text.Json.Serialization; +using Bencodex; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Libplanet.Consensus +{ + /// + /// Represents a drawn for consensus. + /// This can be interpreted as a vote for proposer. + /// + public class DominantLot : IEquatable + { + private static readonly Binary SignatureKey = new Binary(new byte[] { 0x53 }); // 'S' + private static readonly Codec _codec = new Codec(); + + private readonly DominantLotMetadata _dominantLotMetadata; + + /// + /// Instantiates a with given + /// and its . + /// + /// A to vote. + /// A signature signed with . + /// + /// Thrown if given is + /// empty. + /// Thrown if given is + /// invalid and cannot be verified with . + public DominantLot(DominantLotMetadata dominantLotMetadata, ImmutableArray signature) + { + _dominantLotMetadata = dominantLotMetadata; + Signature = signature; + + if (signature.IsDefaultOrEmpty) + { + throw new ArgumentNullException( + nameof(signature), + "Signature cannot be null or empty."); + } + + if (!ValidatorPublicKey.Verify( + _dominantLotMetadata.ByteArray.ToImmutableArray(), + Signature)) + { + throw new ArgumentException("Signature is invalid.", nameof(signature)); + } + } + + public DominantLot(IReadOnlyList marshaled) + : this((Dictionary)_codec.Decode(marshaled.ToArray())) + { + } + +#pragma warning disable SA1118 // The parameter spans multiple lines + public DominantLot(Dictionary encoded) + : this( + new DominantLotMetadata(encoded), + encoded.ContainsKey(SignatureKey) + ? ((Binary)encoded[SignatureKey]).ByteArray + : ImmutableArray.Empty) + { + } +#pragma warning restore SA1118 + + /// + public long Height => _dominantLotMetadata.Height; + + /// + public int Round => _dominantLotMetadata.Round; + + /// + public Lot Lot => _dominantLotMetadata.Lot; + + /// + public DateTimeOffset Timestamp => _dominantLotMetadata.Timestamp; + + /// + public PublicKey ValidatorPublicKey => _dominantLotMetadata.ValidatorPublicKey; + + /// + /// A signature that signed with . + /// + public ImmutableArray Signature { get; } + + /// + /// A Bencodex-encoded value of . + /// + [JsonIgnore] + public Dictionary Bencoded => + !Signature.IsEmpty + ? _dominantLotMetadata.Bencoded.Add(SignatureKey, Signature) + : _dominantLotMetadata.Bencoded; + + /// + /// encoded data. + /// + public ImmutableArray ByteArray => ToByteArray().ToImmutableArray(); + + public byte[] ToByteArray() => _codec.Encode(Bencoded); + + /// + [Pure] + public bool Equals(DominantLot? other) + { + return other is DominantLot dominantLot && + _dominantLotMetadata.Equals(dominantLot._dominantLotMetadata) && + Signature.SequenceEqual(dominantLot.Signature); + } + + /// + [Pure] + public override bool Equals(object? obj) + { + return obj is DominantLot other && Equals(other); + } + + /// + [Pure] + public override int GetHashCode() + { + return HashCode.Combine( + _dominantLotMetadata.GetHashCode(), + ByteUtil.CalculateHashCode(Signature.ToArray())); + } + } +} diff --git a/src/Libplanet/Consensus/DominantLotMetadata.cs b/src/Libplanet/Consensus/DominantLotMetadata.cs new file mode 100644 index 00000000000..b33fdcb8e1c --- /dev/null +++ b/src/Libplanet/Consensus/DominantLotMetadata.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Text.Json.Serialization; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Libplanet.Consensus +{ + public class DominantLotMetadata : IEquatable + { + private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; + + private static readonly Binary LotKey = + new Binary(new byte[] { 0x4C }); // 'L' + + private static readonly Binary TimestampKey = + new Binary(new byte[] { 0x74 }); // 't' + + private static readonly Binary ValidatorPublicKeyKey = + new Binary(new byte[] { 0x50 }); // 'P' + + private static readonly Codec _codec = new Codec(); + + /// + /// Creates a new instance of . + /// + /// The lot that is dominant. + /// The time at which the dominant lot selected. + /// + /// The of the validator selected dominant lot. + public DominantLotMetadata( + Lot lot, + DateTimeOffset timestamp, + PublicKey validatorPublicKey) + { + Lot = lot; + Timestamp = timestamp; + ValidatorPublicKey = validatorPublicKey; + } + +#pragma warning disable SA1118 // The parameter spans multiple lines + public DominantLotMetadata(Dictionary encoded) + : this( + lot: new Lot(encoded[LotKey]), + timestamp: DateTimeOffset.ParseExact( + (Text)encoded[TimestampKey], + TimestampFormat, + CultureInfo.InvariantCulture), + validatorPublicKey: new PublicKey( + ((Binary)encoded[ValidatorPublicKeyKey]).ByteArray)) + { + } +#pragma warning restore SA1118 + + /// + /// The that is dominant. + /// + public Lot Lot { get; } + + /// + /// The time at which the dominant lot selected. + /// + public DateTimeOffset Timestamp { get; } + + /// + /// The of the validator selected dominant lot. + /// + public PublicKey ValidatorPublicKey { get; } + + /// + /// The height of the dominant lot. + /// + public long Height => Lot.Height; + + /// + /// The round of the dominant lot. + /// + public int Round => Lot.Round; + + /// + /// A Bencodex-encoded value of . + /// + [JsonIgnore] + public Dictionary Bencoded + { + get + { + Dictionary encoded = Bencodex.Types.Dictionary.Empty + .Add(LotKey, Lot.Bencoded) + .Add( + TimestampKey, + Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture)) + .Add(ValidatorPublicKeyKey, ValidatorPublicKey.Format(compress: true)); + + return encoded; + } + } + + /// + /// An immutable byte array encoded value of . + /// + public ImmutableArray ByteArray => ToByteArray().ToImmutableArray(); + + /// + /// Retrieve a byte array encoded value of . + /// + /// + /// A byte array encoded value of . + /// + public byte[] ToByteArray() => _codec.Encode(Bencoded); + + /// + /// Signs given with given . + /// + /// A to sign. + /// Returns a signed . + public DominantLot Sign(PrivateKey signer) => + new DominantLot(this, signer.Sign(ByteArray).ToImmutableArray()); + + /// + public bool Equals(DominantLotMetadata? other) + { + return other is { } metadata && + Height == metadata.Height && + Round == metadata.Round && + Lot.Equals(metadata.Lot) && + Timestamp + .ToString(TimestampFormat, CultureInfo.InvariantCulture).Equals( + metadata.Timestamp.ToString( + TimestampFormat, + CultureInfo.InvariantCulture)) && + ValidatorPublicKey.Equals(metadata.ValidatorPublicKey); + } + + /// + public override bool Equals(object? obj) => + obj is DominantLotMetadata other && Equals(other); + + /// + public override int GetHashCode() + { + return HashCode.Combine( + Height, + Round, + Lot, + Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture), + ValidatorPublicKey); + } + } +} diff --git a/src/Libplanet/Consensus/Lot.cs b/src/Libplanet/Consensus/Lot.cs new file mode 100644 index 00000000000..16bcb41b067 --- /dev/null +++ b/src/Libplanet/Consensus/Lot.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Libplanet.Consensus +{ + public class Lot : IBencodable, IEquatable + { + private static readonly Binary ProofKey + = new Binary(new byte[] { 0x50 }); // 'P' + + private static readonly Binary PublicKeyKey + = new Binary(new byte[] { 0x70 }); // 'p' + + private static readonly Binary ConsensusInformationKey + = new Binary(new byte[] { 0x43 }); // 'C' + + private static readonly Codec _codec = new Codec(); + + public Lot( + Proof proof, + PublicKey publicKey, + ConsensusInformation consensusInformation) + { + if (!consensusInformation.Verify(proof, publicKey)) + { + throw new ArgumentException( + $"Given {nameof(proof)} is invalid.", + nameof(proof)); + } + + Proof = proof; + PublicKey = publicKey; + ConsensusInformation = consensusInformation; + } + + public Lot(IReadOnlyList encoded) + : this(_codec.Decode(encoded.ToArray())) + { + } + + public Lot(IValue bencoded) + : this(bencoded is Dictionary dict + ? dict + : throw new ArgumentException( + $"Given {nameof(bencoded)} must be of type " + + $"{typeof(Dictionary)}: {bencoded.GetType()}", + nameof(bencoded))) + { + } + + private Lot(Dictionary bencoded) + : this( + new Proof(bencoded[ProofKey]), + new PublicKey(((Binary)bencoded[PublicKeyKey]).ByteArray), + new ConsensusInformation(((Binary)bencoded[ConsensusInformationKey]).ByteArray)) + { + } + + /// + /// can be verified with and + /// . + /// + public Proof Proof { get; } + + /// + /// that proved an . + /// can be verified by it. + /// + public PublicKey PublicKey { get; } + + /// + /// that has been proven by . + /// + public ConsensusInformation ConsensusInformation { get; } + + /// + /// The height of the proven . + /// + public long Height => ConsensusInformation.Height; + + /// + /// The round of the proven . + /// + public int Round => ConsensusInformation.Round; + + /// + /// The last proof of the proven . + /// + public Proof? LastProof => ConsensusInformation.LastProof; + + [JsonIgnore] + public IValue Bencoded + => Dictionary.Empty + .Add(ProofKey, Proof.ByteArray) + .Add(PublicKeyKey, PublicKey.Format(true)) + .Add(ConsensusInformationKey, ConsensusInformation.Encoded); + + /// + /// encoded data. + /// + public ImmutableArray ByteArray => ToByteArray().ToImmutableArray(); + + public byte[] ToByteArray() => _codec.Encode(Bencoded); + + /// + public bool Equals(Lot? other) + => other is Lot lot + && Proof.Equals(lot.Proof) + && PublicKey.Equals(lot.PublicKey) + && ConsensusInformation.Equals(lot.ConsensusInformation); + + /// + public override bool Equals(object? obj) + => obj is Lot otherLot && Equals(otherLot); + + /// + public override int GetHashCode() + => HashCode.Combine( + Proof.GetHashCode(), + PublicKey.GetHashCode(), + ConsensusInformation.GetHashCode()); + + /// + public override string ToString() + { + var dict = new Dictionary + { + { "proof", Proof }, + { "public_key", PublicKey.ToString() }, + { "consensus_information", ConsensusInformation }, + }; + return JsonSerializer.Serialize(dict); + } + } +} diff --git a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs index 79487a95b6d..6a520033be8 100644 --- a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs +++ b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs @@ -16,6 +16,7 @@ using Libplanet.Types.Tx; using Libplanet.Store; using Libplanet.Store.Trie; +using Libplanet.Consensus; namespace Libplanet.Explorer.Tests; @@ -179,6 +180,7 @@ private void AddBlock(ImmutableArray transactions) Chain.Tip.Hash, BlockContent.DeriveTxHash(transactions), Chain.Store.GetChainBlockCommit(Chain.Store.GetCanonicalChainId()!.Value), + new ConsensusInformation(Chain.Tip.Index + 1, 0, Chain.Tip.Proof).Prove(proposer), evidenceHash: null), transactions, evidence: Array.Empty()).Propose(), diff --git a/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs b/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs index 1c47d06289f..af470f580c4 100644 --- a/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs +++ b/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs @@ -15,6 +15,7 @@ using Libplanet.Store; using Xunit; using static Libplanet.Explorer.Tests.GraphQLTestUtils; +using Libplanet.Consensus; namespace Libplanet.Explorer.Tests.GraphTypes { @@ -43,6 +44,7 @@ public async void Query() previousHash: lastBlockHash, txHash: null, lastCommit: lastBlockCommit, + proof: new ConsensusInformation(2, 0, null).Prove(privateKey), evidenceHash: null)).Propose(); var stateRootHash = new HashDigest(TestUtils.GetRandomBytes(HashDigest.Size)); diff --git a/test/Libplanet.Explorer.Tests/Indexing/BlockChainIndexTest.cs b/test/Libplanet.Explorer.Tests/Indexing/BlockChainIndexTest.cs index cbc4ee73beb..f1c9503d9d2 100644 --- a/test/Libplanet.Explorer.Tests/Indexing/BlockChainIndexTest.cs +++ b/test/Libplanet.Explorer.Tests/Indexing/BlockChainIndexTest.cs @@ -11,6 +11,7 @@ using Libplanet.Types.Tx; using Libplanet.Explorer.Indexing; using Xunit; +using Libplanet.Consensus; namespace Libplanet.Explorer.Tests.Indexing; @@ -43,7 +44,9 @@ await index.SynchronizeAsync( var forkedChain = ChainFx.Chain.Fork(ChainFx.Chain.Tip.PreviousHash!.Value); var divergentBlock = forkedChain.ProposeBlock( ChainFx.PrivateKeys[0], - forkedChain.GetBlockCommit(forkedChain.Tip.Hash)); + forkedChain.GetBlockCommit(forkedChain.Tip.Hash), + new ConsensusInformation(forkedChain.Tip.Index + 1, 0, forkedChain.Tip.Proof) + .Prove(ChainFx.PrivateKeys[0])); forkedChain.Append( divergentBlock, new BlockCommit( diff --git a/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs b/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs index a44d928054e..47c1282314d 100644 --- a/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs +++ b/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs @@ -130,7 +130,10 @@ async Task AssertNextNonce(long expected, Address address) { await AssertNextNonce(1, key1.Address); Source.BlockChain.MakeTransaction(key1, ImmutableList.Empty.Add(new NullAction())); await AssertNextNonce(2, key1.Address); - var block = Source.BlockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + var block = Source.BlockChain.ProposeBlock( + proposer, + proof: Libplanet.Tests.TestUtils.CreateZeroRoundProof(Source.BlockChain.Tip, proposer)); Source.BlockChain.Append(block, Libplanet.Tests.TestUtils.CreateBlockCommit(block)); await AssertNextNonce(2, key1.Address); @@ -140,8 +143,9 @@ async Task AssertNextNonce(long expected, Address address) { // staging txs of key2 does not increase nonce of key1 Source.BlockChain.MakeTransaction(key2, ImmutableList.Empty.Add(new NullAction())); block = Source.BlockChain.ProposeBlock( - new PrivateKey(), - Libplanet.Tests.TestUtils.CreateBlockCommit(block)); + proposer, + Libplanet.Tests.TestUtils.CreateBlockCommit(block), + Libplanet.Tests.TestUtils.CreateZeroRoundProof(block, proposer)); Source.BlockChain.Append(block, Libplanet.Tests.TestUtils.CreateBlockCommit(block)); await AssertNextNonce(1, key2.Address); await AssertNextNonce(2, key1.Address); diff --git a/test/Libplanet.Net.Tests/Consensus/ConsensusContextNonProposerTest.cs b/test/Libplanet.Net.Tests/Consensus/ConsensusContextNonProposerTest.cs index d681debcfbe..0b076bfddb2 100644 --- a/test/Libplanet.Net.Tests/Consensus/ConsensusContextNonProposerTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ConsensusContextNonProposerTest.cs @@ -58,7 +58,19 @@ public async void NewHeightWithLastCommit() }; consensusContext.Start(); - var block1 = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + var block1 = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); consensusContext.HandleMessage( TestUtils.CreateConsensusPropose(block1, TestUtils.PrivateKeys[1])); var expectedVotes = new Vote[4]; @@ -93,6 +105,15 @@ public async void NewHeightWithLastCommit() consensusContext.HandleMessage(new ConsensusPreCommitMsg(expectedVotes[i])); } + lotSet = new LotSet(2L, 0, block1.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + await heightTwoProposalSent.WaitAsync(); Assert.NotNull(proposal); @@ -112,9 +133,11 @@ public async void HandleMessageFromHigherHeight() { var codec = new Codec(); ConsensusProposalMsg? proposal = null; + var heightTwoStepChangedToSortition = new AsyncAutoResetEvent(); var heightTwoStepChangedToPreVote = new AsyncAutoResetEvent(); var heightTwoStepChangedToPreCommit = new AsyncAutoResetEvent(); var heightTwoStepChangedToEndCommit = new AsyncAutoResetEvent(); + var heightThreeStepChangedToSortition = new AsyncAutoResetEvent(); var heightThreeStepChangedToPropose = new AsyncAutoResetEvent(); var heightThreeStepChangedToPreVote = new AsyncAutoResetEvent(); var proposalSent = new AsyncAutoResetEvent(); @@ -158,17 +181,31 @@ public async void HandleMessageFromHigherHeight() }; consensusContext.MessagePublished += (_, eventArgs) => { - if (eventArgs.Message is ConsensusProposalMsg proposalMsg) + if (eventArgs.Message is ConsensusProposalMsg proposalMsg + && proposalMsg.Height == 2L) { proposal = proposalMsg; proposalSent.Set(); } }; - Block block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + Block block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); blockChain.Store.PutBlockCommit(TestUtils.CreateBlockCommit(blockChain[1])); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + await proposalSent.WaitAsync(); Assert.Equal(2, consensusContext.Height); @@ -231,7 +268,18 @@ in TestUtils.PrivateKeys.Zip( (Dictionary)codec.Decode(proposal.Proposal.MarshaledBlock)); var blockHeightThree = blockChain.ProposeBlock( TestUtils.PrivateKeys[3], - TestUtils.CreateBlockCommit(blockHeightTwo)); + TestUtils.CreateBlockCommit(blockHeightTwo), + TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[3])); + + lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } // Message from higher height consensusContext.HandleMessage( @@ -269,10 +317,32 @@ public async void UseLastCommitCacheIfHeightContextIsEmpty() }; consensusContext.Start(); - Block block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + Block block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); var createdLastCommit = TestUtils.CreateBlockCommit(block); blockChain.Append(block, createdLastCommit); + lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + // Context for height #2 where node #2 is the proposer is automatically started // by watching blockchain's Tip. await heightTwoProposalSent.WaitAsync(); @@ -289,7 +359,7 @@ public async void NewHeightDelay() // The maximum error margin. (macos-netcore-test) var timeError = 500; var heightOneEndCommit = new AsyncAutoResetEvent(); - var heightTwoProposalSent = new AsyncAutoResetEvent(); + var heightTwoSortitionStarted = new AsyncAutoResetEvent(); var (blockChain, consensusContext) = TestUtils.CreateDummyConsensusContext( newHeightDelay, TestUtils.Policy, @@ -301,17 +371,26 @@ public async void NewHeightDelay() { heightOneEndCommit.Set(); } - }; - consensusContext.MessagePublished += (_, eventArgs) => - { - if (eventArgs.Height == 2 && eventArgs.Message is ConsensusProposalMsg) + else if (eventArgs.Height == 2 && eventArgs.Step == ConsensusStep.Sortition) { - heightTwoProposalSent.Set(); + heightTwoSortitionStarted.Set(); } }; consensusContext.Start(); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 1, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); consensusContext.HandleMessage( TestUtils.CreateConsensusPropose(block, TestUtils.PrivateKeys[1])); @@ -321,15 +400,15 @@ public async void NewHeightDelay() await heightOneEndCommit.WaitAsync(); var endCommitTime = DateTimeOffset.UtcNow; - await heightTwoProposalSent.WaitAsync(); - var proposeTime = DateTimeOffset.UtcNow; - var difference = proposeTime - endCommitTime; + await heightTwoSortitionStarted.WaitAsync(); + var sortitionTime = DateTimeOffset.UtcNow; + var difference = sortitionTime - endCommitTime; _logger.Debug("Difference: {Difference}", difference); // Check new height delay; slight margin of error is allowed as delay task // is run asynchronously from context events. Assert.True( - ((proposeTime - endCommitTime) - newHeightDelay).Duration() < + ((sortitionTime - endCommitTime) - newHeightDelay).Duration() < TimeSpan.FromMilliseconds(timeError)); } } diff --git a/test/Libplanet.Net.Tests/Consensus/ConsensusContextProposerTest.cs b/test/Libplanet.Net.Tests/Consensus/ConsensusContextProposerTest.cs index a9036906c3f..fa0dbac8bed 100644 --- a/test/Libplanet.Net.Tests/Consensus/ConsensusContextProposerTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ConsensusContextProposerTest.cs @@ -49,6 +49,9 @@ public async void IncreaseRoundWhenTimeout() Assert.Equal(1, consensusContext.Height); Assert.Equal(0, consensusContext.Round); + // Triggers sortition timeout. + await timeoutProcessed.WaitAsync(); + // Triggers timeout +2/3 with NIL and Block consensusContext.HandleMessage( new ConsensusPreVoteMsg( diff --git a/test/Libplanet.Net.Tests/Consensus/ConsensusContextTest.cs b/test/Libplanet.Net.Tests/Consensus/ConsensusContextTest.cs index f292c41c5d4..afea7d212b9 100644 --- a/test/Libplanet.Net.Tests/Consensus/ConsensusContextTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ConsensusContextTest.cs @@ -83,13 +83,49 @@ public async void NewHeightIncreasing() } }; - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); var blockCommit = TestUtils.CreateBlockCommit(block); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(TestUtils.PrivateKeys[2], blockCommit); + + lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[2], + blockCommit, + TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[2])); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); Assert.Equal(2, blockChain.Tip.Index); + lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + // Wait for context of height 3 to start. await heightThreeStepChangedToPropose.WaitAsync(); Assert.Equal(3, consensusContext.Height); @@ -134,6 +170,16 @@ public async void NewHeightIncreasing() await onTipChangedToThree.WaitAsync(); Assert.Equal(3, blockChain.Tip.Index); + lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + // Next height starts normally. await heightFourStepChangedToPropose.WaitAsync(); Assert.Equal(4, consensusContext.Height); @@ -176,9 +222,11 @@ public async void NewHeightWhenTipChanged() TestUtils.ActionLoader, TestUtils.PrivateKeys[1]); consensusContext.Start(); - + var proposer = new PrivateKey(); Assert.Equal(1, consensusContext.Height); - Block block = blockChain.ProposeBlock(new PrivateKey()); + Block block = blockChain.ProposeBlock( + proposer, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); Assert.Equal(1, consensusContext.Height); await Task.Delay(newHeightDelay + TimeSpan.FromSeconds(1)); @@ -197,7 +245,10 @@ public void IgnoreMessagesFromLowerHeight() Assert.True(consensusContext.Height == 1); Assert.False(consensusContext.HandleMessage( TestUtils.CreateConsensusPropose( - blockChain.ProposeBlock(TestUtils.PrivateKeys[0]), + blockChain.ProposeBlock( + TestUtils.PrivateKeys[0], + proof: TestUtils.CreateZeroRoundProof( + blockChain.Tip, TestUtils.PrivateKeys[0])), TestUtils.PrivateKeys[0], 0))); } @@ -232,6 +283,16 @@ public async void VoteSetGetOnlyProposeCommitHash() }; consensusContext.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 2, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + await heightOneProposalSent.WaitAsync(); BlockHash proposedblockHash = Assert.IsType(proposal?.BlockHash); @@ -285,7 +346,19 @@ public async Task GetVoteSetBits() TestUtils.ActionLoader, TestUtils.PrivateKeys[0]); consensusContext.Start(); - var block = blockChain.ProposeBlock(proposer); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(proposer); + foreach (int i in new int[] { 1, 2, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + proposer, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); var proposal = new ProposalMetadata( 1, 0, @@ -371,7 +444,19 @@ public async Task HandleVoteSetBits() }; consensusContext.Start(); - var block = blockChain.ProposeBlock(proposer); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + proposer, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); var proposal = new ProposalMetadata( 1, 0, @@ -443,7 +528,19 @@ public async Task HandleProposalClaim() } }; consensusContext.Start(); - var block = blockChain.ProposeBlock(proposer); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + proposer, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); var proposal = new ProposalMetadata( 1, 0, diff --git a/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs index 9c4e0824ad2..c01bdc5e22b 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs @@ -50,7 +50,6 @@ public async Task EnterPreVoteBlockOneThird() var (blockChain, context) = TestUtils.CreateDummyContext( privateKey: TestUtils.PrivateKeys[0]); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); var stateChangedToRoundOnePreVote = new AsyncAutoResetEvent(); context.StateChanged += (_, eventArgs) => { @@ -61,6 +60,19 @@ public async Task EnterPreVoteBlockOneThird() }; context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1], proof: lot.Proof); + context.ProduceMessage( new ConsensusPreVoteMsg( TestUtils.CreateVote( @@ -96,8 +108,6 @@ public async Task EnterPreCommitBlockTwoThird() var (blockChain, context) = TestUtils.CreateDummyContext( privateKey: TestUtils.PrivateKeys[0]); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); - context.StateChanged += (_, eventArgs) => { if (eventArgs.Step == ConsensusStep.PreCommit) @@ -115,6 +125,20 @@ public async Task EnterPreCommitBlockTwoThird() }; context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1], proof: lot.Proof); + context.ProduceMessage( TestUtils.CreateConsensusPropose(block, TestUtils.PrivateKeys[1])); @@ -181,6 +205,7 @@ public async void EnterPreCommitNilTwoThird() previousHash: blockChain.Tip.Hash, txHash: null, lastCommit: null, + proof: null, evidenceHash: null)).Propose(), key); @@ -201,6 +226,18 @@ public async void EnterPreCommitNilTwoThird() }; context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + context.ProduceMessage( TestUtils.CreateConsensusPropose(invalidBlock, TestUtils.PrivateKeys[1])); context.ProduceMessage( @@ -280,10 +317,23 @@ public async Task EnterPreVoteNilOnInvalidBlockHeader() previousHash: blockChain.Tip.Hash, txHash: null, lastCommit: null, + proof: null, evidenceHash: null)).Propose(), TestUtils.PrivateKeys[1]); context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + context.ProduceMessage( TestUtils.CreateConsensusPropose( invalidBlock, TestUtils.PrivateKeys[1])); @@ -355,6 +405,18 @@ public async Task EnterPreVoteNilOnInvalidBlockContent() TestUtils.PrivateKeys[1]); context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + context.ProduceMessage( TestUtils.CreateConsensusPropose( invalidBlock, @@ -429,6 +491,7 @@ message is ConsensusPreCommitMsg commit && previousHash: blockChain.Genesis.Hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: null, evidenceHash: null); var preEval = new PreEvaluationBlock( preEvaluationBlockHeader: new PreEvaluationBlockHeader( @@ -440,6 +503,18 @@ message is ConsensusPreCommitMsg commit && HashDigest.DeriveFrom(TestUtils.GetRandomBytes(1024))); context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + context.ProduceMessage( TestUtils.CreateConsensusPropose( invalidBlock, @@ -575,6 +650,28 @@ public async Task UponRulesCheckAfterTimeout() } }; + // Push DominantLots + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + lotSet.SetRound(1, lot.Proof); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + // Push round 0 and round 1 proposes. context.ProduceMessage( TestUtils.CreateConsensusPropose( @@ -644,11 +741,26 @@ public async Task TimeoutPreVote() privateKey: TestUtils.PrivateKeys[0], contextTimeoutOptions: new ContextTimeoutOption(preVoteSecondBase: 1)); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); var timeoutProcessed = new AsyncAutoResetEvent(); context.TimeoutProcessed += (_, __) => timeoutProcessed.Set(); context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: lot.Proof); + context.ProduceMessage( TestUtils.CreateConsensusPropose( block, TestUtils.PrivateKeys[1], round: 0)); @@ -695,11 +807,25 @@ public async Task TimeoutPreCommit() privateKey: TestUtils.PrivateKeys[0], contextTimeoutOptions: new ContextTimeoutOption(preCommitSecondBase: 1)); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); var timeoutProcessed = new AsyncAutoResetEvent(); context.TimeoutProcessed += (_, __) => timeoutProcessed.Set(); context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: lot.Proof); + context.ProduceMessage( TestUtils.CreateConsensusPropose( block, TestUtils.PrivateKeys[1], round: 0)); @@ -734,7 +860,7 @@ public async Task TimeoutPreCommit() // Wait for timeout. await timeoutProcessed.WaitAsync(); - Assert.Equal(ConsensusStep.Propose, context.Step); + Assert.Equal(ConsensusStep.Sortition, context.Step); Assert.Equal(1, context.Height); Assert.Equal(1, context.Round); } diff --git a/test/Libplanet.Net.Tests/Consensus/ContextProposerTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextProposerTest.cs index fd2cae3923e..a113e74c8aa 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextProposerTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextProposerTest.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Libplanet.Crypto; using Libplanet.Net.Consensus; using Libplanet.Net.Messages; using Libplanet.Types.Blocks; @@ -98,7 +97,8 @@ public async void EnterPreCommitBlock() ConsensusPreCommitMsg? preCommit = null; var preCommitSent = new AsyncAutoResetEvent(); - var (_, context) = TestUtils.CreateDummyContext(); + var (blockChain, context) = TestUtils.CreateDummyContext( + privateKey: TestUtils.PrivateKeys[0]); context.StateChanged += (_, eventArgs) => { if (eventArgs.Step == ConsensusStep.PreCommit) @@ -122,6 +122,17 @@ public async void EnterPreCommitBlock() context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[0]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + // Wait for propose to process. await proposalSent.WaitAsync(); BlockHash proposedblockHash = Assert.IsType(proposal?.BlockHash); @@ -204,7 +215,7 @@ public async void EnterNewRoundNil() await roundChangedToOne.WaitAsync(); Assert.Equal(1, context.Height); Assert.Equal(1, context.Round); - Assert.Equal(ConsensusStep.Propose, context.Step); + Assert.Equal(ConsensusStep.Sortition, context.Step); } [Fact(Timeout = Timeout)] @@ -215,7 +226,8 @@ public async Task EndCommitBlock() ConsensusProposalMsg? proposal = null; var proposalSent = new AsyncAutoResetEvent(); - var (_, context) = TestUtils.CreateDummyContext(); + var (blockChain, context) = TestUtils.CreateDummyContext( + privateKey: TestUtils.PrivateKeys[0]); context.StateChanged += (_, eventArgs) => { if (eventArgs.Step == ConsensusStep.PreCommit) @@ -239,6 +251,17 @@ public async Task EndCommitBlock() context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[0]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + // Wait for propose to process. await proposalSent.WaitAsync(); Assert.NotNull(proposal?.BlockHash); @@ -268,7 +291,7 @@ public async void EnterPreVoteNil() var stepChangedToPreVote = new AsyncAutoResetEvent(); var nilPreVoteSent = new AsyncAutoResetEvent(); var (_, context) = TestUtils.CreateDummyContext( - height: 5, + height: 1, validatorSet: Libplanet.Tests.TestUtils.ValidatorSet); // Peer1 should be a proposer context.StateChanged += (_, eventArgs) => @@ -289,7 +312,7 @@ public async void EnterPreVoteNil() context.Start(); await Task.WhenAll(nilPreVoteSent.WaitAsync(), stepChangedToPreVote.WaitAsync()); Assert.Equal(ConsensusStep.PreVote, context.Step); - Assert.Equal(5, context.Height); + Assert.Equal(1, context.Height); } [Fact(Timeout = Timeout)] @@ -301,7 +324,8 @@ public async void EnterPreVoteBlock() ConsensusPreVoteMsg? preVote = null; var preVoteSent = new AsyncAutoResetEvent(); - var (_, context) = TestUtils.CreateDummyContext(); + var (blockChain, context) = TestUtils.CreateDummyContext( + privateKey: TestUtils.PrivateKeys[0]); context.StateChanged += (_, eventArgs) => { @@ -325,6 +349,18 @@ public async void EnterPreVoteBlock() }; context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[0]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await proposalSent.WaitAsync(); Assert.NotNull(proposal?.BlockHash); @@ -334,57 +370,5 @@ public async void EnterPreVoteBlock() Assert.Equal(0, context.Round); Assert.Equal(ConsensusStep.PreVote, context.Step); } - - [Fact(Timeout = Timeout)] - public async void VoteNilOnSelfProposedInvalidBlock() - { - var privateKey = new PrivateKey(); - ConsensusProposalMsg? proposal = null; - var proposalSent = new AsyncAutoResetEvent(); - ConsensusPreVoteMsg? preVote = null; - var preVoteSent = new AsyncAutoResetEvent(); - - var blockChain = TestUtils.CreateDummyBlockChain(); - var block1 = blockChain.ProposeBlock(new PrivateKey()); - var block1Commit = TestUtils.CreateBlockCommit(block1); - blockChain.Append(block1, block1Commit); - var block2 = blockChain.ProposeBlock(new PrivateKey(), block1Commit); - var block2Commit = TestUtils.CreateBlockCommit(block2); - blockChain.Append(block2, block2Commit); - - var context = TestUtils.CreateDummyContext( - blockChain, - privateKey: TestUtils.PrivateKeys[2], - height: 2, - lastCommit: block2Commit, - validatorSet: TestUtils.ValidatorSet); - context.MessageToPublish += (_, message) => - { - if (message is ConsensusProposalMsg proposalMsg) - { - proposal = proposalMsg; - proposalSent.Set(); - } - else if (message is ConsensusPreVoteMsg preVoteMsg) - { - preVote = preVoteMsg; - preVoteSent.Set(); - } - }; - - Assert.Equal( - TestUtils.PrivateKeys[2].PublicKey, - TestUtils.ValidatorSet.GetProposer(2, 0).PublicKey); - - context.Start(); - await proposalSent.WaitAsync(); - Bencodex.Codec codec = new Bencodex.Codec(); - var proposedBlock = BlockMarshaler.UnmarshalBlock( - (Bencodex.Types.Dictionary)codec.Decode(proposal?.Proposal.MarshaledBlock!)); - Assert.Equal(context.Height + 1, proposedBlock.Index); - await preVoteSent.WaitAsync(); - Assert.Equal(default(BlockHash), preVote?.BlockHash); - Assert.Equal(default(BlockHash), preVote?.PreVote.BlockHash); - } } } diff --git a/test/Libplanet.Net.Tests/Consensus/ContextProposerValidRoundTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextProposerValidRoundTest.cs index 57f30923824..1cb7539d48c 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextProposerValidRoundTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextProposerValidRoundTest.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Libplanet.Crypto; using Libplanet.Net.Consensus; @@ -37,13 +38,19 @@ public async Task EnterValidRoundPreVoteBlock() ConsensusProposalMsg? proposal = null; var proposalSent = new AsyncAutoResetEvent(); var roundTwoVoteSent = new AsyncAutoResetEvent(); + var stateChangedToRoundTwoSortition = new AsyncAutoResetEvent(); var stateChangedToRoundTwoPropose = new AsyncAutoResetEvent(); bool timeoutProcessed = false; - var (_, context) = TestUtils.CreateDummyContext(); + var (blockChain, context) = TestUtils.CreateDummyContext( + privateKey: TestUtils.PrivateKeys[0]); context.StateChanged += (_, eventArgs) => { - if (eventArgs.Round == 2 && eventArgs.Step == ConsensusStep.Propose) + if (eventArgs.Round == 2 && eventArgs.Step == ConsensusStep.Sortition) + { + stateChangedToRoundTwoSortition.Set(); + } + else if (eventArgs.Round == 2 && eventArgs.Step == ConsensusStep.Propose) { stateChangedToRoundTwoPropose.Set(); } @@ -66,6 +73,17 @@ prevote.BlockHash is { } hash && }; context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[0]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + + await Task.Delay(1000); await proposalSent.WaitAsync(); Assert.NotNull(proposal); Block proposedBlock = BlockMarshaler.UnmarshalBlock( @@ -88,6 +106,17 @@ prevote.BlockHash is { } hash && round: 2, hash: proposedBlock.Hash, flag: VoteFlag.PreVote))); + + await stateChangedToRoundTwoSortition.WaitAsync(); + lotSet.SetRound(2, lot.Proof); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + await stateChangedToRoundTwoPropose.WaitAsync(); Assert.Equal(2, context.Round); @@ -128,16 +157,23 @@ public async void EnterValidRoundPreVoteNil() { ConsensusProposalMsg? proposal = null; var proposalSent = new AsyncAutoResetEvent(); + var stateChangedToRoundTwoSortition = new AsyncAutoResetEvent(); var stateChangedToRoundTwoPropose = new AsyncAutoResetEvent(); var stateChangedToRoundTwoPreVote = new AsyncAutoResetEvent(); var stateChangedToRoundTwoPreCommit = new AsyncAutoResetEvent(); + var stateChangedToRoundThreeSortition = new AsyncAutoResetEvent(); var stateChangedToRoundThreePropose = new AsyncAutoResetEvent(); var roundThreeNilPreVoteSent = new AsyncAutoResetEvent(); bool timeoutProcessed = false; - var (blockChain, context) = TestUtils.CreateDummyContext(); + var (blockChain, context) = TestUtils.CreateDummyContext( + privateKey: TestUtils.PrivateKeys[0]); context.StateChanged += (_, eventArgs) => { - if (eventArgs.Round == 2 && eventArgs.Step == ConsensusStep.Propose) + if (eventArgs.Round == 2 && eventArgs.Step == ConsensusStep.Sortition) + { + stateChangedToRoundTwoSortition.Set(); + } + else if (eventArgs.Round == 2 && eventArgs.Step == ConsensusStep.Propose) { stateChangedToRoundTwoPropose.Set(); } @@ -149,6 +185,10 @@ public async void EnterValidRoundPreVoteNil() { stateChangedToRoundTwoPreCommit.Set(); } + else if (eventArgs.Round == 3 && eventArgs.Step == ConsensusStep.Sortition) + { + stateChangedToRoundThreeSortition.Set(); + } else if (eventArgs.Round == 3 && eventArgs.Step == ConsensusStep.Propose) { stateChangedToRoundThreePropose.Set(); @@ -182,10 +222,21 @@ public async void EnterValidRoundPreVoteNil() previousHash: blockChain.Tip.Hash, txHash: null, lastCommit: null, + proof: null, evidenceHash: null)).Propose(), key); context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[0]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + await proposalSent.WaitAsync(); Assert.NotNull(proposal); Block proposedBlock = BlockMarshaler.UnmarshalBlock( @@ -208,6 +259,16 @@ public async void EnterValidRoundPreVoteNil() round: 2, hash: proposedBlock.Hash, flag: VoteFlag.PreVote))); + await stateChangedToRoundTwoSortition.WaitAsync(); + lotSet.SetRound(2, lot.Proof); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot(TestUtils.PrivateKeys[i], lot))); + } + await stateChangedToRoundTwoPropose.WaitAsync(); Assert.Equal(2, context.Round); Assert.False(timeoutProcessed); // Assert no transition is due to timeout. @@ -247,6 +308,17 @@ public async void EnterValidRoundPreVoteNil() round: 3, hash: differentBlock.Hash, flag: VoteFlag.PreVote))); + await stateChangedToRoundThreeSortition.WaitAsync(); + lotSet.SetRound(3, lot.Proof); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in Enumerable.Range(1, 3)) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await stateChangedToRoundThreePropose.WaitAsync(); Assert.Equal(3, context.Round); diff --git a/test/Libplanet.Net.Tests/Consensus/ContextTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextTest.cs index f21cc5615fd..3f58d2499c4 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Numerics; using System.Text.Json; @@ -54,7 +53,7 @@ public async Task StartAsProposer() { var proposalSent = new AsyncAutoResetEvent(); var stepChangedToPreVote = new AsyncAutoResetEvent(); - var (_, context) = TestUtils.CreateDummyContext(); + var (blockChain, context) = TestUtils.CreateDummyContext(); context.StateChanged += (_, eventArgs) => { if (eventArgs.Step == ConsensusStep.PreVote) @@ -71,6 +70,17 @@ public async Task StartAsProposer() }; context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await Task.WhenAll(proposalSent.WaitAsync(), stepChangedToPreVote.WaitAsync()); Assert.Equal(ConsensusStep.PreVote, context.Step); @@ -89,7 +99,9 @@ public async Task StartAsProposerWithLastCommit() // whether the lastCommit of height 1 is used for propose. Note that Peer2 // is the proposer for height 2. var blockChain = TestUtils.CreateDummyBlockChain(); - Block heightOneBlock = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + Block heightOneBlock = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); var lastCommit = TestUtils.CreateBlockCommit(heightOneBlock); blockChain.Append(heightOneBlock, lastCommit); @@ -117,6 +129,17 @@ public async Task StartAsProposerWithLastCommit() }; context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 0, 1, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await Task.WhenAll(stepChangedToPreVote.WaitAsync(), proposalSent.WaitAsync()); Assert.Equal(ConsensusStep.PreVote, context.Step); @@ -134,7 +157,7 @@ public async Task CannotStartTwice() var (_, context) = TestUtils.CreateDummyContext(); context.StateChanged += (_, eventArgs) => { - if (eventArgs.Step == ConsensusStep.Propose) + if (eventArgs.Step == ConsensusStep.Sortition) { stepChanged.Set(); } @@ -159,7 +182,9 @@ public async Task CanAcceptMessagesAfterCommitFailure() // Add block #1 so we can start with a last commit for height 2. var blockChain = TestUtils.CreateDummyBlockChain(); - Block heightOneBlock = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + Block heightOneBlock = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); var lastCommit = TestUtils.CreateBlockCommit(heightOneBlock); blockChain.Append(heightOneBlock, lastCommit); @@ -198,6 +223,16 @@ public async Task CanAcceptMessagesAfterCommitFailure() }; context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[2]); + foreach (int i in new int[] { 0, 1, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } await Task.WhenAll(stepChangedToPreVote.WaitAsync(), proposalSent.WaitAsync()); @@ -266,7 +301,20 @@ public async Task ThrowOnInvalidProposerMessage() exceptionThrown = e; exceptionOccurred.Set(); }; - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 1, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); context.Start(); context.ProduceMessage( @@ -287,7 +335,9 @@ public async Task ThrowOnDifferentHeightMessage() exceptionThrown = e; exceptionOccurred.Set(); }; - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[2]); + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[2], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[2])); context.Start(); context.ProduceMessage( @@ -376,9 +426,22 @@ public async Task CanPreCommitOnEndCommit() genesisHash: blockChain.Genesis.Hash, actions: new[] { action }.ToPlainValues()); blockChain.StageTransaction(tx); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 1, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + context.ProduceMessage( TestUtils.CreateConsensusPropose(block, TestUtils.PrivateKeys[1])); @@ -476,7 +539,7 @@ public async Task CanPreCommitOnEndCommit() /// /// [Fact(Timeout = Timeout)] - public async Task CanReplaceProposal() + public async void CanReplaceProposal() { var codec = new Codec(); var privateKeys = Enumerable.Range(0, 4).Select(_ => new PrivateKey()).ToArray(); @@ -506,10 +569,12 @@ public async Task CanReplaceProposal() validatorSet: validatorSet); var blockA = blockChain.ProposeBlock( proposer, - lastCommit: blockChain.GetBlockCommit(blockChain.Tip.Hash)); + lastCommit: blockChain.GetBlockCommit(blockChain.Tip.Hash), + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); var blockB = blockChain.ProposeBlock( proposer, - lastCommit: blockChain.GetBlockCommit(blockChain.Tip.Hash)); + lastCommit: blockChain.GetBlockCommit(blockChain.Tip.Hash), + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); context.StateChanged += (sender, state) => { if (state.Step != prevStep) @@ -525,6 +590,18 @@ public async Task CanReplaceProposal() } }; context.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, validatorSet, 20); + var lot = lotSet.GenerateLot(proposer); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + privateKeys[i], lot))); + } + + await stepChanged.WaitAsync(); await stepChanged.WaitAsync(); Assert.Equal(ConsensusStep.Propose, context.Step); @@ -666,9 +743,22 @@ public async Task CanCreateContextWithLastingEvaluation() genesisHash: blockChain.Genesis.Hash, actions: new[] { action }.ToPlainValues()); blockChain.StageTransaction(tx); - var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var block = blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, TestUtils.PrivateKeys[1])); consensusContext.Start(); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[1]); + foreach (int i in new int[] { 0, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + context.ProduceMessage( TestUtils.CreateConsensusPropose(block, TestUtils.PrivateKeys[1])); @@ -701,20 +791,182 @@ public async Task CanCreateContextWithLastingEvaluation() } Assert.Equal(1, consensusContext.Height); - var watch = Stopwatch.StartNew(); await onTipChanged.WaitAsync(); - Assert.True(watch.ElapsedMilliseconds < (actionDelay * 0.5)); - watch.Restart(); - await enteredHeightTwo.WaitAsync(); Assert.Equal( 4, context.GetBlockCommit()!.Votes.Count( vote => vote.Flag.Equals(VoteFlag.PreCommit))); - Assert.True(watch.ElapsedMilliseconds > (actionDelay * 0.5)); Assert.Equal(2, consensusContext.Height); } + [Fact] + public async void BroadcastLot() + { + var lotSent = new AsyncAutoResetEvent(); + Lot? lotPublished = null; + var (blockChain, context) = TestUtils.CreateDummyContext( + height: 1, + privateKey: TestUtils.PrivateKeys[0]); + + context.MessageToPublish += (_, message) => + { + if (message is ConsensusLotMsg consensusLotMsg) + { + lotPublished = consensusLotMsg.Lot; + lotSent.Set(); + } + }; + + context.Start(); + + await lotSent.WaitAsync(); + Assert.NotNull(lotPublished); + + Assert.Equal(1L, lotPublished.Height); + Assert.Equal(0, lotPublished.Round); + Assert.Equal(blockChain.Tip.Proof, lotPublished.LastProof); + Assert.Equal(TestUtils.PrivateKeys[0].PublicKey, lotPublished.PublicKey); + } + + [Fact] + public async void BroadcastDominantLot() + { + var dominantLotSent = new AsyncAutoResetEvent(); + DominantLot? dominantLotPublished = null; + var (blockChain, context) = TestUtils.CreateDummyContext( + height: 1, + privateKey: TestUtils.PrivateKeys[0]); + + context.MessageToPublish += (_, message) => + { + if (message is ConsensusDominantLotMsg consensusDominantLotMsg) + { + dominantLotPublished = consensusDominantLotMsg.DominantLot; + dominantLotSent.Set(); + } + }; + + context.Start(); + + var lots = Enumerable.Range(0, 4).Select( + i => new ConsensusInformation(1L, 0, blockChain.Tip.Proof) + .ToLot(TestUtils.PrivateKeys[i])); + + var dominantLotAmongFour = lots.OrderBy( + lot => lot.Proof.Draw( + 20, + TestUtils.ValidatorSet.GetValidator(lot.PublicKey).Power, + TestUtils.ValidatorSet.TotalPower)) + .ThenBy(lot => lot.Proof).LastOrDefault(); + + foreach (var lot in lots.Skip(1)) + { + context.ProduceMessage(new ConsensusLotMsg(lot)); + } + + await dominantLotSent.WaitAsync(); + Assert.NotNull(dominantLotPublished); + + DominantLot dominantLot = dominantLotPublished; + Assert.Equal(1L, dominantLot.Height); + Assert.Equal(0, dominantLot.Round); + Assert.Equal(TestUtils.PrivateKeys[0].PublicKey, dominantLot.ValidatorPublicKey); + Assert.Equal(dominantLotAmongFour, dominantLot.Lot); + } + + [Fact] + public async void SetProposerWithMajorityDominantLot() + { + var enteredPropose = new AsyncAutoResetEvent(); + var (blockChain, context) = TestUtils.CreateDummyContext( + height: 1, + privateKey: TestUtils.PrivateKeys[0]); + context.StateChanged += (_, eventArgs) => + { + if (eventArgs.Step == ConsensusStep.Propose) + { + enteredPropose.Set(); + } + }; + + context.Start(); + + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[0]); + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + await enteredPropose.WaitAsync(); + Assert.Equal(TestUtils.PrivateKeys[0].PublicKey, context.Proposer); + } + + [Fact] + public async void IncreaseRoundAfterTimeoutSortition() + { + var timeoutSortition = new AsyncAutoResetEvent(); + var enteredRoundTwo = new AsyncAutoResetEvent(); + var (blockChain, context) = TestUtils.CreateDummyContext( + height: 1, + privateKey: TestUtils.PrivateKeys[0]); + + context.TimeoutProcessed += (_, eventArgs) => + { + if (eventArgs.Round == 0 && eventArgs.Step == ConsensusStep.Sortition) + { + timeoutSortition.Set(); + } + }; + + context.StateChanged += (_, eventArgs) => + { + if (eventArgs.Round == 1) + { + enteredRoundTwo.Set(); + } + }; + + context.Start(); + await timeoutSortition.WaitAsync(); + + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusPreVoteMsg( + new VoteMetadata( + 1, + 0, + default, + DateTimeOffset.UtcNow, + TestUtils.PrivateKeys[i].PublicKey, + BigInteger.One, + VoteFlag.PreVote).Sign(TestUtils.PrivateKeys[i]))); + } + + foreach (int i in new int[] { 1, 2, 3 }) + { + context.ProduceMessage( + new ConsensusPreCommitMsg( + new VoteMetadata( + 1, + 0, + default, + DateTimeOffset.UtcNow, + TestUtils.PrivateKeys[i].PublicKey, + BigInteger.One, + VoteFlag.PreCommit).Sign(TestUtils.PrivateKeys[i]))); + } + + await enteredRoundTwo.WaitAsync(); + Assert.Equal(1, context.Round); + } + public struct ContextJson { #pragma warning disable SA1300 diff --git a/test/Libplanet.Net.Tests/Consensus/DuplicateVoteEvidenceTest.cs b/test/Libplanet.Net.Tests/Consensus/DuplicateVoteEvidenceTest.cs index 2f7ff981c27..28ba9a6038c 100644 --- a/test/Libplanet.Net.Tests/Consensus/DuplicateVoteEvidenceTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/DuplicateVoteEvidenceTest.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Bencodex; using Bencodex.Types; +using Libplanet.Consensus; using Libplanet.Net.Consensus; using Libplanet.Net.Messages; using Libplanet.Types.Blocks; @@ -49,17 +50,30 @@ public async Task Evidence_WithDuplicateVotes_Test() consensusContext: consensusContext, height: 3, cancellationToken: new CancellationTokenSource(Timeout).Token); - var consensusProposalMsgAt7Task = WaitUntilPublishedAsync( + var consensusProposalMsgAt5Task = WaitUntilPublishedAsync( consensusContext: consensusContext, - height: 7, + height: 5, cancellationToken: new CancellationTokenSource(Timeout).Token); - var block = blockChain.ProposeBlock(privateKeys[1]); + var proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[1]); + var block = blockChain.ProposeBlock(privateKeys[1], proof: proof); var blockCommit = TestUtils.CreateBlockCommit(block); consensusContext.Start(); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[2]); + block = blockChain.ProposeBlock(privateKeys[2], blockCommit, proof); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await consensusProposalMsgAt3Task; var consensusProposalMsgAt3 = consensusProposalMsgAt3Task.Result; var blockHash = consensusProposalMsgAt3.BlockHash; @@ -103,23 +117,27 @@ await WaitUntilHeightAsync( Assert.Equal(0, consensusContext.Round); blockCommit = blockChain.GetBlockCommit(blockChain.Tip.Hash); - block = blockChain.ProposeBlock(privateKeys[0], blockCommit); - blockCommit = TestUtils.CreateBlockCommit(block); - blockChain.Append(block, blockCommit); - - block = blockChain.ProposeBlock(privateKeys[1], blockCommit); - blockCommit = TestUtils.CreateBlockCommit(block); - blockChain.Append(block, blockCommit); - - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[0]); + block = blockChain.ProposeBlock(privateKeys[0], blockCommit, proof); blockCommit = TestUtils.CreateBlockCommit(block); blockChain.Append(block, blockCommit); - await consensusProposalMsgAt7Task; - var consensusProposalMsgAt7 = consensusProposalMsgAt7Task.Result; + lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + + await consensusProposalMsgAt5Task; + var consensusProposalMsgAt5 = consensusProposalMsgAt5Task.Result; Assert.NotNull(consensusProposalMsgAt3?.BlockHash); var actualBlock = BlockMarshaler.UnmarshalBlock( - (Dictionary)_codec.Decode(consensusProposalMsgAt7.Proposal.MarshaledBlock)); + (Dictionary)_codec.Decode(consensusProposalMsgAt5.Proposal.MarshaledBlock)); Assert.Single(actualBlock.Evidence); } @@ -137,13 +155,26 @@ public async Task IgnoreDifferentHeightVote() consensusContext: consensusContext, height: 3, cancellationToken: new CancellationTokenSource(Timeout).Token); - var block = blockChain.ProposeBlock(privateKeys[1]); + var proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[1]); + var block = blockChain.ProposeBlock(privateKeys[1], proof: proof); var blockCommit = TestUtils.CreateBlockCommit(block); consensusContext.Start(); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[2]); + block = blockChain.ProposeBlock(privateKeys[2], blockCommit, proof); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await consensusProposalMsgAt3Task; var consensusProposalMsgAt3 = consensusProposalMsgAt3Task.Result; var blockHash = consensusProposalMsgAt3.BlockHash; @@ -199,13 +230,26 @@ public async Task IgnoreDifferentRoundVote() consensusContext: consensusContext, height: 3, cancellationToken: new CancellationTokenSource(Timeout).Token); - var block = blockChain.ProposeBlock(privateKeys[1]); + var proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[1]); + var block = blockChain.ProposeBlock(privateKeys[1], proof: proof); var blockCommit = TestUtils.CreateBlockCommit(block); consensusContext.Start(); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[2]); + block = blockChain.ProposeBlock(privateKeys[2], blockCommit, proof); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await consensusProposalMsgAt3Task; var consensusProposalMsgAt3 = consensusProposalMsgAt3Task.Result; var blockHash = consensusProposalMsgAt3.BlockHash; @@ -261,13 +305,26 @@ public async Task IgnoreDifferentFlagVote() consensusContext: consensusContext, height: 3, cancellationToken: new CancellationTokenSource(Timeout).Token); - var block = blockChain.ProposeBlock(privateKeys[1]); + var proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[1]); + var block = blockChain.ProposeBlock(privateKeys[1], proof: proof); var blockCommit = TestUtils.CreateBlockCommit(block); consensusContext.Start(); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[2]); + block = blockChain.ProposeBlock(privateKeys[2], blockCommit, proof); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await consensusProposalMsgAt3Task; var consensusProposalMsgAt3 = consensusProposalMsgAt3Task.Result; var blockHash = consensusProposalMsgAt3.BlockHash; @@ -323,13 +380,26 @@ public async Task IgnoreSameBlockHashVote() consensusContext: consensusContext, height: 3, cancellationToken: new CancellationTokenSource(Timeout).Token); - var block = blockChain.ProposeBlock(privateKeys[1]); + var proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[1]); + var block = blockChain.ProposeBlock(privateKeys[1], proof: proof); var blockCommit = TestUtils.CreateBlockCommit(block); consensusContext.Start(); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[2]); + block = blockChain.ProposeBlock(privateKeys[2], blockCommit, proof); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await consensusProposalMsgAt3Task; var consensusProposalMsgAt3 = consensusProposalMsgAt3Task.Result; var blockHash = consensusProposalMsgAt3.BlockHash; @@ -385,13 +455,26 @@ public async Task IgnoreNillVote() consensusContext: consensusContext, height: 3, cancellationToken: new CancellationTokenSource(Timeout).Token); - var block = blockChain.ProposeBlock(privateKeys[1]); + var proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[1]); + var block = blockChain.ProposeBlock(privateKeys[1], proof: proof); var blockCommit = TestUtils.CreateBlockCommit(block); consensusContext.Start(); blockChain.Append(block, blockCommit); - block = blockChain.ProposeBlock(privateKeys[2], blockCommit); + proof = TestUtils.CreateZeroRoundProof(blockChain.Tip, privateKeys[2]); + block = blockChain.ProposeBlock(privateKeys[2], blockCommit, proof); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + var lotSet = new LotSet( + blockChain.Tip.Index + 1, 0, blockChain.Tip.Proof, TestUtils.ValidatorSet, 20); + var lot = lotSet.GenerateLot(TestUtils.PrivateKeys[3]); + foreach (int i in new int[] { 0, 1, 2 }) + { + consensusContext.HandleMessage( + new ConsensusDominantLotMsg( + TestUtils.CreateDominantLot( + TestUtils.PrivateKeys[i], lot))); + } + await consensusProposalMsgAt3Task; var consensusProposalMsgAt3 = consensusProposalMsgAt3Task.Result; var blockHash = consensusProposalMsgAt3.BlockHash; diff --git a/test/Libplanet.Net.Tests/Consensus/HeightVoteSetTest.cs b/test/Libplanet.Net.Tests/Consensus/HeightVoteSetTest.cs index a17cebe7170..09c4eca7272 100644 --- a/test/Libplanet.Net.Tests/Consensus/HeightVoteSetTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/HeightVoteSetTest.cs @@ -24,7 +24,9 @@ public class HeightVoteSetTest public HeightVoteSetTest() { _blockChain = TestUtils.CreateDummyBlockChain(); - var block = _blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); + var block = _blockChain.ProposeBlock( + TestUtils.PrivateKeys[1], + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, TestUtils.PrivateKeys[1])); _lastCommit = TestUtils.CreateBlockCommit(block); _heightVoteSet = new HeightVoteSet(2, TestUtils.ValidatorSet); _blockChain.Append(block, TestUtils.CreateBlockCommit(block)); diff --git a/test/Libplanet.Net.Tests/Consensus/LotSetTest.cs b/test/Libplanet.Net.Tests/Consensus/LotSetTest.cs new file mode 100644 index 00000000000..172474c12da --- /dev/null +++ b/test/Libplanet.Net.Tests/Consensus/LotSetTest.cs @@ -0,0 +1,196 @@ +using System; +using System.Linq; +using Libplanet.Consensus; +using Libplanet.Crypto; +using Libplanet.Net.Consensus; +using Xunit; + +namespace Libplanet.Net.Tests.Consensus +{ + public class LotSetTest + { + private const int DrawSize = 20; + private readonly Proof _lastProof; + private readonly ConsensusInformation _consensusInformation; + private readonly LotSet _lotSet; + + public LotSetTest() + { + _lastProof = new ConsensusInformation(1L, 1, null).Prove(new PrivateKey()); + _consensusInformation = new ConsensusInformation(1L, 2, _lastProof); + _lotSet = new LotSet(_consensusInformation, TestUtils.ValidatorSet, DrawSize); + } + + [Fact] + public void Constructor() + { + var lotSet = new LotSet( + 1L, + 2, + _lastProof, + TestUtils.ValidatorSet, + DrawSize); + + Assert.Equal(1L, lotSet.Height); + Assert.Equal(2, lotSet.Round); + Assert.Equal(_lastProof, lotSet.LastProof); + Assert.Equal(_consensusInformation, lotSet.ConsensusInformation); + Assert.Null(lotSet.Maj23); + Assert.Null(lotSet.DominantLot); + Assert.Empty(lotSet.Lots); + Assert.Empty(lotSet.DominantLots); + } + + [Fact] + public void SetRound() + { + _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[0])); + _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[1])); + _lotSet.AddDominantLot( + new DominantLotMetadata( + _lotSet.Lots[TestUtils.PrivateKeys[0].PublicKey], + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[1].PublicKey).Sign(TestUtils.PrivateKeys[1])); + _lotSet.AddDominantLot( + new DominantLotMetadata( + _lotSet.Lots[TestUtils.PrivateKeys[0].PublicKey], + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[2].PublicKey).Sign(TestUtils.PrivateKeys[2])); + _lotSet.AddDominantLot( + new DominantLotMetadata( + _lotSet.Lots[TestUtils.PrivateKeys[0].PublicKey], + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[3].PublicKey).Sign(TestUtils.PrivateKeys[3])); + + Assert.NotNull(_lotSet.Maj23); + Assert.NotNull(_lotSet.DominantLot); + Assert.Equal(2, _lotSet.Lots.Count); + Assert.Equal(3, _lotSet.DominantLots.Count); + + Assert.Throws(() => _lotSet.SetRound(1, null)); + _lotSet.SetRound(3, null); + Assert.Null(_lotSet.Maj23); + Assert.Null(_lotSet.DominantLot); + Assert.Empty(_lotSet.Lots); + Assert.Empty(_lotSet.DominantLots); + } + + [Fact] + public void GenerateProof() + { + var prover = new PrivateKey(); + var proof = _lotSet.GenerateProof(prover); + Assert.Equal(_consensusInformation.Prove(prover), proof); + Assert.True(_consensusInformation.Verify(proof, prover.PublicKey)); + } + + [Fact] + public void GenerateLot() + { + var prover = new PrivateKey(); + var lot = _lotSet.GenerateLot(prover); + Assert.Equal(_consensusInformation.ToLot(prover), lot); + Assert.Equal(1L, lot.Height); + Assert.Equal(2, lot.Round); + Assert.Equal(_lastProof, lot.LastProof); + Assert.Equal(_consensusInformation, lot.ConsensusInformation); + Assert.Equal(prover.PublicKey, lot.PublicKey); + } + + [Fact] + public void AddLot() + { + Assert.Throws( + () => _lotSet.AddLot(_consensusInformation.ToLot(new PrivateKey()))); + Assert.Throws( + () => _lotSet.AddLot( + new ConsensusInformation(0L, 0, null) + .ToLot(TestUtils.PrivateKeys[0]))); + _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[0])); + Assert.Throws( + () => _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[0]))); + Assert.Single(_lotSet.Lots); + Assert.Equal(_lotSet.DominantLot, _lotSet.Lots[TestUtils.PrivateKeys[0].PublicKey]); + _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[1])); + _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[2])); + _lotSet.AddLot(_consensusInformation.ToLot(TestUtils.PrivateKeys[3])); + Assert.Equal(4, _lotSet.Lots.Count); + Lot dominantLot = _lotSet.DominantLot!; + var nonDominantLots + = _lotSet.Lots.Remove(dominantLot.PublicKey).Select(e => e.Value); + Assert.All(nonDominantLots, lot => Assert.True(FormerLotWins(dominantLot, lot))); + } + + [Fact] + public void AddDominantLot() + { + var nonValidator = new PrivateKey(); + Assert.Throws( + () => _lotSet.AddDominantLot( + new DominantLotMetadata( + _consensusInformation.ToLot(TestUtils.PrivateKeys[0]), + DateTimeOffset.MinValue, + nonValidator.PublicKey).Sign(nonValidator))); + Assert.Throws( + () => _lotSet.AddDominantLot( + new DominantLotMetadata( + _consensusInformation.ToLot(new PrivateKey()), + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[0].PublicKey).Sign(TestUtils.PrivateKeys[0]))); + Assert.Throws( + () => _lotSet.AddDominantLot( + new DominantLotMetadata( + new ConsensusInformation(0L, 0, null) + .ToLot(TestUtils.PrivateKeys[0]), + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[0].PublicKey).Sign(TestUtils.PrivateKeys[0]))); + var lotByValidator0 = _consensusInformation.ToLot(TestUtils.PrivateKeys[0]); + _lotSet.AddDominantLot(new DominantLotMetadata( + lotByValidator0, + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[0].PublicKey).Sign(TestUtils.PrivateKeys[0])); + Assert.Throws( + () => _lotSet.AddDominantLot( + new DominantLotMetadata( + lotByValidator0, + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[0].PublicKey).Sign(TestUtils.PrivateKeys[0]))); + Assert.Throws( + () => _lotSet.AddDominantLot( + new DominantLotMetadata( + _consensusInformation.ToLot(TestUtils.PrivateKeys[1]), + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[0].PublicKey).Sign(TestUtils.PrivateKeys[0]))); + Assert.Single(_lotSet.DominantLots); + Assert.Null(_lotSet.Maj23); + _lotSet.AddDominantLot(new DominantLotMetadata( + lotByValidator0, + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[1].PublicKey).Sign(TestUtils.PrivateKeys[1])); + _lotSet.AddDominantLot(new DominantLotMetadata( + _consensusInformation.ToLot(TestUtils.PrivateKeys[1]), + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[2].PublicKey).Sign(TestUtils.PrivateKeys[2])); + Assert.Equal(3, _lotSet.DominantLots.Count); + _lotSet.AddDominantLot(new DominantLotMetadata( + lotByValidator0, + DateTimeOffset.MinValue, + TestUtils.PrivateKeys[3].PublicKey).Sign(TestUtils.PrivateKeys[3])); + Assert.Equal(lotByValidator0, _lotSet.Maj23); + } + + private static bool FormerLotWins(Lot former, Lot latter) + { + var formerDraw = former.Proof.Draw( + DrawSize, + TestUtils.ValidatorSet.GetValidator(former.PublicKey).Power, + TestUtils.ValidatorSet.TotalPower); + var latterDraw = latter.Proof.Draw( + DrawSize, + TestUtils.ValidatorSet.GetValidator(former.PublicKey).Power, + TestUtils.ValidatorSet.TotalPower); + return formerDraw > latterDraw + || (formerDraw == latterDraw && former.Proof > latter.Proof); + } + } +} diff --git a/test/Libplanet.Net.Tests/Messages/MessageTest.cs b/test/Libplanet.Net.Tests/Messages/MessageTest.cs index b98a1b5f4c3..a0b85a58632 100644 --- a/test/Libplanet.Net.Tests/Messages/MessageTest.cs +++ b/test/Libplanet.Net.Tests/Messages/MessageTest.cs @@ -115,7 +115,7 @@ public void GetId() var message = new BlockHeaderMsg(genesis.Hash, genesis.Header); Assert.Equal( new MessageId(ByteUtil.ParseHex( - "1aa2c8fad502f8890b2e8cf6f9afe57e6f718f454f14f8304165e921b28905bf")), + "f83e5f07b464bb41d182ec373a1f1bf087defaaaedf466b302c040601c5254e9")), message.Id); } diff --git a/test/Libplanet.Net.Tests/SwarmTest.Broadcast.cs b/test/Libplanet.Net.Tests/SwarmTest.Broadcast.cs index 77cb117613b..7d90aecb5e8 100644 --- a/test/Libplanet.Net.Tests/SwarmTest.Broadcast.cs +++ b/test/Libplanet.Net.Tests/SwarmTest.Broadcast.cs @@ -58,9 +58,12 @@ public async Task BroadcastBlock() foreach (int i in Enumerable.Range(0, numBlocks)) { + var proposer = new PrivateKey(); var block = chainA.ProposeBlock( - new PrivateKey(), TestUtils.CreateBlockCommit(chainA.Tip)); - chainA.Append(block, TestUtils.CreateBlockCommit(block)); + proposer, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, proposer)); + chainA.Append(block, CreateBlockCommit(block)); } Assert.Equal(numBlocks, chainA.Tip.Index); @@ -101,8 +104,10 @@ public async Task BroadcastBlockToReconnectedPeer() foreach (int i in Enumerable.Range(0, 10)) { Block block = minerChain.ProposeBlock( - miner, CreateBlockCommit(minerChain.Tip)); - minerChain.Append(block, TestUtils.CreateBlockCommit(block)); + miner, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, miner)); + minerChain.Append(block, CreateBlockCommit(block)); } Swarm seed = await CreateSwarm( @@ -201,8 +206,10 @@ public async Task BroadcastIgnoreFromDifferentGenesisHash() await StartAsync(seedSwarm); await receiverSwarm.AddPeersAsync(new[] { seedSwarm.AsPeer }, null); - Block block = seedChain.ProposeBlock(seedMiner); - seedChain.Append(block, TestUtils.CreateBlockCommit(block)); + Block block = seedChain.ProposeBlock( + seedMiner, + proof: CreateZeroRoundProof(seedChain.Tip, seedMiner)); + seedChain.Append(block, CreateBlockCommit(block)); seedSwarm.BroadcastBlock(block); Assert.NotEqual(seedChain.Tip, receiverChain.Tip); } @@ -239,8 +246,10 @@ CancellationToken cancellationToken try { var block = chain.ProposeBlock( - miner, CreateBlockCommit(chain.Tip)); - chain.Append(block, TestUtils.CreateBlockCommit(block)); + miner, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, miner)); + chain.Append(block, CreateBlockCommit(block)); Log.Debug( "Block mined. [Node: {0}, Block: {1}]", @@ -311,8 +320,10 @@ public async Task BroadcastTx() ); chainA.StageTransaction(tx); - Block block = chainA.ProposeBlock(minerA); - chainA.Append(block, TestUtils.CreateBlockCommit(block)); + Block block = chainA.ProposeBlock( + minerA, + proof: CreateZeroRoundProof(chainA.Tip, minerA)); + chainA.Append(block, CreateBlockCommit(block)); try { @@ -374,8 +385,10 @@ public async Task BroadcastTxWhileMining() for (var i = 0; i < 10; i++) { Block block = chainC.ProposeBlock( - minerC, CreateBlockCommit(chainC.Tip)); - chainC.Append(block, TestUtils.CreateBlockCommit(block)); + minerC, + CreateBlockCommit(chainC.Tip), + CreateZeroRoundProof(chainC.Tip, minerC)); + chainC.Append(block, CreateBlockCommit(block)); } }); @@ -585,7 +598,9 @@ public async Task DoNotRebroadcastTxsWithLowerNonce() await StartAsync(swarmA); await StartAsync(swarmB); - Block block = chainB.ProposeBlock(keyB); + Block block = chainB.ProposeBlock( + keyB, + proof: CreateZeroRoundProof(chainB.Tip, keyB)); chainB.Append(block, TestUtils.CreateBlockCommit(block)); var tx3 = chainA.MakeTransaction( @@ -650,14 +665,18 @@ public async Task CanBroadcastBlock() foreach (int i in Enumerable.Range(0, 10)) { Block block = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block, TestUtils.CreateBlockCommit(block)); } foreach (int i in Enumerable.Range(0, 3)) { Block block = chainB.ProposeBlock( - keyB, CreateBlockCommit(chainB.Tip)); + keyB, + CreateBlockCommit(chainB.Tip), + CreateZeroRoundProof(chainB.Tip, keyB)); chainB.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -754,13 +773,15 @@ public async Task BroadcastBlockWithSkip() Block block1 = blockChain.ProposeBlock( GenesisProposer, new[] { transactions[0] }.ToImmutableList(), - TestUtils.CreateBlockCommit(blockChain.Tip), + CreateBlockCommit(blockChain.Tip), + CreateZeroRoundProof(blockChain.Tip, GenesisProposer), ImmutableArray.Empty); blockChain.Append(block1, TestUtils.CreateBlockCommit(block1), true); Block block2 = blockChain.ProposeBlock( GenesisProposer, new[] { transactions[1] }.ToImmutableList(), CreateBlockCommit(blockChain.Tip), + CreateZeroRoundProof(blockChain.Tip, GenesisProposer), ImmutableArray.Empty); blockChain.Append(block2, TestUtils.CreateBlockCommit(block2), true); @@ -808,7 +829,9 @@ public async Task BroadcastBlockWithoutGenesis() await BootstrapAsync(swarmB, swarmA.AsPeer); var block = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block, TestUtils.CreateBlockCommit(block)); swarmA.BroadcastBlock(chainA[-1]); @@ -817,7 +840,9 @@ public async Task BroadcastBlockWithoutGenesis() Assert.Equal(chainB.BlockHashes, chainA.BlockHashes); block = chainA.ProposeBlock( - keyB, CreateBlockCommit(chainA.Tip)); + keyB, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyB)); chainA.Append(block, TestUtils.CreateBlockCommit(block)); swarmA.BroadcastBlock(chainA[-1]); @@ -846,7 +871,9 @@ public async Task IgnoreExistingBlocks() BlockChain chainB = swarmB.BlockChain; var block = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); BlockCommit blockCommit = TestUtils.CreateBlockCommit(block); chainA.Append(block, blockCommit); chainB.Append(block, blockCommit); @@ -854,7 +881,9 @@ public async Task IgnoreExistingBlocks() foreach (int i in Enumerable.Range(0, 3)) { block = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -905,7 +934,9 @@ public async Task PullBlocks() foreach (int i in Enumerable.Range(0, 10)) { Block block = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -914,14 +945,18 @@ public async Task PullBlocks() foreach (int i in Enumerable.Range(0, 5)) { Block block = chainB.ProposeBlock( - keyB, CreateBlockCommit(chainB.Tip)); + keyB, + CreateBlockCommit(chainB.Tip), + CreateZeroRoundProof(chainB.Tip, keyB)); chainB.Append(block, TestUtils.CreateBlockCommit(block)); } foreach (int i in Enumerable.Range(0, 3)) { Block block = chainC.ProposeBlock( - keyB, CreateBlockCommit(chainC.Tip)); + keyB, + CreateBlockCommit(chainC.Tip), + CreateZeroRoundProof(chainC.Tip, keyB)); chainC.Append(block, TestUtils.CreateBlockCommit(block)); } diff --git a/test/Libplanet.Net.Tests/SwarmTest.Fixtures.cs b/test/Libplanet.Net.Tests/SwarmTest.Fixtures.cs index 91c60f4ff6f..a89304e3573 100644 --- a/test/Libplanet.Net.Tests/SwarmTest.Fixtures.cs +++ b/test/Libplanet.Net.Tests/SwarmTest.Fixtures.cs @@ -59,7 +59,9 @@ private static (Address, Block[]) } Block block = chain.ProposeBlock( - miner, CreateBlockCommit(chain.Tip)); + miner, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, miner)); Log.Logger.Information(" #{0,2} {1}", block.Index, block.Hash); chain.Append(block, CreateBlockCommit(block)); } diff --git a/test/Libplanet.Net.Tests/SwarmTest.Preload.cs b/test/Libplanet.Net.Tests/SwarmTest.Preload.cs index 5d1a8396813..1fdb8919f9a 100644 --- a/test/Libplanet.Net.Tests/SwarmTest.Preload.cs +++ b/test/Libplanet.Net.Tests/SwarmTest.Preload.cs @@ -46,7 +46,9 @@ public async Task InitialBlockDownload() foreach (int i in Enumerable.Range(0, 10)) { Block block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -87,17 +89,23 @@ public async Task InitialBlockDownloadStates() minerChain.MakeTransaction(key, new[] { action }); var block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, TestUtils.CreateBlockCommit(block)); minerChain.MakeTransaction(key, new[] { DumbAction.Create((address1, "bar")) }); block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, TestUtils.CreateBlockCommit(block)); minerChain.MakeTransaction(key, new[] { DumbAction.Create((address1, "baz")) }); block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, TestUtils.CreateBlockCommit(block)); try @@ -137,7 +145,8 @@ public async Task Preload() ProposeNext( previousBlock: i == 0 ? minerChain.Genesis : blocks[i - 1], miner: ChainPrivateKey.PublicKey, - lastCommit: CreateBlockCommit(minerChain.Tip)), + lastCommit: CreateBlockCommit(minerChain.Tip), + proof: CreateZeroRoundProof(minerChain.Tip, ChainPrivateKey)), ChainPrivateKey); blocks.Add(block); if (i != 11) @@ -289,8 +298,11 @@ public async Task PreloadWithMaliciousPeer() // Setup initial state where all chains share the same blockchain state. for (int i = 1; i <= initialSharedTipHeight; i++) { + var proposerKey = new PrivateKey(); var block = chainA.ProposeBlock( - new PrivateKey(), TestUtils.CreateBlockCommit(chainA.Tip)); + proposerKey, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, proposerKey)); chainA.Append(block, TestUtils.CreateBlockCommit(block)); chainB.Append(block, TestUtils.CreateBlockCommit(block)); chainC.Append(block, TestUtils.CreateBlockCommit(block)); @@ -299,14 +311,20 @@ public async Task PreloadWithMaliciousPeer() // Setup malicious node to broadcast. for (int i = initialSharedTipHeight + 1; i < maliciousTipHeight; i++) { + var proposerKey = new PrivateKey(); var block = chainB.ProposeBlock( - new PrivateKey(), TestUtils.CreateBlockCommit(chainB.Tip)); + proposerKey, + CreateBlockCommit(chainB.Tip), + CreateZeroRoundProof(chainB.Tip, proposerKey)); chainB.Append(block, TestUtils.CreateBlockCommit(block)); chainC.Append(block, TestUtils.CreateBlockCommit(block)); } + var specialPK = new PrivateKey(); var specialBlock = chainB.ProposeBlock( - new PrivateKey(), TestUtils.CreateBlockCommit(chainB.Tip)); + specialPK, + CreateBlockCommit(chainB.Tip), + CreateZeroRoundProof(chainB.Tip, specialPK)); var invalidBlockCommit = new BlockCommit( maliciousTipHeight, 0, @@ -327,8 +345,11 @@ public async Task PreloadWithMaliciousPeer() // Setup honest node with higher tip for (int i = maliciousTipHeight + 1; i <= honestTipHeight; i++) { + var privateKey = new PrivateKey(); var block = chainC.ProposeBlock( - new PrivateKey(), TestUtils.CreateBlockCommit(chainC.Tip)); + privateKey, + CreateBlockCommit(chainC.Tip), + CreateZeroRoundProof(chainC.Tip, privateKey)); chainC.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -415,7 +436,9 @@ public async Task NoRenderInPreload() sender.BlockChain.MakeTransaction( privKey, new[] { DumbAction.Create((addr, item)) }); Block block = sender.BlockChain.ProposeBlock( - senderKey, CreateBlockCommit(sender.BlockChain.Tip)); + senderKey, + CreateBlockCommit(sender.BlockChain.Tip), + CreateZeroRoundProof(sender.BlockChain.Tip, senderKey)); sender.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -456,9 +479,11 @@ public async Task PreloadWithFailedActions() foreach (var unused in Enumerable.Range(0, 10)) { - Block block = minerSwarm.BlockChain.ProposeBlock( - minerKey, CreateBlockCommit(minerSwarm.BlockChain.Tip)); - minerSwarm.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); + Block block = minerChain.ProposeBlock( + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); + minerChain.Append(block, TestUtils.CreateBlockCommit(block)); } try @@ -484,8 +509,9 @@ public async Task PreloadWithFailedActions() ChainPrivateKey, new[] { tx }.ToImmutableList(), CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, ChainPrivateKey), ImmutableArray.Empty); - minerSwarm.BlockChain.Append(block, CreateBlockCommit(block), true); + minerChain.Append(block, CreateBlockCommit(block), true); await receiverSwarm.PreloadAsync(); @@ -535,7 +561,9 @@ public async Task PreloadFromNominer() foreach (int i in Enumerable.Range(0, 10)) { Block block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, CreateBlockCommit(block)); } @@ -650,7 +678,9 @@ public async Task PreloadRetryWithNextPeers(int blockCount) for (int i = 0; i < blockCount; ++i) { var block = swarm0.BlockChain.ProposeBlock( - key0, CreateBlockCommit(swarm0.BlockChain.Tip)); + key0, + CreateBlockCommit(swarm0.BlockChain.Tip), + CreateZeroRoundProof(swarm0.BlockChain.Tip, key0)); swarm0.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); swarm1.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -839,11 +869,12 @@ public async Task PreloadAfterReorg() BlockChain minerChain = minerSwarm.BlockChain; BlockChain receiverChain = receiverSwarm.BlockChain; - foreach (int i in Enumerable.Range(0, 25)) { Block block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, CreateBlockCommit(block)); receiverChain.Append(block, CreateBlockCommit(block)); } @@ -852,7 +883,9 @@ public async Task PreloadAfterReorg() foreach (int i in Enumerable.Range(0, 20)) { Block block = receiverForked.ProposeBlock( - minerKey, CreateBlockCommit(receiverForked.Tip)); + minerKey, + CreateBlockCommit(receiverForked.Tip), + CreateZeroRoundProof(receiverForked.Tip, minerKey)); receiverForked.Append(block, CreateBlockCommit(block)); } @@ -861,7 +894,9 @@ public async Task PreloadAfterReorg() foreach (int i in Enumerable.Range(0, 1)) { Block block = minerChain.ProposeBlock( - minerKey, CreateBlockCommit(minerChain.Tip)); + minerKey, + CreateBlockCommit(minerChain.Tip), + CreateZeroRoundProof(minerChain.Tip, minerKey)); minerChain.Append(block, CreateBlockCommit(block)); } @@ -905,7 +940,9 @@ public async Task GetDemandBlockHashesDuringReorg() while (forked.Count <= minerChain.Count + 1) { Block block = forked.ProposeBlock( - minerKey, CreateBlockCommit(forked.Tip)); + minerKey, + CreateBlockCommit(forked.Tip), + CreateZeroRoundProof(forked.Tip, minerKey)); forked.Append(block, CreateBlockCommit(block)); } @@ -988,14 +1025,20 @@ public async Task PreloadFromTheHighestTipIndexChain() BlockChain receiverChain = receiverSwarm.BlockChain; Block block1 = minerChain1.ProposeBlock( - minerKey1, CreateBlockCommit(minerChain1.Tip)); + minerKey1, + CreateBlockCommit(minerChain1.Tip), + CreateZeroRoundProof(minerChain1.Tip, minerKey1)); minerChain1.Append(block1, CreateBlockCommit(block1)); Block block2 = minerChain1.ProposeBlock( - minerKey1, CreateBlockCommit(minerChain1.Tip)); + minerKey1, + CreateBlockCommit(minerChain1.Tip), + CreateZeroRoundProof(minerChain1.Tip, minerKey1)); minerChain1.Append(block2, CreateBlockCommit(block2)); Block block = minerChain2.ProposeBlock( - ChainPrivateKey, CreateBlockCommit(minerChain2.Tip)); + ChainPrivateKey, + CreateBlockCommit(minerChain2.Tip), + CreateZeroRoundProof(minerChain2.Tip, ChainPrivateKey)); minerChain2.Append(block, CreateBlockCommit(block)); Assert.True(minerChain1.Count > minerChain2.Count); @@ -1059,14 +1102,18 @@ public async Task PreloadIgnorePeerWithDifferentGenesisBlock() for (int i = 0; i < 10; i++) { Block block = validSeedChain.ProposeBlock( - key1, CreateBlockCommit(validSeedChain.Tip)); + key1, + CreateBlockCommit(validSeedChain.Tip), + CreateZeroRoundProof(validSeedChain.Tip, key1)); validSeedChain.Append(block, CreateBlockCommit(block)); } for (int i = 0; i < 20; i++) { Block block = invalidSeedChain.ProposeBlock( - key1, CreateBlockCommit(invalidSeedChain.Tip)); + key1, + CreateBlockCommit(invalidSeedChain.Tip), + CreateZeroRoundProof(invalidSeedChain.Tip, key1)); invalidSeedChain.Append(block, CreateBlockCommit(block)); } @@ -1113,7 +1160,9 @@ public async Task ActionExecutionWithBranchpoint() for (int i = 0; i < 10; i++) { var block = seedChain.ProposeBlock( - seedKey, CreateBlockCommit(seedChain.Tip)); + seedKey, + CreateBlockCommit(seedChain.Tip), + CreateZeroRoundProof(seedChain.Tip, seedKey)); seedChain.Append(block, TestUtils.CreateBlockCommit(block)); receiverChain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -1122,7 +1171,9 @@ public async Task ActionExecutionWithBranchpoint() for (int i = 0; i < 10; i++) { Block block = forked.ProposeBlock( - seedKey, CreateBlockCommit(forked.Tip)); + seedKey, + CreateBlockCommit(forked.Tip), + CreateZeroRoundProof(forked.Tip, seedKey)); forked.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -1183,7 +1234,9 @@ public async Task UpdateTxExecution() DumbAction.Create((default, $"Item{i}")), }); Block block = seedChain.ProposeBlock( - seedKey, CreateBlockCommit(seedChain.Tip)); + seedKey, + CreateBlockCommit(seedChain.Tip), + CreateZeroRoundProof(seedChain.Tip, seedKey)); seedChain.Append(block, TestUtils.CreateBlockCommit(block)); transactions.Add(transaction); } diff --git a/test/Libplanet.Net.Tests/SwarmTest.cs b/test/Libplanet.Net.Tests/SwarmTest.cs index 184fe796c81..8192bebcce7 100644 --- a/test/Libplanet.Net.Tests/SwarmTest.cs +++ b/test/Libplanet.Net.Tests/SwarmTest.cs @@ -430,12 +430,11 @@ public async Task BootstrapContext() try { - // swarms[1] is the round 0 proposer for height 1. - // swarms[2] is the round 1 proposer for height 2. _ = swarms[0].StartAsync(); - _ = swarms[3].StartAsync(); + _ = swarms[1].StartAsync(); - swarms[0].ConsensusReactor.ConsensusContext.StateChanged += (_, eventArgs) => + swarms[0].ConsensusReactor.ConsensusContext.StateChanged + += (_, eventArgs) => { if (eventArgs.VoteCount == 2) { @@ -443,13 +442,13 @@ public async Task BootstrapContext() } }; - // Make sure both swarms time out and swarm[0] collects two PreVotes. + // Make sure both swarms time out and non-proposer swarm[0] collects two PreVotes. await collectedTwoMessages[0].WaitAsync(); - // Dispose swarm[3] to simulate shutdown during bootstrap. - swarms[3].Dispose(); + // Dispose non-proposer swarm[1] to simulate shutdown during bootstrap. + swarms[1].Dispose(); - // Bring swarm[2] online. + // Bring non-proposer swarm[2] online. _ = swarms[2].StartAsync(); swarms[0].ConsensusReactor.ConsensusContext.StateChanged += (_, eventArgs) => { @@ -466,38 +465,30 @@ public async Task BootstrapContext() } }; - // Since we already have swarm[3]'s PreVote, when swarm[2] times out, - // swarm[2] adds additional PreVote, making it possible to reach PreCommit. + // Since we already have non-proposer swarm[1]'s PreVote, + // when non-proposer swarm[2] times out, + // non-proposer swarm[0] adds additional PreVote, + // making it possible to reach PreCommit. // Current network's context state should be: // Proposal: null - // PreVote: swarm[0], swarm[2], swarm[3], - // PreCommit: swarm[0], swarm[2] - await Task.WhenAll( - stepChangedToPreCommits[0].WaitAsync(), stepChangedToPreCommits[2].WaitAsync()); + // PreVote: non-proposer swarm[0],[1],[2], + // PreCommit: non-proposer swarm[0],[2] + await Task.Delay(1500); - // After swarm[1] comes online, eventually it'll catch up to vote PreCommit, - // at which point the round will move to 1 where swarm[2] is the proposer. - _ = swarms[1].StartAsync(); - swarms[2].ConsensusReactor.ConsensusContext.MessagePublished += (_, eventArgs) => - { - if (eventArgs.Message is ConsensusProposalMsg proposalMsg && - proposalMsg.Round == 1 && - proposalMsg.ValidatorPublicKey.Equals(TestUtils.PrivateKeys[2].PublicKey)) - { - roundOneProposed.Set(); - } - }; + await Task.WhenAll( + stepChangedToPreCommits[0].WaitAsync(), + stepChangedToPreCommits[2].WaitAsync()); - await roundOneProposed.WaitAsync(); + _ = swarms[3].StartAsync(); - await AssertThatEventually(() => swarms[0].BlockChain.Tip.Index == 1, int.MaxValue); - Assert.Equal(1, swarms[0].BlockChain.GetBlockCommit(1).Round); + await AssertThatEventually( + () => swarms[0].BlockChain.Tip.Index == 1, int.MaxValue); } finally { CleaningSwarm(swarms[0]); - CleaningSwarm(swarms[1]); CleaningSwarm(swarms[2]); + CleaningSwarm(swarms[3]); } } @@ -514,9 +505,14 @@ public async Task GetBlocks() BlockChain chainA = swarmA.BlockChain; - Block block1 = chainA.ProposeBlock(keyA); + Block block1 = chainA.ProposeBlock( + keyA, + proof: CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block1, TestUtils.CreateBlockCommit(block1)); - Block block2 = chainA.ProposeBlock(keyA, CreateBlockCommit(block1)); + Block block2 = chainA.ProposeBlock( + keyA, + CreateBlockCommit(block1), + CreateZeroRoundProof(block1, keyA)); chainA.Append(block2, TestUtils.CreateBlockCommit(block2)); try @@ -584,10 +580,14 @@ public async Task GetMultipleBlocksAtOnce() BlockChain chainB = swarmB.BlockChain; Block block1 = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block1, TestUtils.CreateBlockCommit(block1)); Block block2 = chainA.ProposeBlock( - keyA, CreateBlockCommit(chainA.Tip)); + keyA, + CreateBlockCommit(chainA.Tip), + CreateZeroRoundProof(chainA.Tip, keyA)); chainA.Append(block2, TestUtils.CreateBlockCommit(block2)); try @@ -649,7 +649,9 @@ public async Task GetTx() Array.Empty().ToPlainValues() ); chainB.StageTransaction(tx); - Block block = chainB.ProposeBlock(keyB); + Block block = chainB.ProposeBlock( + keyB, + proof: CreateZeroRoundProof(chainB.Tip, keyB)); chainB.Append(block, TestUtils.CreateBlockCommit(block)); try @@ -834,7 +836,9 @@ async Task MineAndBroadcast(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { - var block = seed.BlockChain.ProposeBlock(seedKey); + var block = seed.BlockChain.ProposeBlock( + seedKey, + proof: CreateZeroRoundProof(seed.BlockChain.Tip, seedKey)); seed.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); seed.BroadcastBlock(block); await Task.Delay(1000, cancellationToken); @@ -911,17 +915,23 @@ public async Task RenderInFork() miner1.BlockChain.MakeTransaction(privKey, new[] { DumbAction.Create((addr, item)) }); Block block1 = miner1.BlockChain.ProposeBlock( - key1, CreateBlockCommit(miner1.BlockChain.Tip)); + key1, + CreateBlockCommit(miner1.BlockChain.Tip), + CreateZeroRoundProof(miner1.BlockChain.Tip, key1)); miner1.BlockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); miner2.BlockChain.MakeTransaction(privKey, new[] { DumbAction.Create((addr, item)) }); Block block2 = miner2.BlockChain.ProposeBlock( - key2, CreateBlockCommit(miner2.BlockChain.Tip)); + key2, + CreateBlockCommit(miner2.BlockChain.Tip), + CreateZeroRoundProof(miner2.BlockChain.Tip, key2)); miner2.BlockChain.Append(block2, TestUtils.CreateBlockCommit(block2)); miner2.BlockChain.MakeTransaction(privKey, new[] { DumbAction.Create((addr, item)) }); var latest = miner2.BlockChain.ProposeBlock( - key2, CreateBlockCommit(miner2.BlockChain.Tip)); + key2, + CreateBlockCommit(miner2.BlockChain.Tip), + CreateZeroRoundProof(miner2.BlockChain.Tip, key2)); miner2.BlockChain.Append(latest, TestUtils.CreateBlockCommit(latest)); renderer.RenderEventHandler += (_, a) => @@ -985,7 +995,8 @@ await CreateSwarm( CreateBlockCommit( miner1.BlockChain.Tip.Hash, miner1.BlockChain.Tip.Index, - 0)); + 0), + CreateZeroRoundProof(miner1.BlockChain.Tip, key1)); miner1.BlockChain.Append(b, TestUtils.CreateBlockCommit(b)); miner2.BlockChain.Append(b, TestUtils.CreateBlockCommit(b)); } @@ -999,11 +1010,17 @@ await CreateSwarm( await BootstrapAsync(receiver, miner1.AsPeer); var t = receiver.PreloadAsync(); - Block block1 = miner1.BlockChain.ProposeBlock(key1); + Block block1 = miner1.BlockChain.ProposeBlock( + key1, + proof: CreateZeroRoundProof(miner1.BlockChain.Tip, key1)); miner1.BlockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); - Block block2 = miner2.BlockChain.ProposeBlock(key1); + Block block2 = miner2.BlockChain.ProposeBlock( + key1, + proof: CreateZeroRoundProof(miner2.BlockChain.Tip, key1)); miner2.BlockChain.Append(block2, TestUtils.CreateBlockCommit(block2)); - Block latest = miner2.BlockChain.ProposeBlock(key2); + Block latest = miner2.BlockChain.ProposeBlock( + key2, + proof: CreateZeroRoundProof(miner2.BlockChain.Tip, key2)); miner2.BlockChain.Append(latest, TestUtils.CreateBlockCommit(latest)); miner2.BroadcastBlock(latest); await t; @@ -1055,13 +1072,19 @@ public async void RestageTransactionsOnceLocallyMinedAfterReorg(bool restage) Log.Debug("Make minerB's chain longer than minerA's chain"); Block blockA = minerA.BlockChain.ProposeBlock( - keyA, CreateBlockCommit(minerA.BlockChain.Tip)); + keyA, + CreateBlockCommit(minerA.BlockChain.Tip), + CreateZeroRoundProof(minerA.BlockChain.Tip, keyA)); minerA.BlockChain.Append(blockA, TestUtils.CreateBlockCommit(blockA)); Block blockB = minerB.BlockChain.ProposeBlock( - keyB, CreateBlockCommit(minerB.BlockChain.Tip)); + keyB, + CreateBlockCommit(minerB.BlockChain.Tip), + CreateZeroRoundProof(minerB.BlockChain.Tip, keyB)); minerB.BlockChain.Append(blockB, TestUtils.CreateBlockCommit(blockB)); Block blockC = minerB.BlockChain.ProposeBlock( - keyB, CreateBlockCommit(minerB.BlockChain.Tip)); + keyB, + CreateBlockCommit(minerB.BlockChain.Tip), + CreateZeroRoundProof(minerB.BlockChain.Tip, keyB)); minerB.BlockChain.Append(blockC, TestUtils.CreateBlockCommit(blockC)); Assert.Equal( @@ -1107,7 +1130,9 @@ public async void RestageTransactionsOnceLocallyMinedAfterReorg(bool restage) minerA.BlockChain.GetStagedTransactionIds().Contains(txA.Id)); Block block = minerA.BlockChain.ProposeBlock( - keyA, CreateBlockCommit(minerA.BlockChain.Tip)); + keyA, + CreateBlockCommit(minerA.BlockChain.Tip), + CreateZeroRoundProof(minerA.BlockChain.Tip, keyA)); minerA.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); minerA.BroadcastBlock(minerA.BlockChain.Tip); await minerB.BlockAppended.WaitAsync(); @@ -1289,26 +1314,31 @@ public async Task CreateNewChainWhenBranchPointNotExist() Block aBlock1 = ProposeNextBlock( genesis, keyA, - stateRootHash: nextSrh); + stateRootHash: nextSrh, + proof: CreateZeroRoundProof(genesis, keyA)); Block aBlock2 = ProposeNextBlock( aBlock1, keyA, stateRootHash: nextSrh, - lastCommit: CreateBlockCommit(aBlock1)); + lastCommit: CreateBlockCommit(aBlock1), + proof: CreateZeroRoundProof(aBlock1, keyA)); Block aBlock3 = ProposeNextBlock( aBlock2, keyA, stateRootHash: nextSrh, - lastCommit: CreateBlockCommit(aBlock2)); + lastCommit: CreateBlockCommit(aBlock2), + proof: CreateZeroRoundProof(aBlock2, keyA)); Block bBlock1 = ProposeNextBlock( genesis, keyB, - stateRootHash: nextSrh); + stateRootHash: nextSrh, + proof: CreateZeroRoundProof(genesis, keyB)); Block bBlock2 = ProposeNextBlock( bBlock1, keyB, stateRootHash: nextSrh, - lastCommit: CreateBlockCommit(bBlock1)); + lastCommit: CreateBlockCommit(bBlock1), + proof: CreateZeroRoundProof(bBlock1, keyB)); policyA.BlockedMiners.Add(keyB.Address); policyB.BlockedMiners.Add(keyA.Address); @@ -1437,7 +1467,9 @@ public async Task DoNotReceiveBlockFromNodeHavingDifferenceGenesisBlock() await swarmB.AddPeersAsync(new[] { swarmA.AsPeer }, null); await swarmC.AddPeersAsync(new[] { swarmA.AsPeer }, null); - var block = swarmA.BlockChain.ProposeBlock(privateKeyA); + var block = swarmA.BlockChain.ProposeBlock( + privateKeyA, + proof: CreateZeroRoundProof(swarmA.BlockChain.Tip, privateKeyA)); swarmA.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); Task.WaitAll(new[] @@ -1627,7 +1659,9 @@ public async Task DoNotFillWhenGetAllBlockAtFirstTimeFromSender() for (int i = 0; i < 6; i++) { Block block = chain.ProposeBlock( - ChainPrivateKey, TestUtils.CreateBlockCommit(chain.Tip)); + ChainPrivateKey, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, ChainPrivateKey)); chain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -1667,7 +1701,9 @@ public async Task FillWhenGetAChunkOfBlocksFromSender() for (int i = 0; i < 6; i++) { Block block = chain.ProposeBlock( - ChainPrivateKey, TestUtils.CreateBlockCommit(chain.Tip)); + ChainPrivateKey, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, ChainPrivateKey)); chain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -1708,7 +1744,9 @@ public async Task FillWhenGetAllBlocksFromSender() for (int i = 0; i < 6; i++) { Block block = chain.ProposeBlock( - ChainPrivateKey, CreateBlockCommit(chain.Tip)); + ChainPrivateKey, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, ChainPrivateKey)); chain.Append(block, TestUtils.CreateBlockCommit(block)); } @@ -1782,7 +1820,9 @@ public async Task GetPeerChainStateAsync() peerChainState.First() ); - Block block = swarm2.BlockChain.ProposeBlock(key2); + Block block = swarm2.BlockChain.ProposeBlock( + key2, + proof: CreateZeroRoundProof(swarm2.BlockChain.Tip, key2)); swarm2.BlockChain.Append(block, TestUtils.CreateBlockCommit(block)); peerChainState = await swarm1.GetPeerChainStateAsync( TimeSpan.FromSeconds(1), default); diff --git a/test/Libplanet.Net.Tests/TestUtils.cs b/test/Libplanet.Net.Tests/TestUtils.cs index 3659ed28284..94e9bb7ddc6 100644 --- a/test/Libplanet.Net.Tests/TestUtils.cs +++ b/test/Libplanet.Net.Tests/TestUtils.cs @@ -63,6 +63,64 @@ public static class TestUtils private static readonly Random Random = new Random(); + public static List GetProposers(IEnumerable<(long Height, int Round)> sequence) + { + List proposers = new List(); + (long Height, int Round, Proof? Proof) prev = (-1, 0, null); + foreach (var consensus in sequence) + { + if ((prev.Height < 0 || prev.Round < 0 || prev.Proof is null) && + (consensus.Height != 0 || consensus.Round != 0)) + { + throw new ArgumentException("First item of sequence have to be (0L, 0)."); + } + + if (prev.Height + prev.Round + 1 != consensus.Height + consensus.Round) + { + throw new ArgumentException( + "Height + Round of sequence have to be gradually increased by 1."); + } + + var result = GetProposer(consensus.Height, consensus.Round, prev.Proof); + prev = (consensus.Height, consensus.Round, result.Proof); + proposers.Add(result.Proposer); + } + + return proposers; + } + + public static (PrivateKey Proposer, Proof Proof) GetProposer( + long height, + int round, + Proof? lastProof) + => GetProposer(height, round, lastProof, PrivateKeys, ValidatorSet); + + public static (PrivateKey Proposer, Proof Proof) GetProposer( + long height, + int round, + Proof? lastProof, + IReadOnlyList privateKeys, + ValidatorSet validatorSet) + { + var lotSet = new LotSet(height, round, lastProof, validatorSet, 20); + foreach (var privateKey in privateKeys) + { + lotSet.AddLot(lotSet.ConsensusInformation.ToLot(privateKey)); + } + + return (privateKeys.First( + pk => pk.PublicKey.Equals(lotSet.DominantLot!.PublicKey)), + lotSet.DominantLot!.Proof); + } + + public static DominantLot CreateDominantLot( + PrivateKey privateKey, + Lot lot) => + new DominantLotMetadata( + lot, + DateTimeOffset.Now, + privateKey.PublicKey).Sign(privateKey); + public static Vote CreateVote( PrivateKey privateKey, BigInteger power, @@ -134,6 +192,9 @@ public static BlockCommit CreateBlockCommit(Block block) => public static BlockCommit CreateBlockCommit(BlockHash blockHash, long height, int round) => Libplanet.Tests.TestUtils.CreateBlockCommit(blockHash, height, round); + public static Proof CreateZeroRoundProof(Block tip, PrivateKey proposerKey) => + Libplanet.Tests.TestUtils.CreateZeroRoundProof(tip, proposerKey); + public static void HandleFourPeersPreCommitMessages( ConsensusContext consensusContext, PrivateKey nodePrivateKey, diff --git a/test/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs b/test/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs index 19edd2969bd..b9af6c3168b 100644 --- a/test/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs +++ b/test/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs @@ -311,7 +311,8 @@ public void MigrateThroughBlockWorldState() Assert.Equal(0, chain.GetWorldState().Version); // A block that doesn't touch any state does not migrate its state. - var block2 = chain.ProposeBlock(miner, blockCommit); + var block2 = chain.ProposeBlock( + miner, blockCommit, CreateZeroRoundProof(chain.Tip, miner)); blockCommit = CreateBlockCommit(block2); chain.Append(block2, blockCommit); Assert.Equal(0, chain.GetWorldState().Version); @@ -329,7 +330,8 @@ public void MigrateThroughBlockWorldState() actions: new[] { action }.ToPlainValues()); chain.StageTransaction(tx); - var block3 = chain.ProposeBlock(miner, blockCommit); + var block3 = chain.ProposeBlock( + miner, blockCommit, CreateZeroRoundProof(chain.Tip, miner)); chain.Append(block3, CreateBlockCommit(block3)); Assert.Equal(BlockMetadata.CurrentProtocolVersion, chain.GetNextWorldState().Version); var accountStateRoot = stateStore.GetStateRoot(chain.GetNextStateRootHash(block3.Hash)) @@ -365,7 +367,8 @@ public void MigrateThroughBlockCurrencyAccount() Assert.Equal(0, chain.GetWorldState().Version); // A block that doesn't touch any state does not migrate its state. - var block2 = chain.ProposeBlock(miner, blockCommit); + var block2 = chain.ProposeBlock( + miner, blockCommit, CreateZeroRoundProof(chain.Tip, miner)); blockCommit = CreateBlockCommit(block2); chain.Append(block2, blockCommit); Assert.Equal(0, chain.GetWorldState().Version); @@ -382,7 +385,8 @@ public void MigrateThroughBlockCurrencyAccount() actions: new[] { action }.ToPlainValues()); chain.StageTransaction(tx); - var block3 = chain.ProposeBlock(miner, blockCommit); + var block3 = chain.ProposeBlock( + miner, blockCommit, CreateZeroRoundProof(chain.Tip, miner)); chain.Append(block3, CreateBlockCommit(block3)); Assert.Equal(BlockMetadata.CurrentProtocolVersion, chain.GetNextWorldState().Version); diff --git a/test/Libplanet.Tests/Action/ActionEvaluatorTest.cs b/test/Libplanet.Tests/Action/ActionEvaluatorTest.cs index 6a465c66a5e..f732b508fd3 100644 --- a/test/Libplanet.Tests/Action/ActionEvaluatorTest.cs +++ b/test/Libplanet.Tests/Action/ActionEvaluatorTest.cs @@ -10,6 +10,7 @@ using Libplanet.Action.State; using Libplanet.Action.Tests.Common; using Libplanet.Blockchain.Policies; +using Libplanet.Consensus; using Libplanet.Crypto; using Libplanet.Mocks; using Libplanet.Store; @@ -95,7 +96,7 @@ public void Idempotent() }; var evs = Array.Empty(); var stateStore = new TrieStateStore(new MemoryKeyValueStore()); - var noStateRootBlock = new BlockContent( + var noStateRootBlockWithoutProof = new BlockContent( new BlockMetadata( protocolVersion: Block.CurrentProtocolVersion, index: 0, @@ -105,6 +106,7 @@ public void Idempotent() previousHash: null, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: null, evidenceHash: null), transactions: txs, evidence: evs).Propose(); @@ -112,32 +114,90 @@ public void Idempotent() new PolicyActionsRegistry(), stateStore, new SingleActionLoader(typeof(ContextRecordingAction))); - Block stateRootBlock = noStateRootBlock.Sign( + Block stateRootBlockWithoutProof = noStateRootBlockWithoutProof.Sign( GenesisProposer, MerkleTrie.EmptyRootHash); - var generatedRandomNumbers = new List(); + var generatedRandomNumbersWithoutProof = new List(); - AssertPreEvaluationBlocksEqual(stateRootBlock, noStateRootBlock); + AssertPreEvaluationBlocksEqual( + stateRootBlockWithoutProof, noStateRootBlockWithoutProof); for (int i = 0; i < repeatCount; ++i) { - var actionEvaluations = actionEvaluator.Evaluate(noStateRootBlock, null); - generatedRandomNumbers.Add( + var actionEvaluations = actionEvaluator.Evaluate( + noStateRootBlockWithoutProof, null); + generatedRandomNumbersWithoutProof.Add( (Integer)new WorldBaseState( stateStore.GetStateRoot(actionEvaluations[0].OutputState), stateStore) .GetAccountState(ReservedAddresses.LegacyAccount) .GetState(ContextRecordingAction.RandomRecordAddress)); - actionEvaluations = actionEvaluator.Evaluate(stateRootBlock, null); - generatedRandomNumbers.Add( + actionEvaluations = actionEvaluator.Evaluate(stateRootBlockWithoutProof, null); + generatedRandomNumbersWithoutProof.Add( (Integer)new WorldBaseState( stateStore.GetStateRoot(actionEvaluations[0].OutputState), stateStore) .GetAccountState(ReservedAddresses.LegacyAccount) .GetState(ContextRecordingAction.RandomRecordAddress)); } - for (int i = 1; i < generatedRandomNumbers.Count; ++i) + for (int i = 1; i < generatedRandomNumbersWithoutProof.Count; ++i) { - Assert.Equal(generatedRandomNumbers[0], generatedRandomNumbers[i]); + Assert.Equal( + generatedRandomNumbersWithoutProof[0], + generatedRandomNumbersWithoutProof[i]); + } + + var noStateRootBlockWithProof = new BlockContent( + new BlockMetadata( + protocolVersion: Block.CurrentProtocolVersion, + index: 0, + timestamp: timestamp, + miner: GenesisProposer.Address, + publicKey: GenesisProposer.PublicKey, + previousHash: null, + txHash: BlockContent.DeriveTxHash(txs), + lastCommit: null, + proof: new ConsensusInformation(0, 0, null).Prove(GenesisProposer), + evidenceHash: null), + transactions: txs, + evidence: evs).Propose(); + + // Since there is no static method determine state root hash of common block, + // used method for genesis block instead. + var stateRootBlockWithProof = noStateRootBlockWithProof.Sign( + GenesisProposer, + MerkleTrie.EmptyRootHash); + var generatedRandomNumbersWithProof = new List(); + + AssertPreEvaluationBlocksEqual(stateRootBlockWithProof, noStateRootBlockWithProof); + + for (int i = 0; i < repeatCount; ++i) + { + var actionEvaluations = actionEvaluator.Evaluate(noStateRootBlockWithProof, null); + generatedRandomNumbersWithProof.Add( + (Integer)new WorldBaseState( + stateStore.GetStateRoot(actionEvaluations[0].OutputState), stateStore) + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(ContextRecordingAction.RandomRecordAddress)); + actionEvaluations = actionEvaluator.Evaluate(stateRootBlockWithProof, null); + generatedRandomNumbersWithProof.Add( + (Integer)new WorldBaseState( + stateStore.GetStateRoot(actionEvaluations[0].OutputState), stateStore) + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(ContextRecordingAction.RandomRecordAddress)); + } + + for (int i = 1; i < generatedRandomNumbersWithProof.Count; ++i) + { + Assert.Equal( + generatedRandomNumbersWithProof[0], + generatedRandomNumbersWithProof[i]); + } + + for (int i = 0; i < repeatCount * 2; i++) + { + Assert.NotEqual( + generatedRandomNumbersWithoutProof[i], + generatedRandomNumbersWithProof[i]); } } @@ -164,11 +224,13 @@ public void Evaluate() chain.StageTransaction(tx); var miner = new PrivateKey(); - Block block = chain.ProposeBlock(miner); + Block block = chain.ProposeBlock( + miner, + proof: CreateZeroRoundProof(chain.Tip, miner)); chain.Append(block, CreateBlockCommit(block)); var evaluations = chain.ActionEvaluator.Evaluate( - chain.Tip, chain.Store.GetStateRootHash(chain.Tip.PreviousHash)); + chain.Tip, chain.Store.GetStateRootHash(chain.Tip.Hash)); Assert.False(evaluations[0].InputContext.IsPolicyAction); Assert.Single(evaluations); @@ -247,6 +309,7 @@ public void EvaluateWithPolicyActions() proposer: GenesisProposer, transactions: txs.ToImmutableList(), lastCommit: CreateBlockCommit(chain.Tip), + proof: CreateZeroRoundProof(chain.Tip, GenesisProposer), evidence: ImmutableArray.Empty); var evaluations = actionEvaluator.Evaluate( block, chain.Store.GetStateRootHash(chain.Tip.Hash)).ToArray(); @@ -305,10 +368,13 @@ public void EvaluateWithException() actions: new[] { action }.ToPlainValues()); chain.StageTransaction(tx); - Block block = chain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = chain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(chain.Tip, proposer)); chain.Append(block, CreateBlockCommit(block)); var evaluations = chain.ActionEvaluator.Evaluate( - chain.Tip, chain.Store.GetStateRootHash(chain.Tip.PreviousHash)); + chain.Tip, chain.Store.GetStateRootHash(chain.Tip.Hash)); Assert.False(evaluations[0].InputContext.IsPolicyAction); Assert.Single(evaluations); @@ -349,18 +415,20 @@ public void EvaluateWithCriticalException() actions: new[] { action }.ToPlainValues()); var txs = new Transaction[] { tx }; var evs = Array.Empty(); + var proposer = new PrivateKey(); PreEvaluationBlock block = new BlockContent( new BlockMetadata( index: 1L, timestamp: DateTimeOffset.UtcNow, - publicKey: new PrivateKey().PublicKey, + publicKey: proposer.PublicKey, previousHash: genesis.Hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: CreateZeroRoundProof(genesis, proposer), evidenceHash: null), transactions: txs, evidence: evs).Propose(); - IWorld previousState = stateStore.GetWorld(genesis.StateRootHash); + IWorld previousState = new World(chain.GetNextWorldState()); Assert.Throws( () => actionEvaluator.EvaluateTx( @@ -369,7 +437,7 @@ public void EvaluateWithCriticalException() previousState: previousState).ToList()); Assert.Throws( () => chain.ActionEvaluator.Evaluate( - block, chain.Store.GetStateRootHash(block.PreviousHash)).ToList()); + block, previousState.Trie.Hash).ToList()); } [Fact] @@ -463,21 +531,45 @@ DumbAction MakeAction(Address address, char identifier, Address? transferTo = nu _logger.Debug("{0}[{1}] = {2}", nameof(block1Txs), i, tx.Id); } + var block1Srh = actionEvaluator.Evaluate( + genesis, genesis.StateRootHash).Last().OutputState; + Block block1 = ProposeNextBlock( genesis, GenesisProposer, - block1Txs); - IWorld previousState = stateStore.GetWorld(genesis.StateRootHash); + block1Txs, + stateRootHash: block1Srh, + proof: CreateZeroRoundProof(genesis, GenesisProposer)); + IWorld previousState = stateStore.GetWorld(block1.StateRootHash); var evals = actionEvaluator.EvaluateBlock( block1, previousState).ToImmutableArray(); + + // Logging for fixing test expectations. + foreach (var eval in evals) + { + int txIdx = block1Txs.Select( + (e, idx) => new { e, idx }).First( + x => x.e.Id.Equals(eval.InputContext.TxId)).idx; + int actionIdx = block1Txs[txIdx].Actions.Select( + (e, idx) => new { e, idx }).First( + x => x.e.Equals(eval.Action.PlainValue)).idx; + string updatedStates = "new[] { " + string.Join(", ", addresses.Select( + eval.OutputState.GetAccount(ReservedAddresses.LegacyAccount).GetState) + .Select(x => x is Text t ? '"' + t.Value + '"' : "null")) + " }"; + string signerIdx = "_txFx.Address" + (addresses.Select( + (e, idx) => new { e, idx }).First( + x => x.e.Equals(eval.InputContext.Signer)).idx + 1); + _logger.Information($"({txIdx}, {actionIdx}, {updatedStates}, {signerIdx})"); + } + // Once the BlockMetadata.CurrentProtocolVersion gets bumped, expectations may also // have to be updated, since the order may change due to different PreEvaluationHash. (int TxIdx, int ActionIdx, string[] UpdatedStates, Address Signer)[] expectations = { - (0, 0, new[] { "A", null, null, null, null }, _txFx.Address1), // Adds "C" - (0, 1, new[] { "A", "B", null, null, null }, _txFx.Address1), // Adds "A" - (1, 0, new[] { "A", "B", "C", null, null }, _txFx.Address2), // Adds "B" + (1, 0, new[] { null, null, "C", null, null }, _txFx.Address2), // Adds "C" + (0, 0, new[] { "A", null, "C", null, null }, _txFx.Address1), // Adds "A" + (0, 1, new[] { "A", "B", "C", null, null }, _txFx.Address1), // Adds "B" }; Assert.Equal(expectations.Length, evals.Length); foreach (var (expect, eval) in expectations.Zip(evals, (x, y) => (x, y))) @@ -496,10 +588,7 @@ DumbAction MakeAction(Address address, char identifier, Address? transferTo = nu Assert.Equal(block1.Index, eval.InputContext.BlockIndex); } - previousState = stateStore.GetWorld(genesis.StateRootHash); - ActionEvaluation[] evals1 = - actionEvaluator.EvaluateBlock(block1, previousState).ToArray(); - var output1 = new WorldBaseState(evals1.Last().OutputState.Trie, stateStore); + var output1 = new WorldBaseState(evals.Last().OutputState.Trie, stateStore); Assert.Equal( (Text)"A", output1.GetAccountState(ReservedAddresses.LegacyAccount).GetState(addresses[0])); @@ -584,21 +673,40 @@ DumbAction MakeAction(Address address, char identifier, Address? transferTo = nu block1, GenesisProposer, block2Txs, + stateRootHash: evals.Last().OutputState.Trie.Hash, lastCommit: CreateBlockCommit(block1, true)); // Forcefully reset to null delta - previousState = evals1.Last().OutputState; + previousState = evals.Last().OutputState; evals = actionEvaluator.EvaluateBlock( block2, previousState).ToImmutableArray(); + // Logging for fixing test expectations. + foreach (var eval in evals) + { + int txIdx = block2Txs.Select( + (e, idx) => new { e, idx }).First( + x => x.e.Id.Equals(eval.InputContext.TxId)).idx; + int actionIdx = block2Txs[txIdx].Actions.Select( + (e, idx) => new { e, idx }).First( + x => x.e.Equals(eval.Action.PlainValue)).idx; + string updatedStates = "new[] { " + string.Join(", ", addresses.Select( + eval.OutputState.GetAccount(ReservedAddresses.LegacyAccount).GetState) + .Select(x => x is Text t ? '"' + t.Value + '"' : "null")) + " }"; + string signerIdx = "_txFx.Address" + (addresses.Select( + (e, idx) => new { e, idx }).First( + x => x.e.Equals(eval.InputContext.Signer)).idx + 1); + _logger.Information($"({txIdx}, {actionIdx}, {updatedStates}, {signerIdx})"); + } + // Once the BlockMetadata.CurrentProtocolVersion gets bumped, expectations may also // have to be updated, since the order may change due to different PreEvaluationHash. expectations = new (int TxIdx, int ActionIdx, string[] UpdatedStates, Address Signer)[] { - (2, 0, new[] { "A", "B", "C", null, "F" }, _txFx.Address3), - (1, 0, new[] { "A", "B", "C", "E", "F" }, _txFx.Address2), - (0, 0, new[] { "A,D", "B", "C", "E", "F" }, _txFx.Address1), + (0, 0, new[] { "A,D", "B", "C", null, null }, _txFx.Address1), + (1, 0, new[] { "A,D", "B", "C", "E", null }, _txFx.Address2), + (2, 0, new[] { "A,D", "B", "C", "E", "F" }, _txFx.Address3), }; Assert.Equal(expectations.Length, evals.Length); foreach (var (expect, eval) in expectations.Zip(evals, (x, y) => (x, y))) @@ -618,9 +726,7 @@ DumbAction MakeAction(Address address, char identifier, Address? transferTo = nu Assert.Null(eval.Exception); } - previousState = evals1.Last().OutputState; - var evals2 = actionEvaluator.EvaluateBlock(block2, previousState).ToArray(); - var output2 = new WorldBaseState(evals2.Last().OutputState.Trie, stateStore); + var output2 = new WorldBaseState(evals.Last().OutputState.Trie, stateStore); Assert.Equal( (Text)"A,D", output2.GetAccountState(ReservedAddresses.LegacyAccount).GetState(addresses[0])); @@ -662,6 +768,7 @@ public void EvaluateTx() previousHash: default(BlockHash), txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: new ConsensusInformation(1L, 0, null).Prove(GenesisProposer), evidenceHash: null), transactions: txs, evidence: evs).Propose(); @@ -779,6 +886,7 @@ public void EvaluateTxResultThrowingException() previousHash: hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: CreateBlockCommit(hash, 122, 0), + proof: new ConsensusInformation(123L, 0, null).Prove(GenesisProposer), evidenceHash: null), transactions: txs, evidence: evs).Propose(); @@ -936,6 +1044,7 @@ public void EvaluatePolicyBeginBlockActions() proposer: GenesisProposer, transactions: txs.ToImmutableList(), lastCommit: CreateBlockCommit(chain.Tip), + proof: CreateZeroRoundProof(chain.Tip, GenesisProposer), evidence: ImmutableArray.Empty); IWorld previousState = _storeFx.StateStore.GetWorld(null); @@ -987,6 +1096,7 @@ public void EvaluatePolicyEndBlockActions() GenesisProposer, txs.ToImmutableList(), CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, GenesisProposer), ImmutableArray.Empty); IWorld previousState = _storeFx.StateStore.GetWorld(null); @@ -1039,6 +1149,7 @@ public void EvaluatePolicyBeginTxActions() proposer: GenesisProposer, transactions: txs.ToImmutableList(), lastCommit: CreateBlockCommit(chain.Tip), + proof: CreateZeroRoundProof(chain.Tip, GenesisProposer), evidence: ImmutableArray.Empty); IWorld previousState = _storeFx.StateStore.GetWorld(null); @@ -1094,6 +1205,7 @@ public void EvaluatePolicyEndTxActions() proposer: GenesisProposer, transactions: txs.ToImmutableList(), lastCommit: CreateBlockCommit(chain.Tip), + proof: CreateZeroRoundProof(chain.Tip, GenesisProposer), evidence: ImmutableArray.Empty); IWorld previousState = _storeFx.StateStore.GetWorld(null); @@ -1279,7 +1391,9 @@ public void EvaluateActionAndCollectFee() chain.StageTransaction(tx); var miner = new PrivateKey(); - Block block = chain.ProposeBlock(miner); + Block block = chain.ProposeBlock( + miner, + proof: CreateZeroRoundProof(chain.Tip, miner)); var evaluations = chain.ActionEvaluator.Evaluate( block, chain.GetNextStateRootHash((BlockHash)block.PreviousHash)); @@ -1348,7 +1462,9 @@ public void EvaluateThrowingExceedGasLimit() chain.StageTransaction(tx); var miner = new PrivateKey(); - Block block = chain.ProposeBlock(miner); + Block block = chain.ProposeBlock( + miner, + proof: CreateZeroRoundProof(chain.Tip, miner)); var evaluations = chain.ActionEvaluator.Evaluate( block, @@ -1420,7 +1536,9 @@ public void EvaluateThrowingInsufficientBalanceForGasFee() chain.StageTransaction(tx); var miner = new PrivateKey(); - Block block = chain.ProposeBlock(miner); + Block block = chain.ProposeBlock( + miner, + proof: CreateZeroRoundProof(chain.Tip, miner)); var evaluations = chain.ActionEvaluator.Evaluate( block, chain.Store.GetStateRootHash(block.PreviousHash)); @@ -1436,6 +1554,29 @@ public void EvaluateThrowingInsufficientBalanceForGasFee() [Fact] public void GenerateRandomSeed() + { + byte[] proofBytes = + { + 0x03, 0x47, 0xfc, 0xcb, 0x9f, 0x8b, 0x62, 0x8c, 0x00, 0x92, + 0x62, 0x7a, 0x7b, 0x91, 0x1a, 0x8e, 0x5b, 0xfb, 0xb4, 0x0b, + 0x5a, 0x25, 0xc1, 0x83, 0xf3, 0x4e, 0x91, 0x51, 0x3b, 0xaa, + 0xbd, 0x11, 0xfd, 0x9f, 0x72, 0xcd, 0x88, 0xac, 0x09, 0xab, + 0xe4, 0x97, 0xdb, 0x2b, 0x5e, 0x05, 0xb2, 0x52, 0x2c, 0x02, + 0xab, 0xd9, 0xb8, 0x5c, 0x62, 0x37, 0xcb, 0x48, 0x54, 0x08, + 0xd4, 0x6a, 0x13, 0x1e, 0xc1, 0xcd, 0xa7, 0xbc, 0xe3, 0x6c, + 0xce, 0x94, 0xaa, 0xd4, 0xca, 0x00, 0xcb, 0x3a, 0x3f, 0x24, + 0x9d, 0x4f, 0xaf, 0x76, 0x22, 0xa7, 0x28, 0x67, 0x2b, 0x08, + 0xa9, 0x8c, 0xa0, 0x63, 0xda, 0x27, 0xfa, + }; + + Proof proof = new Proof(proofBytes); + + int seed = proof.Seed; + Assert.Equal(-713621093, seed); + } + + [Fact] + public void GenerateLegacyRandomSeed() { byte[] preEvaluationHashBytes = { @@ -1452,9 +1593,11 @@ public void GenerateRandomSeed() 0x7d, 0x37, 0x67, 0xe1, 0xe9, }; - int seed = ActionEvaluator.GenerateRandomSeed(preEvaluationHashBytes, signature, 0); + int seed = ActionEvaluator.GenerateLegacyRandomSeed( + preEvaluationHashBytes, signature, 0); Assert.Equal(353767086, seed); - seed = ActionEvaluator.GenerateRandomSeed(preEvaluationHashBytes, signature, 1); + seed = ActionEvaluator.GenerateLegacyRandomSeed( + preEvaluationHashBytes, signature, 1); Assert.Equal(353767087, seed); } @@ -1481,13 +1624,11 @@ public void CheckRandomSeedInAction() stateStore: fx.StateStore, isPolicyAction: false).ToArray(); - byte[] preEvaluationHashBytes = blockA.PreEvaluationHash.ToByteArray(); + Assert.NotNull(blockA.Proof); + Proof proof = blockA.Proof; int[] randomSeeds = Enumerable .Range(0, txA.Actions.Count) - .Select(offset => ActionEvaluator.GenerateRandomSeed( - preEvaluationHashBytes, - txA.Signature, - offset)) + .Select(offset => proof.Seed + offset) .ToArray(); for (int i = 0; i < evalsA.Length; i++) diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs index 763255956d0..ad53644dd6a 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs @@ -42,19 +42,21 @@ Func getTxExecution (Address[] addresses, Transaction[] txs) = MakeFixturesForAppendTests(keys: keys); var genesis = _blockChain.Genesis; - Assert.Equal(1, _blockChain.Count); Assert.Empty(_renderer.ActionRecords); Assert.Empty(_renderer.BlockRecords); var block1 = _blockChain.ProposeBlock( - keys[4], TestUtils.CreateBlockCommit(_blockChain.Tip)); + keys[4], + TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, keys[4])); _blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); Assert.NotNull(_blockChain.GetBlockCommit(block1.Hash)); Block block2 = _blockChain.ProposeBlock( keys[4], txs.ToImmutableList(), lastCommit: TestUtils.CreateBlockCommit(block1), - evidence: ImmutableArray.Empty); + proof: TestUtils.CreateZeroRoundProof(block1, keys[4]), + ImmutableArray.Empty); foreach (Transaction tx in txs) { Assert.Null(getTxExecution(genesis.Hash, tx.Id)); @@ -75,10 +77,8 @@ Func getTxExecution } Assert.True(_blockChain.ContainsBlock(block2.Hash)); - RenderRecord.ActionSuccess[] renders = _renderer.ActionSuccessRecords - .Where(r => TestUtils.IsDumbAction(r.Action)) - .ToArray(); + .Where(r => TestUtils.IsDumbAction(r.Action)).ToArray(); DumbAction[] actions = renders.Select(r => TestUtils.ToDumbAction(r.Action)).ToArray(); Assert.Equal(4, renders.Length); Assert.True(renders.All(r => r.Render)); @@ -163,7 +163,6 @@ Func getTxExecution RenderRecord.ActionSuccess[] blockRenders = _renderer.ActionSuccessRecords .Where(r => TestUtils.IsMinerReward(r.Action)) .ToArray(); - Assert.Equal( (Integer)2, (Integer)_blockChain @@ -174,7 +173,6 @@ Func getTxExecution Assert.True(blockRenders.All(r => r.Render)); Assert.Equal(1, blockRenders[0].Context.BlockIndex); Assert.Equal(2, blockRenders[1].Context.BlockIndex); - Assert.Equal( (Integer)1, (Integer)_blockChain @@ -201,7 +199,6 @@ Func getTxExecution { Assert.Null(getTxExecution(genesis.Hash, tx.Id)); Assert.Null(getTxExecution(block1.Hash, tx.Id)); - TxExecution e = getTxExecution(block2.Hash, tx.Id); Assert.False(e.Fail); Assert.Equal(block2.Hash, e.BlockHash); @@ -265,6 +262,7 @@ Func getTxExecution keys[4], new[] { tx1Transfer, tx2Error, tx3Transfer }.ToImmutableList(), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, keys[4]), ImmutableArray.Empty); _blockChain.Append(block3, TestUtils.CreateBlockCommit(block3)); var txExecution1 = getTxExecution(block3.Hash, tx1Transfer.Id); @@ -346,6 +344,7 @@ public void AppendModern() miner, new[] { tx1 }.ToImmutableList(), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, miner), ImmutableArray.Empty); var commit1 = TestUtils.CreateBlockCommit(block1); _blockChain.Append(block1, commit1); @@ -358,6 +357,7 @@ public void AppendModern() miner, new[] { tx2 }.ToImmutableList(), commit1, + TestUtils.CreateZeroRoundProof(block1, miner), ImmutableArray.Empty); _blockChain.Append(block2, TestUtils.CreateBlockCommit(block2)); var world2 = _blockChain.GetNextWorldState(); @@ -396,6 +396,7 @@ public void AppendFailDueToInvalidBytesLength() miner, heavyTxs.ToImmutableList(), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, miner), ImmutableArray.Empty); long maxBytes = _blockChain.Policy.GetMaxTransactionsBytes(block.Index); Assert.True(block.MarshalBlock().EncodingLength > maxBytes); @@ -426,6 +427,7 @@ public void AppendFailDueToInvalidTxCount() miner, manyTxs.ToImmutableList(), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, miner), ImmutableArray.Empty); Assert.Equal(manyTxs.Count, block.Transactions.Count); @@ -452,7 +454,10 @@ public void AppendWhenActionEvaluationFailed() blockChain.MakeTransaction(privateKey, new[] { action }); renderer.ResetRecords(); - Block block = blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = blockChain.ProposeBlock( + proposer, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, proposer)); blockChain.Append(block, TestUtils.CreateBlockCommit(block)); Assert.Equal(2, blockChain.Count); @@ -502,6 +507,7 @@ TxPolicyViolationException IsSignerValid( miner, new[] { validTx }.ToImmutableList(), TestUtils.CreateBlockCommit(blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, miner), ImmutableArray.Empty); blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); @@ -509,6 +515,7 @@ TxPolicyViolationException IsSignerValid( miner, new[] { invalidTx }.ToImmutableList(), TestUtils.CreateBlockCommit(blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, miner), ImmutableArray.Empty); Assert.Throws(() => blockChain.Append( block2, TestUtils.CreateBlockCommit(block2))); @@ -526,7 +533,8 @@ public void UnstageAfterAppendComplete() // Mining with empty staged. Block block1 = _blockChain.ProposeBlock( privateKey, - TestUtils.CreateBlockCommit(_blockChain.Tip)); + TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, privateKey)); _blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); Assert.Empty(_blockChain.GetStagedTransactionIds()); @@ -538,6 +546,7 @@ public void UnstageAfterAppendComplete() privateKey, ImmutableList.Empty.Add(txs[0]), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, privateKey), ImmutableArray.Empty); _blockChain.Append(block2, TestUtils.CreateBlockCommit(block2)); Assert.Equal(1, _blockChain.GetStagedTransactionIds().Count); @@ -556,6 +565,7 @@ public void UnstageAfterAppendComplete() privateKey, ImmutableList.Empty.Add(txs[1]), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, privateKey), ImmutableArray.Empty); _blockChain.Append(block3, TestUtils.CreateBlockCommit(block3)); Assert.Empty(_blockChain.GetStagedTransactionIds()); @@ -583,6 +593,7 @@ public void DoesNotUnstageOnAppendForForkedChain() privateKey, ImmutableList.Empty.Add(txs[0]), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, privateKey), ImmutableArray.Empty); // Not actually unstaged, but lower nonce is filtered for workspace. @@ -602,6 +613,7 @@ public void DoesNotUnstageOnAppendForForkedChain() privateKey, ImmutableList.Empty.Add(txs[1]), TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, privateKey), ImmutableArray.Empty); // Actually gets unstaged. @@ -652,7 +664,9 @@ void AssertTxIdSetEqual( txA1 = Transaction.Create(1, signerA, genesis, emptyActions); _blockChain.StageTransaction(txA0); _blockChain.StageTransaction(txA1); - Block block = _blockChain.ProposeBlock(signerA); + Block block = _blockChain.ProposeBlock( + signerA, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, signerA)); Transaction txA2 = Transaction.Create(2, signerA, genesis, emptyActions), @@ -734,6 +748,7 @@ public void CannotAppendBlockWithInvalidActions() previousHash: _blockChain.Genesis.Hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null); var preEval = new PreEvaluationBlock( preEvaluationBlockHeader: new PreEvaluationBlockHeader( @@ -788,6 +803,7 @@ public void DoesNotMigrateStateWithoutAction() previousHash: null, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: null, evidenceHash: null), transactions: txs, evidence: evs).Propose(); @@ -807,6 +823,7 @@ public void DoesNotMigrateStateWithoutAction() fx.Proposer, ImmutableList.Empty, TestUtils.CreateBlockCommit(blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, fx.Proposer), ImmutableArray.Empty); blockChain.Append(emptyBlock, TestUtils.CreateBlockCommit(emptyBlock)); Assert.True(blockChain.GetNextWorldState(emptyBlock.Hash).Legacy); @@ -862,13 +879,12 @@ public void AppendSRHPostponeBPVBump() action = DumbAction.Create((new Address(TestUtils.GetRandomBytes(20)), "bar")); tx = Transaction.Create(1, miner, genesis.Hash, new[] { action }.ToPlainValues()); var blockAfterBump1 = blockChain.ProposeBlock( - miner, - new[] { tx }.ToImmutableList(), - commitBeforeBump, + proposer: miner, + transactions: new[] { tx }.ToImmutableList(), + lastCommit: commitBeforeBump, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, miner), evidence: ImmutableArray.Empty); - Assert.Equal( - BlockMetadata.CurrentProtocolVersion, - blockAfterBump1.ProtocolVersion); + Assert.True(blockAfterBump1.ProtocolVersion >= BlockMetadata.SlothProtocolVersion); var commitAfterBump1 = TestUtils.CreateBlockCommit(blockAfterBump1); blockChain.Append(blockAfterBump1, commitAfterBump1); Assert.Equal(blockBeforeBump.StateRootHash, blockAfterBump1.StateRootHash); @@ -877,13 +893,12 @@ public void AppendSRHPostponeBPVBump() action = DumbAction.Create((new Address(TestUtils.GetRandomBytes(20)), "baz")); tx = Transaction.Create(2, miner, genesis.Hash, new[] { action }.ToPlainValues()); var blockAfterBump2 = blockChain.ProposeBlock( - miner, - new[] { tx }.ToImmutableList(), - commitAfterBump1, + proposer: miner, + transactions: new[] { tx }.ToImmutableList(), + lastCommit: commitAfterBump1, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, miner), evidence: ImmutableArray.Empty); - Assert.Equal( - BlockMetadata.CurrentProtocolVersion, - blockAfterBump2.ProtocolVersion); + Assert.True(blockAfterBump1.ProtocolVersion >= BlockMetadata.SlothProtocolVersion); var commitAfterBump2 = TestUtils.CreateBlockCommit(blockAfterBump2); blockChain.Append(blockAfterBump2, commitAfterBump2); Assert.Equal( diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.Evidence.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.Evidence.cs index 3cf4749d40f..47255a39999 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.Evidence.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.Evidence.cs @@ -581,6 +581,7 @@ private static Block NextBlock( var block = blockChain.ProposeBlock( proposer: proposer, lastCommit: TestUtils.CreateBlockCommit(tip, true), + proof: TestUtils.CreateZeroRoundProof(tip, proposer), evidence: evidence); blockChain.Append(block, TestUtils.CreateBlockCommit(block, true)); return block; diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.Fork.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.Fork.cs new file mode 100644 index 00000000000..ec1a7b7e81e --- /dev/null +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.Fork.cs @@ -0,0 +1,716 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Action.Tests.Common; +using Libplanet.Blockchain; +using Libplanet.Blockchain.Policies; +using Libplanet.Blockchain.Renderers.Debug; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Tests.Store; +using Libplanet.Types.Blocks; +using Libplanet.Types.Evidence; +using Libplanet.Types.Tx; +using Xunit; +using static Libplanet.Tests.TestUtils; + +namespace Libplanet.Tests.Blockchain +{ + public partial class BlockChainTest + { + [SkippableFact] + public void ForkTxNonce() + { + // An active account, so that its some recent transactions became "stale" due to a fork. + var privateKey = new PrivateKey(); + Address address = privateKey.Address; + + // An inactive account, so that it has no recent transactions but only an old + // transaction, so that its all transactions are stale-proof (stale-resistant). + var lessActivePrivateKey = new PrivateKey(); + Address lessActiveAddress = lessActivePrivateKey.Address; + + var actions = new[] { DumbAction.Create((address, "foo")) }; + + var genesis = _blockChain.Genesis; + + Transaction[] txsA = + { + _fx.MakeTransaction(actions, privateKey: privateKey), + _fx.MakeTransaction(privateKey: lessActivePrivateKey), + }; + + Assert.Equal(0, _blockChain.GetNextTxNonce(address)); + + Block b1 = _blockChain.ProposeBlock( + _fx.Proposer, + txsA.ToImmutableList(), + CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), + ImmutableArray.Empty); + _blockChain.Append(b1, CreateBlockCommit(b1)); + + Assert.Equal(1, _blockChain.GetNextTxNonce(address)); + + Transaction[] txsB = + { + _fx.MakeTransaction( + actions, + nonce: 1, + privateKey: privateKey), + }; + Block b2 = _blockChain.ProposeBlock( + _fx.Proposer, + txsB.ToImmutableList(), + CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), + ImmutableArray.Empty); + _blockChain.Append(b2, CreateBlockCommit(b2)); + + Assert.Equal(2, _blockChain.GetNextTxNonce(address)); + + BlockChain forked = _blockChain.Fork(b1.Hash); + Assert.Equal(1, forked.GetNextTxNonce(address)); + Assert.Equal(1, forked.GetNextTxNonce(lessActiveAddress)); + } + + [SkippableFact] + public void FindNextHashesAfterFork() + { + var key = new PrivateKey(); + + Block block = _blockChain.ProposeBlock( + key, + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block, CreateBlockCommit(block)); + Block block2 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block2, CreateBlockCommit(block2)); + Block block3 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block3, CreateBlockCommit(block3)); + + BlockChain forked = _blockChain.Fork(_blockChain.Genesis.Hash); + Block forkedBlock = forked.ProposeBlock( + key, + proof: CreateZeroRoundProof(forked.Tip, key)); + forked.Append(forkedBlock, CreateBlockCommit(forkedBlock)); + + BlockLocator locator = _blockChain.GetBlockLocator(); + forked.FindNextHashes(locator) + .Deconstruct(out long? offset, out IReadOnlyList hashes); + + Assert.Equal(forked[0].Index, offset); + Assert.Equal(new[] { forked[0].Hash, forked[1].Hash }, hashes); + } + + [SkippableFact] + public void Fork() + { + var key = new PrivateKey(); + + Block block1 = _blockChain.ProposeBlock( + key, + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block1, CreateBlockCommit(block1)); + Block block2 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block2, CreateBlockCommit(block2)); + Block block3 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block3, CreateBlockCommit(block3)); + + BlockChain forked = _blockChain.Fork(block2.Hash); + + Assert.Equal( + new[] { block1, block2, block3 }, + new[] { _blockChain[1], _blockChain[2], _blockChain[3] } + ); + Assert.Equal(4, _blockChain.Count); + + Assert.Equal( + new[] { block1, block2 }, + new[] { forked[1], forked[2] } + ); + Assert.Equal(3, forked.Count); + } + + [SkippableFact] + public void ForkAndSwapCanonicity() + { + // Fork is not canonical. + var key = new PrivateKey(); + var workspace = _blockChain.Fork(_blockChain.Genesis.Hash); + var b = workspace.ProposeBlock( + key, + lastCommit: CreateBlockCommit(workspace.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + workspace.Append(b, CreateBlockCommit(b)); + Assert.True(_blockChain.IsCanonical); + Assert.False(workspace.IsCanonical); + + // Both are canonical after swap. + _blockChain.Swap(workspace, false); + Assert.True(_blockChain.IsCanonical); + Assert.True(workspace.IsCanonical); + } + + [SkippableFact] + public void ForkWithBlockNotExistInChain() + { + var key = new PrivateKey(); + var genesis = _blockChain.Genesis; + + for (var i = 0; i < 2; i++) + { + Block block = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(block, CreateBlockCommit(block)); + } + + Block newBlock = _blockChain.EvaluateAndSign( + ProposeNext( + genesis, + miner: key.PublicKey, + proof: CreateZeroRoundProof(_blockChain.Tip, key)), key); + + Assert.Throws(() => + _blockChain.Fork(newBlock.Hash)); + + _blockChain.Store.PutBlock(newBlock); + Assert.Throws(() => + _blockChain.Fork(newBlock.Hash)); + } + + [SkippableFact] + public void ForkChainWithIncompleteBlockStates() + { + var fx = new MemoryStoreFixture(_policy.PolicyActionsRegistry); + (_, _, BlockChain chain) = + MakeIncompleteBlockStates(fx.Store, fx.StateStore); + BlockChain forked = chain.Fork(chain[5].Hash); + Assert.Equal(chain[5], forked.Tip); + Assert.Equal(6, forked.Count); + } + + [SkippableFact] + public void StateAfterForkingAndAddingExistingBlock() + { + var miner = new PrivateKey(); + var signer = new PrivateKey(); + var address = signer.Address; + var actions1 = new[] { DumbAction.Create((address, "foo")) }; + var actions2 = new[] { DumbAction.Create((address, "bar")) }; + + _blockChain.MakeTransaction(signer, actions1); + var b1 = _blockChain.ProposeBlock( + miner, + proof: CreateZeroRoundProof(_blockChain.Tip, miner)); + _blockChain.Append(b1, CreateBlockCommit(b1)); + + _blockChain.MakeTransaction(signer, actions2); + var b2 = _blockChain.ProposeBlock( + miner, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, miner)); + _blockChain.Append(b2, CreateBlockCommit(b2)); + var state = _blockChain + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(address); + + Assert.Equal((Text)"foo,bar", state); + + var forked = _blockChain.Fork(b1.Hash); + state = forked + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(address); + Assert.Equal((Text)"foo", state); + + forked.Append(b2, CreateBlockCommit(b2)); + state = forked + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(address); + Assert.Equal((Text)"foo,bar", state); + } + + [SkippableFact] + public void ForkShouldSkipExecuteAndRenderGenesis() + { + var miner = new PrivateKey(); + var action = DumbAction.Create((_fx.Address1, "genesis")); + + using (IStore store = new MemoryStore()) + using (var stateStore = new TrieStateStore(new MemoryKeyValueStore())) + { + var actionEvaluator = new ActionEvaluator( + _policy.PolicyActionsRegistry, + stateStore, + new SingleActionLoader(typeof(DumbAction))); + var privateKey = new PrivateKey(); + var genesis = ProposeGenesisBlock( + ProposeGenesis( + GenesisProposer.PublicKey, + transactions: new[] + { + new Transaction( + new UnsignedTx( + new TxInvoice( + genesisHash: null, + updatedAddresses: ImmutableHashSet.Create(_fx.Address1), + timestamp: DateTimeOffset.UtcNow, + actions: new TxActionList(new[] { action }.ToPlainValues()), + maxGasPrice: null, + gasLimit: null), + new TxSigningMetadata(privateKey.PublicKey, 0)), + privateKey), + }), + privateKey: GenesisProposer); + + store.PutBlock(genesis); + var renderer = new RecordingActionRenderer(); + var blockChain = BlockChain.Create( + _policy, + new VolatileStagePolicy(), + store, + stateStore, + genesis, + new ActionEvaluator( + _policy.PolicyActionsRegistry, + stateStore: stateStore, + actionTypeLoader: new SingleActionLoader(typeof(DumbAction))), + renderers: new[] { renderer } + ); + + // Creation does not render anything + Assert.Equal(0, renderer.ActionRecords.Count); + Assert.Equal(0, renderer.BlockRecords.Count); + + Block block1 = blockChain.ProposeBlock( + miner, + proof: CreateZeroRoundProof(_blockChain.Tip, miner)); + blockChain.Append(block1, CreateBlockCommit(block1)); + Block block2 = blockChain.ProposeBlock( + miner, + lastCommit: CreateBlockCommit(blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, miner)); + blockChain.Append(block2, CreateBlockCommit(block2)); + + int blockRecordsBeforeFork = renderer.BlockRecords.Count; + + blockChain.Fork(blockChain.Tip.Hash); + + Assert.Equal(0, renderer.ActionRecords.Count(r => IsDumbAction(r.Action))); + Assert.Equal(blockRecordsBeforeFork, renderer.BlockRecords.Count); + } + } + + [SkippableFact] + public void GetBlockCommitAfterFork() + { + var proposer = new PrivateKey(); + Block block1 = _blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); + _blockChain.Append(block1, CreateBlockCommit(block1)); + proposer = new PrivateKey(); + Block block2 = _blockChain.ProposeBlock( + proposer, + lastCommit: CreateBlockCommit(block1), + proof: CreateZeroRoundProof(block1, proposer)); + _blockChain.Append(block2, CreateBlockCommit(block2)); + proposer = new PrivateKey(); + Block block3 = _blockChain.ProposeBlock( + proposer, + lastCommit: CreateBlockCommit(block2), + proof: CreateZeroRoundProof(block2, proposer)); + _blockChain.Append(block3, CreateBlockCommit(block3)); + + var forked = _blockChain.Fork(block2.Hash); + Assert.NotNull(forked.GetBlockCommit(forked.Tip.Index)); + Assert.Equal( + forked.GetBlockCommit(forked.Tip.Index), + block3.LastCommit); + } + + [SkippableFact] + public void GetStateReturnsValidStateAfterFork() + { + var privateKey = new PrivateKey(); + var store = new MemoryStore(); + var stateStore = + new TrieStateStore(new MemoryKeyValueStore()); + var actionLoader = new SingleActionLoader(typeof(DumbAction)); + var chain = MakeBlockChain( + new NullBlockPolicy(), + store, + stateStore, + actionLoader, + new[] { DumbAction.Create((_fx.Address1, "item0.0")) }); + Assert.Equal( + "item0.0", + (Text)chain + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(_fx.Address1)); + + chain.MakeTransaction( + privateKey, + new[] { DumbAction.Create((_fx.Address1, "item1.0")), } + ); + var proposer = new PrivateKey(); + Block block = chain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(chain.Tip, proposer)); + + chain.Append(block, CreateBlockCommit(block)); + Assert.Equal( + new IValue[] { (Text)"item0.0,item1.0" }, + chain + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetStates(new[] { _fx.Address1 }) + ); + Assert.Equal( + "item0.0,item1.0", + (Text)chain + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(_fx.Address1)); + + var forked = chain.Fork(chain.Tip.Hash); + Assert.Equal(2, forked.Count); + Assert.Equal( + new IValue[] { (Text)"item0.0,item1.0" }, + forked + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetStates(new[] { _fx.Address1 }) + ); + Assert.Equal( + "item0.0,item1.0", + (Text)forked + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(_fx.Address1)); + } + + [SkippableTheory] + [InlineData(true)] + [InlineData(false)] + public void Swap(bool render) + { + Assert.Throws(() => _blockChain.Swap(null, render)()); + + (var addresses, Transaction[] txs1) = + MakeFixturesForAppendTests(); + var genesis = _blockChain.Genesis; + var miner = new PrivateKey(); + var minerAddress = miner.Address; + + Block block1 = _blockChain.ProposeBlock( + miner, + txs1.ToImmutableList(), + CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, miner), + ImmutableArray.Empty); + _blockChain.Append(block1, CreateBlockCommit(block1)); + + PrivateKey privateKey = new PrivateKey(new byte[] + { + 0xa8, 0x21, 0xc7, 0xc2, 0x08, 0xa9, 0x1e, 0x53, 0xbb, 0xb2, + 0x71, 0x15, 0xf4, 0x23, 0x5d, 0x82, 0x33, 0x44, 0xd1, 0x16, + 0x82, 0x04, 0x13, 0xb6, 0x30, 0xe7, 0x96, 0x4f, 0x22, 0xe0, + 0xec, 0xe0, + }); + + BlockHash tipHash = _blockChain.Tip.Hash; + BlockChain fork = _blockChain.Fork(tipHash); + + Transaction[][] txsA = + { + new[] // block #2 + { + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[0], "2-0")), + }, + timestamp: DateTimeOffset.MinValue, + nonce: 2, + privateKey: privateKey), + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[1], "2-1")), + }, + timestamp: DateTimeOffset.MinValue.AddSeconds(3), + nonce: 3, + privateKey: privateKey), + }, + new[] // block #3 + { + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[2], "3-0")), + }, + timestamp: DateTimeOffset.MinValue, + nonce: 4, + privateKey: privateKey), + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[3], "3-1")), + }, + timestamp: DateTimeOffset.MinValue.AddSeconds(4), + nonce: 5, + privateKey: privateKey), + }, + }; + + foreach (Transaction[] txs in txsA) + { + Block b = _blockChain.ProposeBlock( + miner, + txs.ToImmutableList(), + CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, miner), + ImmutableArray.Empty); + _blockChain.Append(b, CreateBlockCommit(b)); + } + + Transaction[][] txsB = + { + new[] + { + // block #2' + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[0], "2'-0")), + }, + timestamp: DateTimeOffset.MinValue, + nonce: 2, + privateKey: privateKey), + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[1], "2'-1-0")), + DumbAction.Create((addresses[2], "2'-1-1")), + }, + timestamp: DateTimeOffset.MinValue.AddSeconds(2), + nonce: 3, + privateKey: privateKey), + }, + new[] + { + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[0], "3'-0")), + }, + timestamp: DateTimeOffset.MinValue, + nonce: 4, + privateKey: privateKey), + }, + new[] + { + _fx.MakeTransaction( + new[] + { + DumbAction.Create((addresses[0], "4'-0")), + }, + timestamp: DateTimeOffset.MinValue, + nonce: 5, + privateKey: privateKey), + }, + }; + + foreach (Transaction[] txs in txsB) + { + Block b = fork.ProposeBlock( + miner, + txs.ToImmutableList(), + CreateBlockCommit(fork.Tip), + CreateZeroRoundProof(fork.Tip, miner), + ImmutableArray.Empty); + fork.Append(b, CreateBlockCommit(b), render: true); + } + + Guid previousChainId = _blockChain.Id; + _renderer.ResetRecords(); + _blockChain.Swap(fork, render)(); // #3 -> #2 -> #1 -> #2' -> #3' -> #4' + + Assert.Empty(_blockChain.Store.IterateIndexes(previousChainId)); + Assert.Empty(_blockChain.Store.ListTxNonces(previousChainId)); + + RenderRecord.BlockBase[] blockLevelRenders = _renderer.Records + .OfType() + .ToArray(); + + RenderRecord.ActionBase[] actionRenders = _renderer.ActionRecords + .Where(r => IsDumbAction(r.Action)) + .ToArray(); + DumbAction[] actions = actionRenders.Select(r => ToDumbAction(r.Action)).ToArray(); + + int actionsCountA = txsA.Sum(a => a.Sum(tx => tx.Actions.Count)); + int actionsCountB = txsB.Sum(b => b.Sum(tx => tx.Actions.Count)); + + int totalBlockCount = (int)_blockChain.Tip.Index + 1; + + if (render) + { + Assert.Equal(2, blockLevelRenders.Length); + Assert.IsType(blockLevelRenders[0]); + Assert.True(blockLevelRenders[0].Begin); + Assert.IsType(blockLevelRenders[1]); + Assert.True(blockLevelRenders[1].End); + + Assert.True(blockLevelRenders[0].Index < actionRenders[0].Index); + Assert.True(blockLevelRenders[0].Index < actionRenders.First(r => r.Render).Index); + Assert.True(actionRenders.Last().Index < blockLevelRenders[1].Index); + + Assert.Equal(actionsCountB, actionRenders.Length); + Assert.Equal(0, actionRenders.Count(r => r.Unrender)); + Assert.True(actionRenders.All(r => r.Render)); + + Assert.Equal("2'-0", actions[0].Append?.Item); + Assert.Equal("2'-1-0", actions[1].Append?.Item); + Assert.Equal("2'-1-1", actions[2].Append?.Item); + Assert.Equal("3'-0", actions[3].Append?.Item); + Assert.Equal("4'-0", actions[4].Append?.Item); + + RenderRecord.ActionBase[] blockActionRenders = _renderer.ActionRecords + .Where(r => IsMinerReward(r.Action)) + .ToArray(); + + // except genesis block. + Assert.Equal( + (Integer)(totalBlockCount - 1), + (Integer)_blockChain + .GetNextWorldState() + .GetAccountState(ReservedAddresses.LegacyAccount) + .GetState(minerAddress) + ); + Assert.Equal(3, blockActionRenders.Length); // #1 -> #2' -> #3' -> #4' + Assert.True(blockActionRenders.All(r => r.Render)); + } + else + { + Assert.Empty(actionRenders); + } + } + + [SkippableTheory] + [InlineData(true)] + [InlineData(false)] + public void CannotSwapForSameHeightTip(bool render) + { + BlockChain fork = _blockChain.Fork(_blockChain.Tip.Hash); + IReadOnlyList prevRecords = _renderer.Records; + Assert.Throws(() => _blockChain.Swap(fork, render: render)()); + + // Render methods should be invoked if and only if the tip changes + Assert.Equal(prevRecords, _renderer.Records); + } + + [SkippableFact] + public void FindBranchPoint() + { + var key = new PrivateKey(); + Block b1 = _blockChain.ProposeBlock( + key, + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(b1, CreateBlockCommit(b1)); + Block b2 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(b2, CreateBlockCommit(b2)); + Block b3 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(b3, CreateBlockCommit(b3)); + Block b4 = _blockChain.ProposeBlock( + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append(b4, CreateBlockCommit(b4)); + + Assert.Equal(b1.PreviousHash, _blockChain.Genesis.Hash); + + var emptyLocator = new BlockLocator(new[] { _blockChain.Genesis.Hash }); + var invalidLocator = new BlockLocator( + new[] { new BlockHash(TestUtils.GetRandomBytes(BlockHash.Size)) }); + var locator = new BlockLocator( + new[] { b4.Hash, b3.Hash, b1.Hash, _blockChain.Genesis.Hash }); + + using (var emptyFx = new MemoryStoreFixture(_policy.PolicyActionsRegistry)) + using (var forkFx = new MemoryStoreFixture(_policy.PolicyActionsRegistry)) + { + var emptyChain = BlockChain.Create( + _blockChain.Policy, + new VolatileStagePolicy(), + emptyFx.Store, + emptyFx.StateStore, + emptyFx.GenesisBlock, + new ActionEvaluator( + _blockChain.Policy.PolicyActionsRegistry, + stateStore: emptyFx.StateStore, + actionTypeLoader: new SingleActionLoader(typeof(DumbAction)))); + var fork = BlockChain.Create( + _blockChain.Policy, + new VolatileStagePolicy(), + forkFx.Store, + forkFx.StateStore, + forkFx.GenesisBlock, + new ActionEvaluator( + _blockChain.Policy.PolicyActionsRegistry, + stateStore: forkFx.StateStore, + actionTypeLoader: new SingleActionLoader(typeof(DumbAction)))); + fork.Append(b1, CreateBlockCommit(b1)); + fork.Append(b2, CreateBlockCommit(b2)); + Block b5 = fork.ProposeBlock( + key, + lastCommit: CreateBlockCommit(fork.Tip), + proof: CreateZeroRoundProof(fork.Tip, key)); + fork.Append(b5, CreateBlockCommit(b5)); + + // Testing emptyChain + Assert.Equal(_blockChain.Genesis.Hash, emptyChain.FindBranchpoint(emptyLocator)); + Assert.Equal(_blockChain.Genesis.Hash, emptyChain.FindBranchpoint(locator)); + Assert.Null(emptyChain.FindBranchpoint(invalidLocator)); + + // Testing _blockChain + Assert.Equal(_blockChain.Genesis.Hash, _blockChain.FindBranchpoint(emptyLocator)); + Assert.Equal(b4.Hash, _blockChain.FindBranchpoint(locator)); + Assert.Null(_blockChain.FindBranchpoint(invalidLocator)); + + // Testing fork + Assert.Equal(_blockChain.Genesis.Hash, fork.FindBranchpoint(emptyLocator)); + Assert.Equal(b1.Hash, fork.FindBranchpoint(locator)); + Assert.Null(_blockChain.FindBranchpoint(invalidLocator)); + } + } + } +} diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.Internals.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.Internals.cs index d27b747728b..9c7ffbcb7da 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.Internals.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.Internals.cs @@ -113,6 +113,7 @@ public void ExecuteActions() _fx.Proposer, txs.ToImmutableList(), CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), ImmutableArray.Empty); _blockChain.Append(block1, CreateBlockCommit(block1), render: true); diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.ProposeBlock.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.ProposeBlock.cs index 94d50146efc..94fc1d920d2 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.ProposeBlock.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.ProposeBlock.cs @@ -37,7 +37,9 @@ public void ProposeBlock() .GetState(default)); var proposerA = new PrivateKey(); - Block block = _blockChain.ProposeBlock(proposerA); + Block block = _blockChain.ProposeBlock( + proposerA, + proof: CreateZeroRoundProof(_blockChain.Tip, proposerA)); _blockChain.Append(block, CreateBlockCommit(block)); Assert.True(_blockChain.ContainsBlock(block.Hash)); Assert.Equal(2, _blockChain.Count); @@ -55,6 +57,7 @@ public void ProposeBlock() Block anotherBlock = _blockChain.ProposeBlock( proposerB, CreateBlockCommit(_blockChain.Tip.Hash, _blockChain.Tip.Index, 0), + CreateZeroRoundProof(_blockChain.Tip, proposerB), _blockChain.GetPendingEvidence()); _blockChain.Append(anotherBlock, CreateBlockCommit(anotherBlock)); Assert.True(_blockChain.ContainsBlock(anotherBlock.Hash)); @@ -72,9 +75,11 @@ public void ProposeBlock() .GetState(default) ); + var proposer = new PrivateKey(); Block block3 = _blockChain.ProposeBlock( - new PrivateKey(), + proposer, CreateBlockCommit(_blockChain.Tip.Hash, _blockChain.Tip.Index, 0), + CreateZeroRoundProof(_blockChain.Tip, proposer), _blockChain.GetPendingEvidence()); Assert.False(_blockChain.ContainsBlock(block3.Hash)); Assert.Equal(3, _blockChain.Count); @@ -111,9 +116,11 @@ public void ProposeBlock() _blockChain.StageTransaction(heavyTx); } + proposer = new PrivateKey(); Block block4 = _blockChain.ProposeBlock( - new PrivateKey(), + proposer, CreateBlockCommit(_blockChain.Tip.Hash, _blockChain.Tip.Index, 0), + CreateZeroRoundProof(_blockChain.Tip, proposer), _blockChain.GetPendingEvidence()); Assert.False(_blockChain.ContainsBlock(block4.Hash)); _logger.Debug( @@ -200,8 +207,13 @@ public void CanProposeInvalidBlock() }.ToPlainValues()), }.ToImmutableList(); + var proposer = new PrivateKey(); var block = blockChain.ProposeBlock( - new PrivateKey(), txs, null, ImmutableArray.Empty); + proposer, + txs, + null, + CreateZeroRoundProof(_blockChain.Tip, proposer), + ImmutableArray.Empty); Assert.Throws( () => blockChain.Append(block, CreateBlockCommit(block))); } @@ -318,7 +330,9 @@ public void ProposeBlockWithPendingTxs() Assert.Null(_blockChain.GetTxExecution(_blockChain.Genesis.Hash, tx.Id)); } - Block block = _blockChain.ProposeBlock(keyA); + Block block = _blockChain.ProposeBlock( + keyA, + proof: CreateZeroRoundProof(_blockChain.Tip, keyA)); _blockChain.Append(block, CreateBlockCommit(block)); Assert.True(_blockChain.ContainsBlock(block.Hash)); @@ -418,7 +432,9 @@ TxPolicyViolationException IsSignerValid( var invalidTx = blockChain.MakeTransaction(invalidKey, new DumbAction[] { }); var proposer = new PrivateKey(); - var block = blockChain.ProposeBlock(proposer); + var block = blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(blockChain.Tip, proposer)); blockChain.Append(block, CreateBlockCommit(block)); var txs = block.Transactions.ToHashSet(); @@ -475,7 +491,10 @@ public void ProposeBlockWithLowerNonces() ), } ); - Block block1 = _blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block1 = _blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); _blockChain.Append(block1, CreateBlockCommit(block1)); // Trying to propose with lower nonce (0) than expected. @@ -490,12 +509,11 @@ public void ProposeBlockWithLowerNonces() ), } ); + proposer = new PrivateKey(); Block block2 = _blockChain.ProposeBlock( - new PrivateKey(), - CreateBlockCommit( - _blockChain.Tip.Hash, - _blockChain.Tip.Index, - 0), + proposer, + CreateBlockCommit(_blockChain.Tip.Hash, _blockChain.Tip.Index, 0), + CreateZeroRoundProof(_blockChain.Tip, proposer), _blockChain.GetPendingEvidence()); _blockChain.Append(block2, CreateBlockCommit(block2)); @@ -537,6 +555,7 @@ public void ProposeBlockWithBlockAction() var block = blockChain.ProposeBlock( privateKey1, CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, privateKey1), _blockChain.GetPendingEvidence()); blockChain.Append(block, CreateBlockCommit(block)); @@ -558,6 +577,7 @@ public void ProposeBlockWithBlockAction() block = blockChain.ProposeBlock( privateKey1, CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, privateKey1), _blockChain.GetPendingEvidence()); blockChain.Append(block, CreateBlockCommit(block)); @@ -628,9 +648,11 @@ public void ProposeBlockWithLastCommit() VoteFlag.PreCommit).Sign(key)).ToImmutableArray(); var blockCommit = new BlockCommit( _blockChain.Tip.Index, 0, _blockChain.Tip.Hash, votes); + var proposer = new PrivateKey(); Block block = _blockChain.ProposeBlock( - new PrivateKey(), + proposer, blockCommit, + CreateZeroRoundProof(_blockChain.Tip, proposer), _blockChain.GetPendingEvidence()); Assert.NotNull(block.LastCommit); @@ -647,7 +669,10 @@ public void IgnoreLowerNonceTxsAndPropose() nonce: nonce, privateKey: privateKey, timestamp: DateTimeOffset.Now)) .ToArray(); StageTransactions(txsA); - Block b1 = _blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block b1 = _blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); _blockChain.Append(b1, CreateBlockCommit(b1)); Assert.Equal( txsA, @@ -665,9 +690,11 @@ public void IgnoreLowerNonceTxsAndPropose() StageTransactions(txsB); // Propose only txs having higher or equal with nonce than expected nonce. + proposer = new PrivateKey(); Block b2 = _blockChain.ProposeBlock( - new PrivateKey(), + proposer, CreateBlockCommit(b1), + CreateZeroRoundProof(_blockChain.Tip, proposer), _blockChain.GetPendingEvidence()); Assert.Single(b2.Transactions); Assert.Contains(txsB[3], b2.Transactions); @@ -684,7 +711,9 @@ public void IgnoreDuplicatedNonceTxs() timestamp: DateTimeOffset.Now)) .ToArray(); StageTransactions(txs); - Block b = _blockChain.ProposeBlock(privateKey); + Block b = _blockChain.ProposeBlock( + privateKey, + proof: CreateZeroRoundProof(_blockChain.Tip, privateKey)); _blockChain.Append(b, CreateBlockCommit(b)); Assert.Single(b.Transactions); @@ -799,9 +828,11 @@ public void MarkTransactionsToIgnoreWhileProposing() StageTransactions(txs); Assert.Equal(txs.Length, _blockChain.ListStagedTransactions().Count); + var proposer = new PrivateKey(); var block = _blockChain.ProposeBlock( - new PrivateKey(), + proposer, CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, proposer), _blockChain.GetPendingEvidence()); Assert.DoesNotContain(txWithInvalidNonce, block.Transactions); diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.Stage.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.Stage.cs index 2900005a001..3434166461d 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.Stage.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.Stage.cs @@ -81,7 +81,9 @@ public void TransactionsWithDuplicatedNonce() // stage tx_0_0 -> mine tx_0_0 -> stage tx_0_1 Assert.True(_blockChain.StageTransaction(tx_0_0)); - var block = _blockChain.ProposeBlock(key); + var block = _blockChain.ProposeBlock( + key, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(block, TestUtils.CreateBlockCommit(block)); Assert.Empty(_blockChain.GetStagedTransactionIds()); Assert.Empty(_blockChain.StagePolicy.Iterate(_blockChain, filtered: true)); @@ -102,8 +104,13 @@ public void TransactionsWithDuplicatedNonce() txIds.OrderBy(id => id), _blockChain.GetStagedTransactionIds().OrderBy(id => id) ); - block = _blockChain.ProposeBlock(key, TestUtils.CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(block, TestUtils.CreateBlockCommit(block)); + block = _blockChain.ProposeBlock( + key, + TestUtils.CreateBlockCommit(_blockChain.Tip), + TestUtils.CreateZeroRoundProof(_blockChain.Tip, key)); + _blockChain.Append( + block, + TestUtils.CreateBlockCommit(block)); // tx_0_1 and tx_1_x should be still staged, just filtered Assert.Empty(_blockChain.GetStagedTransactionIds()); Assert.Empty(_blockChain.StagePolicy.Iterate(_blockChain, filtered: true)); diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs index e59c99e95ea..475802d49b5 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs @@ -31,6 +31,7 @@ public void ValidateNextBlock() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(validNextBlock, TestUtils.CreateBlockCommit(validNextBlock)); @@ -52,6 +53,7 @@ public void ValidateNextBlockProtocolVersion() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); @@ -67,6 +69,7 @@ public void ValidateNextBlockProtocolVersion() previousHash: block1.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer)); @@ -83,6 +86,7 @@ public void ValidateNextBlockProtocolVersion() previousHash: block1.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(block3, TestUtils.CreateBlockCommit(block3)); @@ -104,6 +108,7 @@ public void ValidateNextBlockInvalidIndex() previousHash: prev.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); Assert.Throws( @@ -121,8 +126,8 @@ public void ValidateNextBlockInvalidIndex() previousHash: prev.Hash, txHash: null, lastCommit: TestUtils.CreateBlockCommit(prev.Hash, prev.Index + 1, 0), - evidenceHash: null)) - .Propose(), + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), + evidenceHash: null)).Propose(), _fx.Proposer); Assert.Throws( () => _blockChain.Append( @@ -148,6 +153,7 @@ public void ValidateNextBlockInvalidPreviousHash() // ReSharper disable once PossibleInvalidOperationException lastCommit: TestUtils.CreateBlockCommit( _validNext.PreviousHash.Value, 1, 0), + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); Assert.Throws(() => @@ -170,6 +176,7 @@ public void ValidateNextBlockInvalidTimestamp() previousHash: _validNext.Hash, txHash: null, lastCommit: TestUtils.CreateBlockCommit(_validNext), + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); Assert.Throws(() => @@ -231,6 +238,7 @@ public void ValidateNextBlockInvalidStateRootHash() previousHash: genesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), TestUtils.GenesisProposer); @@ -276,6 +284,7 @@ public void ValidateNextBlockInvalidStateRootHashBeforePostpone() previousHash: genesisBlock.Hash, txHash: null, lastCommit: null, + proof: null, evidenceHash: null)).Propose(), TestUtils.GenesisProposer); @@ -341,6 +350,7 @@ public void ValidateNextBlockInvalidStateRootHashOnPostpone() previousHash: genesisBlock.Hash, txHash: null, lastCommit: null, + proof: null, evidenceHash: null)).Propose(); Block block1 = chain.EvaluateAndSign( preBlock1, @@ -369,6 +379,7 @@ public void ValidateNextBlockLastCommitNullAtIndexOne() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(validNextBlock, TestUtils.CreateBlockCommit(validNextBlock)); @@ -387,6 +398,7 @@ public void ValidateNextBlockLastCommitUpperIndexOne() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); @@ -401,6 +413,7 @@ public void ValidateNextBlockLastCommitUpperIndexOne() previousHash: block1.Hash, txHash: null, lastCommit: blockCommit, + proof: TestUtils.CreateZeroRoundProof(block1, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(block2, TestUtils.CreateBlockCommit(block2)); @@ -419,6 +432,7 @@ public void ValidateNextBlockLastCommitFailsUnexpectedValidator() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); @@ -447,6 +461,7 @@ public void ValidateNextBlockLastCommitFailsUnexpectedValidator() previousHash: block1.Hash, txHash: null, lastCommit: blockCommit, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); Assert.Throws(() => @@ -465,6 +480,7 @@ public void ValidateNextBlockLastCommitFailsDropExpectedValidator() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); _blockChain.Append(block1, TestUtils.CreateBlockCommit(block1)); @@ -489,6 +505,7 @@ public void ValidateNextBlockLastCommitFailsDropExpectedValidator() previousHash: block1.Hash, txHash: null, lastCommit: blockCommit, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); Assert.Throws(() => @@ -530,6 +547,7 @@ public void ValidateBlockCommitFailsDifferentBlockHash() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); @@ -554,6 +572,7 @@ public void ValidateBlockCommitFailsDifferentHeight() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); @@ -578,6 +597,7 @@ public void ValidateBlockCommitFailsDifferentValidatorSet() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); @@ -612,6 +632,7 @@ public void ValidateBlockCommitFailsNullBlockCommit() previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); @@ -647,6 +668,7 @@ public void ValidateBlockCommitFailsInsufficientPower() previousHash: blockChain.Genesis.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); @@ -770,6 +792,8 @@ public void ValidateNextBlockAEVChangedOnChainRestart() previousHash: newChain.Tip.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof( + newChain.Tip, TestUtils.GenesisProposer), evidenceHash: null)).Propose(), TestUtils.GenesisProposer); diff --git a/test/Libplanet.Tests/Blockchain/BlockChainTest.cs b/test/Libplanet.Tests/Blockchain/BlockChainTest.cs index acccd29c3be..a8a128c276f 100644 --- a/test/Libplanet.Tests/Blockchain/BlockChainTest.cs +++ b/test/Libplanet.Tests/Blockchain/BlockChainTest.cs @@ -84,6 +84,7 @@ public BlockChainTest(ITestOutputHelper output) previousHash: _fx.GenesisBlock.Hash, txHash: null, lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), evidenceHash: null)).Propose(), _fx.Proposer); } @@ -111,8 +112,10 @@ public void CanFindBlockByIndex() { var genesis = _blockChain.Genesis; Assert.Equal(genesis, _blockChain[0]); - - Block block = _blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = _blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); _blockChain.Append(block, TestUtils.CreateBlockCommit(block)); Assert.Equal(block, _blockChain[1]); } @@ -122,13 +125,21 @@ public void CanonicalId() { var chain1 = _blockChain; var key = new PrivateKey(); - Block block1 = chain1.ProposeBlock(key); + Block block1 = chain1.ProposeBlock( + key, + proof: CreateZeroRoundProof(chain1.Tip, key)); chain1.Append(block1, CreateBlockCommit(block1)); - Block block2 = chain1.ProposeBlock(key, CreateBlockCommit(chain1.Tip)); + Block block2 = chain1.ProposeBlock( + key, + CreateBlockCommit(chain1.Tip), + CreateZeroRoundProof(chain1.Tip, key)); chain1.Append(block2, CreateBlockCommit(block2)); Assert.Equal(chain1.Id, _fx.Store.GetCanonicalChainId()); var chain2 = chain1.Fork(chain1.Tip.Hash); - Block block3 = chain2.ProposeBlock(key, CreateBlockCommit(chain1.Tip)); + Block block3 = chain2.ProposeBlock( + key, + CreateBlockCommit(chain1.Tip), + CreateZeroRoundProof(chain1.Tip, key)); chain2.Append(block3, CreateBlockCommit(block3)); Assert.Equal(chain1.Id, _fx.Store.GetCanonicalChainId()); @@ -166,12 +177,16 @@ public void BlockHashes() Assert.Single(_blockChain.BlockHashes); - Block b1 = _blockChain.ProposeBlock(key); + Block b1 = _blockChain.ProposeBlock( + key, + proof: CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(b1, CreateBlockCommit(b1)); Assert.Equal(new[] { genesis.Hash, b1.Hash }, _blockChain.BlockHashes); Block b2 = _blockChain.ProposeBlock( - key, CreateBlockCommit(_blockChain.Tip)); + key, + CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(b2, CreateBlockCommit(b2)); Assert.Equal( new[] { genesis.Hash, b1.Hash, b2.Hash }, @@ -179,7 +194,9 @@ public void BlockHashes() ); Block b3 = _blockChain.ProposeBlock( - key, CreateBlockCommit(_blockChain.Tip)); + key, + CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(b3, CreateBlockCommit(b3)); Assert.Equal( new[] { genesis.Hash, b1.Hash, b2.Hash, b3.Hash }, @@ -254,7 +271,10 @@ public void ProcessActions() ); chain.StageTransaction(tx1); - Block block1 = chain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block1 = chain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(chain.Tip, proposer)); chain.Append(block1, CreateBlockCommit(block1)); IValue state = chain .GetNextWorldState() @@ -285,8 +305,11 @@ public void ProcessActions() ); chain.StageTransaction(tx2); + proposer = new PrivateKey(); Block block2 = chain.ProposeBlock( - new PrivateKey(), CreateBlockCommit(chain.Tip)); + proposer, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, proposer)); chain.Append(block2, CreateBlockCommit(block2)); state = chain @@ -310,8 +333,12 @@ public void ProcessActions() }, }.ToPlainValues() ); + + proposer = new PrivateKey(); Block block3 = chain.ProposeBlock( - new PrivateKey(), CreateBlockCommit(chain.Tip)); + proposer, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, proposer)); chain.StageTransaction(tx3); chain.Append(block3, CreateBlockCommit(block3)); state = chain @@ -353,7 +380,10 @@ public void ActionRenderersHaveDistinctContexts() var action = DumbAction.Create((default, string.Empty)); var actions = new[] { action }; blockChain.MakeTransaction(privateKey, actions); - Block block = blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(blockChain.Tip, proposer)); generatedRandomValueLogs.Clear(); Assert.Empty(generatedRandomValueLogs); @@ -380,7 +410,10 @@ public void RenderActionsAfterBlockIsRendered() blockChain.MakeTransaction(privateKey, actions); recordingRenderer.ResetRecords(); Block prevBlock = blockChain.Tip; - Block block = blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(prevBlock, proposer)); blockChain.Append(block, CreateBlockCommit(block)); Assert.Equal(2, blockChain.Count); @@ -426,7 +459,10 @@ public void RenderActionsAfterAppendComplete() var action = DumbAction.Create((default, string.Empty)); var actions = new[] { action }; blockChain.MakeTransaction(privateKey, actions); - Block block = blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(blockChain.Tip, proposer)); ThrowException.SomeException e = Assert.Throws( () => blockChain.Append(block, CreateBlockCommit(block))); @@ -447,13 +483,19 @@ public void FindNextHashes() Assert.Single(hashes); Assert.Equal(_blockChain.Genesis.Hash, hashes.First()); var block0 = _blockChain.Genesis; - var block1 = _blockChain.ProposeBlock(key); + var block1 = _blockChain.ProposeBlock( + key, + proof: CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(block1, CreateBlockCommit(block1)); var block2 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(block2, CreateBlockCommit(block2)); var block3 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(block3, CreateBlockCommit(block3)); _blockChain.FindNextHashes(new BlockLocator(new[] { block0.Hash })) @@ -477,221 +519,6 @@ public void FindNextHashes() Assert.Equal(new[] { block0.Hash, block1.Hash }, hashes); } - [SkippableFact] - public void FindNextHashesAfterFork() - { - var key = new PrivateKey(); - - Block block = _blockChain.ProposeBlock(key); - _blockChain.Append(block, CreateBlockCommit(block)); - Block block2 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(block2, CreateBlockCommit(block2)); - Block block3 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(block3, CreateBlockCommit(block3)); - - BlockChain forked = _blockChain.Fork(_blockChain.Genesis.Hash); - Block forkedBlock = forked.ProposeBlock(key); - forked.Append(forkedBlock, CreateBlockCommit(forkedBlock)); - - BlockLocator locator = _blockChain.GetBlockLocator(); - forked.FindNextHashes(locator) - .Deconstruct(out long? offset, out IReadOnlyList hashes); - - Assert.Equal(forked[0].Index, offset); - Assert.Equal(new[] { forked[0].Hash, forked[1].Hash }, hashes); - } - - [SkippableFact] - public void Fork() - { - var key = new PrivateKey(); - - Block block1 = _blockChain.ProposeBlock(key); - _blockChain.Append(block1, CreateBlockCommit(block1)); - Block block2 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(block2, CreateBlockCommit(block2)); - Block block3 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(block3, CreateBlockCommit(block3)); - - BlockChain forked = _blockChain.Fork(block2.Hash); - - Assert.Equal( - new[] { block1, block2, block3 }, - new[] { _blockChain[1], _blockChain[2], _blockChain[3] } - ); - Assert.Equal(4, _blockChain.Count); - - Assert.Equal( - new[] { block1, block2 }, - new[] { forked[1], forked[2] } - ); - Assert.Equal(3, forked.Count); - } - - [SkippableFact] - public void ForkAndSwapCanonicity() - { - // Fork is not canonical. - var workspace = _blockChain.Fork(_blockChain.Genesis.Hash); - var b = workspace.ProposeBlock( - new PrivateKey(), - lastCommit: CreateBlockCommit(workspace.Tip)); - workspace.Append(b, CreateBlockCommit(b)); - Assert.True(_blockChain.IsCanonical); - Assert.False(workspace.IsCanonical); - - // Both are canonical after swap. - _blockChain.Swap(workspace, false); - Assert.True(_blockChain.IsCanonical); - Assert.True(workspace.IsCanonical); - } - - [SkippableFact] - public void ForkWithBlockNotExistInChain() - { - var key = new PrivateKey(); - var genesis = _blockChain.Genesis; - - for (var i = 0; i < 2; i++) - { - Block block = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(block, CreateBlockCommit(block)); - } - - Block newBlock = _blockChain.EvaluateAndSign( - ProposeNext(genesis, miner: key.PublicKey), key); - - Assert.Throws(() => - _blockChain.Fork(newBlock.Hash)); - - _blockChain.Store.PutBlock(newBlock); - Assert.Throws(() => - _blockChain.Fork(newBlock.Hash)); - } - - [SkippableFact] - public void ForkChainWithIncompleteBlockStates() - { - var fx = new MemoryStoreFixture( - _policy.PolicyActionsRegistry); - (_, _, BlockChain chain) = - MakeIncompleteBlockStates(fx.Store, fx.StateStore); - BlockChain forked = chain.Fork(chain[5].Hash); - Assert.Equal(chain[5], forked.Tip); - Assert.Equal(6, forked.Count); - } - - [SkippableFact] - public void StateAfterForkingAndAddingExistingBlock() - { - var miner = new PrivateKey(); - var signer = new PrivateKey(); - var address = signer.Address; - var actions1 = new[] { DumbAction.Create((address, "foo")) }; - var actions2 = new[] { DumbAction.Create((address, "bar")) }; - - _blockChain.MakeTransaction(signer, actions1); - var b1 = _blockChain.ProposeBlock(miner); - _blockChain.Append(b1, CreateBlockCommit(b1)); - - _blockChain.MakeTransaction(signer, actions2); - var b2 = _blockChain.ProposeBlock( - miner, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(b2, CreateBlockCommit(b2)); - var state = _blockChain - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(address); - - Assert.Equal((Text)"foo,bar", state); - - var forked = _blockChain.Fork(b1.Hash); - state = forked - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(address); - Assert.Equal((Text)"foo", state); - - forked.Append(b2, CreateBlockCommit(b2)); - state = forked - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(address); - Assert.Equal((Text)"foo,bar", state); - } - - [SkippableFact] - public void ForkShouldSkipExecuteAndRenderGenesis() - { - var miner = new PrivateKey(); - var action = DumbAction.Create((_fx.Address1, "genesis")); - - using (IStore store = new MemoryStore()) - using (var stateStore = new TrieStateStore(new MemoryKeyValueStore())) - { - var actionEvaluator = new ActionEvaluator( - _policy.PolicyActionsRegistry, - stateStore, - new SingleActionLoader(typeof(DumbAction))); - var privateKey = new PrivateKey(); - var genesis = ProposeGenesisBlock( - ProposeGenesis( - GenesisProposer.PublicKey, - transactions: new[] - { - new Transaction( - new UnsignedTx( - new TxInvoice( - genesisHash: null, - updatedAddresses: ImmutableHashSet.Create(_fx.Address1), - timestamp: DateTimeOffset.UtcNow, - actions: new TxActionList(new[] { action }.ToPlainValues()), - maxGasPrice: null, - gasLimit: null), - new TxSigningMetadata(privateKey.PublicKey, 0)), - privateKey), - }), - privateKey: GenesisProposer); - - store.PutBlock(genesis); - var renderer = new RecordingActionRenderer(); - var blockChain = BlockChain.Create( - _policy, - new VolatileStagePolicy(), - store, - stateStore, - genesis, - new ActionEvaluator( - _policy.PolicyActionsRegistry, - stateStore: stateStore, - actionTypeLoader: new SingleActionLoader(typeof(DumbAction))), - renderers: new[] { renderer } - ); - - // Creation does not render anything - Assert.Equal(0, renderer.ActionRecords.Count); - Assert.Equal(0, renderer.BlockRecords.Count); - - Block block1 = blockChain.ProposeBlock(miner); - blockChain.Append(block1, CreateBlockCommit(block1)); - Block block2 = blockChain.ProposeBlock( - miner, lastCommit: CreateBlockCommit(blockChain.Tip)); - blockChain.Append(block2, CreateBlockCommit(block2)); - - int blockRecordsBeforeFork = renderer.BlockRecords.Count; - - blockChain.Fork(blockChain.Tip.Hash); - - Assert.Equal(0, renderer.ActionRecords.Count(r => IsDumbAction(r.Action))); - Assert.Equal(blockRecordsBeforeFork, renderer.BlockRecords.Count); - } - } - [SkippableFact] public void DetectInvalidTxNonce() { @@ -709,6 +536,7 @@ public void DetectInvalidTxNonce() _fx.Proposer, txsA.ToImmutableList(), CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), ImmutableArray.Empty); _blockChain.Append(b1, TestUtils.CreateBlockCommit(b1)); @@ -716,6 +544,7 @@ public void DetectInvalidTxNonce() _fx.Proposer, txsA.ToImmutableList(), CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), ImmutableArray.Empty); Assert.Throws(() => _blockChain.Append(b2, CreateBlockCommit(b2))); @@ -731,64 +560,11 @@ public void DetectInvalidTxNonce() _fx.Proposer, txsB.ToImmutableList(), CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), ImmutableArray.Empty); _blockChain.Append(b2, CreateBlockCommit(b2)); } - [SkippableFact] - public void ForkTxNonce() - { - // An active account, so that its some recent transactions became "stale" due to a fork. - var privateKey = new PrivateKey(); - Address address = privateKey.Address; - - // An inactive account, so that it has no recent transactions but only an old - // transaction, so that its all transactions are stale-proof (stale-resistant). - var lessActivePrivateKey = new PrivateKey(); - Address lessActiveAddress = lessActivePrivateKey.Address; - - var actions = new[] { DumbAction.Create((address, "foo")) }; - - var genesis = _blockChain.Genesis; - - Transaction[] txsA = - { - _fx.MakeTransaction(actions, privateKey: privateKey), - _fx.MakeTransaction(privateKey: lessActivePrivateKey), - }; - - Assert.Equal(0, _blockChain.GetNextTxNonce(address)); - - Block b1 = _blockChain.ProposeBlock( - _fx.Proposer, - txsA.ToImmutableList(), - CreateBlockCommit(_blockChain.Tip), - ImmutableArray.Empty); - _blockChain.Append(b1, CreateBlockCommit(b1)); - - Assert.Equal(1, _blockChain.GetNextTxNonce(address)); - - Transaction[] txsB = - { - _fx.MakeTransaction( - actions, - nonce: 1, - privateKey: privateKey), - }; - Block b2 = _blockChain.ProposeBlock( - _fx.Proposer, - txsB.ToImmutableList(), - CreateBlockCommit(_blockChain.Tip), - ImmutableArray.Empty); - _blockChain.Append(b2, CreateBlockCommit(b2)); - - Assert.Equal(2, _blockChain.GetNextTxNonce(address)); - - BlockChain forked = _blockChain.Fork(b1.Hash); - Assert.Equal(1, forked.GetNextTxNonce(address)); - Assert.Equal(1, forked.GetNextTxNonce(lessActiveAddress)); - } - [SkippableFact] public void GetBlockLocator() { @@ -798,7 +574,8 @@ public void GetBlockLocator() { var block = _blockChain.ProposeBlock( key, - lastCommit: CreateBlockCommit(_blockChain.Tip)); + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(block, CreateBlockCommit(block)); blocks.Add(block); } @@ -818,210 +595,6 @@ public void GetBlockLocator() Assert.Equal(expected, actual); } - [SkippableTheory] - [InlineData(true)] - [InlineData(false)] - public void Swap(bool render) - { - Assert.Throws(() => _blockChain.Swap(null, render)()); - - (var addresses, Transaction[] txs1) = - MakeFixturesForAppendTests(); - var genesis = _blockChain.Genesis; - var miner = new PrivateKey(); - var minerAddress = miner.Address; - - Block block1 = _blockChain.ProposeBlock( - miner, - txs1.ToImmutableList(), - CreateBlockCommit(_blockChain.Tip), - ImmutableArray.Empty); - _blockChain.Append(block1, CreateBlockCommit(block1)); - - PrivateKey privateKey = new PrivateKey(new byte[] - { - 0xa8, 0x21, 0xc7, 0xc2, 0x08, 0xa9, 0x1e, 0x53, 0xbb, 0xb2, - 0x71, 0x15, 0xf4, 0x23, 0x5d, 0x82, 0x33, 0x44, 0xd1, 0x16, - 0x82, 0x04, 0x13, 0xb6, 0x30, 0xe7, 0x96, 0x4f, 0x22, 0xe0, - 0xec, 0xe0, - }); - - BlockHash tipHash = _blockChain.Tip.Hash; - BlockChain fork = _blockChain.Fork(tipHash); - - Transaction[][] txsA = - { - new[] // block #2 - { - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[0], "2-0")), - }, - timestamp: DateTimeOffset.MinValue, - nonce: 2, - privateKey: privateKey), - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[1], "2-1")), - }, - timestamp: DateTimeOffset.MinValue.AddSeconds(3), - nonce: 3, - privateKey: privateKey), - }, - new[] // block #3 - { - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[2], "3-0")), - }, - timestamp: DateTimeOffset.MinValue, - nonce: 4, - privateKey: privateKey), - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[3], "3-1")), - }, - timestamp: DateTimeOffset.MinValue.AddSeconds(4), - nonce: 5, - privateKey: privateKey), - }, - }; - - foreach (Transaction[] txs in txsA) - { - Block b = _blockChain.ProposeBlock( - miner, - txs.ToImmutableList(), - CreateBlockCommit(_blockChain.Tip), - ImmutableArray.Empty); - _blockChain.Append(b, CreateBlockCommit(b)); - } - - Transaction[][] txsB = - { - new[] - { - // block #2' - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[0], "2'-0")), - }, - timestamp: DateTimeOffset.MinValue, - nonce: 2, - privateKey: privateKey), - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[1], "2'-1-0")), - DumbAction.Create((addresses[2], "2'-1-1")), - }, - timestamp: DateTimeOffset.MinValue.AddSeconds(2), - nonce: 3, - privateKey: privateKey), - }, - new[] - { - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[0], "3'-0")), - }, - timestamp: DateTimeOffset.MinValue, - nonce: 4, - privateKey: privateKey), - }, - new[] - { - _fx.MakeTransaction( - new[] - { - DumbAction.Create((addresses[0], "4'-0")), - }, - timestamp: DateTimeOffset.MinValue, - nonce: 5, - privateKey: privateKey), - }, - }; - - foreach (Transaction[] txs in txsB) - { - Block b = fork.ProposeBlock( - miner, - txs.ToImmutableList(), - CreateBlockCommit(fork.Tip), - ImmutableArray.Empty); - fork.Append(b, CreateBlockCommit(b), render: true); - } - - Guid previousChainId = _blockChain.Id; - _renderer.ResetRecords(); - _blockChain.Swap(fork, render)(); // #3 -> #2 -> #1 -> #2' -> #3' -> #4' - - Assert.Empty(_blockChain.Store.IterateIndexes(previousChainId)); - Assert.Empty(_blockChain.Store.ListTxNonces(previousChainId)); - - RenderRecord.BlockBase[] blockLevelRenders = _renderer.Records - .OfType() - .ToArray(); - - RenderRecord.ActionBase[] actionRenders = _renderer.ActionRecords - .Where(r => IsDumbAction(r.Action)) - .ToArray(); - DumbAction[] actions = actionRenders.Select(r => ToDumbAction(r.Action)).ToArray(); - - int actionsCountA = txsA.Sum(a => a.Sum(tx => tx.Actions.Count)); - int actionsCountB = txsB.Sum(b => b.Sum(tx => tx.Actions.Count)); - - int totalBlockCount = (int)_blockChain.Tip.Index + 1; - - if (render) - { - Assert.Equal(2, blockLevelRenders.Length); - Assert.IsType(blockLevelRenders[0]); - Assert.True(blockLevelRenders[0].Begin); - Assert.IsType(blockLevelRenders[1]); - Assert.True(blockLevelRenders[1].End); - - Assert.True(blockLevelRenders[0].Index < actionRenders[0].Index); - Assert.True(blockLevelRenders[0].Index < actionRenders.First(r => r.Render).Index); - Assert.True(actionRenders.Last().Index < blockLevelRenders[1].Index); - - Assert.Equal(actionsCountB, actionRenders.Length); - Assert.Equal(0, actionRenders.Count(r => r.Unrender)); - Assert.True(actionRenders.All(r => r.Render)); - - Assert.Equal("2'-0", actions[0].Append?.Item); - Assert.Equal("2'-1-0", actions[1].Append?.Item); - Assert.Equal("2'-1-1", actions[2].Append?.Item); - Assert.Equal("3'-0", actions[3].Append?.Item); - Assert.Equal("4'-0", actions[4].Append?.Item); - - RenderRecord.ActionBase[] blockActionRenders = _renderer.ActionRecords - .Where(r => IsMinerReward(r.Action)) - .ToArray(); - - // except genesis block. - Assert.Equal( - (Integer)(totalBlockCount - 1), - (Integer)_blockChain - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(minerAddress) - ); - Assert.Equal(3, blockActionRenders.Length); // #1 -> #2' -> #3' -> #4' - Assert.True(blockActionRenders.All(r => r.Render)); - } - else - { - Assert.Empty(actionRenders); - } - } - [SkippableFact] public void GetBlockCommit() { @@ -1031,16 +604,21 @@ public void GetBlockCommit() Assert.Null(_blockChain.GetBlockCommit(_blockChain.Genesis.Hash)); // BlockCommit is put to store when block is appended. - Block block1 = _blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block1 = _blockChain.ProposeBlock( + new PrivateKey(), + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); BlockCommit blockCommit1 = CreateBlockCommit(block1); _blockChain.Append(block1, blockCommit1); Assert.Equal(blockCommit1, _blockChain.GetBlockCommit(block1.Index)); Assert.Equal(blockCommit1, _blockChain.GetBlockCommit(block1.Hash)); // BlockCommit is retrieved from lastCommit. + proposer = new PrivateKey(); Block block2 = _blockChain.ProposeBlock( - new PrivateKey(), - lastCommit: CreateBlockCommit(_blockChain.Tip)); + proposer, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); BlockCommit blockCommit2 = CreateBlockCommit(block2); _blockChain.Append(block2, blockCommit2); @@ -1050,27 +628,6 @@ public void GetBlockCommit() Assert.Equal(block2.LastCommit, _blockChain.GetBlockCommit(block1.Hash)); } - [SkippableFact] - public void GetBlockCommitAfterFork() - { - Block block1 = _blockChain.ProposeBlock(new PrivateKey()); - _blockChain.Append(block1, CreateBlockCommit(block1)); - Block block2 = _blockChain.ProposeBlock( - new PrivateKey(), - lastCommit: CreateBlockCommit(block1)); - _blockChain.Append(block2, CreateBlockCommit(block2)); - Block block3 = _blockChain.ProposeBlock( - new PrivateKey(), - lastCommit: CreateBlockCommit(block2)); - _blockChain.Append(block3, CreateBlockCommit(block3)); - - var forked = _blockChain.Fork(block2.Hash); - Assert.NotNull(forked.GetBlockCommit(forked.Tip.Index)); - Assert.Equal( - forked.GetBlockCommit(forked.Tip.Index), - block3.LastCommit); - } - [SkippableFact] public void CleanupBlockCommitStore() { @@ -1091,19 +648,6 @@ public void CleanupBlockCommitStore() Assert.Equal(blockCommit3, _blockChain.Store.GetBlockCommit(blockCommit3.BlockHash)); } - [SkippableTheory] - [InlineData(true)] - [InlineData(false)] - public void CannotSwapForSameHeightTip(bool render) - { - BlockChain fork = _blockChain.Fork(_blockChain.Tip.Hash); - IReadOnlyList prevRecords = _renderer.Records; - Assert.Throws(() => _blockChain.Swap(fork, render: render)()); - - // Render methods should be invoked if and only if the tip changes - Assert.Equal(prevRecords, _renderer.Records); - } - [SkippableTheory] [InlineData(true)] [InlineData(false)] @@ -1131,16 +675,22 @@ public void ReorgIsUnableToHeterogenousChain(bool render) for (int i = 0; i < 5; i++) { Block block1 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); + key, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, key)); _blockChain.Append(block1, CreateBlockCommit(block1)); Block block2 = chain2.ProposeBlock( - key, lastCommit: CreateBlockCommit(chain2.Tip)); + key, + lastCommit: CreateBlockCommit(chain2.Tip), + proof: CreateZeroRoundProof(chain2.Tip, key)); chain2.Append(block2, CreateBlockCommit(block2)); } Block extraBlock2 = chain2.ProposeBlock( - key, lastCommit: CreateBlockCommit(chain2.Tip)); + key, + lastCommit: CreateBlockCommit(chain2.Tip), + proof: CreateZeroRoundProof(chain2.Tip, key)); chain2.Append(extraBlock2, CreateBlockCommit(extraBlock2)); Log.Logger.CompareBothChains( @@ -1238,6 +788,7 @@ public void GetStateOnlyDrillsDownUntilRequestedAddressesAreFound() _fx.Proposer, txs.ToImmutableList(), CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, _fx.Proposer), ImmutableArray.Empty); chain.Append(b, CreateBlockCommit(b)); } @@ -1283,7 +834,10 @@ public void GetStateReturnsEarlyForNonexistentAccount() Block b = chain.Genesis; for (int i = 0; i < 20; ++i) { - b = chain.ProposeBlock(_fx.Proposer, CreateBlockCommit(chain.Tip)); + b = chain.ProposeBlock( + _fx.Proposer, + CreateBlockCommit(chain.Tip), + CreateZeroRoundProof(chain.Tip, _fx.Proposer)); chain.Append(b, CreateBlockCommit(b)); } @@ -1303,65 +857,6 @@ public void GetStateReturnsEarlyForNonexistentAccount() ); } - [SkippableFact] - public void GetStateReturnsValidStateAfterFork() - { - var privateKey = new PrivateKey(); - var store = new MemoryStore(); - var stateStore = - new TrieStateStore(new MemoryKeyValueStore()); - var actionLoader = new SingleActionLoader(typeof(DumbAction)); - var chain = MakeBlockChain( - new NullBlockPolicy(), - store, - stateStore, - actionLoader, - new[] { DumbAction.Create((_fx.Address1, "item0.0")) }); - Assert.Equal( - "item0.0", - (Text)chain - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(_fx.Address1)); - - chain.MakeTransaction( - privateKey, - new[] { DumbAction.Create((_fx.Address1, "item1.0")), } - ); - Block block = chain.ProposeBlock(new PrivateKey()); - - chain.Append(block, CreateBlockCommit(block)); - Assert.Equal( - new IValue[] { (Text)"item0.0,item1.0" }, - chain - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetStates(new[] { _fx.Address1 }) - ); - Assert.Equal( - "item0.0,item1.0", - (Text)chain - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(_fx.Address1)); - - var forked = chain.Fork(chain.Tip.Hash); - Assert.Equal(2, forked.Count); - Assert.Equal( - new IValue[] { (Text)"item0.0,item1.0" }, - forked - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetStates(new[] { _fx.Address1 }) - ); - Assert.Equal( - "item0.0,item1.0", - (Text)forked - .GetNextWorldState() - .GetAccountState(ReservedAddresses.LegacyAccount) - .GetState(_fx.Address1)); - } - [SkippableFact] public void GetStateReturnsLatestStatesWhenMultipleAddresses() { @@ -1402,7 +897,9 @@ public void GetStateReturnsLatestStatesWhenMultipleAddresses() } Block block1 = chain.ProposeBlock( - privateKeys[0], lastCommit: CreateBlockCommit(chain.Tip)); + privateKeys[0], + lastCommit: CreateBlockCommit(chain.Tip), + proof: CreateZeroRoundProof(chain.Tip, privateKeys[0])); chain.Append(block1, CreateBlockCommit(block1)); @@ -1424,7 +921,9 @@ public void GetStateReturnsLatestStatesWhenMultipleAddresses() chain.MakeTransaction(privateKeys[0], new[] { DumbAction.Create((addresses[0], "2")) }); Block block2 = chain.ProposeBlock( - privateKeys[0], lastCommit: CreateBlockCommit(chain.Tip)); + privateKeys[0], + lastCommit: CreateBlockCommit(chain.Tip), + proof: CreateZeroRoundProof(chain.Tip, privateKeys[0])); chain.Append(block2, CreateBlockCommit(block2)); Assert.Equal( (Text)"1,2", @@ -1441,76 +940,6 @@ public void GetStateReturnsLatestStatesWhenMultipleAddresses() ); } - [SkippableFact] - public void FindBranchPoint() - { - var key = new PrivateKey(); - Block b1 = _blockChain.ProposeBlock(key); - _blockChain.Append(b1, CreateBlockCommit(b1)); - Block b2 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(b2, CreateBlockCommit(b2)); - Block b3 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(b3, CreateBlockCommit(b3)); - Block b4 = _blockChain.ProposeBlock( - key, lastCommit: CreateBlockCommit(_blockChain.Tip)); - _blockChain.Append(b4, CreateBlockCommit(b4)); - - Assert.Equal(b1.PreviousHash, _blockChain.Genesis.Hash); - - var emptyLocator = new BlockLocator(new[] { _blockChain.Genesis.Hash }); - var invalidLocator = new BlockLocator( - new[] { new BlockHash(TestUtils.GetRandomBytes(BlockHash.Size)) }); - var locator = new BlockLocator( - new[] { b4.Hash, b3.Hash, b1.Hash, _blockChain.Genesis.Hash }); - - using (var emptyFx = new MemoryStoreFixture(_policy.PolicyActionsRegistry)) - using (var forkFx = new MemoryStoreFixture(_policy.PolicyActionsRegistry)) - { - var emptyChain = BlockChain.Create( - _blockChain.Policy, - new VolatileStagePolicy(), - emptyFx.Store, - emptyFx.StateStore, - emptyFx.GenesisBlock, - new ActionEvaluator( - _blockChain.Policy.PolicyActionsRegistry, - stateStore: emptyFx.StateStore, - actionTypeLoader: new SingleActionLoader(typeof(DumbAction)))); - var fork = BlockChain.Create( - _blockChain.Policy, - new VolatileStagePolicy(), - forkFx.Store, - forkFx.StateStore, - forkFx.GenesisBlock, - new ActionEvaluator( - _blockChain.Policy.PolicyActionsRegistry, - stateStore: forkFx.StateStore, - actionTypeLoader: new SingleActionLoader(typeof(DumbAction)))); - fork.Append(b1, CreateBlockCommit(b1)); - fork.Append(b2, CreateBlockCommit(b2)); - Block b5 = fork.ProposeBlock( - key, lastCommit: CreateBlockCommit(fork.Tip)); - fork.Append(b5, CreateBlockCommit(b5)); - - // Testing emptyChain - Assert.Equal(_blockChain.Genesis.Hash, emptyChain.FindBranchpoint(emptyLocator)); - Assert.Equal(_blockChain.Genesis.Hash, emptyChain.FindBranchpoint(locator)); - Assert.Null(emptyChain.FindBranchpoint(invalidLocator)); - - // Testing _blockChain - Assert.Equal(_blockChain.Genesis.Hash, _blockChain.FindBranchpoint(emptyLocator)); - Assert.Equal(b4.Hash, _blockChain.FindBranchpoint(locator)); - Assert.Null(_blockChain.FindBranchpoint(invalidLocator)); - - // Testing fork - Assert.Equal(_blockChain.Genesis.Hash, fork.FindBranchpoint(emptyLocator)); - Assert.Equal(b1.Hash, fork.FindBranchpoint(locator)); - Assert.Null(_blockChain.FindBranchpoint(invalidLocator)); - } - } - [SkippableFact] public void GetNextTxNonce() { @@ -1530,6 +959,7 @@ public void GetNextTxNonce() _fx.Proposer, txsA.ToImmutableList(), CreateBlockCommit(_blockChain.Tip), + CreateZeroRoundProof(_blockChain.Tip, _fx.Proposer), ImmutableArray.Empty); _blockChain.Append(b1, CreateBlockCommit(b1)); @@ -1597,7 +1027,9 @@ public void GetNextTxNonceWithStaleTx() }; StageTransactions(txs); - Block block = _blockChain.ProposeBlock(privateKey); + Block block = _blockChain.ProposeBlock( + privateKey, + proof: CreateZeroRoundProof(_blockChain.Tip, privateKey)); _blockChain.Append(block, CreateBlockCommit(block)); Transaction[] staleTxs = @@ -1634,7 +1066,8 @@ IReadOnlyList txs txs, blockInterval: TimeSpan.FromSeconds(10), miner: _fx.Proposer.PublicKey, - lastCommit: CreateBlockCommit(block)), + lastCommit: CreateBlockCommit(block), + proof: CreateZeroRoundProof(block, _fx.Proposer)), _fx.Proposer); Transaction[] txsA = @@ -1769,13 +1202,19 @@ public void BlockActionWithMultipleAddress() var rewardRecordAddress = MinerReward.RewardRecordAddress; Block block1 = _blockChain.ProposeBlock( - miner1, lastCommit: CreateBlockCommit(_blockChain.Tip)); + miner1, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, miner1)); _blockChain.Append(block1, CreateBlockCommit(block1)); Block block2 = _blockChain.ProposeBlock( - miner1, lastCommit: CreateBlockCommit(_blockChain.Tip)); + miner1, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, miner1)); _blockChain.Append(block2, CreateBlockCommit(block2)); Block block3 = _blockChain.ProposeBlock( - miner2, lastCommit: CreateBlockCommit(_blockChain.Tip)); + miner2, + lastCommit: CreateBlockCommit(_blockChain.Tip), + proof: CreateZeroRoundProof(_blockChain.Tip, miner2)); _blockChain.Append(block3, CreateBlockCommit(block3)); IValue miner1state = _blockChain @@ -1897,7 +1336,8 @@ void BuildIndex(Guid id, Block block) new[] { tx }, blockInterval: TimeSpan.FromSeconds(10), miner: GenesisProposer.PublicKey, - lastCommit: CreateBlockCommit(b)), + lastCommit: CreateBlockCommit(b), + proof: CreateZeroRoundProof(b, GenesisProposer)), GenesisProposer); var evals = actionEvaluator.EvaluateBlock(b, previousState); @@ -2008,7 +1448,10 @@ private void TipChanged() _renderer.ResetRecords(); Assert.Empty(_renderer.BlockRecords); - Block block = _blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + Block block = _blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); _blockChain.Append(block, CreateBlockCommit(block)); IReadOnlyList records = _renderer.BlockRecords; Assert.Equal(2, records.Count); @@ -2163,7 +1606,9 @@ private void FilterLowerNonceTxAfterStaging() nonce: nonce, privateKey: privateKey, timestamp: DateTimeOffset.Now)) .ToArray(); StageTransactions(txsA); - Block b1 = _blockChain.ProposeBlock(privateKey); + Block b1 = _blockChain.ProposeBlock( + privateKey, + proof: CreateZeroRoundProof(_blockChain.Tip, privateKey)); _blockChain.Append(b1, CreateBlockCommit(b1)); Assert.Equal( txsA, @@ -2235,7 +1680,8 @@ private void CheckIfTxPolicyExceptionHasInnerException() previousBlock: chain.Genesis, miner: GenesisProposer, txs: new[] { blockTx }, - stateRootHash: (HashDigest)nextStateRootHash); + stateRootHash: (HashDigest)nextStateRootHash, + proof: CreateZeroRoundProof(chain.Genesis, GenesisProposer)); var e = Assert.Throws( () => chain.Append(block, CreateBlockCommit(block))); @@ -2301,7 +1747,10 @@ private void ValidateNextBlockCommitOnValidatorSetChange() new Validator(newValidatorPrivateKey.PublicKey, BigInteger.One)), } ); - var newBlock = blockChain.ProposeBlock(new PrivateKey()); + var proposer = new PrivateKey(); + var newBlock = blockChain.ProposeBlock( + proposer, + proof: CreateZeroRoundProof(blockChain.Tip, proposer)); var newBlockCommit = new BlockCommit( newBlock.Index, 0, newBlock.Hash, ValidatorPrivateKeys.Select( pk => new VoteMetadata( @@ -2323,8 +1772,11 @@ private void ValidateNextBlockCommitOnValidatorSetChange() new SetValidator(new Validator(new PrivateKey().PublicKey, BigInteger.One)), } ); + proposer = new PrivateKey(); var nextBlock = blockChain.ProposeBlock( - new PrivateKey(), lastCommit: newBlockCommit); + proposer, + lastCommit: newBlockCommit, + proof: CreateZeroRoundProof(_blockChain.Tip, proposer)); var nextBlockCommit = new BlockCommit( nextBlock.Index, 0, @@ -2350,8 +1802,11 @@ private void ValidateNextBlockCommitOnValidatorSetChange() new SetValidator(new Validator(new PrivateKey().PublicKey, BigInteger.One)), } ); + proposer = new PrivateKey(); var invalidCommitBlock = blockChain.ProposeBlock( - new PrivateKey(), lastCommit: nextBlockCommit); + proposer, + lastCommit: nextBlockCommit, + proof: CreateZeroRoundProof(blockChain.Tip, proposer)); Assert.Throws( () => blockChain.Append( diff --git a/test/Libplanet.Tests/Blocks/BlockContentTest.cs b/test/Libplanet.Tests/Blocks/BlockContentTest.cs index f620ebb5f11..d90db048aa6 100644 --- a/test/Libplanet.Tests/Blocks/BlockContentTest.cs +++ b/test/Libplanet.Tests/Blocks/BlockContentTest.cs @@ -86,6 +86,7 @@ public void Transactions() previousHash: Block1Content.PreviousHash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: null, evidenceHash: null), transactions: txs, evidence: evs); @@ -124,6 +125,7 @@ public void TransactionsWithDuplicateNonce() previousHash: Block1Content.PreviousHash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: null, evidenceHash: null), transactions: txs, evidence: evs)); @@ -162,6 +164,7 @@ public void TransactionsWithMissingNonce() previousHash: Block1Content.PreviousHash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: null, evidenceHash: null), transactions: txs, evidence: evs)); @@ -203,6 +206,7 @@ public void TransactionsWithInconsistentGenesisHashes() previousHash: Block1Content.PreviousHash, txHash: BlockContent.DeriveTxHash(inconsistentTxs), lastCommit: null, + proof: null, evidenceHash: null), transactions: inconsistentTxs, evidence: evs)); diff --git a/test/Libplanet.Tests/Blocks/BlockMetadataExtensionsTest.cs b/test/Libplanet.Tests/Blocks/BlockMetadataExtensionsTest.cs index 4a8bd992a63..9e4e870d531 100644 --- a/test/Libplanet.Tests/Blocks/BlockMetadataExtensionsTest.cs +++ b/test/Libplanet.Tests/Blocks/BlockMetadataExtensionsTest.cs @@ -21,6 +21,7 @@ public void ValidateTimestamp() previousHash: null, txHash: null, lastCommit: null, + proof: null, evidenceHash: null); Assert.Throws(() => metadata.ValidateTimestamp(now)); diff --git a/test/Libplanet.Tests/Blocks/BlockMetadataTest.cs b/test/Libplanet.Tests/Blocks/BlockMetadataTest.cs index 1f1810421f6..0a19c85874b 100644 --- a/test/Libplanet.Tests/Blocks/BlockMetadataTest.cs +++ b/test/Libplanet.Tests/Blocks/BlockMetadataTest.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Security.Cryptography; using Libplanet.Common; +using Libplanet.Consensus; using Libplanet.Crypto; using Libplanet.Tests.Fixtures; using Libplanet.Types.Blocks; @@ -45,6 +46,7 @@ public void ProtocolVersion() previousHash: Block1Metadata.PreviousHash, txHash: Block1Metadata.TxHash, lastCommit: null, + proof: null, evidenceHash: null)); Assert.Throws( () => new BlockMetadata( @@ -56,6 +58,7 @@ public void ProtocolVersion() previousHash: Block1Metadata.PreviousHash, txHash: Block1Metadata.TxHash, lastCommit: null, + proof: null, evidenceHash: null)); } @@ -69,6 +72,7 @@ public void Index() previousHash: Block1Metadata.PreviousHash, txHash: Block1Metadata.TxHash, lastCommit: null, + proof: null, evidenceHash: null)); } @@ -86,6 +90,7 @@ public void Timestamp() previousHash: Block1Metadata.PreviousHash, txHash: Block1Metadata.TxHash, lastCommit: null, + proof: null, evidenceHash: null); Assert.Equal(TimeSpan.Zero, metadata.Timestamp.Offset); Assert.Equal( @@ -104,6 +109,7 @@ public void PreviousHash() previousHash: Block1Metadata.PreviousHash, txHash: GenesisMetadata.TxHash, lastCommit: null, + proof: null, evidenceHash: null)); Assert.Throws(() => new BlockMetadata( index: Block1Metadata.Index, @@ -112,6 +118,7 @@ public void PreviousHash() previousHash: null, txHash: Block1Metadata.TxHash, lastCommit: null, + proof: null, evidenceHash: null)); } @@ -131,6 +138,12 @@ public void MakeCandidateData() "transaction_fingerprint", ParseHex("3d8e87977b1142863435b9385657e69557df8951a0698e9719f7d06c5fb8db1f") ) + .Add( + "proof", + ParseHex("0224313f215eb0c9d511f43a4aa55c5df1cd5dffbc29197f1dc7b936aa5a" + + "30813d88ad81348df92917b8fd2e88b513d3c4381e554c41f91f201fe07a7c6967718" + + "03b0740d363ab3e15a76823b092d2f245f288291bb01063fecd2298011449668b") + ) .Add( "evidence_hash", ParseHex("bd1b6bc740c7d74fe39f8c78dd6860b7b5bf9c58336a703a583a5a59651a4af3") @@ -154,6 +167,13 @@ public void MakeCandidateData() ParseHex("654698d34b6d9a55b0c93e4ffb2639278324868c91965bc5f96cb3071d6903a0") ) .Add("protocol_version", BlockMetadata.CurrentProtocolVersion) + .Add( + "proof", + ParseHex("02b5de417ca459f8ab048625c55daf95fb47f7a9a41de80dcbbb0a454247" + + "e315ee845dda470812bf8b16741a8aacf7702c57d74e35c9e751089ee478d35a4fdcc" + + "7e150b5880ff57799bb7cd64fff33f99b663e98156f698d1590a472c520ffa9c8" + ) + ) .Add( "evidence_hash", ParseHex("e7198889cc4a82a8b7be4b7f428b6201400ef222709f756e540b32bc1e8d5d86") @@ -207,7 +227,7 @@ public void DerivePreEvaluationHash() HashDigest hash = GenesisMetadata.DerivePreEvaluationHash(); AssertBytesEqual( - FromHex("9ff328716814fed03de454dfc0a9d9aeb0077ad0c393513bf29895a45ded13aa"), + FromHex("b96b3a3bcb0afe52fb4b90a9bc00678dd18b4c4adf3a38099ebc1fd7b8fca4c8"), hash.ByteArray); } @@ -241,6 +261,7 @@ public void ValidateLastCommit() previousHash: blockHash, txHash: null, lastCommit: invalidHeightLastCommit, + proof: new ConsensusInformation(2L, 0, null).Prove(validatorA), evidenceHash: null)); // BlockHash of the last commit is invalid. @@ -263,6 +284,7 @@ public void ValidateLastCommit() previousHash: GenesisHash, txHash: null, lastCommit: invalidBlockHashLastCommit, + proof: new ConsensusInformation(2L, 0, null).Prove(validatorA), evidenceHash: null)); var validLastCommit = new BlockCommit( @@ -292,6 +314,7 @@ public void ValidateLastCommit() previousHash: blockHash, txHash: null, lastCommit: validLastCommit, + proof: new ConsensusInformation(2L, 0, null).Prove(validatorA), evidenceHash: null); } diff --git a/test/Libplanet.Tests/Blocks/PreEvaluationBlockHeaderTest.cs b/test/Libplanet.Tests/Blocks/PreEvaluationBlockHeaderTest.cs index e7cea982f7d..333a6e98f60 100644 --- a/test/Libplanet.Tests/Blocks/PreEvaluationBlockHeaderTest.cs +++ b/test/Libplanet.Tests/Blocks/PreEvaluationBlockHeaderTest.cs @@ -71,6 +71,12 @@ public void MakeCandidateData() "transaction_fingerprint", ParseHex("3d8e87977b1142863435b9385657e69557df8951a0698e9719f7d06c5fb8db1f") ) + .Add( + "proof", + ParseHex("0224313f215eb0c9d511f43a4aa55c5df1cd5dffbc29197f1dc7b936aa5a" + + "30813d88ad81348df92917b8fd2e88b513d3c4381e554c41f91f201fe07a7c6967718" + + "03b0740d363ab3e15a76823b092d2f245f288291bb01063fecd2298011449668b") + ) .Add( "evidence_hash", ParseHex("bd1b6bc740c7d74fe39f8c78dd6860b7b5bf9c58336a703a583a5a59651a4af3") @@ -105,6 +111,13 @@ public void MakeCandidateData() "654698d34b6d9a55b0c93e4ffb2639278324868c91965bc5f96cb3071d6903a0" ) ) + .Add( + "proof", + ParseHex("02b5de417ca459f8ab048625c55daf95fb47f7a9a41de80dcbbb0a454247" + + "e315ee845dda470812bf8b16741a8aacf7702c57d74e35c9e751089ee478d35a4fdcc" + + "7e150b5880ff57799bb7cd64fff33f99b663e98156f698d1590a472c520ffa9c8" + ) + ) .Add( "evidence_hash", ParseHex( @@ -181,8 +194,8 @@ public void VerifySignature() // Same as block1.MakeSignature(_contents.Block1Key, arbitraryHash) ImmutableArray validSig = ByteUtil.ParseHexToImmutable( - "3045022100f975e902971092f16dbbb1fe6b7c956de648a8cd62346dbadc07e5fca4ce3" + - "07a02200987a349f0763efd0448659ed66c6bd0ad0971dd57fbb89c672aed592fbd70d6"); + "3045022100bc8efa3cff74cea1da682c815d35c701d181bf13129734336d260fe14ed75" + + "2940220305f9c1f6ecfb7880acccb56b9886ace22c7b4cac1ad78bc2077ab53d5efd160"); AssertBytesEqual( validSig, block1.MakeSignature(_contents.Block1Key, arbitraryHash)); @@ -215,22 +228,22 @@ public void DeriveBlockHash() _contents.GenesisMetadata, _contents.GenesisMetadata.DerivePreEvaluationHash()); AssertBytesEqual( - fromHex("d790aa3b2d985d58e6fe6547336ca9d2bfdb749a27cd58a17dbfd0c6880da8e3"), + fromHex("74b76fccf910cf6dbdf9e0ee63d096122763d025152691bbd3093f0cc0b61d7a"), genesis.DeriveBlockHash(default, null) ); AssertBytesEqual( - fromHex("47b5227dfdd99af4faf9ae9e82ef3b615063179d275081eae4c122685bbf7dcb"), + fromHex("af99a7832c3e1fdd880b7c69e3db9a101f57c0ac2370e26b2f30e8bbb7f1d0d0"), genesis.DeriveBlockHash( default, genesis.MakeSignature(_contents.GenesisKey, default) ) ); AssertBytesEqual( - fromHex("2c45bb52e4c7d79caa28da9b63ec0f492262836c975bfa5bb27f62e96f2aad99"), + fromHex("061f08d4f8e52e00647191c64d21c74d43b26256ca2b5ce14b80fc430d390c6a"), genesis.DeriveBlockHash(arbitraryHash, null) ); AssertBytesEqual( - fromHex("e985fcdce3f73dee90a4eaec9399283f856bb6f9326e4300bbe1d6126ff7ad55"), + fromHex("b2fa69b10237a2f3ae1fe51050418b445a40ffc9e9ea1b7f474cfee134e84ce8"), genesis.DeriveBlockHash( arbitraryHash, genesis.MakeSignature(_contents.GenesisKey, arbitraryHash)) @@ -240,19 +253,19 @@ public void DeriveBlockHash() _contents.Block1Metadata, _contents.Block1Metadata.DerivePreEvaluationHash()); AssertBytesEqual( - fromHex("ade696a646c9e4321cc90160807cba3d15d7cd28556d2dfb4103e8730a46038c"), + fromHex("007908f9ca1335ec065c8302a6627e97da6fa627dc049f44b970d02b20898f02"), block1.DeriveBlockHash(default, null) ); AssertBytesEqual( - fromHex("b3157a151d2168653e21ffc850f9d1a96bca6310275cccbeb9bd705f67c2e1c9"), + fromHex("61bdf03b8ff2666d813352259c6d454eed8268c1386ea749b3a557e03e33919a"), block1.DeriveBlockHash(default, block1.MakeSignature(_contents.Block1Key, default)) ); AssertBytesEqual( - fromHex("3fd4ee37ed2fc5dae5a9533984f06b3975e176bdaa70689a3c14acd8b4ea384d"), + fromHex("7dc612d7b0579135ed8db8b9a4861386be97c05fa343a3e4233f535c0a2ffb26"), block1.DeriveBlockHash(arbitraryHash, null) ); AssertBytesEqual( - fromHex("83ceb4d1e5bbc385daaebfd044a5e4ba65bf1d8b63ef0aabe4d68fc5642b4516"), + fromHex("f7de4a0462dde48da9b57de8a642f71376ae706f2a58ca28ec2dce2d50641e27"), block1.DeriveBlockHash( arbitraryHash, block1.MakeSignature(_contents.Block1Key, arbitraryHash) ) diff --git a/test/Libplanet.Tests/Blocks/PreEvaluationBlockTest.cs b/test/Libplanet.Tests/Blocks/PreEvaluationBlockTest.cs index 84b2388742b..345f0f54a5f 100644 --- a/test/Libplanet.Tests/Blocks/PreEvaluationBlockTest.cs +++ b/test/Libplanet.Tests/Blocks/PreEvaluationBlockTest.cs @@ -85,6 +85,7 @@ public void Evaluate() previousHash: genesis.Hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: _contents.Block1Content.Proof, evidenceHash: null), transactions: txs, evidence: evs); @@ -161,6 +162,7 @@ public void DetermineStateRootHash() previousHash: genesis.Hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: _contents.Block1Content.Proof, evidenceHash: null), transactions: txs, evidence: evs); diff --git a/test/Libplanet.Tests/Consensus/ConsensusInformationTest.cs b/test/Libplanet.Tests/Consensus/ConsensusInformationTest.cs new file mode 100644 index 00000000000..ecd384daf8a --- /dev/null +++ b/test/Libplanet.Tests/Consensus/ConsensusInformationTest.cs @@ -0,0 +1,112 @@ +using System; +using Libplanet.Consensus; +using Libplanet.Crypto; +using Xunit; + +namespace Libplanet.Tests.Consensus +{ + public class ConsensusInformationTest + { + private readonly ConsensusInformation _genesisConsensusInformation; + private readonly PrivateKey _genesisProver; + private readonly Proof _genesisProof; + + private readonly ConsensusInformation _consensusInformation; + + public ConsensusInformationTest() + { + _genesisProver = new PrivateKey(); + _genesisConsensusInformation = new ConsensusInformation(0, 0, null); + _genesisProof = _genesisConsensusInformation.Prove(_genesisProver); + _consensusInformation = new ConsensusInformation(1, 2, _genesisProof); + } + + [Fact] + public void Constructor() + { + Assert.Equal(0, _genesisConsensusInformation.Height); + Assert.Equal(0, _genesisConsensusInformation.Round); + Assert.Null(_genesisConsensusInformation.LastProof); + + Assert.Throws(() => new ConsensusInformation(-1, 0, null)); + Assert.Throws(() => new ConsensusInformation(0, -2, null)); + + var ci = new ConsensusInformation(1, 2, _genesisProof); + Assert.Equal(1, ci.Height); + Assert.Equal(2, ci.Round); + Assert.Equal(_genesisProof, ci.LastProof); + Assert.Equal(ci, new ConsensusInformation(ci.Encoded)); + } + + [Fact] + public void CannotProveOrVerifyWithNegativeRound() + { + var prover = new PrivateKey(); + var negativeRoundConsensusInformation = new ConsensusInformation(0, -1, _genesisProof); + Assert.Throws( + () => negativeRoundConsensusInformation.Prove(prover)); + + var negativeRoundProof = prover.Prove(negativeRoundConsensusInformation.Encoded); + Assert.Throws( + () => negativeRoundConsensusInformation.Verify( + negativeRoundProof, prover.PublicKey)); + } + + [Fact] + public void ProveAndVerify() + { + var prover = new PrivateKey(); + var nonProver = new PrivateKey(); + var proof = _consensusInformation.Prove(prover); + Assert.True(_consensusInformation.Verify(proof, prover.PublicKey)); + Assert.False(_consensusInformation.Verify(proof, nonProver.PublicKey)); + } + + [Fact] + public void ToLot() + { + var prover = new PrivateKey(); + var lot = _consensusInformation.ToLot(prover); + Assert.Equal(_consensusInformation, lot.ConsensusInformation); + Assert.Equal(prover.PublicKey, lot.PublicKey); + Assert.Equal(_consensusInformation.Prove(prover), lot.Proof); + Assert.True(_consensusInformation.Verify(lot.Proof, prover.PublicKey)); + } + + [Fact] + public void Equal() + { + var differentProof = _genesisConsensusInformation.Prove(new PrivateKey()); + Assert.Equal( + _consensusInformation, + new ConsensusInformation(1, 2, _genesisProof)); + Assert.NotEqual( + _consensusInformation, + new ConsensusInformation(2, 2, _genesisProof)); + Assert.NotEqual( + _consensusInformation, + new ConsensusInformation(1, 3, _genesisProof)); + Assert.NotEqual( + _consensusInformation, + new ConsensusInformation(1, 2, differentProof)); + } + + [Fact] + public void HashCode() + { + var differentProof = _genesisConsensusInformation.Prove(new PrivateKey()); + Assert.Equal( + _consensusInformation.GetHashCode(), + new ConsensusInformation(1, 2, _genesisProof).GetHashCode()); + Assert.NotEqual( + _consensusInformation.GetHashCode(), + new ConsensusInformation(2, 2, _genesisProof).GetHashCode()); + Assert.NotEqual( + _consensusInformation.GetHashCode(), + new ConsensusInformation(1, 3, _genesisProof).GetHashCode()); + Assert.NotEqual( + _consensusInformation.GetHashCode(), + new ConsensusInformation(1, 2, differentProof).GetHashCode()); + } + } +} diff --git a/test/Libplanet.Tests/Consensus/DominantLotMetadataTest.cs b/test/Libplanet.Tests/Consensus/DominantLotMetadataTest.cs new file mode 100644 index 00000000000..910e0ddffaa --- /dev/null +++ b/test/Libplanet.Tests/Consensus/DominantLotMetadataTest.cs @@ -0,0 +1,94 @@ +using System; +using Libplanet.Consensus; +using Libplanet.Crypto; +using Xunit; + +namespace Libplanet.Tests.Consensus +{ + public class DominantLotMetadataTest + { + private readonly Lot _lot; + private readonly PrivateKey _prover; + private readonly PrivateKey _signer; + private readonly DominantLotMetadata _dominantLotMetadata; + + public DominantLotMetadataTest() + { + _prover = new PrivateKey(); + _signer = new PrivateKey(); + _lot = new ConsensusInformation(0, 0, null).ToLot(_prover); + _dominantLotMetadata = new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, _signer.PublicKey); + } + + [Fact] + public void Constructor() + { + var dominantLotMetadata = new DominantLotMetadata( + _lot, DateTimeOffset.UtcNow, _signer.PublicKey); + Assert.Equal(0, dominantLotMetadata.Height); + Assert.Equal(0, dominantLotMetadata.Round); + Assert.Equal(_lot, dominantLotMetadata.Lot); + Assert.Equal(_signer.PublicKey, dominantLotMetadata.ValidatorPublicKey); + Assert.Equal( + dominantLotMetadata, new DominantLotMetadata(dominantLotMetadata.Bencoded)); + + // DominantLotMetadata can be generated by non-prover. + Assert.NotEqual(_prover.PublicKey, dominantLotMetadata.ValidatorPublicKey); + } + + [Fact] + public void Sign() + { + var stranger = new PrivateKey(); + _dominantLotMetadata.Sign(_signer); + Assert.Throws(() => _dominantLotMetadata.Sign(stranger)); + } + + [Fact] + public void Equal() + { + Assert.Equal( + _dominantLotMetadata, + new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, _signer.PublicKey)); + Assert.NotEqual( + _dominantLotMetadata, + new DominantLotMetadata( + new ConsensusInformation(0, 0, null).ToLot(new PrivateKey()), + DateTimeOffset.MinValue, + _signer.PublicKey)); + Assert.NotEqual( + _dominantLotMetadata, + new DominantLotMetadata( + _lot, DateTimeOffset.MaxValue, _signer.PublicKey)); + Assert.NotEqual( + _dominantLotMetadata, + new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, new PrivateKey().PublicKey)); + } + + [Fact] + public void HashCode() + { + Assert.Equal( + _dominantLotMetadata.GetHashCode(), + new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, _signer.PublicKey).GetHashCode()); + Assert.NotEqual( + _dominantLotMetadata.GetHashCode(), + new DominantLotMetadata( + new ConsensusInformation(0, 0, null).ToLot(new PrivateKey()), + DateTimeOffset.MinValue, + _signer.PublicKey).GetHashCode()); + Assert.NotEqual( + _dominantLotMetadata.GetHashCode(), + new DominantLotMetadata( + _lot, DateTimeOffset.MaxValue, _signer.PublicKey).GetHashCode()); + Assert.NotEqual( + _dominantLotMetadata.GetHashCode(), + new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, new PrivateKey().PublicKey).GetHashCode()); + } + } +} diff --git a/test/Libplanet.Tests/Consensus/DominantLotTest.cs b/test/Libplanet.Tests/Consensus/DominantLotTest.cs new file mode 100644 index 00000000000..a88e7295a32 --- /dev/null +++ b/test/Libplanet.Tests/Consensus/DominantLotTest.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Immutable; +using Libplanet.Consensus; +using Libplanet.Crypto; +using Xunit; + +namespace Libplanet.Tests.Consensus +{ + public class DominantLotTest + { + private readonly Lot _lot; + private readonly PrivateKey _prover; + private readonly PrivateKey _signer; + private readonly DominantLotMetadata _dominantLotMetadata; + private readonly DominantLot _dominantLot; + + public DominantLotTest() + { + _prover = new PrivateKey(); + _signer = new PrivateKey(); + _lot = new ConsensusInformation(0, 0, null).ToLot(_prover); + _dominantLotMetadata = new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, _signer.PublicKey); + _dominantLot = _dominantLotMetadata.Sign(_signer); + } + + [Fact] + public void Constructor() + { + Assert.Throws( + () => new DominantLot( + _dominantLotMetadata, + ImmutableArray.Empty)); + Assert.Throws( + () => new DominantLot( + _dominantLotMetadata, + ImmutableArray.Create(0x00, 0x00))); + Assert.Throws( + () => new DominantLot( + _dominantLotMetadata, + new PrivateKey().Sign(_dominantLotMetadata.ByteArray))); + var signature = _signer.Sign(_dominantLotMetadata.ByteArray); + var dominantLot = new DominantLot(_dominantLotMetadata, signature); + Assert.Equal(_dominantLot, dominantLot); + Assert.Equal(_dominantLotMetadata.Height, dominantLot.Height); + Assert.Equal(_dominantLotMetadata.Round, dominantLot.Round); + Assert.Equal(_dominantLotMetadata.Lot, dominantLot.Lot); + Assert.Equal(_dominantLotMetadata.Timestamp, dominantLot.Timestamp); + Assert.Equal(_signer.PublicKey, dominantLot.ValidatorPublicKey); + Assert.Equal(signature, dominantLot.Signature); + Assert.Equal(_dominantLot, new DominantLot(_dominantLot.Bencoded)); + Assert.Equal(_dominantLot, new DominantLot(_dominantLot.ByteArray)); + + // DominantLotMetadata can be generated by non-prover. + Assert.NotEqual(_prover.PublicKey, _dominantLot.ValidatorPublicKey); + } + + [Fact] + public void Equal() + { + Assert.Equal( + _dominantLot, + _dominantLotMetadata + .Sign(_signer)); + Assert.NotEqual( + _dominantLot, + new DominantLotMetadata( + new ConsensusInformation(0, 0, null).ToLot(new PrivateKey()), + DateTimeOffset.MinValue, + _signer.PublicKey) + .Sign(_signer)); + Assert.NotEqual( + _dominantLot, + new DominantLotMetadata( + _lot, DateTimeOffset.MaxValue, _signer.PublicKey) + .Sign(_signer)); + var stranger = new PrivateKey(); + Assert.NotEqual( + _dominantLot, + new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, stranger.PublicKey) + .Sign(stranger)); + } + + [Fact] + public void HashCode() + { + Assert.Equal( + _dominantLot.GetHashCode(), + _dominantLotMetadata + .Sign(_signer).GetHashCode()); + Assert.NotEqual( + _dominantLot.GetHashCode(), + new DominantLotMetadata( + new ConsensusInformation(0, 0, null).ToLot(new PrivateKey()), + DateTimeOffset.MinValue, + _signer.PublicKey) + .Sign(_signer).GetHashCode()); + Assert.NotEqual( + _dominantLot.GetHashCode(), + new DominantLotMetadata( + _lot, DateTimeOffset.MaxValue, _signer.PublicKey) + .Sign(_signer).GetHashCode()); + var stranger = new PrivateKey(); + Assert.NotEqual( + _dominantLot.GetHashCode(), + new DominantLotMetadata( + _lot, DateTimeOffset.MinValue, stranger.PublicKey) + .Sign(stranger).GetHashCode()); + } + } +} diff --git a/test/Libplanet.Tests/Consensus/LotTest.cs b/test/Libplanet.Tests/Consensus/LotTest.cs new file mode 100644 index 00000000000..80a246f9c17 --- /dev/null +++ b/test/Libplanet.Tests/Consensus/LotTest.cs @@ -0,0 +1,75 @@ +using System; +using Libplanet.Consensus; +using Libplanet.Crypto; +using Xunit; + +namespace Libplanet.Tests.Consensus +{ + public class LotTest + { + private readonly ConsensusInformation _genesisConsensusInformation; + private readonly PrivateKey _genesisProver; + private readonly Proof _genesisProof; + + private readonly ConsensusInformation _consensusInformation; + private readonly PrivateKey _prover; + private readonly Proof _proof; + private readonly Lot _lot; + + public LotTest() + { + _genesisProver = new PrivateKey(); + _genesisConsensusInformation = new ConsensusInformation(0, 0, null); + _genesisProof = _genesisConsensusInformation.Prove(_genesisProver); + _consensusInformation = new ConsensusInformation(1, 2, _genesisProof); + _prover = new PrivateKey(); + _proof = _consensusInformation.Prove(_prover); + _lot = new Lot(_proof, _prover.PublicKey, _consensusInformation); + } + + [Fact] + public void Constructor() + { + Assert.Throws( + () => new Lot( + _proof, + new PrivateKey().PublicKey, + _consensusInformation)); + Assert.Throws( + () => new Lot( + _proof, + _prover.PublicKey, + new ConsensusInformation(0, 1, null))); + + var lot = new Lot(_proof, _prover.PublicKey, _consensusInformation); + + Assert.Equal(_proof, lot.Proof); + Assert.Equal(_prover.PublicKey, lot.PublicKey); + Assert.Equal(_consensusInformation, lot.ConsensusInformation); + Assert.Equal(lot, new Lot(lot.Bencoded)); + Assert.Equal(lot, new Lot(lot.ByteArray)); + } + + [Fact] + public void Equal() + { + Assert.Equal(_lot, new Lot(_proof, _prover.PublicKey, _consensusInformation)); + Assert.NotEqual(_lot, _consensusInformation.ToLot(new PrivateKey())); + Assert.NotEqual(_lot, new ConsensusInformation(0, 1, null).ToLot(_prover)); + } + + [Fact] + public void HashCode() + { + Assert.Equal( + _lot.GetHashCode(), + new Lot(_proof, _prover.PublicKey, _consensusInformation).GetHashCode()); + Assert.NotEqual( + _lot.GetHashCode(), + _consensusInformation.ToLot(new PrivateKey()).GetHashCode()); + Assert.NotEqual( + _lot.GetHashCode(), + new ConsensusInformation(0, 1, null).ToLot(_prover).GetHashCode()); + } + } +} diff --git a/test/Libplanet.Tests/Crypto/PrivateKeyTest.cs b/test/Libplanet.Tests/Crypto/PrivateKeyTest.cs index a3125b66904..e8879861f21 100644 --- a/test/Libplanet.Tests/Crypto/PrivateKeyTest.cs +++ b/test/Libplanet.Tests/Crypto/PrivateKeyTest.cs @@ -187,6 +187,49 @@ public void SignTest() Assert.False(wrongPubKey.Verify(payload, pk.Sign(imPayload).ToArray())); } + [Fact] + public void ProveTest() + { + var pk = new PrivateKey( + new byte[] + { + 0x52, 0x09, 0x38, 0xfa, 0xe0, 0x79, 0x78, 0x95, 0x61, 0x26, + 0x8c, 0x29, 0x33, 0xf6, 0x36, 0xd8, 0xb5, 0xa0, 0x01, 0x1e, + 0xa0, 0x41, 0x12, 0xdb, 0xab, 0xab, 0xf2, 0x95, 0xe5, 0xdd, + 0xef, 0x88, + } + ); + var pubKey = pk.PublicKey; + var wrongPubKey = new PrivateKey().PublicKey; + var payload = new byte[] + { + 0x64, 0x37, 0x3a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x6c, 0x65, 0x31, 0x30, 0x3a, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x36, 0x35, 0x3a, 0x04, 0xb5, + 0xa2, 0x4a, 0xa2, 0x11, 0x27, 0x20, 0x42, 0x3b, 0xad, 0x39, + 0xa0, 0x20, 0x51, 0x82, 0x37, 0x9d, 0x6f, 0x2b, 0x33, 0xe3, + 0x48, 0x7c, 0x9a, 0xb6, 0xcc, 0x8f, 0xc4, 0x96, 0xf8, 0xa5, + 0x48, 0x34, 0x40, 0xef, 0xbb, 0xef, 0x06, 0x57, 0xac, 0x2e, + 0xf6, 0xc6, 0xee, 0x05, 0xdb, 0x06, 0xa9, 0x45, 0x32, 0xfd, + 0xa7, 0xdd, 0xc4, 0x4a, 0x16, 0x95, 0xe5, 0xce, 0x1a, 0x3d, + 0x3c, 0x76, 0xdb, 0x39, 0x3a, 0x72, 0x65, 0x63, 0x69, 0x70, + 0x69, 0x65, 0x6e, 0x74, 0x32, 0x30, 0x3a, 0x8a, 0xe7, 0x2e, + 0xfa, 0xb0, 0x95, 0x94, 0x66, 0x51, 0x12, 0xe6, 0xd4, 0x9d, + 0xfd, 0x19, 0x41, 0x53, 0x8c, 0xf3, 0x74, 0x36, 0x3a, 0x73, + 0x65, 0x6e, 0x64, 0x65, 0x72, 0x32, 0x30, 0x3a, 0xb6, 0xc0, + 0x3d, 0xe5, 0x7d, 0xdf, 0x03, 0x69, 0xc7, 0x20, 0x7d, 0x2d, + 0x11, 0x3a, 0xdf, 0xf8, 0x20, 0x51, 0x99, 0xcf, 0x39, 0x3a, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x32, + 0x37, 0x3a, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x31, 0x2d, + 0x30, 0x32, 0x54, 0x30, 0x33, 0x3a, 0x30, 0x34, 0x3a, 0x30, + 0x35, 0x2e, 0x30, 0x30, 0x36, 0x30, 0x30, 0x30, 0x5a, 0x65, + }; + + Assert.True(pubKey.VerifyProof(payload, pk.Prove(payload))); + Assert.False(pubKey.VerifyProof(payload.Skip(1).ToArray(), pk.Prove(payload))); + Assert.False(wrongPubKey.VerifyProof(payload, pk.Prove(payload))); + } + [Fact] public void ExchangeTest() { diff --git a/test/Libplanet.Tests/Crypto/ProofTest.cs b/test/Libplanet.Tests/Crypto/ProofTest.cs new file mode 100644 index 00000000000..d121e5979cc --- /dev/null +++ b/test/Libplanet.Tests/Crypto/ProofTest.cs @@ -0,0 +1,151 @@ +using System; +using System.Linq; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Xunit; + +namespace Libplanet.Tests.Crypto +{ + public class ProofTest + { + private readonly Random _rnd = new Random(); + private readonly byte[] _validProofBytes = new byte[] + { + 0x03, 0x47, 0xfc, 0xcb, 0x9f, 0x8b, 0x62, 0x8c, 0x00, 0x92, + 0x62, 0x7a, 0x7b, 0x91, 0x1a, 0x8e, 0x5b, 0xfb, 0xb4, 0x0b, + 0x5a, 0x25, 0xc1, 0x83, 0xf3, 0x4e, 0x91, 0x51, 0x3b, 0xaa, + 0xbd, 0x11, 0xfd, 0x9f, 0x72, 0xcd, 0x88, 0xac, 0x09, 0xab, + 0xe4, 0x97, 0xdb, 0x2b, 0x5e, 0x05, 0xb2, 0x52, 0x2c, 0x02, + 0xab, 0xd9, 0xb8, 0x5c, 0x62, 0x37, 0xcb, 0x48, 0x54, 0x08, + 0xd4, 0x6a, 0x13, 0x1e, 0xc1, 0xcd, 0xa7, 0xbc, 0xe3, 0x6c, + 0xce, 0x94, 0xaa, 0xd4, 0xca, 0x00, 0xcb, 0x3a, 0x3f, 0x24, + 0x9d, 0x4f, 0xaf, 0x76, 0x22, 0xa7, 0x28, 0x67, 0x2b, 0x08, + 0xa9, 0x8c, 0xa0, 0x63, 0xda, 0x27, 0xfa, + }; + + [Fact] + public void Constructor() + { + new Proof(_validProofBytes); + + var randomPiBytes = new byte[97]; + _rnd.NextBytes(randomPiBytes); + Assert.Throws(() => new Proof(randomPiBytes)); + } + + [Fact] + public void ConstructorDifferentEncoding() + { + Assert.Equal( + new Proof(_validProofBytes), new Proof(new Binary(_validProofBytes))); + Assert.Equal( + new Proof(_validProofBytes), new Proof((IValue)new Binary(_validProofBytes))); + } + + [Fact] + public void Bencoded() + { + var proof = new Proof(_validProofBytes); + Assert.Equal(proof, new Proof(proof.Bencoded)); + } + + [Fact] + public void ByteArray() + { + var proof = new Proof(_validProofBytes); + Assert.Equal(proof, new Proof(proof.ByteArray)); + Assert.Equal(proof, new Proof(proof.ToByteArray())); + } + + [Fact] + public void Verify() + { + var payload = new byte[100]; + _rnd.NextBytes(payload); + + var privateKey = new PrivateKey(); + var wrongPrivateKey = new PrivateKey(); + var proof = privateKey.Prove(payload); + Assert.True(proof.Verify(privateKey.PublicKey, payload)); + Assert.False(proof.Verify(wrongPrivateKey.PublicKey, payload)); + + _rnd.NextBytes(payload); + Assert.False(proof.Verify(privateKey.PublicKey, payload)); + } + + [Fact] + public void Hash() + { + var payload = new byte[100]; + _rnd.NextBytes(payload); + + var privateKey = new PrivateKey(); + var proof = privateKey.Prove(payload); + Assert.Equal(64, proof.Hash.Length); + Assert.True(proof.Hash.SequenceEqual(privateKey.Prove(payload).Hash)); + Assert.NotEqual(proof.Hash, new PrivateKey().Prove(payload).Hash); + _rnd.NextBytes(payload); + Assert.NotEqual(proof.Hash, new PrivateKey().Prove(payload).Hash); + } + + [Fact] + public void HashInt() + { + var maxInt = new BigInteger( + Enumerable.Repeat(byte.MaxValue, 64).Concat(new byte[] { 0 }).ToArray()); + + var privateKey = new PrivateKey(); + var payload = new byte[100]; + _rnd.NextBytes(payload); + var proof = privateKey.Prove(payload); + + for (int i = 0; i < 100; i++) + { + Assert.True(proof.HashInt >= 0 && proof.HashInt <= maxInt); + Assert.Equal(proof, privateKey.Prove(payload)); + Assert.NotEqual(proof, new PrivateKey().Prove(payload)); + + _rnd.NextBytes(payload); + Assert.NotEqual(proof, privateKey.Prove(payload)); + + privateKey = new PrivateKey(); + var newProof = privateKey.Prove(payload); + Assert.NotEqual(proof.HashInt, new PrivateKey().Prove(payload).HashInt); + Assert.Equal(proof.HashInt > newProof.HashInt ? 1 : -1, proof.CompareTo(newProof)); + proof = newProof; + } + } + + [Fact] + public void Seed() + { + var payload = new byte[100]; + _rnd.NextBytes(payload); + + var privateKey = new PrivateKey(); + var proof = privateKey.Prove(payload); + Assert.Equal(proof.Seed, privateKey.Prove(payload).Seed); + Assert.NotEqual(proof.Seed, new PrivateKey().Prove(payload).Seed); + _rnd.NextBytes(payload); + Assert.NotEqual(proof.Seed, new PrivateKey().Prove(payload).Seed); + } + + [Fact] + public void Draw() + { + var payload = new byte[100]; + _rnd.NextBytes(payload); + + var privateKey = new PrivateKey(); + var proof = privateKey.Prove(payload); + + for (int i = 0; i < 10; i++) + { + var drawn = proof.Draw(5, i, 20); + Assert.True(drawn <= i); + Assert.Equal(drawn, proof.Draw(5, i, 20)); + } + } + } +} diff --git a/test/Libplanet.Tests/Crypto/PublicKeyTest.cs b/test/Libplanet.Tests/Crypto/PublicKeyTest.cs index 9e62c6fdff1..5bbb97c8a60 100644 --- a/test/Libplanet.Tests/Crypto/PublicKeyTest.cs +++ b/test/Libplanet.Tests/Crypto/PublicKeyTest.cs @@ -110,6 +110,61 @@ public void Verify() Assert.False(pubKey.Verify(payload, default(ImmutableArray))); } + [Fact] + public void VerifyProof() + { + var pubKey = new PublicKey( + new byte[] + { + 0x04, 0x93, 0x63, 0xa5, 0x0b, 0x13, 0xda, 0xd9, 0xa4, 0xd4, + 0xde, 0x97, 0x03, 0xf7, 0xcc, 0x32, 0x6e, 0x2b, 0xb0, 0x5e, + 0x00, 0xe6, 0x7a, 0x44, 0x03, 0x6f, 0x4d, 0x96, 0x5a, 0xb6, + 0xff, 0xcf, 0x88, 0x4d, 0x26, 0x07, 0x33, 0x2f, 0x91, 0x51, + 0xe3, 0xcf, 0xd8, 0x1a, 0x60, 0x5b, 0xd9, 0x3e, 0x6d, 0x69, + 0x8a, 0x4e, 0x70, 0xb9, 0xac, 0x1d, 0xb1, 0xd4, 0xbe, 0xc1, + 0x41, 0x08, 0x95, 0xa9, 0xc0, + } + ); + var payload = new byte[] + { + 0x64, 0x37, 0x3a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x6c, 0x65, 0x31, 0x30, 0x3a, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x36, 0x35, 0x3a, 0x04, 0xb5, + 0xa2, 0x4a, 0xa2, 0x11, 0x27, 0x20, 0x42, 0x3b, 0xad, 0x39, + 0xa0, 0x20, 0x51, 0x82, 0x37, 0x9d, 0x6f, 0x2b, 0x33, 0xe3, + 0x48, 0x7c, 0x9a, 0xb6, 0xcc, 0x8f, 0xc4, 0x96, 0xf8, 0xa5, + 0x48, 0x34, 0x40, 0xef, 0xbb, 0xef, 0x06, 0x57, 0xac, 0x2e, + 0xf6, 0xc6, 0xee, 0x05, 0xdb, 0x06, 0xa9, 0x45, 0x32, 0xfd, + 0xa7, 0xdd, 0xc4, 0x4a, 0x16, 0x95, 0xe5, 0xce, 0x1a, 0x3d, + 0x3c, 0x76, 0xdb, 0x39, 0x3a, 0x72, 0x65, 0x63, 0x69, 0x70, + 0x69, 0x65, 0x6e, 0x74, 0x32, 0x30, 0x3a, 0x8a, 0xe7, 0x2e, + 0xfa, 0xb0, 0x95, 0x94, 0x66, 0x51, 0x12, 0xe6, 0xd4, 0x9d, + 0xfd, 0x19, 0x41, 0x53, 0x8c, 0xf3, 0x74, 0x36, 0x3a, 0x73, + 0x65, 0x6e, 0x64, 0x65, 0x72, 0x32, 0x30, 0x3a, 0xb6, 0xc0, + 0x3d, 0xe5, 0x7d, 0xdf, 0x03, 0x69, 0xc7, 0x20, 0x7d, 0x2d, + 0x11, 0x3a, 0xdf, 0xf8, 0x20, 0x51, 0x99, 0xcf, 0x39, 0x3a, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x32, + 0x37, 0x3a, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x31, 0x2d, + 0x30, 0x32, 0x54, 0x30, 0x33, 0x3a, 0x30, 0x34, 0x3a, 0x30, + 0x35, 0x2e, 0x30, 0x30, 0x36, 0x30, 0x30, 0x30, 0x5a, 0x65, + }; + var proof = new Proof(new byte[] + { + 0x03, 0x47, 0xfc, 0xcb, 0x9f, 0x8b, 0x62, 0x8c, 0x00, 0x92, + 0x62, 0x7a, 0x7b, 0x91, 0x1a, 0x8e, 0x5b, 0xfb, 0xb4, 0x0b, + 0x5a, 0x25, 0xc1, 0x83, 0xf3, 0x4e, 0x91, 0x51, 0x3b, 0xaa, + 0xbd, 0x11, 0xfd, 0x9f, 0x72, 0xcd, 0x88, 0xac, 0x09, 0xab, + 0xe4, 0x97, 0xdb, 0x2b, 0x5e, 0x05, 0xb2, 0x52, 0x2c, 0x02, + 0xab, 0xd9, 0xb8, 0x5c, 0x62, 0x37, 0xcb, 0x48, 0x54, 0x08, + 0xd4, 0x6a, 0x13, 0x1e, 0xc1, 0xcd, 0xa7, 0xbc, 0xe3, 0x6c, + 0xce, 0x94, 0xaa, 0xd4, 0xca, 0x00, 0xcb, 0x3a, 0x3f, 0x24, + 0x9d, 0x4f, 0xaf, 0x76, 0x22, 0xa7, 0x28, 0x67, 0x2b, 0x08, + 0xa9, 0x8c, 0xa0, 0x63, 0xda, 0x27, 0xfa, + }); + Assert.True(pubKey.VerifyProof(payload, proof)); + Assert.False(pubKey.VerifyProof(payload, new PrivateKey().Prove(payload))); + } + [Fact] public void VerifyShouldNotCrashForAnyInputs() { diff --git a/test/Libplanet.Tests/Fixtures/BlockContentFixture.cs b/test/Libplanet.Tests/Fixtures/BlockContentFixture.cs index 41f24de8d05..3786fd97ef2 100644 --- a/test/Libplanet.Tests/Fixtures/BlockContentFixture.cs +++ b/test/Libplanet.Tests/Fixtures/BlockContentFixture.cs @@ -70,6 +70,7 @@ public BlockContentFixture() previousHash: null, txHash: BlockContent.DeriveTxHash(genTxs), lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(null, GenesisKey), evidenceHash: BlockContent.DeriveEvidenceHash(genEvidence)), transactions: genTxs, evidence: genEvidence); @@ -136,6 +137,7 @@ public BlockContentFixture() previousHash: GenesisHash, txHash: BlockContent.DeriveTxHash(block1Transactions), lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(GenesisMetadata, Block1Key), evidenceHash: BlockContent.DeriveEvidenceHash(block1Evidence)), transactions: block1Transactions, evidence: block1Evidence); @@ -153,6 +155,7 @@ public BlockContentFixture() previousHash: null, txHash: null, lastCommit: null, + proof: null, evidenceHash: null), transactions: new List(), evidence: new List()); // Tweaked GenesisContent @@ -167,6 +170,7 @@ public BlockContentFixture() previousHash: GenesisHash, txHash: BlockContent.DeriveTxHash(block1Transactions), lastCommit: null, + proof: null, evidenceHash: BlockContent.DeriveEvidenceHash(block1Evidence)), transactions: block1Transactions, evidence: block1Evidence); // Tweaked Block1Content diff --git a/test/Libplanet.Tests/Fixtures/IntegerSet.cs b/test/Libplanet.Tests/Fixtures/IntegerSet.cs index 679eb971e35..482de8d1ac7 100644 --- a/test/Libplanet.Tests/Fixtures/IntegerSet.cs +++ b/test/Libplanet.Tests/Fixtures/IntegerSet.cs @@ -173,7 +173,13 @@ public TxWithContext Sign(int signerIndex, params Arithmetic[] actions) => Sign(PrivateKeys[signerIndex], actions); public Block Propose() => Chain.ProposeBlock( - Miner, TestUtils.CreateBlockCommit(Chain.Tip)); + Miner, + TestUtils.CreateBlockCommit(Chain.Tip), + TestUtils.CreateZeroRoundProof(Chain.Tip, Miner)); + + public Block ProposeEmptyProof() => Chain.ProposeBlock( + Miner, + TestUtils.CreateBlockCommit(Chain.Tip)); public void Append(Block block) => Chain.Append(block, TestUtils.CreateBlockCommit(block)); diff --git a/test/Libplanet.Tests/Store/StoreFixture.cs b/test/Libplanet.Tests/Store/StoreFixture.cs index 0c480826495..fca24c5cfcb 100644 --- a/test/Libplanet.Tests/Store/StoreFixture.cs +++ b/test/Libplanet.Tests/Store/StoreFixture.cs @@ -114,22 +114,28 @@ protected StoreFixture(PolicyActionsRegistry policyActionsRegistry = null) GenesisBlock, miner: Proposer, stateRootHash: genesisNextSrh, - lastCommit: null); + lastCommit: null, + proof: TestUtils.CreateZeroRoundProof(GenesisBlock, Proposer)); stateRootHashes[Block1.Hash] = Block1.StateRootHash; Block2 = TestUtils.ProposeNextBlock( Block1, miner: Proposer, stateRootHash: genesisNextSrh, - lastCommit: TestUtils.CreateBlockCommit(Block1)); + lastCommit: TestUtils.CreateBlockCommit(Block1), + proof: TestUtils.CreateZeroRoundProof(Block1, Proposer)); stateRootHashes[Block2.Hash] = Block2.StateRootHash; Block3 = TestUtils.ProposeNextBlock( Block2, miner: Proposer, stateRootHash: genesisNextSrh, - lastCommit: TestUtils.CreateBlockCommit(Block2)); + lastCommit: TestUtils.CreateBlockCommit(Block2), + proof: TestUtils.CreateZeroRoundProof(Block2, Proposer)); stateRootHashes[Block3.Hash] = Block3.StateRootHash; Block3Alt = TestUtils.ProposeNextBlock( - Block2, miner: Proposer, stateRootHash: genesisNextSrh); + Block2, + miner: Proposer, + stateRootHash: genesisNextSrh, + proof: TestUtils.CreateZeroRoundProof(Block2, Proposer)); stateRootHashes[Block3Alt.Hash] = Block3Alt.StateRootHash; Block4 = TestUtils.ProposeNextBlock( Block3, miner: Proposer, stateRootHash: genesisNextSrh); diff --git a/test/Libplanet.Tests/Store/StoreTest.cs b/test/Libplanet.Tests/Store/StoreTest.cs index 5a25950f8b6..bffa328a8a6 100644 --- a/test/Libplanet.Tests/Store/StoreTest.cs +++ b/test/Libplanet.Tests/Store/StoreTest.cs @@ -1026,11 +1026,19 @@ public void Copy() // FIXME: Need to add more complex blocks/transactions. var key = new PrivateKey(); - var block = blocks.ProposeBlock(key); + var block = blocks.ProposeBlock( + key, + proof: TestUtils.CreateZeroRoundProof(blocks.Tip, key)); blocks.Append(block, CreateBlockCommit(block)); - block = blocks.ProposeBlock(key, CreateBlockCommit(blocks.Tip)); + block = blocks.ProposeBlock( + key, + CreateBlockCommit(blocks.Tip), + TestUtils.CreateZeroRoundProof(blocks.Tip, key)); blocks.Append(block, CreateBlockCommit(block)); - block = blocks.ProposeBlock(key, CreateBlockCommit(blocks.Tip)); + block = blocks.ProposeBlock( + key, + CreateBlockCommit(blocks.Tip), + TestUtils.CreateZeroRoundProof(blocks.Tip, key)); blocks.Append(block, CreateBlockCommit(block)); s1.Copy(to: Fx.Store); diff --git a/test/Libplanet.Tests/TestUtils.cs b/test/Libplanet.Tests/TestUtils.cs index 2d582511f26..03b9b10436c 100644 --- a/test/Libplanet.Tests/TestUtils.cs +++ b/test/Libplanet.Tests/TestUtils.cs @@ -25,6 +25,7 @@ using Libplanet.Blockchain.Renderers; using Libplanet.Blockchain.Renderers.Debug; using Libplanet.Common; +using Libplanet.Consensus; using Libplanet.Crypto; using Libplanet.Store; using Libplanet.Store.Trie; @@ -411,12 +412,18 @@ public static BlockCommit CreateBlockCommit( height, round, blockHash, votes); } + public static Proof CreateZeroRoundProof( + IBlockMetadata tip, + PrivateKey proposerKey) + => new ConsensusInformation((tip?.Index ?? -1) + 1, 0, tip?.Proof).Prove(proposerKey); + public static PreEvaluationBlock ProposeGenesis( PublicKey proposer = null, IReadOnlyList transactions = null, ValidatorSet validatorSet = null, DateTimeOffset? timestamp = null, - int protocolVersion = Block.CurrentProtocolVersion + int protocolVersion = Block.CurrentProtocolVersion, + Proof proof = null ) { var txs = transactions?.ToList() ?? new List(); @@ -448,6 +455,7 @@ public static PreEvaluationBlock ProposeGenesis( previousHash: null, txHash: BlockContent.DeriveTxHash(txs), lastCommit: null, + proof: proof, evidenceHash: null), transactions: txs, evidence: Array.Empty()); @@ -488,6 +496,7 @@ public static PreEvaluationBlock ProposeNext( TimeSpan? blockInterval = null, int protocolVersion = Block.CurrentProtocolVersion, BlockCommit lastCommit = null, + Proof proof = null, ImmutableArray? evidence = null) { var txs = transactions is null @@ -513,6 +522,7 @@ public static PreEvaluationBlock ProposeNext( previousHash: previousBlock.Hash, txHash: BlockContent.DeriveTxHash(txs), lastCommit: lastCommit, + proof: proof, evidenceHash: evidenceHash), transactions: txs, evidence: Array.Empty()); @@ -529,6 +539,7 @@ public static Block ProposeNextBlock( int protocolVersion = Block.CurrentProtocolVersion, HashDigest stateRootHash = default, BlockCommit lastCommit = null, + Proof proof = null, ImmutableArray? evidence = null) { Skip.IfNot( @@ -543,6 +554,7 @@ public static Block ProposeNextBlock( blockInterval, protocolVersion, lastCommit, + proof, evidence); return preEval.Sign(miner, stateRootHash); } @@ -641,7 +653,8 @@ public static (BlockChain BlockChain, ActionEvaluator ActionEvaluator) txs, validatorSet, timestamp, - protocolVersion); + protocolVersion, + new ConsensusInformation(0, 0, null).Prove(GenesisProposer)); var evaluatedSrh = actionEvaluator.Evaluate(preEval, null).Last().OutputState; genesisBlock = protocolVersion < BlockMetadata.SignatureProtocolVersion ? new Block(