diff --git a/Lib9c.Policy/AccessControlService/IAccessControlService.cs b/Lib9c.Policy/AccessControlService/IAccessControlService.cs new file mode 100644 index 0000000000..42e7dc1208 --- /dev/null +++ b/Lib9c.Policy/AccessControlService/IAccessControlService.cs @@ -0,0 +1,9 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Blockchain +{ + public interface IAccessControlService + { + public int? GetTxQuota(Address address); + } +} diff --git a/Lib9c.Policy/NCStagePolicy.cs b/Lib9c.Policy/NCStagePolicy.cs new file mode 100644 index 0000000000..0dbd00c1f5 --- /dev/null +++ b/Lib9c.Policy/NCStagePolicy.cs @@ -0,0 +1,140 @@ +namespace Nekoyume.Blockchain +{ + 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; + + 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 Iterate(BlockChain blockChain, bool filtered = true) + { + if (filtered) + { + var txsPerSigner = new Dictionary>(); + foreach (Transaction tx in _impl.Iterate(blockChain, filtered)) + { + if (!txsPerSigner.TryGetValue(tx.Signer, out var s)) + { + txsPerSigner[tx.Signer] = s = new SortedSet(new TxComparer()); + } + + s.Add(tx); + int txQuotaPerSigner = _quotaPerSigner; + + // update txQuotaPerSigner if ACS returns a value for the signer. + if (_accessControlService?.GetTxQuota(tx.Signer) is { } acsTxQuota) + { + txQuotaPerSigner = acsTxQuota; + } + + + if (s.Count > txQuotaPerSigner) + { + s.Remove(s.Max); + } + } + +#pragma warning disable LAA1002 // DictionariesOrSetsShouldBeOrderedToEnumerate + return txsPerSigner.Values.SelectMany(i => i); +#pragma warning restore LAA1002 // DictionariesOrSetsShouldBeOrderedToEnumerate + } + else + { + return _impl.Iterate(blockChain, filtered); + } + } + + public bool Stage(BlockChain blockChain, Transaction transaction) + { + if (_accessControlService?.GetTxQuota(transaction.Signer) is { } acsTxQuota + && acsTxQuota == 0) + { + return false; + } + + var deniedTxs = new[] + { + // CreatePledge Transaction with 50000 addresses + TxId.FromHex("300826da62b595d8cd663dadf04995a7411534d1cdc17dac75ce88754472f774"), + // CreatePledge Transaction with 5000 addresses + TxId.FromHex("210d1374d8f068de657de6b991e63888da9cadbc68e505ac917b35568b5340f8"), + }; + if (deniedTxs.Contains(transaction.Id)) + { + return false; + } + + return _impl.Stage(blockChain, transaction); + } + + public bool Unstage(BlockChain blockChain, TxId id) + => _impl.Unstage(blockChain, id); + + private class TxComparer : IComparer + { + public int Compare(Transaction x, Transaction y) + { + if (x.Nonce < y.Nonce) + { + return -1; + } + else if (x.Nonce > y.Nonce) + { + return 1; + } + else if (x.Timestamp < y.Timestamp) + { + return -1; + } + else if (x.Timestamp > y.Timestamp) + { + return 1; + } + else + { + return 0; + } + } + } + } +}