Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Combine creating and staging transaction interfaces
  • Loading branch information
earlbread committed Jun 18, 2019
1 parent 9a7cc89 commit 4ef0cad
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 214 deletions.
7 changes: 7 additions & 0 deletions CHANGES.md
Expand Up @@ -8,11 +8,17 @@ To be released.

### Backward-incompatible interface changes

- Removed `Transaction<T>.Create()` method. [[#294]]
- Removed `BlockChain<T>.GetNonce()` method. [[#294]]
- Removed `BlockChain<T>.StageTransactions()` method. [[#294]]

### Added interfaces

- Added `LiteDBStore` backend that uses [LiteDB] under the hood. [[#269]]
- All `*Async()` methods belonging to `TurnClient` class became to have
`cancellationToken` option. [[#287]]
- Added `BlockChain<T>.MakeTransaction(PrivateKey, IEnumerable<T>,
IImmutableSet<Address>, DateTimeOffset?)` method. [[#294]]

### Behavioral changes

Expand Down Expand Up @@ -49,6 +55,7 @@ To be released.
[#277]: https://github.com/planetarium/libplanet/pull/277
[#281]: https://github.com/planetarium/libplanet/pull/281
[#287]: https://github.com/planetarium/libplanet/pull/287
[#294]: https://github.com/planetarium/libplanet/pull/294


Version 0.3.0
Expand Down
29 changes: 29 additions & 0 deletions Libplanet.Tests/Blockchain/BlockChainTest.cs
Expand Up @@ -840,6 +840,35 @@ public void ValidateNonce()
_blockChain.ValidateNonce(b2));
}

[Fact]
public void MakeTransaction()
{
var privateKey = new PrivateKey();
Address address = privateKey.PublicKey.ToAddress();
var actions = new[] { new DumbAction(address, "foo") };

_blockChain.MakeTransaction(privateKey, actions);
_blockChain.MakeTransaction(privateKey, actions);

List<Transaction<DumbAction>> txs = _blockChain.Store
.IterateStagedTransactionIds()
.Select(_blockChain.Store.GetTransaction<DumbAction>)
.OrderBy(tx => tx.Nonce)
.ToList();

Assert.Equal(2, txs.Count());

var transaction = txs[0];
Assert.Equal(0, transaction.Nonce);
Assert.Equal(address, transaction.Signer);
Assert.Equal(actions, transaction.Actions);

transaction = txs[1];
Assert.Equal(1, transaction.Nonce);
Assert.Equal(address, transaction.Signer);
Assert.Equal(actions, transaction.Actions);
}

private sealed class NullPolicy<T> : IBlockPolicy<T>
where T : IAction, new()
{
Expand Down
199 changes: 159 additions & 40 deletions Libplanet/Blockchain/BlockChain.cs
Expand Up @@ -9,6 +9,7 @@
using Libplanet.Action;
using Libplanet.Blockchain.Policies;
using Libplanet.Blocks;
using Libplanet.Crypto;
using Libplanet.Store;
using Libplanet.Tx;

Expand All @@ -19,6 +20,7 @@ public class BlockChain<T> : IReadOnlyList<Block<T>>
where T : IAction, new()
{
private readonly ReaderWriterLockSlim _rwlock;
private readonly object _txLock;

public BlockChain(IBlockPolicy<T> policy, IStore store)
: this(policy, store, Guid.NewGuid())
Expand All @@ -35,6 +37,7 @@ public BlockChain(IBlockPolicy<T> policy, IStore store, Guid id)

_rwlock = new ReaderWriterLockSlim(
LockRecursionPolicy.SupportsRecursion);
_txLock = new object();
}

~BlockChain()
Expand Down Expand Up @@ -259,57 +262,17 @@ IEnumerator IEnumerable.GetEnumerator()
public void Append(Block<T> block, DateTimeOffset currentTime) =>
Append(block, currentTime, render: true);

/// <summary>
/// Adds <paramref name="transactions"/> to the pending list so that
/// a next <see cref="Block{T}"/> to be mined contains these
/// <paramref name="transactions"/>.
/// </summary>
/// <param name="transactions">
/// <see cref="Transaction{T}"/>s to add to the pending list.</param>
public void StageTransactions(ISet<Transaction<T>> transactions)
{
foreach (Transaction<T> tx in transactions)
{
Transactions[tx.Id] = tx;
}

Store.StageTransactionIds(
transactions.Select(tx => tx.Id).ToImmutableHashSet()
);
}

/// <summary>
/// Removes <paramref name="transactions"/> from the pending list.
/// </summary>
/// <param name="transactions"><see cref="Transaction{T}"/>s
/// to remove from the pending list.</param>
/// <seealso cref="StageTransactions"/>
public void UnstageTransactions(ISet<Transaction<T>> transactions)
{
Store.UnstageTransactionIds(
transactions.Select(tx => tx.Id).ToImmutableHashSet());
}

public long GetNonce(Address address)
{
long nonce = Store.GetTxNonce(Id.ToString(), address);

IEnumerable<Transaction<T>> stagedTxs = Store
.IterateStagedTransactionIds()
.Select(Store.GetTransaction<T>)
.Where(tx => tx.Signer.Equals(address));

foreach (Transaction<T> tx in stagedTxs)
{
if (nonce <= tx.Nonce)
{
nonce = tx.Nonce + 1;
}
}

return nonce;
}

public Block<T> MineBlock(
Address miner,
DateTimeOffset currentTime
Expand Down Expand Up @@ -342,6 +305,162 @@ DateTimeOffset currentTime
public Block<T> MineBlock(Address miner) =>
MineBlock(miner, DateTimeOffset.UtcNow);

/// <summary>
/// A fa&#xe7;ade factory to create a new <see cref="Transaction{T}"/>
/// and stage the transaction.
/// Unlike the <see cref="Transaction{T}(long, Address, PublicKey,
/// IImmutableSet{Address}, DateTimeOffset, IEnumerable{T}, byte[])"/>
/// constructor, it automatically fills the following values from:
/// <list type="table">
/// <listheader>
/// <term>Property</term>
/// <description>Parameter the filled value derived from</description>
/// </listheader>
/// <item>
/// <term><see cref="Transaction{T}.Signer"/></term>
/// <description><paramref name="privateKey"/></description>
/// </item>
/// <item>
/// <term><see cref="Transaction{T}.PublicKey"/></term>
/// <description><paramref name="privateKey"/></description>
/// </item>
/// <item>
/// <term><see cref="Transaction{T}.UpdatedAddresses"/></term>
/// <description><paramref name="actions"/> and
/// <paramref name="updatedAddresses"/></description>
/// </item>
/// </list>
/// <para>Note that the <paramref name="privateKey"/> in itself is not
/// included in the created <see cref="Transaction{T}"/>.</para>
/// </summary>
/// <remarks>
/// This factory method tries its best to fill the <see
/// cref="Transaction{T}.UpdatedAddresses"/> property by actually
/// evaluating the given <paramref name="actions"/>
/// (we call it &#x201c;rehearsal mode&#x201d;), but remember that its
/// result is approximated in some degree, because the result of
/// <paramref name="actions"/> are not deterministic until
/// the <see cref="Transaction{T}"/> belongs to a <see
/// cref="Libplanet.Blocks.Block{T}"/>.
/// <para>If an <see cref="IAction"/> depends on previous states or
/// some randomness to determine what <see cref="Address"/> to update,
/// the automatically filled
/// <see cref="Transaction{T}.UpdatedAddresses"/> became mismatched
/// from the <see cref="Address"/>es <paramref name="actions"/> actually
/// update after a <see cref="Libplanet.Blocks.Block{T}"/> is mined.
/// Although such case would be rare, a programmer could manually give
/// the <paramref name="updatedAddresses"/> parameter
/// the <see cref="Address"/>es they predict to be updated.</para>
/// <para>If an <see cref="IAction"/> oversimplifies the assumption
/// about the <see cref="Libplanet.Blocks.Block{T}"/> it belongs to,
/// runtime exceptions could be thrown from this factory method.
/// The best solution to that is not to oversimplify things,
/// there is an option to check <see cref="IActionContext"/>'s
/// <see cref="IActionContext.Rehearsal"/> is <c>true</c> and
/// a conditional logic for the case.</para>
/// </remarks>
/// <param name="privateKey">A <see cref="PrivateKey"/> of the account
/// who creates and signs a new transaction. This key is used to fill
/// the <see cref="Transaction{T}.Signer"/>, <see cref="PublicKey"/>,
/// and <see cref="Transaction{T}.Signature"/> properties, but this in
/// itself is not included in the transaction.</param>
/// <param name="actions">A list of <see cref="IAction"/>s. This
/// can be empty, but cannot be <c>null</c>. This goes to
/// the <see cref="Transaction{T}.Actions"/> property, and
/// <see cref="IAction"/>s are evaluated before a
/// <see cref="Transaction{T}"/> is created in order to fill the
/// <see cref="Transaction{T}.UpdatedAddresses"/>. See also
/// <em>Remarks</em> section.</param>
/// <param name="updatedAddresses"><see cref="Address"/>es whose
/// states affected by <paramref name="actions"/>.
/// These <see cref="Address"/>es are also included in
/// the <see cref="Transaction{T}.UpdatedAddresses"/> property, besides
/// <see cref="Address"/>es projected by evaluating
/// <paramref name="actions"/>. See also <em>Remarks</em> section.
/// </param>
/// <param name="timestamp">The time this <see cref="Transaction{T}"/>
/// is created and signed. This goes to the
/// <see cref="Transaction{T}.Timestamp"/> property. If <c>null</c>
/// (which is default) is passed this will be the current time.</param>
/// <returns>A created new <see cref="Transaction{T}"/> signed by
/// the given <paramref name="privateKey"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <c>null</c>
/// is passed to <paramref name="privateKey"/> or
/// or <paramref name="actions"/>.
/// </exception>
/// <exception cref="UnexpectedlyTerminatedTxRehearsalException">
/// Thrown when one of <paramref name="actions"/> throws some
/// exception during their rehearsal.
/// <para>This exception is thrown probably because the logic of some of
/// the <paramref name="actions"/> is not enough generic so that
/// it can cover every case including &#x201c;rehearsal mode.&#x201d;
/// The <see cref="IActionContext.Rehearsal"/> property also might be
/// useful to make the <see cref="IAction"/> can deal with the case of
/// rehearsal mode.</para>
/// <para>The actual exception that an <see cref="IAction"/> threw
/// is stored in its <see cref="Exception.InnerException"/> property.
/// </para>
/// </exception>
public Transaction<T> MakeTransaction(
PrivateKey privateKey,
IEnumerable<T> actions,
IImmutableSet<Address> updatedAddresses = null,
DateTimeOffset? timestamp = null)
{
timestamp = timestamp ?? DateTimeOffset.UtcNow;
lock (_txLock)
{
Transaction<T> tx = Transaction<T>.Create(
GetNonce(privateKey.PublicKey.ToAddress()),
privateKey,
actions,
updatedAddresses,
timestamp);
StageTransactions(new HashSet<Transaction<T>> { tx });

return tx;
}
}

/// <summary>
/// Adds <paramref name="transactions"/> to the pending list so that
/// a next <see cref="Block{T}"/> to be mined contains these
/// <paramref name="transactions"/>.
/// </summary>
/// <param name="transactions">
/// <see cref="Transaction{T}"/>s to add to the pending list.</param>
internal void StageTransactions(ISet<Transaction<T>> transactions)
{
foreach (Transaction<T> tx in transactions)
{
Transactions[tx.Id] = tx;
}

Store.StageTransactionIds(
transactions.Select(tx => tx.Id).ToImmutableHashSet()
);
}

internal long GetNonce(Address address)
{
long nonce = Store.GetTxNonce(Id.ToString(), address);

IEnumerable<Transaction<T>> stagedTxs = Store
.IterateStagedTransactionIds()
.Select(Store.GetTransaction<T>)
.Where(tx => tx.Signer.Equals(address));

foreach (Transaction<T> tx in stagedTxs)
{
if (nonce <= tx.Nonce)
{
nonce = tx.Nonce + 1;
}
}

return nonce;
}

internal void Append(
Block<T> block,
DateTimeOffset currentTime,
Expand Down

0 comments on commit 4ef0cad

Please sign in to comment.