Skip to content

Commit

Permalink
Make the order of Block<T>.Transactions aware of their nonce
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Sep 19, 2019
1 parent 73dd818 commit 392508b
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 5 deletions.
7 changes: 6 additions & 1 deletion CHANGES.md
Expand Up @@ -96,7 +96,11 @@ To be released.
- The order of `Block<T>.Transactions` became to be determined by
both a `Block<T>.Hash` and a `Transaction<T>.Id`, so that signers cannot
predict the order of transactions in a block before it's mined.
[[#244], [#355], [#511]]
If there are multiple transactions signed by the same signer in a block
these transactions become grouped together and the order is determined by
a `Block<T>.Hash` and a fingerprint derived from all these transactions,
and transactions in each group (per signer) are ordered by
`Transaction<T>.Nonce`. [[#244], [#355], [#511], [#520]]

### Bug fixes

Expand All @@ -120,6 +124,7 @@ To be released.
[#508]: https://github.com/planetarium/libplanet/pull/508
[#511]: https://github.com/planetarium/libplanet/pull/511
[#512]: https://github.com/planetarium/libplanet/pull/512
[#520]: https://github.com/planetarium/libplanet/pull/520
[Kademlia]: https://en.wikipedia.org/wiki/Kademlia
[Guid]: https://docs.microsoft.com/ko-kr/dotnet/api/system.guid?view=netframework-4.8

Expand Down
38 changes: 38 additions & 0 deletions Libplanet.Tests/Blocks/BlockTest.cs
Expand Up @@ -20,6 +20,44 @@ public class BlockTest : IClassFixture<BlockFixture>

public BlockTest(BlockFixture fixture) => _fx = fixture;

[Fact]
public void Transactions()
{
// Creates fixtures.
PrivateKey[] privKeys =
Enumerable.Repeat((object)null, 5).Select(_ => new PrivateKey()).ToArray();
var random = new System.Random();
Transaction<DumbAction>[] txs = Enumerable.Range(0, 50)
.Select(i => (privKeys[i % privKeys.Length], i / privKeys.Length))
.Select(pair =>
Transaction<DumbAction>.Create(
nonce: pair.Item2,
privateKey: pair.Item1,
actions: new DumbAction[0]
)
)
.OrderBy(_ => random.Next())
.ToArray();
var block = new Block<DumbAction>(
index: 0,
difficulty: 0,
nonce: new Nonce(new byte[0]),
miner: null,
previousHash: null,
timestamp: DateTimeOffset.UtcNow,
transactions: txs
);

// For transactions signed by the same signer, these should be ordered by its tx nonce.
Address[] signers = privKeys.Select(pk => pk.PublicKey.ToAddress()).ToArray();
foreach (Address signer in signers)
{
IEnumerable<Transaction<DumbAction>> signersTxs =
block.Transactions.Where(tx => tx.Signer == signer);
Assert.Equal(signersTxs.OrderBy(tx => tx.Nonce).ToArray(), signersTxs.ToArray());
}
}

[Fact]
public void CanMine()
{
Expand Down
58 changes: 54 additions & 4 deletions Libplanet/Blocks/Block.cs
Expand Up @@ -25,6 +25,28 @@ public class Block<T> : ISerializable
private static readonly TimeSpan TimestampThreshold =
TimeSpan.FromSeconds(900);

/// <summary>
/// Creates a <see cref="Block{T}"/> instance by manually filling all field values.
/// For a more automated way, see also <see cref="Mine"/> method.
/// </summary>
/// <param name="index">The height of the block to create. Goes to the <see cref="Index"/>.
/// </param>
/// <param name="difficulty">The mining difficulty that <paramref name="nonce"/> has to
/// satisfy. Goes to the <see cref="Difficulty"/>.</param>
/// <param name="nonce">The nonce which satisfy the given <paramref name="difficulty"/> with
/// any other field values. Goes to the <see cref="Nonce"/>.</param>
/// <param name="miner">An optional address refers to who mines this block.
/// Goes to the <see cref="Miner"/>.</param>
/// <param name="previousHash">The previous block's <see cref="Hash"/>. If it's a genesis
/// block (i.e., <paramref name="index"/> is 0) this should be <c>null</c>.
/// Goes to the <see cref="PreviousHash"/>.</param>
/// <param name="timestamp">The time this block is created. Goes to
/// the <see cref="Timestamp"/>.</param>
/// <param name="transactions">The transactions to be mined together with this block.
/// Transactions become sorted in an unpredicted-before-mined order and then go to
/// the <see cref="Transactions"/> property.
/// </param>
/// <seealso cref="Mine"/>
public Block(
long index,
long difficulty,
Expand All @@ -43,11 +65,39 @@ public class Block<T> : ISerializable
Transactions = transactions.OrderBy(tx => tx.Id).ToArray();
Hash = Hashcash.Hash(ToBencodex(false, false));

// As the order of transactions should be unpredictable until a block is mined,
// the sorter key should be derived from both a block hash and a txid.
var hashInteger = new BigInteger(Hash.ToByteArray());
Transactions =
Transactions
.OrderBy(tx =>
new BigInteger(tx.Id.ToByteArray()) ^ hashInteger).ToArray();

// If there are multiple transactions for the same signer these should be ordered by
// their tx nonces. So transactions of the same signer should have the same sort key.
// The following logic "flattens" multiple tx ids having the same signer into a single
// txid by applying XOR between them.
IImmutableDictionary<Address, IImmutableSet<Transaction<T>>> signerTxs = Transactions
.GroupBy(tx => tx.Signer)
.ToImmutableDictionary(
g => g.Key,
g => (IImmutableSet<Transaction<T>>)g.ToImmutableHashSet()
);
IImmutableDictionary<Address, BigInteger> signerTxIds = signerTxs
.ToImmutableDictionary(
pair => pair.Key,
pair => pair.Value
.Select(tx => new BigInteger(tx.Id.ToByteArray()))
.OrderBy(txid => txid)
.Aggregate((a, b) => a ^ b)
);

// Order signers by values derivied from both block hash and their "flatten" txid:
IImmutableList<Address> signers = signerTxIds
.OrderBy(pair => pair.Value ^ hashInteger)
.Select(pair => pair.Key)
.ToImmutableArray();

// Order transactions for each signer by their tx nonces:
Transactions = signers
.SelectMany(signer => signerTxs[signer].OrderBy(tx => tx.Nonce))
.ToImmutableArray();
}

protected Block(SerializationInfo info, StreamingContext context)
Expand Down

0 comments on commit 392508b

Please sign in to comment.