Skip to content

Commit

Permalink
UntypedBlock class
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed May 24, 2022
1 parent 12c8763 commit c347e62
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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

Expand Down
120 changes: 120 additions & 0 deletions Libplanet.Node.Tests/UntypedBlockTest.cs
Original file line number Diff line number Diff line change
@@ -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<SHA256>();
private static readonly Codec Codec = new Codec();
private readonly PrivateKey _signerKey;
private readonly Transaction<NullAction>[] _txs;
private readonly PrivateKey _minerKey;
private readonly BlockContent<NullAction> _content;
private readonly PreEvaluationBlock<NullAction> _preEval;
private readonly Block<NullAction> _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<NullAction>.Create(
nonce: 0L,
privateKey: _signerKey,
genesisHash: null,
actions: Enumerable.Empty<NullAction>(),
timestamp: new DateTimeOffset(2022, 5, 24, 0, 0, 0, TimeSpan.Zero)
);
var txB = Transaction<NullAction>.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<NullAction>
{
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<byte> preEvalHash = Sha256.Digest(blockBytes).ToImmutableArray();
var proof = (nonce, preEvalHash);
_preEval = new PreEvaluationBlock<NullAction>(_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),
pair => pair.First.Id.Equals(pair.Second.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());
}
}
}
183 changes: 183 additions & 0 deletions Libplanet.Node/UntypedBlock.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Untyped equivalent of <see cref="Block{T}"/>. It's guaranteed that all proofs are valid.
/// </summary>
public sealed class UntypedBlock : IBlockHeader
{
private static readonly Codec Codec = new Codec();
private BlockHeader _header;

/// <summary>
/// Creates an <see cref="UntypedBlock"/> instance.
/// </summary>
/// <param name="header">A block header without transactions.</param>
/// <param name="untypedTransactions">A list of transactions. Ordering does not matter.
/// </param>
/// <exception cref="InvalidTxNonceException">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 <see cref="Blockchain.BlockChain{T}"/>.</exception>
/// <exception cref="InvalidTxGenesisHashException">Thrown when transactions to set have
/// inconsistent genesis hashes.</exception>
/// <exception cref="InvalidBlockTxHashException">Thrown when the given
/// <paramref name="header"/>'s <see cref="IBlockMetadata.TxHash"/> is invalid.</exception>
public UntypedBlock(
IBlockHeader header,
IEnumerable<UntypedTransaction> 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
);
}
}

/// <summary>
/// Decodes a Bencodex <paramref name="dictionary"/> into an <see cref="UntypedBlock"/>
/// instance.
/// </summary>
/// <param name="hashAlgorithmGetter">A function to determine the hash algorithm used
/// for the block to decode. See also <see cref="IBlockPolicy{T}.GetHashAlgorithm(long)"/>
/// method.</param>
/// <param name="dictionary">A Bencodex dictionary made using <see cref="ToBencodex()"/>
/// method or <see cref="BlockMarshaler.MarshalBlock{T}(Block{T})"/> method.</param>
/// <seealso cref="ToBencodex()"/>
/// <seealso cref="BlockMarshaler.MarshalBlock{T}(Block{T})"/>
public UntypedBlock(
HashAlgorithmGetter hashAlgorithmGetter,
Bencodex.Types.Dictionary dictionary
)
: this(
BlockMarshaler.UnmarshalBlockHeader(
hashAlgorithmGetter,
dictionary.GetValue<Dictionary>(BlockMarshaler.HeaderKey)),
dictionary.GetValue<List>(BlockMarshaler.TransactionsKey)
.Select(b => new UntypedTransaction((Dictionary)Codec.Decode((Binary)b)))
)
{
}

/// <inheritdoc cref="IBlockMetadata.ProtocolVersion"/>
public int ProtocolVersion => _header.ProtocolVersion;

/// <inheritdoc cref="IPreEvaluationBlockHeader.HashAlgorithm"/>
public HashAlgorithmType HashAlgorithm => _header.HashAlgorithm;

/// <inheritdoc cref="IBlockMetadata.Index"/>
public long Index => _header.Index;

/// <inheritdoc cref="IBlockMetadata.Timestamp"/>
public DateTimeOffset Timestamp => _header.Timestamp;

/// <inheritdoc cref="IPreEvaluationBlockHeader.Nonce"/>
public Nonce Nonce => _header.Nonce;

/// <inheritdoc cref="IBlockMetadata.Miner"/>
public Address Miner => _header.Miner;

/// <inheritdoc cref="IBlockMetadata.PublicKey"/>
public PublicKey? PublicKey => _header.PublicKey;

/// <inheritdoc cref="IBlockMetadata.Difficulty"/>
public long Difficulty => _header.Difficulty;

/// <inheritdoc cref="IBlockMetadata.TotalDifficulty"/>
public BigInteger TotalDifficulty => _header.TotalDifficulty;

/// <inheritdoc cref="IBlockMetadata.PreviousHash"/>
public BlockHash? PreviousHash => _header.PreviousHash;

/// <inheritdoc cref="IBlockMetadata.TxHash"/>
public HashDigest<SHA256>? TxHash => _header.TxHash;

/// <inheritdoc cref="IBlockHeader.Signature"/>
public ImmutableArray<byte>? Signature => _header.Signature;

/// <inheritdoc cref="IPreEvaluationBlockHeader.PreEvaluationHash"/>
public ImmutableArray<byte> PreEvaluationHash => _header.PreEvaluationHash;

/// <inheritdoc cref="IBlockHeader.StateRootHash"/>
public HashDigest<SHA256> StateRootHash => _header.StateRootHash;

/// <inheritdoc cref="IBlockExcerpt.Hash"/>
public BlockHash Hash => _header.Hash;

/// <summary>
/// The list of untyped transactions belonging to the block.
/// </summary>
/// <remarks>This is always ordered by <see cref="UntypedTransaction.Id"/>.</remarks>
public IReadOnlyList<UntypedTransaction> UntypedTransactions { get; }

/// <summary>
/// Encodes this block into a Bencodex dictionary.
/// </summary>
/// <returns>A Bencodex dictionary which encodes this block. This is equivalent to
/// <see cref="BlockMarshaler.MarshalBlock{T}(Block{T})"/> method's return value.
/// This can be decoded back to <see cref="UntypedBlock"/> using
/// <see cref="UntypedBlock(HashAlgorithmGetter, Dictionary)"/> constructor or
/// <see cref="BlockMarshaler.UnmarshalBlock{T}(HashAlgorithmGetter, Dictionary)"/>
/// method.</returns>
/// <seealso cref="UntypedBlock(HashAlgorithmGetter, Dictionary)"/>
/// <seealso cref="BlockMarshaler.UnmarshalBlock{T}(HashAlgorithmGetter, Dictionary)"/>
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<IValue>());
return BlockMarshaler.MarshalBlock(headerDict, txs);
}

/// <summary>
/// Derives <see cref="TxHash"/> from the given <paramref name="transactions"/>.
/// </summary>
/// <param name="transactions">The transactions to derive <see cref="TxHash"/> from.
/// This must be ordered by <see cref="TxId"/>.</param>
/// <returns>The derived <see cref="TxHash"/>.</returns>
/// <exception cref="ArgumentException">Thrown when the <paramref name="transactions"/> are
/// not ordered by their <see cref="TxId"/>s.</exception>
// FIXME: It does the same thing with BlockContent<T>.DeriveTxHash() method.
private static HashDigest<SHA256>? DeriveTxHash(
IReadOnlyList<UntypedTransaction> transactions)
{
if (!transactions.Any())
{
return null;
}

var list = new Bencodex.Types.List(
transactions.Select(tx => tx.ToBencodex()));
byte[] payload = Codec.Encode(list);
return HashDigest<SHA256>.DeriveFrom(payload);
}
}
}
8 changes: 4 additions & 4 deletions Libplanet/Blocks/BlockMarshaler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ namespace Libplanet.Blocks
/// </summary>
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:
Expand All @@ -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 =
Expand Down

0 comments on commit c347e62

Please sign in to comment.