diff --git a/CHANGES.md b/CHANGES.md index 9186837458e..7206aa5095c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,11 +8,17 @@ To be released. ### Backward-incompatible interface changes + - Removed `Transaction.Create()` method. [[#294]] + - Removed `BlockChain.GetNonce()` method. [[#294]] + - Removed `BlockChain.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.MakeTransaction(PrivateKey, IEnumerable, + IImmutableSet
, DateTimeOffset?)` method. [[#294]] ### Behavioral changes @@ -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 diff --git a/Libplanet.Tests/Blockchain/BlockChainTest.cs b/Libplanet.Tests/Blockchain/BlockChainTest.cs index 3184c4e0632..3b746db55e1 100644 --- a/Libplanet.Tests/Blockchain/BlockChainTest.cs +++ b/Libplanet.Tests/Blockchain/BlockChainTest.cs @@ -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> txs = _blockChain.Store + .IterateStagedTransactionIds() + .Select(_blockChain.Store.GetTransaction) + .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 : IBlockPolicy where T : IAction, new() { diff --git a/Libplanet/Blockchain/BlockChain.cs b/Libplanet/Blockchain/BlockChain.cs index 59a43fc48e3..8cec454fad1 100644 --- a/Libplanet/Blockchain/BlockChain.cs +++ b/Libplanet/Blockchain/BlockChain.cs @@ -9,6 +9,7 @@ using Libplanet.Action; using Libplanet.Blockchain.Policies; using Libplanet.Blocks; +using Libplanet.Crypto; using Libplanet.Store; using Libplanet.Tx; @@ -19,6 +20,7 @@ public class BlockChain : IReadOnlyList> where T : IAction, new() { private readonly ReaderWriterLockSlim _rwlock; + private readonly object _txLock; public BlockChain(IBlockPolicy policy, IStore store) : this(policy, store, Guid.NewGuid()) @@ -35,6 +37,7 @@ public BlockChain(IBlockPolicy policy, IStore store, Guid id) _rwlock = new ReaderWriterLockSlim( LockRecursionPolicy.SupportsRecursion); + _txLock = new object(); } ~BlockChain() @@ -259,57 +262,17 @@ IEnumerator IEnumerable.GetEnumerator() public void Append(Block block, DateTimeOffset currentTime) => Append(block, currentTime, render: true); - /// - /// Adds to the pending list so that - /// a next to be mined contains these - /// . - /// - /// - /// s to add to the pending list. - public void StageTransactions(ISet> transactions) - { - foreach (Transaction tx in transactions) - { - Transactions[tx.Id] = tx; - } - - Store.StageTransactionIds( - transactions.Select(tx => tx.Id).ToImmutableHashSet() - ); - } - /// /// Removes from the pending list. /// /// s /// to remove from the pending list. - /// public void UnstageTransactions(ISet> transactions) { Store.UnstageTransactionIds( transactions.Select(tx => tx.Id).ToImmutableHashSet()); } - public long GetNonce(Address address) - { - long nonce = Store.GetTxNonce(Id.ToString(), address); - - IEnumerable> stagedTxs = Store - .IterateStagedTransactionIds() - .Select(Store.GetTransaction) - .Where(tx => tx.Signer.Equals(address)); - - foreach (Transaction tx in stagedTxs) - { - if (nonce <= tx.Nonce) - { - nonce = tx.Nonce + 1; - } - } - - return nonce; - } - public Block MineBlock( Address miner, DateTimeOffset currentTime @@ -342,6 +305,162 @@ DateTimeOffset currentTime public Block MineBlock(Address miner) => MineBlock(miner, DateTimeOffset.UtcNow); + /// + /// A façade factory to create a new + /// and stage the transaction. + /// Unlike the + /// constructor, it automatically fills the following values from: + /// + /// + /// Property + /// Parameter the filled value derived from + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// and + /// + /// + /// + /// Note that the in itself is not + /// included in the created . + /// + /// + /// This factory method tries its best to fill the property by actually + /// evaluating the given + /// (we call it “rehearsal mode”), but remember that its + /// result is approximated in some degree, because the result of + /// are not deterministic until + /// the belongs to a . + /// If an depends on previous states or + /// some randomness to determine what to update, + /// the automatically filled + /// became mismatched + /// from the es actually + /// update after a is mined. + /// Although such case would be rare, a programmer could manually give + /// the parameter + /// the es they predict to be updated. + /// If an oversimplifies the assumption + /// about the 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 's + /// is true and + /// a conditional logic for the case. + /// + /// A of the account + /// who creates and signs a new transaction. This key is used to fill + /// the , , + /// and properties, but this in + /// itself is not included in the transaction. + /// A list of s. This + /// can be empty, but cannot be null. This goes to + /// the property, and + /// s are evaluated before a + /// is created in order to fill the + /// . See also + /// Remarks section. + /// es whose + /// states affected by . + /// These es are also included in + /// the property, besides + /// es projected by evaluating + /// . See also Remarks section. + /// + /// The time this + /// is created and signed. This goes to the + /// property. If null + /// (which is default) is passed this will be the current time. + /// A created new signed by + /// the given . + /// Thrown when null + /// is passed to or + /// or . + /// + /// + /// Thrown when one of throws some + /// exception during their rehearsal. + /// This exception is thrown probably because the logic of some of + /// the is not enough generic so that + /// it can cover every case including “rehearsal mode.” + /// The property also might be + /// useful to make the can deal with the case of + /// rehearsal mode. + /// The actual exception that an threw + /// is stored in its property. + /// + /// + public Transaction MakeTransaction( + PrivateKey privateKey, + IEnumerable actions, + IImmutableSet
updatedAddresses = null, + DateTimeOffset? timestamp = null) + { + timestamp = timestamp ?? DateTimeOffset.UtcNow; + lock (_txLock) + { + Transaction tx = Transaction.Create( + GetNonce(privateKey.PublicKey.ToAddress()), + privateKey, + actions, + updatedAddresses, + timestamp); + StageTransactions(new HashSet> { tx }); + + return tx; + } + } + + /// + /// Adds to the pending list so that + /// a next to be mined contains these + /// . + /// + /// + /// s to add to the pending list. + internal void StageTransactions(ISet> transactions) + { + foreach (Transaction 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> stagedTxs = Store + .IterateStagedTransactionIds() + .Select(Store.GetTransaction) + .Where(tx => tx.Signer.Equals(address)); + + foreach (Transaction tx in stagedTxs) + { + if (nonce <= tx.Nonce) + { + nonce = tx.Nonce + 1; + } + } + + return nonce; + } + internal void Append( Block block, DateTimeOffset currentTime, diff --git a/Libplanet/Tx/Transaction.cs b/Libplanet/Tx/Transaction.cs index 88c1d466adf..bfef28670d2 100644 --- a/Libplanet/Tx/Transaction.cs +++ b/Libplanet/Tx/Transaction.cs @@ -268,180 +268,6 @@ public static Transaction FromBencodex(byte[] bytes) } } - /// - /// A façade factory to create a new . - /// Unlike the - /// constructor, it automatically fills the following values from: - /// - /// - /// Property - /// Parameter the filled value derived from - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// and - /// - /// - /// - /// Note that the in itself is not - /// included in the created . - /// - /// - /// This factory method tries its best to fill the property by actually evaluating - /// the given (we call it “rehearsal - /// mode”), but remember that its result - /// is approximated in some degree, because the result of - /// are not deterministic until - /// the belongs to a . - /// If an depends on previous states or - /// some randomness to determine what to update, - /// the automatically filled became - /// mismatched from the es - /// actually update after - /// a is mined. - /// Although such case would be rare, a programmer could manually give - /// the parameter - /// the es they predict to be updated. - /// If an oversimplifies the assumption - /// about the 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 's - /// is true and - /// a conditional logic for the case. - /// - /// The number of previous - /// s committed by the - /// of this transaction. This goes to the - /// property. - /// A of the account - /// who creates and signs a new transaction. This key is used to fill - /// the , , and - /// properties, but this in itself is not - /// included in the transaction. - /// A list of s. This - /// can be empty, but cannot be null. This goes to - /// the property, and s - /// are evaluated before a is created - /// in order to fill the . See also - /// Remarks section. - /// es whose - /// states affected by . - /// These es are also included in - /// the property, besides - /// es projected by evaluating - /// . See also Remarks section. - /// - /// The time this - /// is created and signed. This goes to the - /// property. If null (which is default) is passed this will - /// be the current time. - /// A created new signed by - /// the given . - /// Thrown when null - /// is passed to or - /// or . - /// - /// - /// Thrown when one of throws some - /// exception during their rehearsal. - /// This exception is thrown probably because the logic of some of - /// the is not enough generic so that - /// it can cover every case including “rehearsal mode.” - /// The property also might be - /// useful to make the can deal with the case of - /// rehearsal mode. - /// The actual exception that an threw - /// is stored in its property. - /// - /// - public static Transaction Create( - long nonce, - PrivateKey privateKey, - IEnumerable actions, - IImmutableSet
updatedAddresses = null, - DateTimeOffset? timestamp = null - ) - { - if (ReferenceEquals(privateKey, null)) - { - throw new ArgumentNullException(nameof(privateKey)); - } - - PublicKey publicKey = privateKey.PublicKey; - var signer = new Address(publicKey); - - if (ReferenceEquals(updatedAddresses, null)) - { - updatedAddresses = ImmutableHashSet
.Empty; - } - - DateTimeOffset ts = timestamp ?? DateTimeOffset.UtcNow; - - ImmutableArray actionsArray = actions.ToImmutableArray(); - byte[] payload = new Transaction( - nonce, - signer, - publicKey, - updatedAddresses, - ts, - actionsArray - ).ToBencodex(false); - - if (!actionsArray.IsEmpty) - { - IAccountStateDelta delta = new Transaction( - nonce, - signer, - publicKey, - updatedAddresses, - ts, - actionsArray - ).EvaluateActions( - default(HashDigest), - 0, - new AccountStateDeltaImpl(_ => null), - signer, - rehearsal: true - ); - if (!updatedAddresses.IsSupersetOf(delta.UpdatedAddresses)) - { - updatedAddresses = - updatedAddresses.Union(delta.UpdatedAddresses); - payload = new Transaction( - nonce, - signer, - publicKey, - updatedAddresses, - ts, - actionsArray - ).ToBencodex(false); - } - } - - byte[] sig = privateKey.Sign(payload); - return new Transaction( - nonce, - signer, - publicKey, - updatedAddresses, - ts, - actionsArray, - sig - ); - } - /// /// Encodes this into a /// array. @@ -713,6 +539,82 @@ public override int GetHashCode() return Id.GetHashCode(); } + internal static Transaction Create( + long nonce, + PrivateKey privateKey, + IEnumerable actions, + IImmutableSet
updatedAddresses = null, + DateTimeOffset? timestamp = null + ) + { + if (ReferenceEquals(privateKey, null)) + { + throw new ArgumentNullException(nameof(privateKey)); + } + + PublicKey publicKey = privateKey.PublicKey; + var signer = new Address(publicKey); + + if (ReferenceEquals(updatedAddresses, null)) + { + updatedAddresses = ImmutableHashSet
.Empty; + } + + DateTimeOffset ts = timestamp ?? DateTimeOffset.UtcNow; + + ImmutableArray actionsArray = actions.ToImmutableArray(); + byte[] payload = new Transaction( + nonce, + signer, + publicKey, + updatedAddresses, + ts, + actionsArray + ).ToBencodex(false); + + if (!actionsArray.IsEmpty) + { + IAccountStateDelta delta = new Transaction( + nonce, + signer, + publicKey, + updatedAddresses, + ts, + actionsArray + ).EvaluateActions( + default(HashDigest), + 0, + new AccountStateDeltaImpl(_ => null), + signer, + rehearsal: true + ); + if (!updatedAddresses.IsSupersetOf(delta.UpdatedAddresses)) + { + updatedAddresses = + updatedAddresses.Union(delta.UpdatedAddresses); + payload = new Transaction( + nonce, + signer, + publicKey, + updatedAddresses, + ts, + actionsArray + ).ToBencodex(false); + } + } + + byte[] sig = privateKey.Sign(payload); + return new Transaction( + nonce, + signer, + publicKey, + updatedAddresses, + ts, + actionsArray, + sig + ); + } + internal RawTransaction ToRawTransaction(bool includeSign) { var rawTx = new RawTransaction(