diff --git a/CHANGES.md b/CHANGES.md index 451d8c7432..0361bb25f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,8 +30,8 @@ To be released. ### Added APIs - - Added `IStagePolicy` interface. [[#1130], [#1131]] - - Added `VolatileStagePolicy` class. [[#1130], [#1131]] + - Added `IStagePolicy` interface. [[#1130], [#1131]] + - Added `VolatileStagePolicy` class. [[#1130], [#1131], [#1136]] - Added `ITransport` interface. [[#1052]] - Added `NetMQTransport` class which implements `ITransport`. [[#1052]] - Added `Message` abstract class. [[#1052]] @@ -74,7 +74,8 @@ To be released. - Removed `Swarm.TraceTable()` method. [[#1120]] - Added `Swarm.PeerStates` property. [[#1120]] - Added `IProtocol` interface. [[#1120]] - - Added `KademliaProtocol` class which implements `IProtocol`. [[#1120]] + - Added `KademliaProtocol` class which implements `IProtocol`. + [[#1120], [#1135]] ### Behavioral changes @@ -84,7 +85,9 @@ To be released. - When a `BlockChain` follows `VolatileStagePolicy`, which is Libplanet's the only built-in `IStagePolicy` implementation at the moment, as its `StagePolicy`, its staged transactions are no longer - persistent but volatile instead. [[#1130], [#1131]] + persistent but volatile instead. It also automatically purges staged + transactions after the given `Lifetime`, which is 3 hours by default. + [[#1130], [#1131], [#1136]] - `Swarm` became not to receive states from trusted peers. [[#1061], [#1102]] - `Swarm` became not to retry when block downloading. [[#1062], [#1102]] @@ -135,6 +138,8 @@ To be released. [#1130]: https://github.com/planetarium/libplanet/issues/1130 [#1131]: https://github.com/planetarium/libplanet/pull/1131 [#1132]: https://github.com/planetarium/libplanet/pull/1132 +[#1135]: https://github.com/planetarium/libplanet/pull/1135 +[#1136]: https://github.com/planetarium/libplanet/pull/1136 Version 0.10.2 diff --git a/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs b/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs index 47457c4352..5d4fca29be 100644 --- a/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs +++ b/Libplanet.Tests/Blockchain/BlockChainTest.Append.cs @@ -313,7 +313,7 @@ public void UnstageAfterAppendComplete() { PrivateKey privateKey = new PrivateKey(); (Address[] addresses, Transaction[] txs) = - MakeFixturesForAppendTests(privateKey); + MakeFixturesForAppendTests(privateKey, epoch: DateTimeOffset.UtcNow); var genesis = _blockChain.Genesis; Block block1 = TestUtils.MineNext( diff --git a/Libplanet.Tests/Blockchain/BlockChainTest.cs b/Libplanet.Tests/Blockchain/BlockChainTest.cs index af5226e7b7..38f5c42e04 100644 --- a/Libplanet.Tests/Blockchain/BlockChainTest.cs +++ b/Libplanet.Tests/Blockchain/BlockChainTest.cs @@ -1639,6 +1639,16 @@ public void GetNextTxNonce() }; StageTransactions(txsE); + foreach (var tx in _blockChain.StagePolicy.Iterate(_blockChain)) + { + _logger.Fatal( + "{Id}; {Signer}; {Nonce}; {Timestamp}", + tx.Id, + tx.Signer, + tx.Nonce, + tx.Timestamp); + } + Assert.Equal(6, _blockChain.GetNextTxNonce(address)); } @@ -2011,8 +2021,10 @@ void BuildIndex(Guid id, Block block) MakeIncompleteBlockStates() => MakeIncompleteBlockStates(_fx.Store, _fx.StateStore); - private (Address[], Transaction[]) - MakeFixturesForAppendTests(PrivateKey privateKey = null) + private (Address[], Transaction[]) MakeFixturesForAppendTests( + PrivateKey privateKey = null, + DateTimeOffset epoch = default + ) { Address[] addresses = { @@ -2039,7 +2051,7 @@ void BuildIndex(Guid id, Block block) new DumbAction(addresses[0], "foo"), new DumbAction(addresses[1], "bar"), }, - timestamp: DateTimeOffset.MinValue, + timestamp: epoch, nonce: 0, privateKey: privateKey), _fx.MakeTransaction( @@ -2048,7 +2060,7 @@ void BuildIndex(Guid id, Block block) new DumbAction(addresses[2], "baz"), new DumbAction(addresses[3], "qux"), }, - timestamp: DateTimeOffset.MinValue.AddSeconds(5), + timestamp: epoch.AddSeconds(5), nonce: 1, privateKey: privateKey), }; diff --git a/Libplanet.Tests/Blockchain/Policies/StagePolicyTest.cs b/Libplanet.Tests/Blockchain/Policies/StagePolicyTest.cs index 6c1012a764..ab1cb676cc 100644 --- a/Libplanet.Tests/Blockchain/Policies/StagePolicyTest.cs +++ b/Libplanet.Tests/Blockchain/Policies/StagePolicyTest.cs @@ -11,10 +11,11 @@ namespace Libplanet.Tests.Blockchain.Policies { public abstract class StagePolicyTest { - private readonly BlockPolicy _policy; - private readonly DefaultStoreFixture _fx; - private readonly BlockChain _chain; - private readonly Transaction[] _txs; + protected readonly BlockPolicy _policy; + protected readonly DefaultStoreFixture _fx; + protected readonly BlockChain _chain; + protected readonly PrivateKey _key; + protected readonly Transaction[] _txs; protected StagePolicyTest() { @@ -27,11 +28,11 @@ protected StagePolicyTest() _fx.StateStore, _fx.GenesisBlock ); - var key = new PrivateKey(); + _key = new PrivateKey(); _txs = Enumerable.Range(0, 5).Select(i => Transaction.Create( i, - key, + _key, _fx.GenesisBlock.Hash, Enumerable.Empty() ) diff --git a/Libplanet.Tests/Blockchain/Policies/VolatileStagePolicyTest.cs b/Libplanet.Tests/Blockchain/Policies/VolatileStagePolicyTest.cs index aa6faa2353..8ba3cf5023 100644 --- a/Libplanet.Tests/Blockchain/Policies/VolatileStagePolicyTest.cs +++ b/Libplanet.Tests/Blockchain/Policies/VolatileStagePolicyTest.cs @@ -1,5 +1,10 @@ +using System; +using System.Linq; +using System.Threading; using Libplanet.Blockchain.Policies; using Libplanet.Tests.Common.Action; +using Libplanet.Tx; +using Xunit; namespace Libplanet.Tests.Blockchain.Policies { @@ -10,5 +15,31 @@ public class VolatileStagePolicyTest : StagePolicyTest protected override IStagePolicy StagePolicy => _stagePolicy; + + [Fact] + public void Lifetime() + { + TimeSpan timeBuffer = TimeSpan.FromSeconds(2.5); + + Transaction tx = Transaction.Create( + 0, + _key, + _fx.GenesisBlock.Hash, + Enumerable.Empty(), + timestamp: DateTimeOffset.UtcNow - _stagePolicy.Lifetime + timeBuffer + ); + _stagePolicy.Stage(_chain, tx); + Assert.True(_stagePolicy.HasStaged(_chain, tx.Id, false)); + Assert.Equal(tx, _stagePolicy.Get(_chain, tx.Id, false)); + Assert.Contains(tx, _stagePolicy.Iterate(_chain)); + + // On some targets TimeSpan * int does not exist: + Thread.Sleep(timeBuffer); + Thread.Sleep(timeBuffer); + + Assert.False(_stagePolicy.HasStaged(_chain, tx.Id, true)); + Assert.Null(_stagePolicy.Get(_chain, tx.Id, true)); + Assert.DoesNotContain(tx, _stagePolicy.Iterate(_chain)); + } } } diff --git a/Libplanet.Tests/Store/StoreFixture.cs b/Libplanet.Tests/Store/StoreFixture.cs index ad9fa708e5..d55a0a60f6 100644 --- a/Libplanet.Tests/Store/StoreFixture.cs +++ b/Libplanet.Tests/Store/StoreFixture.cs @@ -157,8 +157,8 @@ public Transaction MakeTransaction( ) { privateKey = privateKey ?? new PrivateKey(); - timestamp = timestamp ?? - new DateTimeOffset(2018, 11, 21, 0, 0, 0, TimeSpan.Zero); + timestamp = timestamp ?? DateTimeOffset.UtcNow; + return Transaction.Create( nonce, privateKey, diff --git a/Libplanet/Blockchain/BlockChain.cs b/Libplanet/Blockchain/BlockChain.cs index 9b1bd96aff..26cbce459f 100644 --- a/Libplanet/Blockchain/BlockChain.cs +++ b/Libplanet/Blockchain/BlockChain.cs @@ -668,6 +668,11 @@ public long GetNextTxNonce(Address address) foreach (long n in stagedTxNonces) { + if (n < nonce) + { + continue; + } + if (n != nonce) { break; diff --git a/Libplanet/Blockchain/Policies/VolatileStagePolicy.cs b/Libplanet/Blockchain/Policies/VolatileStagePolicy.cs index 85812bfa75..d5b8205ec8 100644 --- a/Libplanet/Blockchain/Policies/VolatileStagePolicy.cs +++ b/Libplanet/Blockchain/Policies/VolatileStagePolicy.cs @@ -1,6 +1,8 @@ #nullable enable +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using Libplanet.Action; using Libplanet.Tx; @@ -23,17 +25,41 @@ public class VolatileStagePolicy : IStagePolicy /// /// Creates a new instance. + /// is configured to 3 hours. /// public VolatileStagePolicy() + : this(TimeSpan.FromHours(3)) { + } + + /// + /// Creates a new instance. + /// + /// Volatilizes staged transactions older than this . See also the property. + public VolatileStagePolicy(TimeSpan lifetime) + { + Lifetime = lifetime; _set = new ConcurrentDictionary>(); _queue = new List(); _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); } + /// + /// Volatilizes staged transactions older than this . + /// Note that transactions older than the lifetime never cannot be staged. + /// + public TimeSpan Lifetime { get; } + /// public void Stage(BlockChain blockChain, Transaction transaction) { + if (DateTimeOffset.UtcNow - Lifetime > transaction.Timestamp) + { + // The transaction is already expired; don't stage it at all. + return; + } + try { if (!_set.TryAdd(transaction.Id, transaction)) @@ -73,46 +99,38 @@ public void Unstage(BlockChain blockChain, TxId id) } /// - public bool HasStaged(BlockChain blockChain, TxId id, bool includeUnstaged) - { - if (includeUnstaged) - { - return _set.ContainsKey(id); - } - - _lock.EnterReadLock(); - try - { - return _queue.Contains(id); - } - finally - { - _lock.ExitReadLock(); - } - } + public bool HasStaged(BlockChain blockChain, TxId id, bool includeUnstaged) => + Get(blockChain, id, includeUnstaged) is { }; /// public Transaction? Get(BlockChain blockChain, TxId id, bool includeUnstaged) { - if (_set.TryGetValue(id, out Transaction? tx)) + if (!_set.TryGetValue(id, out Transaction? tx)) + { + return null; + } + else if (tx.Timestamp >= DateTimeOffset.UtcNow - Lifetime) { - if (includeUnstaged) - { - return tx; - } - _lock.EnterReadLock(); - bool queued; try { - queued = _queue.Contains(tx.Id); + return includeUnstaged || _queue.Contains(id) ? tx : null; } finally { _lock.ExitReadLock(); } + } - return queued ? tx : null; + _lock.EnterWriteLock(); + try + { + _queue.Remove(id); + _set.TryRemove(id, out _); + } + finally + { + _lock.ExitWriteLock(); } return null; @@ -132,13 +150,42 @@ public IEnumerable> Iterate(BlockChain blockChain) _lock.ExitReadLock(); } + DateTimeOffset exp = DateTimeOffset.UtcNow - Lifetime; + var expired = new List(); foreach (TxId txid in queue) { if (_set.TryGetValue(txid, out Transaction? tx)) { - yield return tx; + if (tx.Timestamp > exp) + { + yield return tx; + } + else + { + expired.Add(tx.Id); + } } } + + if (!expired.Any()) + { + yield break; + } + + // Clean up expired transactions (if any exist). + _lock.EnterWriteLock(); + try + { + foreach (TxId txid in expired) + { + _queue.Remove(txid); + _set.TryRemove(txid, out _); + } + } + finally + { + _lock.ExitWriteLock(); + } } } }