diff --git a/Lib9c b/Lib9c index 1969277b9..d6f420a0e 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 1969277b9c5dedcd3d6e5cd46a48d5f259f2afca +Subproject commit d6f420a0ea2c714e3546a4fc346d8bf1cddd8f5f diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs index b5d52f697..716fa8fe2 100644 --- a/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs @@ -1,6 +1,6 @@ using Libplanet.Crypto; using System.Collections.Generic; -using Nekoyume.Blockchain; +using NineChronicles.Headless.AccessControlService; namespace NineChronicles.Headless.AccessControlCenter.AccessControlService { diff --git a/NineChronicles.Headless.Tests/Policies/StagePolicyTest.cs b/NineChronicles.Headless.Tests/Policies/StagePolicyTest.cs new file mode 100644 index 000000000..e291e7f27 --- /dev/null +++ b/NineChronicles.Headless.Tests/Policies/StagePolicyTest.cs @@ -0,0 +1,324 @@ +namespace NineChronicles.Headless.Tests.Policies +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Threading.Tasks; + using Lib9c.Renderers; + using Libplanet.Action; + using Libplanet.Blockchain; + using Libplanet.Blockchain.Policies; + using Libplanet.Crypto; + using Libplanet.Store; + using Libplanet.Store.Trie; + using Libplanet.Types.Blocks; + using Libplanet.Types.Tx; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Action.Loader; + using Nekoyume.Blockchain.Policy; + using Nekoyume.Model; + using Nekoyume.Model.State; + using NineChronicles.Headless.Policies; + using Xunit; + + public class StagePolicyTest + { + private readonly PrivateKey[] _accounts; + + private readonly Dictionary
_txs; + + public StagePolicyTest() + { + _accounts = new[] + { + new PrivateKey(), + new PrivateKey(), + new PrivateKey(), + new PrivateKey(), + }; + _txs = _accounts.ToDictionary( + acc => acc.ToAddress(), + acc => Enumerable + .Range(0, 10) + .Select( + n => Transaction.Create( + n, + acc, + default, + new ActionBase[0].ToPlainValues() + ) + ) + .ToArray() + ); + } + + [Fact] + public void Stage() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][0]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][1]); + stagePolicy.Stage(chain, _txs[_accounts[1].ToAddress()][0]); + stagePolicy.Stage(chain, _txs[_accounts[2].ToAddress()][0]); + + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][0], + _txs[_accounts[0].ToAddress()][1], + _txs[_accounts[1].ToAddress()][0], + _txs[_accounts[2].ToAddress()][0] + ); + } + + [Fact] + public void StageOverQuota() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][0]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][1]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][2]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][3]); + + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][0], + _txs[_accounts[0].ToAddress()][1] + ); + } + + [Fact] + public void StageOverQuotaInverseOrder() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][3]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][2]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][1]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][0]); + + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][0], + _txs[_accounts[0].ToAddress()][1] + ); + } + + [Fact] + public void StageOverQuotaOutOfOrder() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][2]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][1]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][3]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][0]); + + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][0], + _txs[_accounts[0].ToAddress()][1] + ); + } + + [Fact] + public void StageSameNonce() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + var txA = Transaction.Create(0, _accounts[0], default, new ActionBase[0].ToPlainValues()); + var txB = Transaction.Create(0, _accounts[0], default, new ActionBase[0].ToPlainValues()); + var txC = Transaction.Create(0, _accounts[0], default, new ActionBase[0].ToPlainValues()); + + stagePolicy.Stage(chain, txA); + stagePolicy.Stage(chain, txB); + stagePolicy.Stage(chain, txC); + + AssertTxs(chain, stagePolicy, txA, txB); + } + + [Fact] + public async Task StateFromMultiThread() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + await Task.WhenAll( + Enumerable + .Range(0, 40) + .Select(i => Task.Run(() => + { + stagePolicy.Stage(chain, _txs[_accounts[i / 10].ToAddress()][i % 10]); + })) + ); + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][0], + _txs[_accounts[0].ToAddress()][1], + _txs[_accounts[1].ToAddress()][0], + _txs[_accounts[1].ToAddress()][1], + _txs[_accounts[2].ToAddress()][0], + _txs[_accounts[2].ToAddress()][1], + _txs[_accounts[3].ToAddress()][0], + _txs[_accounts[3].ToAddress()][1] + ); + } + + [Fact] + public void IterateAfterUnstage() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][0]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][1]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][2]); + stagePolicy.Stage(chain, _txs[_accounts[0].ToAddress()][3]); + + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][0], + _txs[_accounts[0].ToAddress()][1] + ); + + stagePolicy.Unstage(chain, _txs[_accounts[0].ToAddress()][0].Id); + + AssertTxs( + chain, + stagePolicy, + _txs[_accounts[0].ToAddress()][1], + _txs[_accounts[0].ToAddress()][2] + ); + } + + [Fact] + public void CalculateNextTxNonceCorrectWhenTxOverQuota() + { + NCStagePolicy stagePolicy = new NCStagePolicy(TimeSpan.FromHours(1), 2); + BlockChain chain = MakeChainWithStagePolicy(stagePolicy); + + long nextTxNonce = chain.GetNextTxNonce(_accounts[0].ToAddress()); + Assert.Equal(0, nextTxNonce); + var txA = Transaction.Create(nextTxNonce, _accounts[0], default, new ActionBase[0].ToPlainValues()); + stagePolicy.Stage(chain, txA); + + nextTxNonce = chain.GetNextTxNonce(_accounts[0].ToAddress()); + Assert.Equal(1, nextTxNonce); + var txB = Transaction.Create(nextTxNonce, _accounts[0], default, new ActionBase[0].ToPlainValues()); + stagePolicy.Stage(chain, txB); + + nextTxNonce = chain.GetNextTxNonce(_accounts[0].ToAddress()); + Assert.Equal(2, nextTxNonce); + var txC = Transaction.Create(nextTxNonce, _accounts[0], default, new ActionBase[0].ToPlainValues()); + stagePolicy.Stage(chain, txC); + + nextTxNonce = chain.GetNextTxNonce(_accounts[0].ToAddress()); + Assert.Equal(3, nextTxNonce); + + AssertTxs( + chain, + stagePolicy, + txA, + txB); + } + + private void AssertTxs(BlockChain blockChain, NCStagePolicy policy, params Transaction[] txs) + { + foreach (Transaction tx in txs) + { + Assert.Equal(tx, policy.Get(blockChain, tx.Id, filtered: true)); + } + + Assert.Equal( + txs.ToHashSet(), + policy.Iterate(blockChain, filtered: true).ToHashSet() + ); + } + + private BlockChain MakeChainWithStagePolicy(NCStagePolicy stagePolicy) + { + BlockPolicySource blockPolicySource = new BlockPolicySource(); + IBlockPolicy policy = blockPolicySource.GetPolicy(); + BlockChain chain = + BlockChainHelper.MakeBlockChain( + blockRenderers: new[] { new BlockRenderer() }, + policy: policy, + stagePolicy: stagePolicy); + return chain; + } + + public static class BlockChainHelper + { + public static BlockChain MakeBlockChain( + BlockRenderer[] blockRenderers, + IBlockPolicy policy = null, + IStagePolicy stagePolicy = null, + IStore store = null, + IStateStore stateStore = null) + { + PrivateKey adminPrivateKey = new PrivateKey(); + + policy ??= new BlockPolicy(); + stagePolicy ??= new VolatileStagePolicy(); + store ??= new DefaultStore(null); + stateStore ??= new TrieStateStore(new DefaultKeyValueStore(null)); + Block genesis = MakeGenesisBlock(adminPrivateKey.ToAddress(), ImmutableHashSet.Empty); + return BlockChain.Create( + policy, + stagePolicy, + store, + stateStore, + genesis, + new ActionEvaluator( + policyBlockActionGetter: _ => policy.BlockAction, + stateStore: stateStore, + actionTypeLoader: new NCActionLoader() + ), + renderers: blockRenderers); + } + + public static Block MakeGenesisBlock( + Address adminAddress, + IImmutableSet activatedAddresses, + AuthorizedMinersState authorizedMinersState = null, + DateTimeOffset? timestamp = null, + PendingActivationState[] pendingActivations = null + ) + { + PrivateKey privateKey = new PrivateKey(); + if (pendingActivations is null) + { + var nonce = new byte[] { 0x00, 0x01, 0x02, 0x03 }; + (ActivationKey activationKey, PendingActivationState pendingActivation) = + ActivationKey.Create(privateKey, nonce); + pendingActivations = new[] { pendingActivation }; + } + + var sheets = TableSheetsImporter.ImportSheets(); + return BlockHelper.ProposeGenesisBlock( + sheets, + new GoldDistribution[0], + pendingActivations, + new AdminState(adminAddress, 1500000), + activatedAccounts: activatedAddresses, + isActivateAdminAddress: false, + credits: null, + privateKey: privateKey, + timestamp: timestamp ?? DateTimeOffset.MinValue); + } + } + } +} diff --git a/NineChronicles.Headless/AccessControlService/IAccessControlService.cs b/NineChronicles.Headless/AccessControlService/IAccessControlService.cs new file mode 100644 index 000000000..c96e7c64a --- /dev/null +++ b/NineChronicles.Headless/AccessControlService/IAccessControlService.cs @@ -0,0 +1,9 @@ +using Libplanet.Crypto; + +namespace NineChronicles.Headless.AccessControlService +{ + public interface IAccessControlService + { + public int? GetTxQuota(Address address); + } +} diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index 181f14e6e..7a2db6e4f 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -18,6 +18,8 @@ using Microsoft.Extensions.Hosting; using Nekoyume.Blockchain; using Nekoyume.Blockchain.Policy; +using NineChronicles.Headless.AccessControlService; +using NineChronicles.Headless.Policies; using NineChronicles.Headless.Properties; using NineChronicles.Headless.Utils; using NineChronicles.Headless.Services; diff --git a/NineChronicles.Headless/Policies/NCStagePolicy.cs b/NineChronicles.Headless/Policies/NCStagePolicy.cs new file mode 100644 index 000000000..f5b17bb00 --- /dev/null +++ b/NineChronicles.Headless/Policies/NCStagePolicy.cs @@ -0,0 +1,141 @@ +namespace NineChronicles.Headless.Policies +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using Libplanet.Blockchain; + using Libplanet.Blockchain.Policies; + using Libplanet.Crypto; + using Libplanet.Types.Tx; + using NineChronicles.Headless.AccessControlService; + + public class NCStagePolicy : IStagePolicy + { + private readonly VolatileStagePolicy _impl; + private readonly ConcurrentDictionary> _txs; + private readonly int _quotaPerSigner; + private IAccessControlService? _accessControlService; + + public NCStagePolicy(TimeSpan txLifeTime, int quotaPerSigner, IAccessControlService? accessControlService = null) + { + if (quotaPerSigner < 1) + { + throw new ArgumentOutOfRangeException( + $"{nameof(quotaPerSigner)} must be positive: ${quotaPerSigner}"); + } + + _txs = new ConcurrentDictionary>(); + _quotaPerSigner = quotaPerSigner; + _impl = (txLifeTime == default) + ? new VolatileStagePolicy() + : new VolatileStagePolicy(txLifeTime); + + _accessControlService = accessControlService; + } + + public Transaction Get(BlockChain blockChain, TxId id, bool filtered = true) + => _impl.Get(blockChain, id, filtered)!; + + public long GetNextTxNonce(BlockChain blockChain, Address address) + => _impl.GetNextTxNonce(blockChain, address); + + public void Ignore(BlockChain blockChain, TxId id) + => _impl.Ignore(blockChain, id); + + public bool Ignores(BlockChain blockChain, TxId id) + => _impl.Ignores(blockChain, id); + + public IEnumerable