From 92d35fe38ccce896b95723877be902b222bbecfc Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Fri, 20 May 2022 18:24:02 +0900 Subject: [PATCH 01/14] ITxMetadata interface --- CHANGES.md | 6 +++++ Libplanet/Tx/ITxMetadata.cs | 54 +++++++++++++++++++++++++++++++++++++ Libplanet/Tx/Transaction.cs | 37 +++++-------------------- 3 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 Libplanet/Tx/ITxMetadata.cs diff --git a/CHANGES.md b/CHANGES.md index 485d4e12dc6..99ac7a2f85a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,10 @@ To be released. ### Added APIs + - Added `ITxMetadata` interface. [[#1164], [#1974], [#1978]] + - `Transaction` now implements `ITxMetadata` interface. + [[#1164], [#1974], [#1978]] + ### Behavioral changes ### Bug fixes @@ -37,6 +41,8 @@ To be released. ### CLI tools +[#1974]: https://github.com/planetarium/libplanet/issues/1974 +[#1978]: https://github.com/planetarium/libplanet/pull/1978 [#1981]: https://github.com/planetarium/libplanet/issues/1981 [#1982]: https://github.com/planetarium/libplanet/pull/1982 [#1990]: https://github.com/planetarium/libplanet/pull/1990 diff --git a/Libplanet/Tx/ITxMetadata.cs b/Libplanet/Tx/ITxMetadata.cs new file mode 100644 index 00000000000..5ee8ff7cd90 --- /dev/null +++ b/Libplanet/Tx/ITxMetadata.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Immutable; +using Libplanet.Blocks; +using Libplanet.Crypto; + +namespace Libplanet.Tx +{ + /// + /// A common interface for transactions that do not have any proofs nor actions. + /// + public interface ITxMetadata + { + /// + /// The number of previous s committed by + /// the of this transaction. This nonce is used for preventing + /// replay attack. + /// + /// Don't confuse this with for proof-of-work. + /// + long Nonce { get; } + + /// + /// A of the account who signs this transaction. + /// This is derived from the . + /// + Address Signer { get; } + + /// + /// An approximated list of addresses whose states would be affected by actions in this + /// transaction. However, it could be wrong. + /// + // See also https://github.com/planetarium/libplanet/issues/368 + IImmutableSet
UpdatedAddresses { get; } + + /// + /// The time this transaction is created and signed. + /// + DateTimeOffset Timestamp { get; } + + /// + /// A of the account who signs this transaction. + /// The address is always corresponding to this + /// for each transaction. This cannot be . + /// + PublicKey PublicKey { get; } + + /// + /// A value of the genesis which this transaction is made + /// from. This can be iff the transaction is contained in + /// the genesis block. + /// + public BlockHash? GenesisHash { get; } + } +} diff --git a/Libplanet/Tx/Transaction.cs b/Libplanet/Tx/Transaction.cs index e848d94ddd7..e42167bda0c 100644 --- a/Libplanet/Tx/Transaction.cs +++ b/Libplanet/Tx/Transaction.cs @@ -26,7 +26,7 @@ namespace Libplanet.Tx /// /// /// - public sealed class Transaction : IEquatable> + public sealed class Transaction : IEquatable>, ITxMetadata where T : IAction, new() { private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; @@ -184,24 +184,13 @@ public TxId Id } } - /// - /// The number of previous s committed by - /// the of this transaction. - /// + /// public long Nonce { get; } - /// - /// A of the account who signs this transaction. - /// This is derived from the . - /// + /// public Address Signer { get; } - /// - /// es whose states affected by - /// . - /// - // TODO: We should remove this property. - // See also https://github.com/planetarium/libplanet/issues/368 + /// public IImmutableSet
UpdatedAddresses { get; } /// @@ -235,25 +224,13 @@ private set /// public IImmutableList Actions { get; } - /// - /// The time this is created and signed. - /// + /// public DateTimeOffset Timestamp { get; } - /// - /// A of the account who signs this - /// . - /// The address is always corresponding to this - /// for each transaction. This cannot be null. - /// + /// public PublicKey PublicKey { get; } - /// - /// A value of the genesis which this - /// is made from. - /// This can be null iff the transaction is contained - /// in the genesis block. - /// + /// public BlockHash? GenesisHash { get; } /// From 865b1737efd992a8571d9839f9b66925746cfd0a Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Mon, 23 May 2022 22:54:40 +0900 Subject: [PATCH 02/14] TxMetadata class --- CHANGES.md | 1 + Libplanet.Tests/Tx/TxMetadataTest.cs | 164 +++++++++++++++++++++++++++ Libplanet/Tx/TxMetadata.cs | 125 ++++++++++++++++++++ 3 files changed, 290 insertions(+) create mode 100644 Libplanet.Tests/Tx/TxMetadataTest.cs create mode 100644 Libplanet/Tx/TxMetadata.cs diff --git a/CHANGES.md b/CHANGES.md index 99ac7a2f85a..375e68a2282 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,7 @@ To be released. ### Added APIs - Added `ITxMetadata` interface. [[#1164], [#1974], [#1978]] + - Added `TxMetadata` class. [[#1164], [#1974], [#1978]] - `Transaction` now implements `ITxMetadata` interface. [[#1164], [#1974], [#1978]] diff --git a/Libplanet.Tests/Tx/TxMetadataTest.cs b/Libplanet.Tests/Tx/TxMetadataTest.cs new file mode 100644 index 00000000000..1123869209a --- /dev/null +++ b/Libplanet.Tests/Tx/TxMetadataTest.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Bencodex.Types; +using Libplanet.Blocks; +using Libplanet.Crypto; +using Libplanet.Tx; +using Xunit; +using static Libplanet.Tests.TestUtils; + +namespace Libplanet.Tests.Tx +{ + public class TxMetadataTest + { + private readonly PrivateKey _key1; + private readonly PrivateKey _key2; + + public TxMetadataTest() + { + _key1 = new PrivateKey(new byte[] + { + 0x9b, 0xf4, 0x66, 0x4b, 0xa0, 0x9a, 0x89, 0xfa, 0xeb, 0x68, 0x4b, + 0x94, 0xe6, 0x9f, 0xfd, 0xe0, 0x1d, 0x26, 0xae, 0x14, 0xb5, 0x56, + 0x20, 0x4d, 0x3f, 0x6a, 0xb5, 0x8f, 0x61, 0xf7, 0x84, 0x18, + }); + + _key2 = new PrivateKey(new byte[] + { + 0xfc, 0xf3, 0x0b, 0x33, 0x3d, 0x04, 0xcc, 0xfe, 0xb5, 0x62, 0xf0, + 0x00, 0xa3, 0x2d, 0xf4, 0x88, 0xe7, 0x15, 0x49, 0x49, 0xd3, 0x1d, + 0xdc, 0xac, 0x3c, 0xf9, 0x27, 0x8a, 0xcb, 0x57, 0x86, 0xc7, + }); + } + + [Fact] + public void Constructor() + { + DateTimeOffset before = DateTimeOffset.UtcNow; + var meta = new TxMetadata(_key1.PublicKey); + DateTimeOffset after = DateTimeOffset.UtcNow; + Assert.Equal(0L, meta.Nonce); + AssertBytesEqual(_key1.ToAddress(), meta.Signer); + Assert.Empty(meta.UpdatedAddresses); + Assert.InRange(meta.Timestamp, before, after); + Assert.Equal(_key1.PublicKey, meta.PublicKey); + Assert.Null(meta.GenesisHash); + } + + [Fact] + public void Deserialize() + { + Bencodex.Types.Dictionary dict1 = Dictionary.Empty + .Add(new byte[] { 0x6e }, 123L) + .Add(new byte[] { 0x73 }, _key1.ToAddress().ByteArray) + .Add(new byte[] { 0x75 }, Enumerable.Empty()) + .Add(new byte[] { 0x74 }, "2022-05-23T10:02:00.000000Z") + .Add(new byte[] { 0x70 }, _key1.PublicKey.ToImmutableArray(compress: false)); + var meta1 = new TxMetadata(dict1); + Assert.Equal(123L, meta1.Nonce); + AssertBytesEqual(_key1.ToAddress(), meta1.Signer); + Assert.Empty(meta1.UpdatedAddresses); + Assert.Equal( + new DateTimeOffset(2022, 5, 23, 10, 2, 0, default), + meta1.Timestamp); + Assert.Equal(_key1.PublicKey, meta1.PublicKey); + Assert.Null(meta1.GenesisHash); + + Bencodex.Types.Dictionary dict2 = Dictionary.Empty + .Add(new byte[] { 0x6e }, 0L) + .Add(new byte[] { 0x73 }, _key2.ToAddress().ByteArray) + .Add( + new byte[] { 0x75 }, + (IValue)Bencodex.Types.List.Empty + .Add(_key1.ToAddress().ToByteArray()) + .Add(_key2.ToAddress().ToByteArray())) + .Add(new byte[] { 0x74 }, "2022-01-12T04:56:07.890000Z") + .Add(new byte[] { 0x70 }, _key2.PublicKey.ToImmutableArray(compress: false)) + .Add(new byte[] { 0x61 }, Enumerable.Empty()) + .Add( + new byte[] { 0x67 }, + ByteUtil.ParseHex( + "83915317ebdbf870c567b263dd2e61ec9dca7fb381c592d80993291b6ffe5ad5")); + var meta2 = new TxMetadata(dict2); + Assert.Equal(0L, meta2.Nonce); + AssertBytesEqual(_key2.ToAddress(), meta2.Signer); + Assert.Equal( + new[] { _key1.ToAddress(), _key2.ToAddress() }.ToImmutableHashSet(), + meta2.UpdatedAddresses); + Assert.Equal( + new DateTimeOffset(2022, 1, 12, 4, 56, 7, 890, default), + meta2.Timestamp); + Assert.Equal(_key2.PublicKey, meta2.PublicKey); + AssertBytesEqual( + BlockHash.FromString( + "83915317ebdbf870c567b263dd2e61ec9dca7fb381c592d80993291b6ffe5ad5"), + meta2.GenesisHash + ); + } + + [Fact] + public void ToBencodex() + { + var meta1 = new TxMetadata(_key1.PublicKey) + { + Nonce = 123L, + Timestamp = new DateTimeOffset(2022, 5, 23, 10, 2, 0, default), + }; + Bencodex.Types.Dictionary expected1 = Dictionary.Empty + .Add(new byte[] { 0x6e }, 123L) + .Add(new byte[] { 0x73 }, _key1.ToAddress().ByteArray) + .Add(new byte[] { 0x75 }, Enumerable.Empty()) + .Add(new byte[] { 0x74 }, "2022-05-23T10:02:00.000000Z") + .Add(new byte[] { 0x70 }, _key1.PublicKey.ToImmutableArray(compress: false)) + .Add(new byte[] { 0x61 }, Enumerable.Empty()); + AssertBencodexEqual( + expected1, + meta1.ToBencodex(Array.Empty()) + ); + + IValue[] actions = { new Integer(123), new Integer(456) }; + AssertBencodexEqual( + expected1.SetItem(new byte[] { 0x61 }, actions), + meta1.ToBencodex(actions) + ); + + var meta2 = new TxMetadata(_key2.PublicKey) + { + Nonce = 0L, + UpdatedAddresses = new[] + { + _key1.ToAddress(), + _key2.ToAddress(), + }.ToImmutableHashSet(), + Timestamp = new DateTimeOffset(2022, 1, 12, 4, 56, 7, 890, default), + GenesisHash = BlockHash.FromString( + "83915317ebdbf870c567b263dd2e61ec9dca7fb381c592d80993291b6ffe5ad5"), + }; + Bencodex.Types.Dictionary expected2 = Dictionary.Empty + .Add(new byte[] { 0x6e }, 0L) + .Add(new byte[] { 0x73 }, _key2.ToAddress().ByteArray) + .Add( + new byte[] { 0x75 }, + (IValue)Bencodex.Types.List.Empty + .Add(_key1.ToAddress().ToByteArray()) + .Add(_key2.ToAddress().ToByteArray())) + .Add(new byte[] { 0x74 }, "2022-01-12T04:56:07.890000Z") + .Add(new byte[] { 0x70 }, _key2.PublicKey.ToImmutableArray(compress: false)) + .Add(new byte[] { 0x61 }, Enumerable.Empty()) + .Add( + new byte[] { 0x67 }, + ByteUtil.ParseHex( + "83915317ebdbf870c567b263dd2e61ec9dca7fb381c592d80993291b6ffe5ad5")); + AssertBencodexEqual( + expected2, + meta2.ToBencodex(Array.Empty()) + ); + + AssertBencodexEqual( + expected2.SetItem(new byte[] { 0x61 }, actions), + meta2.ToBencodex(actions) + ); + } + } +} diff --git a/Libplanet/Tx/TxMetadata.cs b/Libplanet/Tx/TxMetadata.cs new file mode 100644 index 00000000000..8bfe4492afa --- /dev/null +++ b/Libplanet/Tx/TxMetadata.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Blocks; +using Libplanet.Crypto; + +namespace Libplanet.Tx +{ + /// + /// A concrete class implementing . It's used to represent drafts + /// of unsigned s. + /// + public sealed class TxMetadata : ITxMetadata + { + internal static readonly byte[] ActionsKey = { 0x61 }; // 'a' + internal static readonly byte[] SignatureKey = { 0x53 }; // 'S' + private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; + private static readonly byte[] NonceKey = { 0x6e }; // 'n' + private static readonly byte[] SignerKey = { 0x73 }; // 's' + private static readonly byte[] GenesisHashKey = { 0x67 }; // 'g' + private static readonly byte[] UpdatedAddressesKey = { 0x75 }; // 'u' + private static readonly byte[] PublicKeyKey = { 0x70 }; // 'p' + private static readonly byte[] TimestampKey = { 0x74 }; // 't' + + /// + /// Creates a instance with a . + /// Other fields can be set using property setters. + /// + /// Configures and . + /// + public TxMetadata(PublicKey publicKey) + { + PublicKey = publicKey; + } + + /// + /// Creates a from a Bencodex . + /// + /// A Bencodex dictionary made using + /// method. + /// Thrown when the given + /// lacks some fields. + /// Thrown when the given + /// has some invalid values. + public TxMetadata(Bencodex.Types.Dictionary dictionary) + { + Nonce = dictionary.GetValue(NonceKey); + GenesisHash + = dictionary.TryGetValue(new Binary(GenesisHashKey), out IValue v) && v is Binary g + ? new BlockHash(g.ByteArray) + : (BlockHash?)null; + UpdatedAddresses = dictionary.GetValue(UpdatedAddressesKey) + .Select(v => new Address((Binary)v)) + .ToImmutableHashSet(); + PublicKey = new PublicKey(dictionary.GetValue(PublicKeyKey).ByteArray); + Timestamp = DateTimeOffset.ParseExact( + dictionary.GetValue(TimestampKey), + TimestampFormat, + CultureInfo.InvariantCulture + ).ToUniversalTime(); + } + + /// + public long Nonce { get; set; } + + /// + /// This is automatically derived from . + public Address Signer => new Address(PublicKey); + + /// + public IImmutableSet
UpdatedAddresses { get; set; } = + ImmutableHashSet
.Empty; + + /// + public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow; + + /// + public PublicKey PublicKey { get; } + + /// + public BlockHash? GenesisHash { get; set; } + + /// + /// Builds a Bencodex dictionary used for signing and calculating . + /// + /// A list of s to include. + /// Optionally specifies the transaction signature. It should be + /// (which is the default) when you make a signature, and should be + /// present when you make a . + /// A Bencodex dictionary that the transaction turns into. + public Bencodex.Types.Dictionary ToBencodex( + IReadOnlyList actions, + ImmutableArray? signature = null + ) + { + IEnumerable updatedAddresses = + UpdatedAddresses.Select(addr => new Binary(addr.ByteArray)); + Bencodex.Types.Dictionary dict = Dictionary.Empty + .Add(NonceKey, Nonce) + .Add(SignerKey, Signer.ByteArray) + .Add(UpdatedAddressesKey, updatedAddresses) + .Add(PublicKeyKey, PublicKey.ToImmutableArray(compress: false)) + .Add( + TimestampKey, + Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture)) + .Add(ActionsKey, actions); + + if (GenesisHash is { } genesisHash) + { + dict = dict.Add(GenesisHashKey, genesisHash.ByteArray); + } + + if (signature is { } sig) + { + dict = dict.Add(SignatureKey, sig); + } + + return dict; + } + } +} From 84f997778c92c338b7ba99b32d274f36e237c745 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Mon, 23 May 2022 23:46:34 +0900 Subject: [PATCH 03/14] TxMetadata(ITxMetadata) constructor [changelog skip] --- Libplanet.Tests/Tx/TxMetadataTest.cs | 37 ++++++++++++++++++++++++++++ Libplanet/Tx/TxMetadata.cs | 17 +++++++++++++ 2 files changed, 54 insertions(+) diff --git a/Libplanet.Tests/Tx/TxMetadataTest.cs b/Libplanet.Tests/Tx/TxMetadataTest.cs index 1123869209a..ac0170734f4 100644 --- a/Libplanet.Tests/Tx/TxMetadataTest.cs +++ b/Libplanet.Tests/Tx/TxMetadataTest.cs @@ -46,6 +46,43 @@ public void Constructor() Assert.Null(meta.GenesisHash); } + [Fact] + public void CopyConstructor() + { + var meta1 = new TxMetadata(_key1.PublicKey) + { + Nonce = 123L, + Timestamp = new DateTimeOffset(2022, 5, 23, 10, 2, 0, default), + }; + var copy1 = new TxMetadata(meta1); + Assert.Equal(meta1.Nonce, copy1.Nonce); + AssertBytesEqual(meta1.Signer, copy1.Signer); + Assert.Equal(meta1.UpdatedAddresses, copy1.UpdatedAddresses); + Assert.Equal(meta1.Timestamp, copy1.Timestamp); + Assert.Equal(meta1.PublicKey, copy1.PublicKey); + AssertBytesEqual(meta1.GenesisHash, copy1.GenesisHash); + + var meta2 = new TxMetadata(_key2.PublicKey) + { + Nonce = 0L, + UpdatedAddresses = new[] + { + _key1.ToAddress(), + _key2.ToAddress(), + }.ToImmutableHashSet(), + Timestamp = new DateTimeOffset(2022, 1, 12, 4, 56, 7, 890, default), + GenesisHash = BlockHash.FromString( + "83915317ebdbf870c567b263dd2e61ec9dca7fb381c592d80993291b6ffe5ad5"), + }; + var copy2 = new TxMetadata(meta2); + Assert.Equal(meta2.Nonce, copy2.Nonce); + AssertBytesEqual(meta2.Signer, copy2.Signer); + Assert.Equal(meta2.UpdatedAddresses, copy2.UpdatedAddresses); + Assert.Equal(meta2.Timestamp, copy2.Timestamp); + Assert.Equal(meta2.PublicKey, copy2.PublicKey); + AssertBytesEqual(meta2.GenesisHash, copy2.GenesisHash); + } + [Fact] public void Deserialize() { diff --git a/Libplanet/Tx/TxMetadata.cs b/Libplanet/Tx/TxMetadata.cs index 8bfe4492afa..b8cc62a5abf 100644 --- a/Libplanet/Tx/TxMetadata.cs +++ b/Libplanet/Tx/TxMetadata.cs @@ -37,6 +37,23 @@ public TxMetadata(PublicKey publicKey) PublicKey = publicKey; } + /// + /// Creates a instance by copying fields from the specified + /// . + /// + /// The transaction metadata whose data to copy. + /// from the specified + /// is ignored. field is automatically derived from + /// instead. + public TxMetadata(ITxMetadata metadata) + { + Nonce = metadata.Nonce; + GenesisHash = metadata.GenesisHash; + UpdatedAddresses = metadata.UpdatedAddresses; + PublicKey = metadata.PublicKey; + Timestamp = metadata.Timestamp; + } + /// /// Creates a from a Bencodex . /// From d399af16d2952cf00b091a9f86e7ac02a9623d67 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 01:01:50 +0900 Subject: [PATCH 04/14] Replace RawTransaction with TxMetadata --- CHANGES.md | 9 + Libplanet.Tests/Blocks/BlockTest.cs | 66 +------- Libplanet.Tests/TransactionExtensions.cs | 8 - Libplanet.Tests/Tx/TransactionTest.cs | 132 +-------------- Libplanet/Blocks/Block.cs | 2 - Libplanet/Blocks/BlockContent.cs | 8 - Libplanet/Blocks/BlockContentExtensions.cs | 2 - Libplanet/Blocks/PreEvaluationBlock.cs | 4 - Libplanet/Tx/InvalidTxPublicKeyException.cs | 26 --- Libplanet/Tx/RawTransaction.cs | 179 -------------------- Libplanet/Tx/Transaction.cs | 148 +++++++--------- Libplanet/Tx/TxMetadata.cs | 4 +- 12 files changed, 80 insertions(+), 508 deletions(-) delete mode 100644 Libplanet/Tx/InvalidTxPublicKeyException.cs delete mode 100644 Libplanet/Tx/RawTransaction.cs diff --git a/CHANGES.md b/CHANGES.md index 375e68a2282..12b5609ab89 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ To be released. ### Backward-incompatible API changes + - Removed `InvalidTxPublicKeyException` class. [[#1164], [#1978]] - (Libplanet.Net) Property `SwarmOptions.BlockDownloadTimeout` removed. [[#1981], [#1982]] - (Libplanet.Net) `Swarm.BootstrapAsync(IEnumerable, TimeSpan?, @@ -33,9 +34,17 @@ To be released. - Added `TxMetadata` class. [[#1164], [#1974], [#1978]] - `Transaction` now implements `ITxMetadata` interface. [[#1164], [#1974], [#1978]] + - Added `Transaction(ITxMetadata, IEnumerable, byte[])` constructor. + [[#1164], [#1978]] ### Behavioral changes + - `Transaction(long, Address, PublicKey, BlockHash?, + IImmutableSet
, DateTimeOffset, IEnumerable, byte[])` constructor + became to ignore its second parameter `Address signer`. Instead, + `Transaction.Signer` property is now automatically derived from its + `PublicKey`. [[#1164], [#1978]] + ### Bug fixes ### Dependencies diff --git a/Libplanet.Tests/Blocks/BlockTest.cs b/Libplanet.Tests/Blocks/BlockTest.cs index 647c1550d59..99872dce409 100644 --- a/Libplanet.Tests/Blocks/BlockTest.cs +++ b/Libplanet.Tests/Blocks/BlockTest.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.Linq; using System.Security.Cryptography; -using Bencodex.Types; using Libplanet.Action; using Libplanet.Blocks; using Libplanet.Crypto; @@ -81,64 +79,16 @@ public void TransactionOrderIdempotent() [Fact] public void DetectInvalidTxSignature() { - RawTransaction rawTx = new RawTransaction( - 0, - _fx.TxFixture.Address1.ByteArray, - _fx.Genesis.Hash.ByteArray, - ImmutableArray>.Empty, - _fx.TxFixture.PublicKey1.Format(false).ToImmutableArray(), - DateTimeOffset.UtcNow.ToString( - "yyyy-MM-ddTHH:mm:ss.ffffffZ", - CultureInfo.InvariantCulture - ), - ImmutableArray.Empty, - new byte[10].ToImmutableArray() - ); - var invalidTx = new Transaction(rawTx); - Assert.Throws(() => - MineNext( - MineGenesisBlock(_fx.GetHashAlgorithm, _fx.Miner), - _fx.GetHashAlgorithm, - new List> - { - invalidTx, - } - ) - ); - } - - [Fact] - public void DetectInvalidTxPublicKey() - { - RawTransaction rawTxWithoutSig = new RawTransaction( - 0, - new PrivateKey().ToAddress().ByteArray, - _fx.Genesis.Hash.ByteArray, - ImmutableArray>.Empty, - _fx.TxFixture.PublicKey1.Format(false).ToImmutableArray(), - DateTimeOffset.UtcNow.ToString( - "yyyy-MM-ddTHH:mm:ss.ffffffZ", - CultureInfo.InvariantCulture - ), - ImmutableArray.Empty, - ImmutableArray.Empty - ); - byte[] sig = _fx.TxFixture.PrivateKey1.Sign( - new Transaction(rawTxWithoutSig).Serialize(false) - ); + var txMeta = new TxMetadata(_fx.TxFixture.PublicKey1) + { + GenesisHash = _fx.Genesis.Hash, + }; var invalidTx = new Transaction( - new RawTransaction( - 0, - rawTxWithoutSig.Signer, - rawTxWithoutSig.GenesisHash, - rawTxWithoutSig.UpdatedAddresses, - rawTxWithoutSig.PublicKey, - rawTxWithoutSig.Timestamp, - rawTxWithoutSig.Actions, - sig.ToImmutableArray() - ) + txMeta, + Enumerable.Empty(), + Array.Empty() ); - Assert.Throws(() => + Assert.Throws(() => MineNext( MineGenesisBlock(_fx.GetHashAlgorithm, _fx.Miner), _fx.GetHashAlgorithm, diff --git a/Libplanet.Tests/TransactionExtensions.cs b/Libplanet.Tests/TransactionExtensions.cs index e0eba71125f..c13d002f8d5 100644 --- a/Libplanet.Tests/TransactionExtensions.cs +++ b/Libplanet.Tests/TransactionExtensions.cs @@ -24,14 +24,6 @@ public static void Validate(this Transaction tx, PrivateKey privateKey) } catch (InvalidTxSignatureException) { - if (!privateKey.ToAddress().Equals(tx.PublicKey.ToAddress())) - { - throw new InvalidTxPublicKeyException( - tx.Id, - "The given private key does not correspond to the transaction's public key." - ); - } - byte[] serialized = tx.Serialize(false); byte[] validSignature = privateKey.Sign(serialized); throw new InvalidTxSignatureException( diff --git a/Libplanet.Tests/Tx/TransactionTest.cs b/Libplanet.Tests/Tx/TransactionTest.cs index f6dcc509818..7414ba129ae 100644 --- a/Libplanet.Tests/Tx/TransactionTest.cs +++ b/Libplanet.Tests/Tx/TransactionTest.cs @@ -572,91 +572,12 @@ public void DetectUnsignedTransaction() [Fact] public void DetectBadSignature() { - var rawTx = _fx.Tx.ToRawTransaction(true); - Transaction tx = new Transaction( - new RawTransaction( - 0, - rawTx.Signer, - rawTx.GenesisHash, - rawTx.UpdatedAddresses, - rawTx.PublicKey, - rawTx.Timestamp, - rawTx.Actions, - new byte[rawTx.Signature.Length].ToImmutableArray() - ) - ); - + Bencodex.Types.Dictionary dict = _fx.Tx.ToBencodex(true) + .SetItem(TxMetadata.SignatureKey, new byte[_fx.Tx.Signature.Length]); + var tx = new Transaction(dict); Assert.Throws(() => tx.Validate()); } - [Fact] - public void DetectAddressMismatch() - { - var privKey = new PrivateKey(); - var mismatchedPrivKey = new PrivateKey(); - var tx = new Transaction( - 0, - new Address(mismatchedPrivKey.PublicKey), - privKey.PublicKey, - null, - ImmutableHashSet
.Empty, - new DateTimeOffset(2018, 11, 21, 0, 0, 0, TimeSpan.Zero), - ImmutableArray.Empty, - new byte[0] - ); - var invalidTx = new Transaction( - tx.Nonce, - tx.Signer, - tx.PublicKey, - tx.GenesisHash, - tx.UpdatedAddresses, - tx.Timestamp, - tx.Actions, - privKey.Sign(tx.Serialize(false)) - ); - - Assert.Throws(() => invalidTx.Validate()); - } - - [Fact] - public void ConvertToRaw() - { - var privateKey = new PrivateKey( - new byte[] - { - 0xcf, 0x36, 0xec, 0xf9, 0xe4, 0x7c, 0x87, 0x9a, 0x0d, 0xbf, - 0x46, 0xb2, 0xec, 0xd8, 0x3f, 0xd2, 0x76, 0x18, 0x2a, 0xde, - 0x02, 0x65, 0x82, 0x5e, 0x3b, 0x8c, 0x6b, 0xa2, 0x14, 0x46, - 0x7b, 0x76, - } - ); - var timestamp = new DateTimeOffset(2018, 11, 21, 0, 0, 0, TimeSpan.Zero); - Transaction tx = Transaction.Create( - 0, - privateKey, - null, - new DumbAction[0], - timestamp: timestamp - ); - - Assert.Equal( - GetExpectedRawTransaction(false), - tx.ToRawTransaction(false) - ); - Assert.Equal( - GetExpectedRawTransaction(true), - tx.ToRawTransaction(true) - ); - } - - [Fact] - public void ConvertFromRawTransaction() - { - RawTransaction rawTx = GetExpectedRawTransaction(true); - var tx = new Transaction(rawTx); - tx.Validate(); - } - [Fact] public void SignatureBufferIsIsolated() { @@ -714,52 +635,5 @@ public void ActionsAreIsolatedFromOutside() Assert.Empty(t1.Actions); Assert.Empty(t2.Actions); } - - internal RawTransaction GetExpectedRawTransaction(bool includeSingature) - { - var privateKey = new PrivateKey(new byte[] - { - 0xcf, 0x36, 0xec, 0xf9, 0xe4, 0x7c, 0x87, 0x9a, 0x0d, 0xbf, - 0x46, 0xb2, 0xec, 0xd8, 0x3f, 0xd2, 0x76, 0x18, 0x2a, 0xde, - 0x02, 0x65, 0x82, 0x5e, 0x3b, 0x8c, 0x6b, 0xa2, 0x14, 0x46, - 0x7b, 0x76, - }); - var tx = new RawTransaction( - nonce: 0, - signer: new byte[] - { - 0xc2, 0xa8, 0x60, 0x14, 0x07, 0x3d, 0x66, 0x2a, 0x4a, 0x9b, - 0xfc, 0xf9, 0xcb, 0x54, 0x26, 0x3d, 0xfa, 0x4f, 0x5c, 0xbc, - }.ToImmutableArray(), - updatedAddresses: ImmutableArray>.Empty, - publicKey: new byte[] - { - 0x04, 0x46, 0x11, 0x5b, 0x01, 0x31, 0xba, 0xcc, 0xf9, 0x4a, - 0x58, 0x56, 0xed, 0xe8, 0x71, 0x29, 0x5f, 0x6f, 0x3d, 0x35, - 0x2e, 0x68, 0x47, 0xcd, 0xa9, 0xc0, 0x3e, 0x89, 0xfe, 0x09, - 0xf7, 0x32, 0x80, 0x87, 0x11, 0xec, 0x97, 0xaf, 0x6e, 0x34, - 0x1f, 0x11, 0x0a, 0x32, 0x6d, 0xa1, 0xbd, 0xb8, 0x1f, 0x5a, - 0xe3, 0xba, 0xdf, 0x76, 0xa9, 0x0b, 0x22, 0xc8, 0xc4, 0x91, - 0xae, 0xd3, 0xaa, 0xa2, 0x96, - }.ToImmutableArray(), - genesisHash: ImmutableArray.Empty, - timestamp: "2018-11-21T00:00:00.000000Z", - actions: ImmutableArray.Empty - ); - if (!includeSingature) - { - return tx; - } - - byte[] signature = - { - 0x30, 0x44, 0x02, 0x20, 0x2f, 0x2d, 0xbe, 0x5a, 0x91, 0x65, 0x59, 0xde, 0xdb, 0xe8, - 0xd8, 0x4f, 0xa9, 0x20, 0xe2, 0x01, 0x29, 0x4d, 0x4f, 0x40, 0xea, 0x1e, 0x97, 0x44, - 0x1f, 0xbf, 0xa2, 0x5c, 0x8b, 0xd0, 0x0e, 0x23, 0x02, 0x20, 0x3c, 0x06, 0x02, 0x1f, - 0xb8, 0x3f, 0x67, 0x49, 0x92, 0x3c, 0x07, 0x59, 0x67, 0x96, 0xa8, 0x63, 0x04, 0xb0, - 0xc3, 0xfe, 0xbb, 0x6c, 0x7a, 0x7b, 0x58, 0x58, 0xe9, 0x7d, 0x37, 0x67, 0xe1, 0xe9, - }; - return tx.AddSignature(signature); - } } } diff --git a/Libplanet/Blocks/Block.cs b/Libplanet/Blocks/Block.cs index d10076f8915..0095d30ed18 100644 --- a/Libplanet/Blocks/Block.cs +++ b/Libplanet/Blocks/Block.cs @@ -45,8 +45,6 @@ public sealed class Block : IPreEvaluationBlock, IBlockHeader, IEquatable< /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, diff --git a/Libplanet/Blocks/BlockContent.cs b/Libplanet/Blocks/BlockContent.cs index 79159413b5e..6afea55efd8 100644 --- a/Libplanet/Blocks/BlockContent.cs +++ b/Libplanet/Blocks/BlockContent.cs @@ -44,8 +44,6 @@ public sealed class BlockContent : BlockMetadata, IBlockContent /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, @@ -81,8 +79,6 @@ public BlockContent(IBlockMetadata metadata, IEnumerable> transac /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, @@ -118,8 +114,6 @@ public BlockContent(IBlockMetadata metadata) /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, @@ -150,8 +144,6 @@ public BlockContent() /// together. /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, diff --git a/Libplanet/Blocks/BlockContentExtensions.cs b/Libplanet/Blocks/BlockContentExtensions.cs index 02b2022b71e..df02344142d 100644 --- a/Libplanet/Blocks/BlockContentExtensions.cs +++ b/Libplanet/Blocks/BlockContentExtensions.cs @@ -33,8 +33,6 @@ public static class BlockContentExtensions /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, diff --git a/Libplanet/Blocks/PreEvaluationBlock.cs b/Libplanet/Blocks/PreEvaluationBlock.cs index eedfd31601c..f0e08a796e3 100644 --- a/Libplanet/Blocks/PreEvaluationBlock.cs +++ b/Libplanet/Blocks/PreEvaluationBlock.cs @@ -51,8 +51,6 @@ public sealed class PreEvaluationBlock : PreEvaluationBlockHeader, IPreEvalua /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, @@ -108,8 +106,6 @@ Nonce nonce /// than its . /// Thrown when any tx signature is invalid or /// not signed by its signer. - /// Thrown when any tx signer is not derived - /// from its its public key. /// Thrown when the same tx nonce is used by /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. /// Note that this validates only a block's intrinsic integrity between its transactions, diff --git a/Libplanet/Tx/InvalidTxPublicKeyException.cs b/Libplanet/Tx/InvalidTxPublicKeyException.cs deleted file mode 100644 index d0ae271fa0e..00000000000 --- a/Libplanet/Tx/InvalidTxPublicKeyException.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Libplanet.Tx -{ - /// - /// The exception that is thrown when a 's - /// is not derived from its - /// . - /// - [Serializable] - public class InvalidTxPublicKeyException : InvalidTxException - { - /// - /// Creates a new object. - /// - /// The invalid 's - /// . It is automatically included to - /// the string. - /// Specifies an . - /// - public InvalidTxPublicKeyException(TxId txid, string message) - : base(txid, message) - { - } - } -} diff --git a/Libplanet/Tx/RawTransaction.cs b/Libplanet/Tx/RawTransaction.cs deleted file mode 100644 index 1601bd36e2b..00000000000 --- a/Libplanet/Tx/RawTransaction.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using Bencodex.Types; - -namespace Libplanet.Tx -{ - internal readonly struct RawTransaction : IEquatable - { - public static readonly byte[] NonceKey = { 0x6e }; // 'n' - - public static readonly byte[] SignerKey = { 0x73 }; // 's' - - public static readonly byte[] GenesisHashKey = { 0x67 }; // 'g' - - public static readonly byte[] UpdatedAddressesKey = { 0x75 }; // 'u' - - public static readonly byte[] PublicKeyKey = { 0x70 }; // 'p' - - public static readonly byte[] TimestampKey = { 0x74 }; // 't' - - public static readonly byte[] ActionsKey = { 0x61 }; // 'a' - - public static readonly byte[] SignatureKey = { 0x53 }; // 'S' - - public RawTransaction( - long nonce, - ImmutableArray signer, - ImmutableArray genesisHash, - ImmutableArray> updatedAddresses, - ImmutableArray publicKey, - string timestamp, - ImmutableArray actions - ) - : this( - nonce, - signer, - genesisHash, - updatedAddresses, - publicKey, - timestamp, - actions, - ImmutableArray.Empty - ) - { - } - - public RawTransaction( - long nonce, - ImmutableArray signer, - ImmutableArray genesisHash, - ImmutableArray> updatedAddresses, - ImmutableArray publicKey, - string timestamp, - ImmutableArray actions, - ImmutableArray signature - ) - { - Nonce = nonce; - Signer = signer; - GenesisHash = genesisHash; - UpdatedAddresses = updatedAddresses; - PublicKey = publicKey; - Timestamp = timestamp; - Actions = actions; - Signature = signature; - } - - public RawTransaction(Bencodex.Types.Dictionary dict) - { - Nonce = dict.GetValue(NonceKey); - Signer = dict.GetValue(SignerKey).ToImmutableArray(); - GenesisHash = dict.ContainsKey((IKey)(Binary)GenesisHashKey) - ? dict.GetValue(GenesisHashKey).ToImmutableArray() - : ImmutableArray.Empty; - UpdatedAddresses = dict.GetValue(UpdatedAddressesKey) - .Select(value => ((Binary)value).ToImmutableArray()).ToImmutableArray(); - PublicKey = dict.GetValue(PublicKeyKey).ToImmutableArray(); - Timestamp = dict.GetValue(TimestampKey); - Actions = dict.GetValue(ActionsKey).ToImmutableArray(); - - Signature = dict.ContainsKey((IKey)(Binary)SignatureKey) - ? dict.GetValue(SignatureKey).ToImmutableArray() - : ImmutableArray.Empty; - } - - public long Nonce { get; } - - public ImmutableArray Signer { get; } - - public ImmutableArray PublicKey { get; } - - public ImmutableArray GenesisHash { get; } - - public ImmutableArray> UpdatedAddresses { get; } - - public string Timestamp { get; } - - public ImmutableArray Signature { get; } - - public ImmutableArray Actions { get; } - - public static bool operator ==(RawTransaction left, RawTransaction right) => - left.Equals(right); - - public static bool operator !=(RawTransaction left, RawTransaction right) => - !left.Equals(right); - - public bool Equals(RawTransaction other) => Nonce == other.Nonce && - Signer.SequenceEqual(other.Signer) && - PublicKey.SequenceEqual(other.PublicKey) && - GenesisHash.SequenceEqual(other.GenesisHash) && - UpdatedAddresses.SequenceEqual( - other.UpdatedAddresses) && - Timestamp == other.Timestamp && - Signature.SequenceEqual(other.Signature) && - Actions.SequenceEqual(other.Actions); - - public override bool Equals(object? obj) => obj is RawTransaction other && Equals(other); - - public RawTransaction AddSignature(byte[] signature) - { - return new RawTransaction( - Nonce, - Signer, - GenesisHash, - UpdatedAddresses, - PublicKey, - Timestamp, - Actions, - signature.ToImmutableArray() - ); - } - - public Bencodex.Types.Dictionary ToBencodex() - { - var updatedAddresses = - UpdatedAddresses.Select(addr => (IValue)(Binary)addr.ToArray()); - var dict = Bencodex.Types.Dictionary.Empty - .Add(NonceKey, Nonce) - .Add(SignerKey, Signer.ToArray()) - .Add(UpdatedAddressesKey, updatedAddresses) - .Add(PublicKeyKey, PublicKey.ToArray()) - .Add(TimestampKey, Timestamp) - .Add(ActionsKey, Actions); - - if (GenesisHash != ImmutableArray.Empty) - { - dict = dict.Add(GenesisHashKey, GenesisHash.ToArray()); - } - - if (Signature != ImmutableArray.Empty) - { - dict = dict.Add(SignatureKey, Signature.ToArray()); - } - - return dict; - } - - public override int GetHashCode() => ByteUtil.CalculateHashCode(Signature.ToArray()); - - public override string ToString() - { - string updatedAddresses = string.Join( - string.Empty, - UpdatedAddresses.Select(a => "\n " + ByteUtil.Hex(a.ToArray())) - ); - return $@"{nameof(RawTransaction)} - {nameof(Nonce)} = {Nonce.ToString(CultureInfo.InvariantCulture)} - {nameof(Signer)} = {ByteUtil.Hex(Signer.ToArray())} - {nameof(PublicKey)} = {ByteUtil.Hex(PublicKey.ToArray())} - {nameof(GenesisHash)} = {ByteUtil.Hex(GenesisHash.ToArray())} - {nameof(UpdatedAddresses)} = {updatedAddresses} - {nameof(Timestamp)} = {Timestamp} - {nameof(Signature)} = {ByteUtil.Hex(Signature.ToArray())}"; - } - } -} diff --git a/Libplanet/Tx/Transaction.cs b/Libplanet/Tx/Transaction.cs index e42167bda0c..98c6f5f89aa 100644 --- a/Libplanet/Tx/Transaction.cs +++ b/Libplanet/Tx/Transaction.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.Linq; using System.Security.Cryptography; using Bencodex; @@ -35,9 +34,33 @@ public sealed class Transaction : IEquatable>, ITxMetadata private const int BytesCacheThreshold = 50 * 1024; private TxId? _id; + private TxMetadata _metadata; private byte[] _signature; private byte[] _bytes; + /// + /// Creates a new instance by copying data from a specified + /// transaction . + /// + /// The transaction metadata that contains data to copy. + /// A list of s to include. This can be empty, + /// but cannot be . This goes to the property. + /// + /// A digital signature of the content of this + /// . This has to be signed by 's + /// . This is copied and then assigned to + /// the property. + /// Thrown when + /// is passed to or . + public Transaction(ITxMetadata metadata, IEnumerable actions, byte[] signature) + { + _metadata = new TxMetadata(metadata); + Actions = actions?.ToImmutableList() + ?? throw new ArgumentNullException(nameof(actions)); + Signature = signature + ?? throw new ArgumentNullException(nameof(signature)); + } + /// /// Creates a new . /// This constructor takes all required and only required values @@ -53,15 +76,12 @@ public sealed class Transaction : IEquatable>, ITxMetadata /// s committed by the /// of this transaction. This goes to the /// property. - /// An of the account - /// who signs this transaction. If this is not derived from is - /// thrown. This goes to the property. - /// A of the account - /// who signs this transaction. If this does not match to address - /// is thrown. This cannot be null. This goes to - /// the property. + /// Ignored. Left only for backward compatibility. It will be + /// completely gone in the future. See also parameter's + /// description. + /// A used for signing this transaction. + /// This cannot be . This goes to the + /// property, and property is also derived from this value. /// A value /// of the genesis which this is made from. /// This can be null iff the transaction is contained @@ -82,10 +102,8 @@ public sealed class Transaction : IEquatable>, ITxMetadata /// or it will throw . /// This is copied and then assigned to the /// property. - /// Thrown when null - /// is passed to , - /// , or . - /// + /// Thrown when + /// is passed to or . public Transaction( long nonce, Address signer, @@ -96,18 +114,20 @@ public Transaction( IEnumerable actions, byte[] signature) { - Nonce = nonce; - Signer = signer; - GenesisHash = genesisHash; - UpdatedAddresses = updatedAddresses ?? - throw new ArgumentNullException(nameof(updatedAddresses)); + _metadata = new TxMetadata(publicKey + ?? throw new ArgumentNullException(nameof(publicKey))) + { + Nonce = nonce, + GenesisHash = genesisHash, + UpdatedAddresses = updatedAddresses + ?? throw new ArgumentNullException(nameof(updatedAddresses)), + Timestamp = timestamp, + }; + Signature = signature ?? throw new ArgumentNullException(nameof(signature)); - Timestamp = timestamp; Actions = actions?.ToImmutableList() ?? throw new ArgumentNullException(nameof(actions)); - PublicKey = publicKey ?? - throw new ArgumentNullException(nameof(publicKey)); } /// @@ -117,30 +137,12 @@ public Transaction( /// representation of instance. /// public Transaction(Bencodex.Types.Dictionary dict) - : this(new RawTransaction(dict)) - { - } - -#pragma warning disable SA1118 // Parameter spans multiple line - internal Transaction(RawTransaction rawTx) - : this( - rawTx.Nonce, - new Address(rawTx.Signer), - new PublicKey(rawTx.PublicKey.ToArray()), - rawTx.GenesisHash != ImmutableArray.Empty - ? new BlockHash(rawTx.GenesisHash.ToArray()) - : (BlockHash?)null, - rawTx.UpdatedAddresses.Select( - a => new Address(a) - ).ToImmutableHashSet(), - DateTimeOffset.ParseExact( - rawTx.Timestamp, - TimestampFormat, - CultureInfo.InvariantCulture).ToUniversalTime(), - rawTx.Actions.Select(ToAction).ToImmutableList(), - rawTx.Signature.ToArray()) -#pragma warning restore SA1118 // Parameter spans multiple line { + _metadata = new TxMetadata(dict); + Actions = dict.GetValue(TxMetadata.ActionsKey) + .Select(ToAction) + .ToImmutableList(); + _signature = dict.GetValue(TxMetadata.SignatureKey).ToByteArray(); } private Transaction( @@ -185,13 +187,13 @@ public TxId Id } /// - public long Nonce { get; } + public long Nonce => _metadata.Nonce; /// - public Address Signer { get; } + public Address Signer => _metadata.Signer; /// - public IImmutableSet
UpdatedAddresses { get; } + public IImmutableSet
UpdatedAddresses => _metadata.UpdatedAddresses; /// /// A digital signature of the content of this @@ -225,13 +227,13 @@ private set public IImmutableList Actions { get; } /// - public DateTimeOffset Timestamp { get; } + public DateTimeOffset Timestamp => _metadata.Timestamp; /// - public PublicKey PublicKey { get; } + public PublicKey PublicKey => _metadata.PublicKey; /// - public BlockHash? GenesisHash { get; } + public BlockHash? GenesisHash => _metadata.GenesisHash; /// /// Decodes a 's @@ -245,9 +247,6 @@ private set /// is invalid or not signed by /// the account who corresponds to . /// - /// Thrown when its - /// is not derived from its - /// . /// public static Transaction Deserialize(byte[] bytes, bool validate = true) { @@ -556,7 +555,7 @@ public byte[] Serialize(bool sign) { codec = new Codec(); byte[] sigDict = - codec.Encode(Dictionary.Empty.Add(RawTransaction.SignatureKey, _signature)); + codec.Encode(Dictionary.Empty.Add(TxMetadata.SignatureKey, _signature)); var sigField = new byte[sigDict.Length - 1]; Array.Copy(sigDict, 1, sigField, 0, sigField.Length); int sigOffset = _bytes.IndexOf(sigField); @@ -591,7 +590,10 @@ public byte[] Serialize(bool sign) /// Bencodex /// representation of this . public Bencodex.Types.Dictionary ToBencodex(bool sign) => - ToRawTransaction(sign).ToBencodex(); + _metadata.ToBencodex( + Actions.Select(a => a.PlainValue), + sign ? ImmutableArray.Create(_signature) : (ImmutableArray?)null + ); /// /// Validates this and throws an appropriate exception @@ -601,9 +603,6 @@ public Bencodex.Types.Dictionary ToBencodex(bool sign) => /// is invalid or not signed by /// the account who corresponds to its . /// - /// Thrown when its - /// is not derived from its - /// . public void Validate() { if (Signature.Length == 0 || !PublicKey.Verify(Serialize(false), Signature)) @@ -613,14 +612,6 @@ public void Validate() "to verify."; throw new InvalidTxSignatureException(Id, message); } - - if (!new Address(PublicKey).Equals(Signer)) - { - string message = - $"The public key ({ByteUtil.Hex(PublicKey.Format(true))} " + - $"is not matched to the address ({Signer})."; - throw new InvalidTxPublicKeyException(Id, message); - } } /// @@ -632,29 +623,6 @@ public void Validate() /// public override int GetHashCode() => Id.GetHashCode(); - internal RawTransaction ToRawTransaction(bool includeSign) - { - ImmutableArray genesisHash = - GenesisHash?.ToByteArray().ToImmutableArray() ?? ImmutableArray.Empty; - var rawTx = new RawTransaction( - nonce: Nonce, - signer: Signer.ByteArray, - genesisHash: genesisHash, - updatedAddresses: UpdatedAddresses.Select(a => - a.ByteArray).ToImmutableArray(), - publicKey: PublicKey.Format(false).ToImmutableArray(), - timestamp: Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture), - actions: Actions.Select(a => a.PlainValue).ToImmutableArray() - ); - - if (includeSign) - { - rawTx = rawTx.AddSignature(Signature); - } - - return rawTx; - } - private static T ToAction(IValue value) { var action = new T(); diff --git a/Libplanet/Tx/TxMetadata.cs b/Libplanet/Tx/TxMetadata.cs index b8cc62a5abf..a5d74e9fc36 100644 --- a/Libplanet/Tx/TxMetadata.cs +++ b/Libplanet/Tx/TxMetadata.cs @@ -58,7 +58,7 @@ public TxMetadata(ITxMetadata metadata) /// Creates a from a Bencodex . /// /// A Bencodex dictionary made using - /// method. + /// method. /// Thrown when the given /// lacks some fields. /// Thrown when the given @@ -110,7 +110,7 @@ public TxMetadata(Bencodex.Types.Dictionary dictionary) /// present when you make a . /// A Bencodex dictionary that the transaction turns into. public Bencodex.Types.Dictionary ToBencodex( - IReadOnlyList actions, + IEnumerable actions, ImmutableArray? signature = null ) { From 2c7754185b5d39b812bab0a413dff227e032e19f Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 11:38:53 +0900 Subject: [PATCH 05/14] Better TxMetadata.ToString() formatting [changelog skip] --- Libplanet/Tx/TxMetadata.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Libplanet/Tx/TxMetadata.cs b/Libplanet/Tx/TxMetadata.cs index a5d74e9fc36..0f283fa8589 100644 --- a/Libplanet/Tx/TxMetadata.cs +++ b/Libplanet/Tx/TxMetadata.cs @@ -138,5 +138,21 @@ public Bencodex.Types.Dictionary ToBencodex( return dict; } + + /// + public override string ToString() + { + return nameof(TxMetadata) + " {\n" + + $" {nameof(Nonce)} = {Nonce},\n" + + $" {nameof(Signer)} = {Signer},\n" + + $" {nameof(UpdatedAddresses)} = ({UpdatedAddresses.Count})" + + (UpdatedAddresses.Any() + ? $"\n {string.Join("\n ", UpdatedAddresses)};\n" + : ";\n") + + $" {nameof(PublicKey)} = {PublicKey},\n" + + $" {nameof(Timestamp)} = {Timestamp},\n" + + $" {nameof(GenesisHash)} = {GenesisHash?.ToString() ?? "(null)"},\n" + + "}"; + } } } From 814d167220d64ae679989479e49173839627c0f8 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 11:51:40 +0900 Subject: [PATCH 06/14] Fix InvalidOperationException thrown by PublicKey.Verify() --- CHANGES.md | 4 ++++ Libplanet.Tests/Crypto/PublicKeyTest.cs | 1 + Libplanet/Crypto/PublicKey.cs | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 12b5609ab89..d3ac416af98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,10 @@ To be released. ### Bug fixes + - Fixed `InvalidOperationException` thrown by `PublicKey.Verify()` method + if `signature` is a `default(ImmutableArray)`. Instead, it silently + returns `false` now. [[#1978]] + ### Dependencies ### CLI tools diff --git a/Libplanet.Tests/Crypto/PublicKeyTest.cs b/Libplanet.Tests/Crypto/PublicKeyTest.cs index a08288dedba..18b9c93642b 100644 --- a/Libplanet.Tests/Crypto/PublicKeyTest.cs +++ b/Libplanet.Tests/Crypto/PublicKeyTest.cs @@ -122,6 +122,7 @@ public void Verify() }; Assert.True(pubKey.Verify(payload, signature)); Assert.False(pubKey.Verify(payload, ImmutableArray.Empty)); + Assert.False(pubKey.Verify(payload, default(ImmutableArray))); } [Fact] diff --git a/Libplanet/Crypto/PublicKey.cs b/Libplanet/Crypto/PublicKey.cs index 5f062a7ae18..2c1ece636d0 100644 --- a/Libplanet/Crypto/PublicKey.cs +++ b/Libplanet/Crypto/PublicKey.cs @@ -164,7 +164,7 @@ public bool Verify(IReadOnlyList message, IReadOnlyList signature) throw new ArgumentNullException(nameof(signature)); } - if (!signature.Any()) + if (signature is ImmutableArray i ? i.IsDefaultOrEmpty : !signature.Any()) { return false; } From ff7f9119f2b798271b1ca322599d6edeee950c5d Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 12:02:13 +0900 Subject: [PATCH 07/14] Fix NullReferenceException thrown by ByteUtil.Hex(ImmutableArray) --- CHANGES.md | 3 +++ Libplanet.Tests/ByteUtilTest.cs | 1 + Libplanet/ByteUtil.cs | 4 +--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d3ac416af98..d5ec35d72e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -50,6 +50,9 @@ To be released. - Fixed `InvalidOperationException` thrown by `PublicKey.Verify()` method if `signature` is a `default(ImmutableArray)`. Instead, it silently returns `false` now. [[#1978]] + - Fixed `NullReferenceException` thrown by `ByteUtil.Hex(in + ImmutabelArray)` method if a `default(ImmutableArray)` is + present. Instead, it silently returns an empty string now. [[#1978]] ### Dependencies diff --git a/Libplanet.Tests/ByteUtilTest.cs b/Libplanet.Tests/ByteUtilTest.cs index 9001122f9c6..50ee3a626e0 100644 --- a/Libplanet.Tests/ByteUtilTest.cs +++ b/Libplanet.Tests/ByteUtilTest.cs @@ -17,6 +17,7 @@ public void HexTest() const string expectedHex = "45a22187e2d8850bb357886958bc3e8560929ccc"; Assert.Equal(expectedHex, ByteUtil.Hex(bs)); Assert.Equal(expectedHex, ByteUtil.Hex(ImmutableArray.Create(bs))); + Assert.Empty(ByteUtil.Hex(default(ImmutableArray))); } [Fact] diff --git a/Libplanet/ByteUtil.cs b/Libplanet/ByteUtil.cs index f1e9620bfa9..7ab7d5791b3 100644 --- a/Libplanet/ByteUtil.cs +++ b/Libplanet/ByteUtil.cs @@ -87,11 +87,9 @@ public static string Hex(byte[] bytes) /// /// A hexadecimal string which encodes the given /// . - /// Thrown when the given - /// is null. [Pure] public static string Hex(in ImmutableArray bytes) => - Hex(bytes.ToArray()); + bytes.IsDefaultOrEmpty ? string.Empty : Hex(bytes.ToArray()); /// /// Calculates a deterministic hash code from a given From ede5432c7c7e11c67c812387951bc8647f878f02 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 13:13:42 +0900 Subject: [PATCH 08/14] Fix TxId(byte[]) constructor's ArgumentOutOfRangeException fields --- CHANGES.md | 2 ++ Libplanet.Tests/Tx/TxIdTest.cs | 1 + Libplanet/Tx/TxId.cs | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d5ec35d72e4..0a931ee53c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -53,6 +53,8 @@ To be released. - Fixed `NullReferenceException` thrown by `ByteUtil.Hex(in ImmutabelArray)` method if a `default(ImmutableArray)` is present. Instead, it silently returns an empty string now. [[#1978]] + - Fixed a `TxId(byte[])` constructor's bug where `ParamName` and `Message` of + `ArgumentOutOfRangeException` it had thrown had been reversed. [[#1978]] ### Dependencies diff --git a/Libplanet.Tests/Tx/TxIdTest.cs b/Libplanet.Tests/Tx/TxIdTest.cs index 9cf3ce6e03a..b47a319fed0 100644 --- a/Libplanet.Tests/Tx/TxIdTest.cs +++ b/Libplanet.Tests/Tx/TxIdTest.cs @@ -27,6 +27,7 @@ public void TxIdMustBe32Bytes() byte[] bytes = TestUtils.GetRandomBytes(size); Assert.Throws( + "txid", () => new TxId(bytes) ); } diff --git a/Libplanet/Tx/TxId.cs b/Libplanet/Tx/TxId.cs index 5cf17761144..fb125ca96dd 100644 --- a/Libplanet/Tx/TxId.cs +++ b/Libplanet/Tx/TxId.cs @@ -51,8 +51,8 @@ public TxId(byte[] txid) if (txid.Length != Size) { throw new ArgumentOutOfRangeException( - $"TxId must be {Size} bytes.", - nameof(txid) + nameof(txid), + $"{nameof(TxId)} must be {Size} bytes." ); } From 76442c8956016ba6b4c3e36d6cfdc21b3ee623ac Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 13:14:03 +0900 Subject: [PATCH 09/14] Missing unit test on TxId.ToHex() --- Libplanet.Tests/Tx/TxIdTest.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Libplanet.Tests/Tx/TxIdTest.cs b/Libplanet.Tests/Tx/TxIdTest.cs index b47a319fed0..780ea7af063 100644 --- a/Libplanet.Tests/Tx/TxIdTest.cs +++ b/Libplanet.Tests/Tx/TxIdTest.cs @@ -58,6 +58,24 @@ public void ToByteArrayShouldNotExposeContents() Assert.Equal(0x45, txId.ToByteArray()[0]); } + [Fact] + public void ToHex() + { + var id = new TxId( + new byte[] + { + 0x45, 0xa2, 0x21, 0x87, 0xe2, 0xd8, 0x85, 0x0b, 0xb3, 0x57, + 0x88, 0x69, 0x58, 0xbc, 0x3e, 0x85, 0x60, 0x92, 0x9c, 0xcc, + 0x88, 0x69, 0x58, 0xbc, 0x3e, 0x85, 0x60, 0x92, 0x9c, 0xcc, + 0x9c, 0xcc, + } + ); + Assert.Equal( + "45a22187e2d8850bb357886958bc3e8560929ccc886958bc3e8560929ccc9ccc", + id.ToHex() + ); + } + [Fact] public void ToString_() { From 73a14c81653cf8b39a5618c9ba55aaf8809ea698 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 13:14:21 +0900 Subject: [PATCH 10/14] TxId.FromHex() static method --- CHANGES.md | 1 + Libplanet.Tests/Tx/TxIdTest.cs | 35 ++++++++++++++++++++++++++++++++++ Libplanet/Tx/TxId.cs | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 0a931ee53c3..744bdc14e57 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,7 @@ To be released. [[#1164], [#1974], [#1978]] - Added `Transaction(ITxMetadata, IEnumerable, byte[])` constructor. [[#1164], [#1978]] + - Added `TxId.FromString()` static method. [[#1978]] ### Behavioral changes diff --git a/Libplanet.Tests/Tx/TxIdTest.cs b/Libplanet.Tests/Tx/TxIdTest.cs index 780ea7af063..23c52fa2dc3 100644 --- a/Libplanet.Tests/Tx/TxIdTest.cs +++ b/Libplanet.Tests/Tx/TxIdTest.cs @@ -33,6 +33,41 @@ public void TxIdMustBe32Bytes() } } + [Fact] + public void FromHex() + { + TxId actual = TxId.FromHex( + "45a22187e2d8850bb357886958bc3e8560929ccc886958bc3e8560929ccc9ccc"); + var expected = new TxId( + new byte[] + { + 0x45, 0xa2, 0x21, 0x87, 0xe2, 0xd8, 0x85, 0x0b, 0xb3, 0x57, + 0x88, 0x69, 0x58, 0xbc, 0x3e, 0x85, 0x60, 0x92, 0x9c, 0xcc, + 0x88, 0x69, 0x58, 0xbc, 0x3e, 0x85, 0x60, 0x92, 0x9c, 0xcc, + 0x9c, 0xcc, + } + ); + Assert.Equal(expected, actual); + + Assert.Throws("hex", () => TxId.FromHex(null)); + Assert.Throws(() => TxId.FromHex("0g")); + Assert.Throws("hex", () => TxId.FromHex("1")); + Assert.Throws( + "hex", + () => TxId.FromHex("45a22187e2d8850bb357886958bc3e8560929ccc886958bc3e8560929ccc9c") + ); + Assert.Throws( + "hex", + () => + TxId.FromHex("45a22187e2d8850bb357886958bc3e8560929ccc886958bc3e8560929ccc9ccc0") + ); + Assert.Throws( + "hex", + () => + TxId.FromHex("45a22187e2d8850bb357886958bc3e8560929ccc886958bc3e8560929ccc9ccc00") + ); + } + [Fact] public void ToByteArray() { diff --git a/Libplanet/Tx/TxId.cs b/Libplanet/Tx/TxId.cs index fb125ca96dd..4509978eb22 100644 --- a/Libplanet/Tx/TxId.cs +++ b/Libplanet/Tx/TxId.cs @@ -90,6 +90,41 @@ public ImmutableArray ByteArray public static bool operator !=(TxId left, TxId right) => !left.Equals(right); + /// + /// Creates a value from a string. + /// + /// A hexadecimal string which encodes a . + /// This has to contain 64 hexadecimal digits and must not be + /// This is usually made by method. + /// A corresponding value. + /// Thrown when the given + /// string is . + /// Thrown when the given + /// is shorter or longer than 64 characters. + /// Thrown when the given string is + /// not a valid hexadecimal string. + /// + public static TxId FromHex(string hex) + { + if (hex is null) + { + throw new ArgumentNullException(nameof(hex)); + } + + byte[] bytes = ByteUtil.ParseHex(hex); + try + { + return new TxId(bytes); + } + catch (ArgumentOutOfRangeException) + { + throw new ArgumentOutOfRangeException( + nameof(hex), + $"Expected {Size * 2} characters, but {hex.Length} characters given." + ); + } + } + public bool Equals(TxId other) => ByteArray.SequenceEqual(other.ByteArray); public override bool Equals(object obj) => obj is TxId other && Equals(other); From 39cfed0ac8473913921ce62f85cabcb27758a887 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 13:14:40 +0900 Subject: [PATCH 11/14] Missing [Pure] attributes on TxMetadata methods --- Libplanet/Tx/TxMetadata.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Libplanet/Tx/TxMetadata.cs b/Libplanet/Tx/TxMetadata.cs index 0f283fa8589..f66acc2f1bf 100644 --- a/Libplanet/Tx/TxMetadata.cs +++ b/Libplanet/Tx/TxMetadata.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; using Bencodex.Types; @@ -109,6 +110,7 @@ public TxMetadata(Bencodex.Types.Dictionary dictionary) /// (which is the default) when you make a signature, and should be /// present when you make a . /// A Bencodex dictionary that the transaction turns into. + [Pure] public Bencodex.Types.Dictionary ToBencodex( IEnumerable actions, ImmutableArray? signature = null @@ -140,6 +142,7 @@ public Bencodex.Types.Dictionary ToBencodex( } /// + [Pure] public override string ToString() { return nameof(TxMetadata) + " {\n" + From af187784e605fca9a170a69c9f8e0ffe7bed722f Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 14:25:26 +0900 Subject: [PATCH 12/14] ITxExcerpt interface & TxExcerptExtensions.ValidateTxNonces() method --- CHANGES.md | 4 +- Libplanet/Blocks/BlockContent.cs | 39 +++-------------- Libplanet/Tx/ITxExcerpt.cs | 16 +++++++ Libplanet/Tx/Transaction.cs | 2 +- Libplanet/Tx/TxExcerptExtensions.cs | 65 +++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 Libplanet/Tx/ITxExcerpt.cs create mode 100644 Libplanet/Tx/TxExcerptExtensions.cs diff --git a/CHANGES.md b/CHANGES.md index 744bdc14e57..eb71e0c3350 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,7 +32,9 @@ To be released. - Added `ITxMetadata` interface. [[#1164], [#1974], [#1978]] - Added `TxMetadata` class. [[#1164], [#1974], [#1978]] - - `Transaction` now implements `ITxMetadata` interface. + - Added `ITxExcerpt` interface. [[#1164], [#1974], [#1978]] + - Added `TxExcerptExtensions` static class. [[#1164], [#1974], [#1978]] + - `Transaction` now implements `ITxExcerpt` interface. [[#1164], [#1974], [#1978]] - Added `Transaction(ITxMetadata, IEnumerable, byte[])` constructor. [[#1164], [#1978]] diff --git a/Libplanet/Blocks/BlockContent.cs b/Libplanet/Blocks/BlockContent.cs index 6afea55efd8..209c63910e7 100644 --- a/Libplanet/Blocks/BlockContent.cs +++ b/Libplanet/Blocks/BlockContent.cs @@ -156,41 +156,12 @@ public IReadOnlyList> Transactions get => _transactions; set { - IEnumerable>> signerTxs = - value.OrderBy(tx => tx.Nonce).GroupBy(tx => tx.Signer); - BlockHash? genesisHash = null; - foreach (IGrouping> txs in signerTxs) + value.ValidateTxNonces(Index); + foreach (Transaction tx in value) { - long lastNonce = -1L; - foreach (Transaction tx in txs) - { - // FIXME: Transaction should disallow illegal states to be represented - // as its instances. https://github.com/planetarium/libplanet/issues/1164 - tx.Validate(); - - long nonce = tx.Nonce; - if (lastNonce >= 0 && lastNonce + 1 != nonce) - { - Address s = tx.Signer; - string msg = nonce <= lastNonce - ? $"The signer {s}'s nonce {nonce} was already consumed before." - : $"The signer {s}'s nonce {lastNonce} has to be added first."; - throw new InvalidTxNonceException(tx.Id, lastNonce + 1, tx.Nonce, msg); - } - - if (genesisHash is { } g && !tx.GenesisHash.Equals(g)) - { - throw new InvalidTxGenesisHashException( - tx.Id, - g, - tx.GenesisHash, - $"Transactions in the block #{Index} are inconsistent." - ); - } - - lastNonce = nonce; - genesisHash = tx.GenesisHash; - } + // FIXME: Transaction should disallow illegal states to be represented + // as its instances. https://github.com/planetarium/libplanet/issues/1164 + tx.Validate(); } _transactions = value.OrderBy(tx => tx.Id).ToImmutableArray(); diff --git a/Libplanet/Tx/ITxExcerpt.cs b/Libplanet/Tx/ITxExcerpt.cs new file mode 100644 index 00000000000..d173e1b1364 --- /dev/null +++ b/Libplanet/Tx/ITxExcerpt.cs @@ -0,0 +1,16 @@ +namespace Libplanet.Tx +{ + /// + /// Similar to except that it has as well. + /// Note that this does not contain actions or signature. + /// + public interface ITxExcerpt : ITxMetadata + { + /// + /// The unique identifier derived from this transaction's content including actions and + /// signature. + /// + /// + TxId Id { get; } + } +} diff --git a/Libplanet/Tx/Transaction.cs b/Libplanet/Tx/Transaction.cs index 98c6f5f89aa..e4f1781c2bc 100644 --- a/Libplanet/Tx/Transaction.cs +++ b/Libplanet/Tx/Transaction.cs @@ -25,7 +25,7 @@ namespace Libplanet.Tx /// /// /// - public sealed class Transaction : IEquatable>, ITxMetadata + public sealed class Transaction : IEquatable>, ITxExcerpt where T : IAction, new() { private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; diff --git a/Libplanet/Tx/TxExcerptExtensions.cs b/Libplanet/Tx/TxExcerptExtensions.cs new file mode 100644 index 00000000000..ecae04eeead --- /dev/null +++ b/Libplanet/Tx/TxExcerptExtensions.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Libplanet.Blocks; + +namespace Libplanet.Tx +{ + /// + /// Useful extension methods for . + /// + public static class TxExcerptExtensions + { + /// + /// Validates if has valid nonces. + /// It assumes all given belong to a block together. + /// + /// A list of transactions. Their order does not matter. + /// The index of the block that transactions will belong to. + /// It's only used for exception messages. + /// A transaction type. + /// Thrown when the same tx nonce is used by + /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. + /// Note that this validates only a block's intrinsic integrity between its transactions, + /// but does not guarantee integrity between blocks. Such validation needs to be conducted + /// by . + /// Thrown when transactions to set have + /// inconsistent genesis hashes. + // FIXME: Needs a unit tests. See also BlockContentTest.Transactions* tests. + public static void ValidateTxNonces(this IEnumerable transactions, long blockIndex) + where T : ITxExcerpt + { + IEnumerable> signerTxs = + transactions.OrderBy(tx => tx.Nonce).GroupBy(tx => tx.Signer); + BlockHash? genesisHash = null; + foreach (IGrouping txs in signerTxs) + { + long lastNonce = -1L; + foreach (T tx in txs) + { + long nonce = tx.Nonce; + if (lastNonce >= 0 && lastNonce + 1 != nonce) + { + Address s = tx.Signer; + string msg = nonce <= lastNonce + ? $"The signer {s}'s nonce {nonce} was already consumed before." + : $"The signer {s}'s nonce {lastNonce} has to be added first."; + throw new InvalidTxNonceException(tx.Id, lastNonce + 1, tx.Nonce, msg); + } + + if (genesisHash is { } g && !tx.GenesisHash.Equals(g)) + { + throw new InvalidTxGenesisHashException( + tx.Id, + g, + tx.GenesisHash, + $"Transactions in the block #{blockIndex} are inconsistent." + ); + } + + lastNonce = nonce; + genesisHash = tx.GenesisHash; + } + } + } + } +} From 767068bb236a6f3424745b5b874e64728f8f37d0 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 17:34:11 +0900 Subject: [PATCH 13/14] UntypedTransaction class --- .github/bin/constants.sh | 1 + CHANGES.md | 2 + CONTRIBUTING.md | 7 + Docs/docfx.json | 1 + .../Libplanet.Node.Tests.csproj | 81 ++++++++++ .../UntypedTransactionTest.cs | 136 +++++++++++++++++ Libplanet.Node.Tests/xunit.runner.mono.json | 6 + Libplanet.Node/Libplanet.Node.csproj | 76 ++++++++++ Libplanet.Node/UntypedTransaction.cs | 138 ++++++++++++++++++ Libplanet.sln | 28 ++++ Libplanet/AssemblyInfo.cs | 2 + 11 files changed, 478 insertions(+) create mode 100644 Libplanet.Node.Tests/Libplanet.Node.Tests.csproj create mode 100644 Libplanet.Node.Tests/UntypedTransactionTest.cs create mode 100644 Libplanet.Node.Tests/xunit.runner.mono.json create mode 100644 Libplanet.Node/Libplanet.Node.csproj create mode 100644 Libplanet.Node/UntypedTransaction.cs diff --git a/.github/bin/constants.sh b/.github/bin/constants.sh index d546f65d35b..85ab220cedd 100644 --- a/.github/bin/constants.sh +++ b/.github/bin/constants.sh @@ -6,6 +6,7 @@ projects=( "Libplanet" "Libplanet.Stun" "Libplanet.Net" + "Libplanet.Node" "Libplanet.RocksDBStore" "Libplanet.Analyzers" "Libplanet.Tools" diff --git a/CHANGES.md b/CHANGES.md index eb71e0c3350..ff43a09ea88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,7 @@ To be released. ### Added APIs + - Introduced *Libplanet.Node* package. [[#1974], [#1978]] - Added `ITxMetadata` interface. [[#1164], [#1974], [#1978]] - Added `TxMetadata` class. [[#1164], [#1974], [#1978]] - Added `ITxExcerpt` interface. [[#1164], [#1974], [#1978]] @@ -39,6 +40,7 @@ To be released. - Added `Transaction(ITxMetadata, IEnumerable, byte[])` constructor. [[#1164], [#1978]] - Added `TxId.FromString()` static method. [[#1978]] + - (Libplanet.Node) Added `UntypedTransaction` class. [[#1974], [#1978]] ### Behavioral changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a67437d8796..3cca42c9ce4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,6 +82,10 @@ on GitHub consists of several projects: *Libplanet*. This is distributed as a distinct NuGet package: *[Libplanet.Net]*. + - *Libplanet.Node*: User-friendly façade API for building your own + peer-to-peer network. This is distributed as a distinct NuGet package: + *[Libplanet.Node]*. + - *Libplanet.Stun*: The project dedicated to implement [TURN & STUN]. This is distributed as a distinct NuGet package: *[Libplanet.Stun]*. @@ -119,6 +123,8 @@ on GitHub consists of several projects: - *Libplanet.Net.Tests*: Unit tests of the *Libplanet.Net* project. + - *Libplanet.Node.Tests*: Unit tests of the *Libplanet.Node* project. + - *Libplanet.Stun.Tests*: Unit tests of the *Libplanet.Stun* project. - *Libplanet.RocksDBStore.Tests*: Unit tests of the *Libplanet.RocksDBStore* @@ -136,6 +142,7 @@ on GitHub consists of several projects: [NuGet package]: https://www.nuget.org/packages/Libplanet/ [Libplanet.Net]: https://www.nuget.org/packages/Libplanet.Net/ +[Libplanet.Node]: https://www.nuget.org/packages/Libplanet.Node/ [TURN & STUN]: https://snack.planetarium.dev/eng/2019/06/nat_traversal_2/ [RocksDB]: https://rocksdb.org/ [Libplanet.Stun]: https://www.nuget.org/packages/Libplanet.Stun/ diff --git a/Docs/docfx.json b/Docs/docfx.json index 1795fb69804..49c4f766755 100644 --- a/Docs/docfx.json +++ b/Docs/docfx.json @@ -6,6 +6,7 @@ "files": [ "Libplanet/Libplanet.csproj", "Libplanet.Net/Libplanet.Net.csproj", + "Libplanet.Node/Libplanet.Node.csproj", "Libplanet.RocksDBStore/Libplanet.RocksDBStore.csproj", "Libplanet.Extensions.Cocona/Libplanet.Extensions.Cocona.csproj", "Libplanet.Stun/Libplanet.Stun.csproj" diff --git a/Libplanet.Node.Tests/Libplanet.Node.Tests.csproj b/Libplanet.Node.Tests/Libplanet.Node.Tests.csproj new file mode 100644 index 00000000000..c0ef4b03eb7 --- /dev/null +++ b/Libplanet.Node.Tests/Libplanet.Node.Tests.csproj @@ -0,0 +1,81 @@ + + + netcoreapp3.1 + false + true + true + true + 8.0 + ..\Libplanet.Tests.ruleset + + + + + Menees.Analyzers.Settings.xml + + + + + + net6.0 + + + + $(TestsTargetFramework) + + + + + + all + + + + + + + all + + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers + all + + + + + + + + + + + + + + + + + + + xunit.runner.json + PreserveNewest + + + diff --git a/Libplanet.Node.Tests/UntypedTransactionTest.cs b/Libplanet.Node.Tests/UntypedTransactionTest.cs new file mode 100644 index 00000000000..365a00e5098 --- /dev/null +++ b/Libplanet.Node.Tests/UntypedTransactionTest.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Bencodex; +using Bencodex.Types; +using Libplanet.Blocks; +using Libplanet.Crypto; +using Libplanet.Tx; +using Xunit; + +namespace Libplanet.Node.Tests +{ + public class UntypedTransactionTest + { + private readonly PrivateKey _key1; + private readonly PrivateKey _key2; + private readonly TxMetadata _meta; + private readonly IValue[] _actionValues; + private readonly ImmutableArray _sig; + + public UntypedTransactionTest() + { + _key1 = new PrivateKey(new byte[] + { + 0x9b, 0xf4, 0x66, 0x4b, 0xa0, 0x9a, 0x89, 0xfa, 0xeb, 0x68, 0x4b, + 0x94, 0xe6, 0x9f, 0xfd, 0xe0, 0x1d, 0x26, 0xae, 0x14, 0xb5, 0x56, + 0x20, 0x4d, 0x3f, 0x6a, 0xb5, 0x8f, 0x61, 0xf7, 0x84, 0x18, + }); + _key2 = new PrivateKey(new byte[] + { + 0xfc, 0xf3, 0x0b, 0x33, 0x3d, 0x04, 0xcc, 0xfe, 0xb5, 0x62, 0xf0, + 0x00, 0xa3, 0x2d, 0xf4, 0x88, 0xe7, 0x15, 0x49, 0x49, 0xd3, 0x1d, + 0xdc, 0xac, 0x3c, 0xf9, 0x27, 0x8a, 0xcb, 0x57, 0x86, 0xc7, + }); + _meta = new TxMetadata(_key2.PublicKey) + { + Nonce = 0L, + UpdatedAddresses = new[] + { + _key1.ToAddress(), + _key2.ToAddress(), + }.ToImmutableHashSet(), + Timestamp = new DateTimeOffset(2022, 1, 12, 4, 56, 7, 890, default), + GenesisHash = BlockHash.FromString( + "83915317ebdbf870c567b263dd2e61ec9dca7fb381c592d80993291b6ffe5ad5"), + }; + _actionValues = new IValue[] { new Integer(123), new Integer(456) }; + Bencodex.Types.Dictionary unsignedDict = _meta.ToBencodex(_actionValues); + var codec = new Codec(); + _sig = ImmutableArray.Create(_key2.Sign(codec.Encode(unsignedDict))); + } + + [Fact] + public void Constructor() + { + var untyped = new UntypedTransaction(_meta, _actionValues, _sig); + Assert.Equal(_meta.Nonce, untyped.Nonce); + Assert.Equal(_meta.Signer, untyped.Signer); + Assert.Equal(_meta.UpdatedAddresses, untyped.UpdatedAddresses); + Assert.Equal(_meta.Timestamp, untyped.Timestamp); + Assert.Equal(_meta.PublicKey, untyped.PublicKey); + Assert.Equal(_meta.GenesisHash, untyped.GenesisHash); + Assert.Equal(_actionValues, untyped.ActionValues); + Assert.Equal(_sig, untyped.Signature); + + InvalidTxSignatureException e; + e = Assert.Throws( + () => new UntypedTransaction(_meta, _actionValues, default) + ); + Assert.Equal( + TxId.FromHex("ea601351c27c3c6291c4352ec060f06650b81c02ded4a4d22858da756098fd4e"), + e.TxId + ); + + e = Assert.Throws( + () => new UntypedTransaction(_meta, Enumerable.Empty(), _sig) + ); + Assert.Equal( + TxId.FromHex("f91abd37cad6962cb206a9c29faffddede8bce47751f3e5e4b0e1c8f714a4a82"), + e.TxId + ); + } + + [Fact] + public void Deserialize() + { + Bencodex.Types.Dictionary signedDict = _meta.ToBencodex(_actionValues, _sig); + var untyped = new UntypedTransaction(signedDict); + Assert.Equal(_meta.Nonce, untyped.Nonce); + Assert.Equal(_meta.Signer, untyped.Signer); + Assert.Equal(_meta.UpdatedAddresses, untyped.UpdatedAddresses); + Assert.Equal(_meta.Timestamp, untyped.Timestamp); + Assert.Equal(_meta.PublicKey, untyped.PublicKey); + Assert.Equal(_meta.GenesisHash, untyped.GenesisHash); + Assert.Equal(_actionValues, untyped.ActionValues); + Assert.Equal(_sig, untyped.Signature); + + InvalidTxSignatureException e; + var invalidSigDict = signedDict.SetItem(TxMetadata.SignatureKey, Array.Empty()); + e = Assert.Throws( + () => new UntypedTransaction(invalidSigDict) + ); + Assert.Equal( + TxId.FromHex("ea601351c27c3c6291c4352ec060f06650b81c02ded4a4d22858da756098fd4e"), + e.TxId + ); + + var invalidActionsDict = signedDict.SetItem(TxMetadata.ActionsKey, (IValue)new List()); + e = Assert.Throws( + () => new UntypedTransaction(invalidActionsDict) + ); + Assert.Equal( + TxId.FromHex("f91abd37cad6962cb206a9c29faffddede8bce47751f3e5e4b0e1c8f714a4a82"), + e.TxId + ); + } + + [Fact] + public void ToBencodex() + { + Bencodex.Types.Dictionary dict = + new UntypedTransaction(_meta, _actionValues, _sig).ToBencodex(); + Assert.Equal(_meta.ToBencodex(_actionValues, _sig), dict); + + var deserialized = new UntypedTransaction(dict); + Assert.Equal(_meta.Nonce, deserialized.Nonce); + Assert.Equal(_meta.Signer, deserialized.Signer); + Assert.Equal(_meta.UpdatedAddresses, deserialized.UpdatedAddresses); + Assert.Equal(_meta.Timestamp, deserialized.Timestamp); + Assert.Equal(_meta.PublicKey, deserialized.PublicKey); + Assert.Equal(_meta.GenesisHash, deserialized.GenesisHash); + Assert.Equal(_actionValues, deserialized.ActionValues); + Assert.Equal(_sig, deserialized.Signature); + } + } +} diff --git a/Libplanet.Node.Tests/xunit.runner.mono.json b/Libplanet.Node.Tests/xunit.runner.mono.json new file mode 100644 index 00000000000..e2b11da551b --- /dev/null +++ b/Libplanet.Node.Tests/xunit.runner.mono.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://xunit.github.io/schema/current/xunit.runner.schema.json", + "appDomain": "denied", + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} diff --git a/Libplanet.Node/Libplanet.Node.csproj b/Libplanet.Node/Libplanet.Node.csproj new file mode 100644 index 00000000000..3ca197b97ee --- /dev/null +++ b/Libplanet.Node/Libplanet.Node.csproj @@ -0,0 +1,76 @@ + + + Libplanet.Node + https://libplanet.io/ + icon.png + Planetarium + Planetarium + LGPL-2.1-or-later + true + https://github.com/planetarium/libplanet/blob/main/CHANGES.md + multiplayer online game;game;blockchain + https://github.com/planetarium/libplanet.git + git + + + + 8.0 + netstandard2.0;netstandard2.1 + enable + Libplanet.Node + Libplanet.Node + true + true + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + enable + true + true + $(NoWarn);NU5104;MEN001 + false + ..\Libplanet.ruleset + + + + + + + + Menees.Analyzers.Settings.xml + + + + + + all + + + all + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + + runtime; build; native; contentfiles; analyzers + + + + + + + + runtime; build; native; contentfiles; analyzers + + all + + + + + + + + + + diff --git a/Libplanet.Node/UntypedTransaction.cs b/Libplanet.Node/UntypedTransaction.cs new file mode 100644 index 00000000000..13e22d31ff4 --- /dev/null +++ b/Libplanet.Node/UntypedTransaction.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Security.Cryptography; +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Blocks; +using Libplanet.Crypto; +using Libplanet.Tx; + +namespace Libplanet.Node +{ + /// + /// Untyped equivalent of . It's guaranteed that the transaction + /// is properly signed. + /// + public sealed class UntypedTransaction : ITxExcerpt + { + private static readonly Codec Codec = new Codec(); + private readonly TxMetadata _metadata; + private TxId? _id; + + /// + /// Creates an instance. + /// + /// A transaction metadata without actions and signature. + /// A list of s. + /// A signature made by transaction's signer. + /// Thrown when + /// the is invalid. + public UntypedTransaction( + ITxMetadata metadata, + IEnumerable actionValues, + ImmutableArray signature + ) + { + _metadata = new TxMetadata(metadata); + ActionValues = actionValues is IImmutableList im + ? im + : actionValues.ToImmutableList(); + Signature = signature; + + byte[] encoded = Codec.Encode(_metadata.ToBencodex(ActionValues)); + if (!_metadata.PublicKey.Verify(encoded, Signature)) + { + throw new InvalidTxSignatureException( + Id, + $"Failed to verify the signature: {ByteUtil.Hex(Signature)}." + ); + } + } + + /// + /// Creates an instance from a Bencodex + /// . + /// + /// A Bencodex dictionary made using + /// method. + /// Thrown when the given + /// lacks some fields. + /// Thrown when the given + /// has some invalid values. + /// Thrown when the signature is invalid. + /// + /// + /// + public UntypedTransaction(Bencodex.Types.Dictionary dictionary) + : this( + new TxMetadata(dictionary), + dictionary.GetValue(TxMetadata.ActionsKey), + dictionary.GetValue(TxMetadata.SignatureKey).ByteArray) + { + } + + /// + /// + public TxId Id + { + get + { + if (_id is { } id) + { + return id; + } + + using var hasher = SHA256.Create(); + byte[] payload = Codec.Encode(ToBencodex()); + id = new TxId(hasher.ComputeHash(payload)); + _id = id; + return id; + } + } + + /// + public long Nonce => _metadata.Nonce; + + /// + public Address Signer => _metadata.Signer; + + /// + public IImmutableSet
UpdatedAddresses => _metadata.UpdatedAddresses; + + /// + public DateTimeOffset Timestamp => _metadata.Timestamp; + + /// + public PublicKey PublicKey => _metadata.PublicKey; + + /// + public BlockHash? GenesisHash => _metadata.GenesisHash; + + /// + /// A list of s. + /// + public IReadOnlyList ActionValues { get; } + + /// + /// A 's signature on this transaction. + /// + public ImmutableArray Signature { get; } + + /// + /// Builds a Bencodex dictionary used for calculating . + /// + /// A Bencodex dictionary that this transaction turns into. + /// This can be deserialized using + /// method or + /// method. + /// + /// + /// + [Pure] + public Bencodex.Types.Dictionary ToBencodex() => + _metadata.ToBencodex(ActionValues, Signature); + } +} diff --git a/Libplanet.sln b/Libplanet.sln index 032ba52b396..1184594292d 100644 --- a/Libplanet.sln +++ b/Libplanet.sln @@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Net", "Libplanet. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Net.Tests", "Libplanet.Net.Tests\Libplanet.Net.Tests.csproj", "{6D7A63C9-16AB-4B7E-B9C0-0956E1E02610}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Node", "Libplanet.Node\Libplanet.Node.csproj", "{9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Node.Tests", "Libplanet.Node.Tests\Libplanet.Node.Tests.csproj", "{67E63D26-F3CE-4D9E-B1B9-851C5945391D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -251,6 +255,30 @@ Global {6D7A63C9-16AB-4B7E-B9C0-0956E1E02610}.Release|x64.Build.0 = Release|Any CPU {6D7A63C9-16AB-4B7E-B9C0-0956E1E02610}.Release|x86.ActiveCfg = Release|Any CPU {6D7A63C9-16AB-4B7E-B9C0-0956E1E02610}.Release|x86.Build.0 = Release|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Debug|x64.Build.0 = Debug|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Debug|x86.Build.0 = Debug|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Release|Any CPU.Build.0 = Release|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Release|x64.ActiveCfg = Release|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Release|x64.Build.0 = Release|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Release|x86.ActiveCfg = Release|Any CPU + {9C0DC1E8-8BCC-4DAD-A9C8-99ACE1AFA308}.Release|x86.Build.0 = Release|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Debug|x64.ActiveCfg = Debug|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Debug|x64.Build.0 = Debug|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Debug|x86.ActiveCfg = Debug|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Debug|x86.Build.0 = Debug|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Release|Any CPU.Build.0 = Release|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Release|x64.ActiveCfg = Release|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Release|x64.Build.0 = Release|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Release|x86.ActiveCfg = Release|Any CPU + {67E63D26-F3CE-4D9E-B1B9-851C5945391D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Libplanet/AssemblyInfo.cs b/Libplanet/AssemblyInfo.cs index 087f4cb1d28..c08d20294f7 100644 --- a/Libplanet/AssemblyInfo.cs +++ b/Libplanet/AssemblyInfo.cs @@ -4,4 +4,6 @@ [assembly: InternalsVisibleTo("Libplanet.Extensions.Cocona.Tests")] [assembly: InternalsVisibleTo("Libplanet.Net")] [assembly: InternalsVisibleTo("Libplanet.Net.Tests")] +[assembly: InternalsVisibleTo("Libplanet.Node")] +[assembly: InternalsVisibleTo("Libplanet.Node.Tests")] [assembly: InternalsVisibleTo("Libplanet.Tests")] From 912d8584384cbc4983e4a375bfd323a36b259a44 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 May 2022 17:34:51 +0900 Subject: [PATCH 14/14] UntypedBlock class --- CHANGES.md | 1 + Libplanet.Node.Tests/UntypedBlockTest.cs | 120 +++++++++++++++ Libplanet.Node/UntypedBlock.cs | 183 +++++++++++++++++++++++ Libplanet/Blocks/BlockMarshaler.cs | 8 +- 4 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 Libplanet.Node.Tests/UntypedBlockTest.cs create mode 100644 Libplanet.Node/UntypedBlock.cs diff --git a/CHANGES.md b/CHANGES.md index ff43a09ea88..2888700ba1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -41,6 +41,7 @@ To be released. [[#1164], [#1978]] - Added `TxId.FromString()` static method. [[#1978]] - (Libplanet.Node) Added `UntypedTransaction` class. [[#1974], [#1978]] + - (Libplanet.Node) Added `UntypedBlock` class. [[#1974], [#1978]] ### Behavioral changes diff --git a/Libplanet.Node.Tests/UntypedBlockTest.cs b/Libplanet.Node.Tests/UntypedBlockTest.cs new file mode 100644 index 00000000000..c8852bd35c8 --- /dev/null +++ b/Libplanet.Node.Tests/UntypedBlockTest.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using Bencodex; +using Libplanet.Action; +using Libplanet.Blocks; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Tx; +using Xunit; + +namespace Libplanet.Node.Tests +{ + public class UntypedBlockTest + { + private static readonly HashAlgorithmType Sha256 = HashAlgorithmType.Of(); + private static readonly Codec Codec = new Codec(); + private readonly PrivateKey _signerKey; + private readonly Transaction[] _txs; + private readonly PrivateKey _minerKey; + private readonly BlockContent _content; + private readonly PreEvaluationBlock _preEval; + private readonly Block _block; + + public UntypedBlockTest() + { + _signerKey = new PrivateKey(new byte[] + { + 0xfc, 0xf3, 0x0b, 0x33, 0x3d, 0x04, 0xcc, 0xfe, 0xb5, 0x62, 0xf0, + 0x00, 0xa3, 0x2d, 0xf4, 0x88, 0xe7, 0x15, 0x49, 0x49, 0xd3, 0x1d, + 0xdc, 0xac, 0x3c, 0xf9, 0x27, 0x8a, 0xcb, 0x57, 0x86, 0xc7, + }); + var txA = Transaction.Create( + nonce: 0L, + privateKey: _signerKey, + genesisHash: null, + actions: Enumerable.Empty(), + timestamp: new DateTimeOffset(2022, 5, 24, 0, 0, 0, TimeSpan.Zero) + ); + var txB = Transaction.Create( + nonce: 1L, + privateKey: _signerKey, + genesisHash: null, + actions: new[] + { + new NullAction(), + new NullAction(), + }, + timestamp: new DateTimeOffset(2022, 5, 24, 0, 0, 1, TimeSpan.Zero) + ); + _txs = new[] { txA, txB }; + _minerKey = new PrivateKey(new byte[] + { + 0x9b, 0xf4, 0x66, 0x4b, 0xa0, 0x9a, 0x89, 0xfa, 0xeb, 0x68, 0x4b, + 0x94, 0xe6, 0x9f, 0xfd, 0xe0, 0x1d, 0x26, 0xae, 0x14, 0xb5, 0x56, + 0x20, 0x4d, 0x3f, 0x6a, 0xb5, 0x8f, 0x61, 0xf7, 0x84, 0x18, + }); + _content = new BlockContent + { + Index = 0L, + Timestamp = new DateTimeOffset(2022, 5, 24, 1, 2, 3, 456, TimeSpan.Zero), + PublicKey = _minerKey.PublicKey, + Difficulty = 0L, + PreviousHash = null, + Transactions = _txs, + }; + var nonce = default(Nonce); + byte[] blockBytes = Codec.Encode(_content.MakeCandidateData(nonce)); + ImmutableArray preEvalHash = Sha256.Digest(blockBytes).ToImmutableArray(); + var proof = (nonce, preEvalHash); + _preEval = new PreEvaluationBlock(_content, Sha256, proof); + _block = _preEval.Evaluate( + _minerKey, + null, + new TrieStateStore(new MemoryKeyValueStore()) + ); + } + + [Fact] + public void Deserialize() + { + Bencodex.Types.Dictionary dict = _block.MarshalBlock(); + var untyped = new UntypedBlock(_ => Sha256, dict); + Assert.Equal(_block.ProtocolVersion, untyped.ProtocolVersion); + Assert.Equal(_block.HashAlgorithm, untyped.HashAlgorithm); + Assert.Equal(_block.Index, untyped.Index); + Assert.Equal(_block.Timestamp, untyped.Timestamp); + Assert.Equal(_block.Nonce, untyped.Nonce); + Assert.Equal(_block.Miner, untyped.Miner); + Assert.Equal(_block.PublicKey, untyped.PublicKey); + Assert.Equal(_block.Difficulty, untyped.Difficulty); + Assert.Equal(_block.TotalDifficulty, untyped.TotalDifficulty); + Assert.Equal(_block.PreviousHash, untyped.PreviousHash); + Assert.Equal(_block.TxHash, untyped.TxHash); + Assert.Equal(_block.Signature, untyped.Signature); + Assert.Equal(_block.PreEvaluationHash, untyped.PreEvaluationHash); + Assert.Equal(_block.StateRootHash, untyped.StateRootHash); + Assert.Equal(_block.Hash, untyped.Hash); + Assert.Equal(_block.Transactions.Count, untyped.UntypedTransactions.Count); + Assert.All( + _block.Transactions.Zip(untyped.UntypedTransactions, (t, u) => (t, u)), + pair => pair.Item1.Id.Equals(pair.Item2.Id) + ); + } + + [Fact] + public void ToBencodex() + { + var untypedTxs = _txs.Select(tx => + new UntypedTransaction( + tx, + tx.Actions.Select(a => a.PlainValue), + tx.Signature.ToImmutableArray())); + var untyped = new UntypedBlock(_block, untypedTxs); + Assert.Equal(_block.MarshalBlock(), untyped.ToBencodex()); + } + } +} diff --git a/Libplanet.Node/UntypedBlock.cs b/Libplanet.Node/UntypedBlock.cs new file mode 100644 index 00000000000..2bda68d2127 --- /dev/null +++ b/Libplanet.Node/UntypedBlock.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using Bencodex; +using Bencodex.Types; +using Libplanet.Blockchain.Policies; +using Libplanet.Blocks; +using Libplanet.Crypto; +using Libplanet.Tx; + +namespace Libplanet.Node +{ + /// + /// Untyped equivalent of . It's guaranteed that all proofs are valid. + /// + public sealed class UntypedBlock : IBlockHeader + { + private static readonly Codec Codec = new Codec(); + private BlockHeader _header; + + /// + /// Creates an instance. + /// + /// A block header without transactions. + /// A list of transactions. Ordering does not matter. + /// + /// Thrown when the same tx nonce is used by + /// a signer twice or more, or a tx nonce is used without its previous nonce by a signer. + /// Note that this validates only a block's intrinsic integrity between its transactions, + /// but does not guarantee integrity between blocks. Such validation needs to be conducted + /// by . + /// Thrown when transactions to set have + /// inconsistent genesis hashes. + /// Thrown when the given + /// 's is invalid. + public UntypedBlock( + IBlockHeader header, + IEnumerable untypedTransactions) + { + _header = header is BlockHeader h + ? h + : new BlockHeader( + new PreEvaluationBlockHeader(header), + header.StateRootHash, + header.Signature, + header.Hash); + UntypedTransactions = untypedTransactions.OrderBy(tx => tx.Id).ToImmutableList(); + UntypedTransactions.ValidateTxNonces(_header.Index); + var expectedTxHash = DeriveTxHash(UntypedTransactions); + if (!Nullable.Equals(expectedTxHash, _header.TxHash)) + { + throw new InvalidBlockTxHashException( + $"{nameof(TxHash)} is invalid.", + _header.TxHash, + expectedTxHash + ); + } + } + + /// + /// Decodes a Bencodex into an + /// instance. + /// + /// A function to determine the hash algorithm used + /// for the block to decode. See also + /// method. + /// A Bencodex dictionary made using + /// method or method. + /// + /// + public UntypedBlock( + HashAlgorithmGetter hashAlgorithmGetter, + Bencodex.Types.Dictionary dictionary + ) + : this( + BlockMarshaler.UnmarshalBlockHeader( + hashAlgorithmGetter, + dictionary.GetValue(BlockMarshaler.HeaderKey)), + dictionary.GetValue(BlockMarshaler.TransactionsKey) + .Select(b => new UntypedTransaction((Dictionary)Codec.Decode((Binary)b))) + ) + { + } + + /// + public int ProtocolVersion => _header.ProtocolVersion; + + /// + public HashAlgorithmType HashAlgorithm => _header.HashAlgorithm; + + /// + public long Index => _header.Index; + + /// + public DateTimeOffset Timestamp => _header.Timestamp; + + /// + public Nonce Nonce => _header.Nonce; + + /// + public Address Miner => _header.Miner; + + /// + public PublicKey? PublicKey => _header.PublicKey; + + /// + public long Difficulty => _header.Difficulty; + + /// + public BigInteger TotalDifficulty => _header.TotalDifficulty; + + /// + public BlockHash? PreviousHash => _header.PreviousHash; + + /// + public HashDigest? TxHash => _header.TxHash; + + /// + public ImmutableArray? Signature => _header.Signature; + + /// + public ImmutableArray PreEvaluationHash => _header.PreEvaluationHash; + + /// + public HashDigest StateRootHash => _header.StateRootHash; + + /// + public BlockHash Hash => _header.Hash; + + /// + /// The list of untyped transactions belonging to the block. + /// + /// This is always ordered by . + public IReadOnlyList UntypedTransactions { get; } + + /// + /// Encodes this block into a Bencodex dictionary. + /// + /// A Bencodex dictionary which encodes this block. This is equivalent to + /// method's return value. + /// This can be decoded back to using + /// constructor or + /// + /// method. + /// + /// + public Bencodex.Types.Dictionary ToBencodex() + { + Bencodex.Types.Dictionary headerDict = _header.MarshalBlockHeader(); + var txs = new List( + UntypedTransactions + .Select(tx => new Binary(Codec.Encode(tx.ToBencodex()))) + .Cast()); + return BlockMarshaler.MarshalBlock(headerDict, txs); + } + + /// + /// Derives from the given . + /// + /// The transactions to derive from. + /// This must be ordered by . + /// The derived . + /// Thrown when the are + /// not ordered by their s. + // FIXME: It does the same thing with BlockContent.DeriveTxHash() method. + private static HashDigest? DeriveTxHash( + IReadOnlyList transactions) + { + if (!transactions.Any()) + { + return null; + } + + var list = new Bencodex.Types.List( + transactions.Select(tx => tx.ToBencodex())); + byte[] payload = Codec.Encode(list); + return HashDigest.DeriveFrom(payload); + } + } +} diff --git a/Libplanet/Blocks/BlockMarshaler.cs b/Libplanet/Blocks/BlockMarshaler.cs index 95295d436b9..306d643ed51 100644 --- a/Libplanet/Blocks/BlockMarshaler.cs +++ b/Libplanet/Blocks/BlockMarshaler.cs @@ -16,6 +16,10 @@ namespace Libplanet.Blocks ///
public static class BlockMarshaler { + // Block fields: + internal static readonly byte[] HeaderKey = { 0x48 }; // 'H' + internal static readonly byte[] TransactionsKey = { 0x54 }; // 'T' + private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; // Header fields: @@ -34,10 +38,6 @@ public static class BlockMarshaler private static readonly byte[] SignatureKey = { 0x53 }; // 'S' private static readonly byte[] PreEvaluationHashKey = { 0x63 }; // 'c' - // Block fields: - private static readonly byte[] HeaderKey = { 0x48 }; // 'H' - private static readonly byte[] TransactionsKey = { 0x54 }; // 'T' - public static Dictionary MarshalBlockMetadata(IBlockMetadata metadata) { string timestamp =