diff --git a/Centaurus.Alpha/Controllers/ConstellationController.cs b/Centaurus.Alpha/Controllers/ConstellationController.cs index 00082b40..962315b0 100644 --- a/Centaurus.Alpha/Controllers/ConstellationController.cs +++ b/Centaurus.Alpha/Controllers/ConstellationController.cs @@ -35,7 +35,8 @@ public ConstellationInfo Info() MinAccountBalance = Global.Constellation.MinAccountBalance, MinAllowedLotSize = Global.Constellation.MinAllowedLotSize, StellarNetwork = network, - Assets = assets + Assets = assets, + RequestRateLimits = Global.Constellation.RequestRateLimits }; } @@ -50,11 +51,23 @@ public async Task Init([FromBody] ConstellationInitModel constell if (constellationInit == null) return StatusCode(415); + if (constellationInit.RequestRateLimits == null) + throw new ArgumentNullException(nameof(constellationInit.RequestRateLimits), "RequestRateLimits parameter is required."); + var requestRateLimits = new RequestRateLimits + { + HourLimit = constellationInit.RequestRateLimits.HourLimit, + MinuteLimit = constellationInit.RequestRateLimits.MinuteLimit + }; + var constellationInitializer = new ConstellationInitializer( - constellationInit.Auditors.Select(a => KeyPair.FromAccountId(a)), - constellationInit.MinAccountBalance, - constellationInit.MinAllowedLotSize, - constellationInit.Assets.Select(a => AssetSettings.FromCode(a)) + new ConstellationInitInfo + { + Auditors = constellationInit.Auditors.Select(a => KeyPair.FromAccountId(a)).ToArray(), + MinAccountBalance = constellationInit.MinAccountBalance, + MinAllowedLotSize = constellationInit.MinAllowedLotSize, + Assets = constellationInit.Assets.Select(a => AssetSettings.FromCode(a)).ToArray(), + RequestRateLimits = requestRateLimits + } ); await constellationInitializer.Init(); diff --git a/Centaurus.Alpha/Models/ConstellationInfo.cs b/Centaurus.Alpha/Models/ConstellationInfo.cs index ed76f165..6a3a85d4 100644 --- a/Centaurus.Alpha/Models/ConstellationInfo.cs +++ b/Centaurus.Alpha/Models/ConstellationInfo.cs @@ -1,15 +1,11 @@ using Centaurus.Models; using stellar_dotnet_sdk; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Centaurus.Alpha { public class ConstellationInfo { - public ApplicationState State{ get; set; } + public ApplicationState State { get; set; } public string Vault { get; set; } @@ -23,6 +19,8 @@ public class ConstellationInfo public Asset[] Assets { get; set; } + public RequestRateLimits RequestRateLimits { get; set; } + public class Network { public Network(string passphrase, string horizon) diff --git a/Centaurus.Alpha/Models/ConstellationInitModel.cs b/Centaurus.Alpha/Models/ConstellationInitModel.cs index 4e684d64..da4224e4 100644 --- a/Centaurus.Alpha/Models/ConstellationInitModel.cs +++ b/Centaurus.Alpha/Models/ConstellationInitModel.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Centaurus.Models; namespace Centaurus.Alpha { @@ -14,5 +11,7 @@ public class ConstellationInitModel public long MinAllowedLotSize { get; set; } public string[] Assets { get; set; } + + public RequestRateLimitsModel RequestRateLimits { get; set; } } } diff --git a/Centaurus.Alpha/Models/RequestRateLimitsModel.cs b/Centaurus.Alpha/Models/RequestRateLimitsModel.cs new file mode 100644 index 00000000..a39c982d --- /dev/null +++ b/Centaurus.Alpha/Models/RequestRateLimitsModel.cs @@ -0,0 +1,9 @@ +namespace Centaurus.Alpha +{ + public class RequestRateLimitsModel + { + public uint MinuteLimit { get; set; } + + public uint HourLimit { get; set; } + } +} diff --git a/Centaurus.Common/Exceptions/ClientExceptions/TooManyRequests.cs b/Centaurus.Common/Exceptions/ClientExceptions/TooManyRequests.cs new file mode 100644 index 00000000..14169a82 --- /dev/null +++ b/Centaurus.Common/Exceptions/ClientExceptions/TooManyRequests.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Centaurus +{ + public class TooManyRequests : BaseClientException + { + public TooManyRequests() + { + + } + + public TooManyRequests(string message) + : base(message) + { + + } + } +} diff --git a/Centaurus.DAL/DiffObject.cs b/Centaurus.DAL/DiffObject.cs index cb657f8b..7f120683 100644 --- a/Centaurus.DAL/DiffObject.cs +++ b/Centaurus.DAL/DiffObject.cs @@ -66,6 +66,8 @@ public class Account : BaseDiffModel public byte[] PubKey { get; set; } public ulong Nonce { get; set; } + + public RequestRateLimitsModel RequestRateLimits { get; set; } } public class Order : BaseDiffModel diff --git a/Centaurus.DAL/Models/AccountModel.cs b/Centaurus.DAL/Models/AccountModel.cs index 19d70ee8..8a79ff8a 100644 --- a/Centaurus.DAL/Models/AccountModel.cs +++ b/Centaurus.DAL/Models/AccountModel.cs @@ -4,7 +4,8 @@ public class AccountModel { public byte[] PubKey { get; set; } - //it stores ulong public long Nonce { get; set; } + + public RequestRateLimitsModel RequestRateLimits { get; set; } } } diff --git a/Centaurus.DAL/Models/RequestRateLimitModel.cs b/Centaurus.DAL/Models/RequestRateLimitModel.cs new file mode 100644 index 00000000..04542c0a --- /dev/null +++ b/Centaurus.DAL/Models/RequestRateLimitModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Centaurus.DAL.Models +{ + public class RequestRateLimitsModel + { + public uint HourLimit { get; set; } + public uint MinuteLimit { get; set; } + } +} diff --git a/Centaurus.DAL/Models/SettingsModel.cs b/Centaurus.DAL/Models/SettingsModel.cs index 64b639c8..1b1bf449 100644 --- a/Centaurus.DAL/Models/SettingsModel.cs +++ b/Centaurus.DAL/Models/SettingsModel.cs @@ -11,10 +11,11 @@ public class SettingsModel public byte[][] Auditors { get; set; } public long MinAccountBalance { get; set; } - + public long MinAllowedLotSize { get; set; } - //it stores ulong public long Apex { get; set; } + + public RequestRateLimitsModel RequestRateLimits { get; set; } } } diff --git a/Centaurus.DAL/Mongo/MongoStorage.cs b/Centaurus.DAL/Mongo/MongoStorage.cs index fb48460c..85fc31eb 100644 --- a/Centaurus.DAL/Mongo/MongoStorage.cs +++ b/Centaurus.DAL/Mongo/MongoStorage.cs @@ -279,7 +279,9 @@ private WriteModel[] GetStellarDataUpdate(DiffObject.Constel if (ledger > 0) updateCommand = update.Set(s => s.Ledger, ledger); if (vaultSequence > 0) - updateCommand = updateCommand.Set(s => s.VaultSequence, vaultSequence) ?? update.Set(s => s.VaultSequence, vaultSequence); + updateCommand = updateCommand == null + ? update.Set(s => s.VaultSequence, vaultSequence) + : updateCommand.Set(s => s.VaultSequence, vaultSequence); updateModel = new UpdateOneModel(filter.Empty, updateCommand); } @@ -302,11 +304,25 @@ private WriteModel[] GetAccountUpdates(List ac var pubKey = acc.PubKey; var currentAccFilter = filter.Eq(a => a.PubKey, pubKey); if (acc.IsInserted) - updates[i] = new InsertOneModel(new AccountModel { Nonce = (long)acc.Nonce, PubKey = pubKey }); + updates[i] = new InsertOneModel(new AccountModel + { + Nonce = (long)acc.Nonce, + PubKey = pubKey, + RequestRateLimits = acc.RequestRateLimits + }); else if (acc.IsDeleted) updates[i] = new DeleteOneModel(currentAccFilter); else - updates[i] = new UpdateOneModel(currentAccFilter, update.Set(a => a.Nonce, (long)acc.Nonce)); + { + UpdateDefinition currentUpdate = null; + if (acc.Nonce != 0) + currentUpdate = update.Set(a => a.Nonce, (long)acc.Nonce); + if (acc.RequestRateLimits != null) + currentUpdate = currentUpdate == null + ? update.Set(a => a.RequestRateLimits, acc.RequestRateLimits) + : currentUpdate.Set(a => a.RequestRateLimits, acc.RequestRateLimits); + updates[i] = new UpdateOneModel(currentAccFilter, currentUpdate); + } } return updates; } diff --git a/Centaurus.Domain/Constellation/ConstellationInitializer.cs b/Centaurus.Domain/Constellation/ConstellationInitializer.cs index c27ca73f..28aaa222 100644 --- a/Centaurus.Domain/Constellation/ConstellationInitializer.cs +++ b/Centaurus.Domain/Constellation/ConstellationInitializer.cs @@ -9,6 +9,15 @@ namespace Centaurus.Domain { + public class ConstellationInitInfo + { + public KeyPair[] Auditors { get; set; } + public long MinAccountBalance { get; set; } + public long MinAllowedLotSize { get; set; } + public AssetSettings[] Assets { get; set; } + public RequestRateLimits RequestRateLimits { get; set; } + } + /// /// Initializes the application. /// It can only be called from the Alpha, and only when it is in the waiting for initialization state. @@ -17,23 +26,30 @@ public class ConstellationInitializer { const int minAuditorsCount = 1; - public ConstellationInitializer(IEnumerable auditors, long minAccountBalance, long minAllowedLotSize, IEnumerable assets) + ConstellationInitInfo constellationInitInfo { get; } + + public ConstellationInitializer(ConstellationInitInfo constellationInitInfo) { - Auditors = auditors.Count() >= minAuditorsCount ? auditors.ToArray() : throw new Exception($"Min auditors count is {minAuditorsCount}"); + if (constellationInitInfo.Auditors == null || constellationInitInfo.Auditors.Count() < minAuditorsCount) + throw new ArgumentException($"Min auditors count is {minAuditorsCount}"); - MinAccountBalance = minAccountBalance > 0 ? minAccountBalance : throw new ArgumentException("Minimal account balance is less then 0"); + if (constellationInitInfo.MinAccountBalance < 1) + throw new ArgumentException("Minimal account balance is less then 0"); - MinAllowedLotSize = minAllowedLotSize > 0 ? minAllowedLotSize : throw new ArgumentException("Minimal allowed lot size is less then 0"); + if (constellationInitInfo.MinAllowedLotSize < 1) + throw new ArgumentException("Minimal allowed lot size is less then 0"); - Assets = !assets.GroupBy(a => a.ToString()).Any(g => g.Count() > 1) - ? assets.Where(a => !a.IsXlm).ToArray() //skip XLM, it's supported by default - : throw new ArgumentException("All asset values should be unique"); - } + if (constellationInitInfo.Assets.GroupBy(a => a.ToString()).Any(g => g.Count() > 1)) + throw new ArgumentException("All asset values should be unique"); - public KeyPair[] Auditors { get; } - public long MinAccountBalance { get; } - public long MinAllowedLotSize { get; } - public AssetSettings[] Assets { get; } + if (constellationInitInfo.Assets.Any(a => a.IsXlm)) + throw new ArgumentException("Specify only custom assets. Native assets are supported by default."); + + if (constellationInitInfo.RequestRateLimits == null || constellationInitInfo.RequestRateLimits.HourLimit < 1 || constellationInitInfo.RequestRateLimits.MinuteLimit < 1) + throw new ArgumentException("Request rate limit values should be greater than 0"); + + this.constellationInitInfo = constellationInitInfo; + } public async Task Init() { @@ -48,19 +64,19 @@ public async Task Init() SetIdToAssets(); - var vaultAccountInfo = await Global.StellarNetwork.Server.Accounts.Account(Global.Settings.KeyPair.AccountId); var initQuantum = new ConstellationInitQuantum { - Assets = Assets.ToList(), - Auditors = Auditors.Select(key => (RawPubKey)key.PublicKey).ToList(), + Assets = constellationInitInfo.Assets.ToList(), + Auditors = constellationInitInfo.Auditors.Select(key => (RawPubKey)key.PublicKey).ToList(), Vault = Global.Settings.KeyPair.PublicKey, - MinAccountBalance = MinAccountBalance, - MinAllowedLotSize = MinAllowedLotSize, + MinAccountBalance = constellationInitInfo.MinAccountBalance, + MinAllowedLotSize = constellationInitInfo.MinAllowedLotSize, PrevHash = new byte[] { }, Ledger = ledgerId, - VaultSequence = vaultAccountInfo.SequenceNumber + VaultSequence = vaultAccountInfo.SequenceNumber, + RequestRateLimits = constellationInitInfo.RequestRateLimits }; var envelope = initQuantum.CreateEnvelope(); @@ -71,9 +87,9 @@ public async Task Init() private void SetIdToAssets() { //start from 1, 0 is reserved by XLM - for (var i = 1; i <= Assets.Length; i++) + for (var i = 1; i <= constellationInitInfo.Assets.Length; i++) { - Assets[i - 1].Id = i; + constellationInitInfo.Assets[i - 1].Id = i; } } @@ -83,7 +99,7 @@ private void SetIdToAssets() /// Ledger id private async Task BuildAndConfigureVault(stellar_dotnet_sdk.responses.AccountResponse vaultAccount) { - var majority = MajorityHelper.GetMajorityCount(Auditors.Count()); + var majority = MajorityHelper.GetMajorityCount(constellationInitInfo.Auditors.Count()); var sourceAccount = await Global.StellarNetwork.Server.Accounts.Account(Global.Settings.KeyPair.AccountId); @@ -94,7 +110,7 @@ private async Task BuildAndConfigureVault(stellar_dotnet_sdk.responses.Acc .Where(b => b.Asset is stellar_dotnet_sdk.AssetTypeCreditAlphaNum) .Select(b => b.Asset) .Cast(); - foreach (var a in Assets) + foreach (var a in constellationInitInfo.Assets) { var asset = a.ToAsset() as stellar_dotnet_sdk.AssetTypeCreditAlphaNum; @@ -114,7 +130,7 @@ private async Task BuildAndConfigureVault(stellar_dotnet_sdk.responses.Acc .SetMediumThreshold(majority) .SetHighThreshold(majority); - foreach (var signer in Auditors) + foreach (var signer in constellationInitInfo.Auditors) optionOperationBuilder.SetSigner(Signer.Ed25519PublicKey(signer), 1); transactionBuilder.AddOperation(optionOperationBuilder.Build()); diff --git a/Centaurus.Domain/Effects/Account/BaseAccountEffectProcessor.cs b/Centaurus.Domain/Effects/Account/BaseAccountEffectProcessor.cs index 0916d5f3..d938014d 100644 --- a/Centaurus.Domain/Effects/Account/BaseAccountEffectProcessor.cs +++ b/Centaurus.Domain/Effects/Account/BaseAccountEffectProcessor.cs @@ -24,7 +24,7 @@ public Account Account get { if (account == null) - account = accountStorage.GetAccount(Effect.Pubkey); + account = accountStorage.GetAccount(Effect.Pubkey).Account; return account; } } diff --git a/Centaurus.Domain/Effects/Account/NonceUpdateEffectProcessor.cs b/Centaurus.Domain/Effects/Account/NonceUpdateEffectProcessor.cs index 3b16a2a0..c5e0ec02 100644 --- a/Centaurus.Domain/Effects/Account/NonceUpdateEffectProcessor.cs +++ b/Centaurus.Domain/Effects/Account/NonceUpdateEffectProcessor.cs @@ -5,24 +5,21 @@ namespace Centaurus.Domain { - public class NonceUpdateEffectProcessor : EffectProcessor + public class NonceUpdateEffectProcessor : BaseAccountEffectProcessor { - private Account account; - - public NonceUpdateEffectProcessor(NonceUpdateEffect effect, Account account) - : base(effect) + public NonceUpdateEffectProcessor(NonceUpdateEffect effect, AccountStorage accountStorage) + : base(effect, accountStorage) { - this.account = account; } public override void CommitEffect() { - account.Nonce = Effect.Nonce; + Account.Nonce = Effect.Nonce; } public override void RevertEffect() { - account.Nonce = Effect.PrevNonce; + Account.Nonce = Effect.PrevNonce; } } } diff --git a/Centaurus.Domain/Effects/Account/RequestRateLimitUpdateEffectProcessor.cs b/Centaurus.Domain/Effects/Account/RequestRateLimitUpdateEffectProcessor.cs new file mode 100644 index 00000000..9dbcf56c --- /dev/null +++ b/Centaurus.Domain/Effects/Account/RequestRateLimitUpdateEffectProcessor.cs @@ -0,0 +1,26 @@ +using Centaurus.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Centaurus.Domain +{ + public class RequestRateLimitUpdateEffectProcessor : BaseAccountEffectProcessor + { + public RequestRateLimitUpdateEffectProcessor(RequestRateLimitUpdateEffect effect, AccountStorage accountStorage) + : base(effect, accountStorage) + { + + } + + public override void CommitEffect() + { + Account.RequestRateLimits = Effect.RequestRateLimits; + } + + public override void RevertEffect() + { + Account.RequestRateLimits = Effect.PrevRequestRateLimits; + } + } +} diff --git a/Centaurus.Domain/Effects/EffectProcessorsContainer.cs b/Centaurus.Domain/Effects/EffectProcessorsContainer.cs index bde288e1..e34121db 100644 --- a/Centaurus.Domain/Effects/EffectProcessorsContainer.cs +++ b/Centaurus.Domain/Effects/EffectProcessorsContainer.cs @@ -199,11 +199,11 @@ public void AddOrderRemoved(Orderbook orderbook, AccountStorage accountStorage, - public void AddNonceUpdate(Account account, ulong newNonce) + public void AddNonceUpdate(AccountStorage accountStorage, RawPubKey publicKey, ulong newNonce, ulong currentNonce) { Add(new NonceUpdateEffectProcessor( - new NonceUpdateEffect { Nonce = newNonce, PrevNonce = account.Nonce, Pubkey = account.Pubkey, Apex = Apex }, - account + new NonceUpdateEffect { Nonce = newNonce, PrevNonce = currentNonce, Pubkey = publicKey, Apex = Apex }, + accountStorage )); } } diff --git a/Centaurus.Domain/Effects/Orders/OrderRemovedEffectProccessor.cs b/Centaurus.Domain/Effects/Orders/OrderRemovedEffectProccessor.cs index 0ebaafd8..d102e468 100644 --- a/Centaurus.Domain/Effects/Orders/OrderRemovedEffectProccessor.cs +++ b/Centaurus.Domain/Effects/Orders/OrderRemovedEffectProccessor.cs @@ -25,7 +25,7 @@ public override void CommitEffect() public override void RevertEffect() { - var order = new Order { OrderId = Effect.OrderId, Price = Effect.Price, Amount = 0, Account = accountStorage.GetAccount(Effect.Pubkey) }; + var order = new Order { OrderId = Effect.OrderId, Price = Effect.Price, Amount = 0, Account = accountStorage.GetAccount(Effect.Pubkey).Account }; orderbook.InsertOrder(order); } } diff --git a/Centaurus.Domain/MessageHandlers/AlphaHandlers/AlphaHandshakeHandler.cs b/Centaurus.Domain/MessageHandlers/AlphaHandlers/AlphaHandshakeHandler.cs index 12f8e0ad..e168fc02 100644 --- a/Centaurus.Domain/MessageHandlers/AlphaHandlers/AlphaHandshakeHandler.cs +++ b/Centaurus.Domain/MessageHandlers/AlphaHandlers/AlphaHandshakeHandler.cs @@ -50,6 +50,7 @@ private async Task HandleClientHandshake(AlphaWebSocketConnection connection, Me { if (Global.AppState.State != ApplicationState.Ready) throw new ConnectionCloseException(WebSocketCloseStatus.ProtocolError, "Alpha is not in Ready state."); + connection.Account = Global.AccountStorage.GetAccount(connection.ClientPubKey); connection.ConnectionState = ConnectionState.Ready; await connection.SendMessage(envelope.CreateResult(ResultStatusCodes.Success)); } diff --git a/Centaurus.Domain/MessageHandlers/AlphaHandlers/BaseAlphaMessageHandler.cs b/Centaurus.Domain/MessageHandlers/AlphaHandlers/BaseAlphaMessageHandler.cs index e1996ae3..6603cb3f 100644 --- a/Centaurus.Domain/MessageHandlers/AlphaHandlers/BaseAlphaMessageHandler.cs +++ b/Centaurus.Domain/MessageHandlers/AlphaHandlers/BaseAlphaMessageHandler.cs @@ -30,6 +30,9 @@ public override async Task Validate(AlphaWebSocketConnection connection, Message if (IsAuditorOnly && !Global.Constellation.Auditors.Contains(connection.ClientPubKey)) throw new UnauthorizedException(); + if (connection.Account != null && !connection.Account.RequestCounter.IncRequestCount(DateTime.UtcNow.Ticks, out string error)) + throw new TooManyRequests(error); + await base.Validate(connection, envelope); } } diff --git a/Centaurus.Domain/Models/AccountRequestCounter.cs b/Centaurus.Domain/Models/AccountRequestCounter.cs new file mode 100644 index 00000000..18202f0e --- /dev/null +++ b/Centaurus.Domain/Models/AccountRequestCounter.cs @@ -0,0 +1,59 @@ +using Centaurus.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Centaurus.Domain +{ + public class AccountRequestCounter + { + private Account account; + private RequestCounter minuteCounter; + private RequestCounter hourCounter; + + public AccountRequestCounter(Account _account) + { + account = _account; + minuteCounter = new RequestCounter(); + hourCounter = new RequestCounter(); + } + + public bool IncRequestCount(long requestDatetime, out string error) + { + lock (this) + { + error = null; + + uint? hourLimit = account.RequestRateLimits?.HourLimit ?? Global.Constellation.RequestRateLimits?.HourLimit; + var hourInTicks = (long)60 * 1000 * 60 * 10_000; + if (!IncSingleCounter(hourCounter, hourInTicks, hourLimit.Value, requestDatetime, out error)) + return false; + + var minuteInTicks = (long)60 * 1000 * 10_000; + uint? minuteLimit = account.RequestRateLimits?.MinuteLimit ?? Global.Constellation.RequestRateLimits?.MinuteLimit; + if (!IncSingleCounter(minuteCounter, minuteInTicks, minuteLimit.Value, requestDatetime, out error)) + return false; + + return true; + } + } + + private bool IncSingleCounter(RequestCounter counter, long counterWindowPeriod, uint maxAllowedRequestsCount, long requestDatetime, out string error) + { + error = null; + if (maxAllowedRequestsCount < 0) //if less than zero than the counter is disabled + return true; + + if ((counter.StartedAt + counterWindowPeriod) < requestDatetime) //window is expired + counter.Reset(requestDatetime); + + if (counter.Count + 1 > maxAllowedRequestsCount) + { + error = $"Too many requests. Max allowed request count is {maxAllowedRequestsCount} per {counterWindowPeriod/10_000}ms."; + return false; + } + counter.IncRequestsCount(); + return true; + } + } +} diff --git a/Centaurus.Domain/Models/AccountWrapper.cs b/Centaurus.Domain/Models/AccountWrapper.cs new file mode 100644 index 00000000..d4c71e44 --- /dev/null +++ b/Centaurus.Domain/Models/AccountWrapper.cs @@ -0,0 +1,22 @@ +using Centaurus.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Centaurus.Domain +{ + public class AccountWrapper + { + public AccountWrapper(Account account) + { + if (account == null) + throw new ArgumentNullException(nameof(account)); + Account = account; + RequestCounter = new AccountRequestCounter(Account); + } + + public Account Account { get; } + + public AccountRequestCounter RequestCounter { get; } + } +} diff --git a/Centaurus.Domain/Models/RequestCounter.cs b/Centaurus.Domain/Models/RequestCounter.cs new file mode 100644 index 00000000..cc3b4e0b --- /dev/null +++ b/Centaurus.Domain/Models/RequestCounter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Centaurus.Domain +{ + public class RequestCounter + { + public RequestCounter() + { + Reset(0); + } + public long StartedAt { get; private set; } + + public int Count { get; private set; } + + public void IncRequestsCount() + { + Count++; + } + + public void Reset(long startedAt) + { + StartedAt = startedAt; + Count = 0; + } + } +} diff --git a/Centaurus.Domain/Quanta/Handlers/QuantumHandler.cs b/Centaurus.Domain/Quanta/Handlers/QuantumHandler.cs index 15930137..2de30485 100644 --- a/Centaurus.Domain/Quanta/Handlers/QuantumHandler.cs +++ b/Centaurus.Domain/Quanta/Handlers/QuantumHandler.cs @@ -153,6 +153,8 @@ async Task AuditorHandleQuantum(MessageEnvelope envelope) var processor = GetProcessor(messageType); + ValidateAccountRequestRate(envelope); + await processor.Validate(envelope); result = await processor.Process(envelope); @@ -176,6 +178,16 @@ async Task AuditorHandleQuantum(MessageEnvelope envelope) return result; } + void ValidateAccountRequestRate(MessageEnvelope envelope) + { + var request = envelope.Message as RequestQuantum; + if (request == null) + return; + var account = Global.AccountStorage.GetAccount(request.RequestMessage.Account); + if (!account.RequestCounter.IncRequestCount(request.Timestamp, out string error)) + throw new TooManyRequests($"Request limit reached for account {account.Account.Pubkey.ToString()}."); + } + void ProcessTransaction(MessageEnvelope envelope, ResultMessage result) { var quantum = envelope.Message; diff --git a/Centaurus.Domain/Quanta/Processors/AccountDataRequestProcessor.cs b/Centaurus.Domain/Quanta/Processors/AccountDataRequestProcessor.cs index 9750934d..7cfd9fc1 100644 --- a/Centaurus.Domain/Quanta/Processors/AccountDataRequestProcessor.cs +++ b/Centaurus.Domain/Quanta/Processors/AccountDataRequestProcessor.cs @@ -23,7 +23,7 @@ public override Task Process(MessageEnvelope envelope) var accountEffects = effectsContainer.GetEffects(requestMessage.Account).ToList(); - var account = Global.AccountStorage.GetAccount(requestMessage.Account); + var account = Global.AccountStorage.GetAccount(requestMessage.Account).Account; var resultMessage = envelope.CreateResult(ResultStatusCodes.Success, accountEffects); resultMessage.Balances = account.Balances; diff --git a/Centaurus.Domain/Quanta/Processors/ClientRequestProcessorBase.cs b/Centaurus.Domain/Quanta/Processors/ClientRequestProcessorBase.cs index 1cca49d3..dee16228 100644 --- a/Centaurus.Domain/Quanta/Processors/ClientRequestProcessorBase.cs +++ b/Centaurus.Domain/Quanta/Processors/ClientRequestProcessorBase.cs @@ -19,9 +19,9 @@ public void UpdateNonce(EffectProcessorsContainer effectProcessorsContainer) var requestQuantum = (RequestQuantum)effectProcessorsContainer.Envelope.Message; var requestMessage = requestQuantum.RequestMessage; - var currentUser = Global.AccountStorage.GetAccount(requestMessage.Account); + var currentUser = Global.AccountStorage.GetAccount(requestMessage.Account).Account; - effectProcessorsContainer.AddNonceUpdate(currentUser, requestMessage.Nonce); + effectProcessorsContainer.AddNonceUpdate(Global.AccountStorage, requestMessage.Account, requestMessage.Nonce, currentUser.Nonce); } public void ValidateNonce(MessageEnvelope envelope) @@ -34,9 +34,9 @@ public void ValidateNonce(MessageEnvelope envelope) if (requestMessage == null) throw new InvalidOperationException($"Invalid message type. {typeof(RequestQuantum).Name} should contain message of type {typeof(RequestMessage).Name}."); - var currentUser = Global.AccountStorage.GetAccount(requestMessage.Account); + var currentUser = Global.AccountStorage.GetAccount(requestMessage.Account).Account; if (currentUser == null) - throw new Exception($"Account with public key '{requestMessage.Account.ToString()}' is not found."); + throw new Exception($"Account with public key '{requestMessage.ToString()}' is not found."); if (currentUser.Nonce >= requestMessage.Nonce) throw new UnauthorizedException(); diff --git a/Centaurus.Domain/Quanta/Processors/LedgerRequestProcessor.cs b/Centaurus.Domain/Quanta/Processors/LedgerRequestProcessor.cs index 49c2a307..ea7cc321 100644 --- a/Centaurus.Domain/Quanta/Processors/LedgerRequestProcessor.cs +++ b/Centaurus.Domain/Quanta/Processors/LedgerRequestProcessor.cs @@ -138,7 +138,7 @@ private void ProcessDeposite(Deposit deposite, EffectProcessorsContainer effects if (deposite.PaymentResult == PaymentResults.Failed) return; - var account = Global.AccountStorage.GetAccount(deposite.Destination); + var account = Global.AccountStorage.GetAccount(deposite.Destination)?.Account; if (account == null && !balanceManager.ContainsAccount(deposite.Destination)) { effectsContainer.AddAccountCreate(Global.AccountStorage, deposite.Destination); diff --git a/Centaurus.Domain/Quanta/Processors/OrderRequestProcessor.cs b/Centaurus.Domain/Quanta/Processors/OrderRequestProcessor.cs index ff72e89d..e834a034 100644 --- a/Centaurus.Domain/Quanta/Processors/OrderRequestProcessor.cs +++ b/Centaurus.Domain/Quanta/Processors/OrderRequestProcessor.cs @@ -45,7 +45,7 @@ public override Task Validate(MessageEnvelope envelope) if (totalXlmAmountToTrade < Global.Constellation.MinAllowedLotSize) throw new BadRequestException("Lot size is smaller than the minimum allowed lot."); //fetch user's account record - var account = Global.AccountStorage.GetAccount(orderRequest.Account); + var account = Global.AccountStorage.GetAccount(orderRequest.Account).Account; //check required balances if (orderRequest.Side == OrderSides.Sell) diff --git a/Centaurus.Domain/Quanta/Processors/Payments/PaymentRequestProcessorBase.cs b/Centaurus.Domain/Quanta/Processors/Payments/PaymentRequestProcessorBase.cs index 22b45e1b..3472a526 100644 --- a/Centaurus.Domain/Quanta/Processors/Payments/PaymentRequestProcessorBase.cs +++ b/Centaurus.Domain/Quanta/Processors/Payments/PaymentRequestProcessorBase.cs @@ -99,7 +99,7 @@ public override Task Validate(MessageEnvelope envelope) if (!Global.AssetIds.Contains(payment.Asset)) throw new InvalidOperationException($"Asset {payment.Asset} is not supported"); - var account = Global.AccountStorage.GetAccount(payment.Account); + var account = Global.AccountStorage.GetAccount(payment.Account)?.Account; if (account == null) throw new Exception("Quantum source has no account"); diff --git a/Centaurus.Domain/Snapshot/DALModelExtensions/AccountModelExtensions.cs b/Centaurus.Domain/Snapshot/DALModelExtensions/AccountModelExtensions.cs index 8fbfd5e8..30a47230 100644 --- a/Centaurus.Domain/Snapshot/DALModelExtensions/AccountModelExtensions.cs +++ b/Centaurus.Domain/Snapshot/DALModelExtensions/AccountModelExtensions.cs @@ -9,7 +9,7 @@ namespace Centaurus.Domain { public static class AccountModelExtensions { - public static Account ToAccount(this AccountModel accountModel, BalanceModel[] balances) + public static Account ToAccount(this DAL.Models.AccountModel accountModel, BalanceModel[] balances) { var acc = new Account { @@ -17,6 +17,9 @@ public static Account ToAccount(this AccountModel accountModel, BalanceModel[] b Pubkey = new RawPubKey { Data = accountModel.PubKey } }; + if (accountModel.RequestRateLimits != null) + acc.RequestRateLimits = new RequestRateLimits { HourLimit = accountModel.RequestRateLimits.HourLimit, MinuteLimit = accountModel.RequestRateLimits.MinuteLimit }; + acc.Balances = balances.Select(b => b.ToBalance(acc)).ToList(); return acc; } diff --git a/Centaurus.Domain/Snapshot/DALModelExtensions/OrderModelExtensions.cs b/Centaurus.Domain/Snapshot/DALModelExtensions/OrderModelExtensions.cs index e62f930c..6bfabd96 100644 --- a/Centaurus.Domain/Snapshot/DALModelExtensions/OrderModelExtensions.cs +++ b/Centaurus.Domain/Snapshot/DALModelExtensions/OrderModelExtensions.cs @@ -15,7 +15,7 @@ public static Order ToOrder(this OrderModel order, AccountStorage accountStorage Amount = order.Amount, OrderId = unchecked((ulong)order.OrderId), Price = order.Price, - Account = accountStorage.GetAccount(order.Pubkey) + Account = accountStorage.GetAccount(order.Pubkey).Account }; } } diff --git a/Centaurus.Domain/Snapshot/DALModelExtensions/SettingsModelExtensions.cs b/Centaurus.Domain/Snapshot/DALModelExtensions/SettingsModelExtensions.cs index 99b1331a..4d53b6e7 100644 --- a/Centaurus.Domain/Snapshot/DALModelExtensions/SettingsModelExtensions.cs +++ b/Centaurus.Domain/Snapshot/DALModelExtensions/SettingsModelExtensions.cs @@ -11,15 +11,21 @@ public static class SettingsModelExtensions { public static ConstellationSettings ToSettings(this SettingsModel settings, List assetSettings) { - return new ConstellationSettings + var resultSettings = new ConstellationSettings { Apex = settings.Apex, Auditors = settings.Auditors.Select(a => (RawPubKey)a).ToList(), MinAccountBalance = settings.MinAccountBalance, MinAllowedLotSize = settings.MinAllowedLotSize, Vault = settings.Vault, - Assets = assetSettings.Select(a => a.ToAssetSettings()).ToList() + Assets = assetSettings.Select(a => a.ToAssetSettings()).ToList(), + RequestRateLimits = new RequestRateLimits + { + HourLimit = settings.RequestRateLimits.HourLimit, + MinuteLimit = settings.RequestRateLimits.MinuteLimit + } }; + return resultSettings; } } } diff --git a/Centaurus.Domain/Snapshot/SnapshotManager.cs b/Centaurus.Domain/Snapshot/SnapshotManager.cs index 12d73b68..282d90d8 100644 --- a/Centaurus.Domain/Snapshot/SnapshotManager.cs +++ b/Centaurus.Domain/Snapshot/SnapshotManager.cs @@ -46,7 +46,7 @@ public static async Task ApplyInitUpdates(MessageEnvelope envelope) Vault = initQuantum.Vault, VaultSequence = initQuantum.VaultSequence, Ledger = initQuantum.Ledger, - Pubkey = envelope.Signatures.First().Signer + RequestRateLimits = initQuantum.RequestRateLimits }; var updates = new PendingUpdates(); @@ -194,7 +194,6 @@ public static async Task GetSnapshot(long apex) { var currentEffect = XdrConverter.Deserialize(effectModels[i].RawEffect); var pubKey = currentEffect.Pubkey; - var currentAccount = new Lazy(() => accountStorage.GetAccount(pubKey)); IEffectProcessor processor = null; switch (currentEffect) { @@ -202,7 +201,7 @@ public static async Task GetSnapshot(long apex) processor = new AccountCreateEffectProcessor(accountCreateEffect, accountStorage); break; case NonceUpdateEffect nonceUpdateEffect: - processor = new NonceUpdateEffectProcessor(nonceUpdateEffect, currentAccount.Value); + processor = new NonceUpdateEffectProcessor(nonceUpdateEffect, accountStorage); break; case BalanceCreateEffect balanceCreateEffect: processor = new BalanceCreateEffectProcessor(balanceCreateEffect, accountStorage); @@ -216,6 +215,9 @@ public static async Task GetSnapshot(long apex) case UnlockLiabilitiesEffect unlockLiabilitiesEffect: processor = new UnlockLiabilitiesEffectProcessor(unlockLiabilitiesEffect, accountStorage); break; + case RequestRateLimitUpdateEffect requestRateLimitUpdateEffect: + processor = new RequestRateLimitUpdateEffectProcessor(requestRateLimitUpdateEffect, accountStorage); + break; case OrderPlacedEffect orderPlacedEffect: { var orderBook = exchange.GetOrderbook(orderPlacedEffect.OrderId); @@ -255,7 +257,7 @@ public static async Task GetSnapshot(long apex) return new Snapshot { Apex = apex, - Accounts = accountStorage.GetAll().ToList(), + Accounts = accountStorage.GetAll().Select(a => a.Account).ToList(), Ledger = stellarData.Ledger, Orders = exchange.OrderMap.GetAllOrders().ToList(), Settings = settings, diff --git a/Centaurus.Domain/Snapshot/UpdatesAggregator.cs b/Centaurus.Domain/Snapshot/UpdatesAggregator.cs index 3e261883..ac41711e 100644 --- a/Centaurus.Domain/Snapshot/UpdatesAggregator.cs +++ b/Centaurus.Domain/Snapshot/UpdatesAggregator.cs @@ -78,8 +78,7 @@ public static async Task Aggregate(List Aggregate(List new AssetModel { @@ -267,7 +284,11 @@ public static DiffObject Aggregate(Snapshot snapshot) return diffObject; } - + private static void EnsureAccountRecordExists(Dictionary accounts, byte[] pubKey) + { + if (!accounts.ContainsKey(pubKey)) + accounts.Add(pubKey, new DiffObject.Account { PubKey = pubKey }); + } private static void EnsureBalanceRowExists(Dictionary> balances, byte[] pubKey) { @@ -290,13 +311,22 @@ private static DiffObject.ConstellationState GetStellarData(long ledger, long va private static SettingsModel GetConstellationSettings(ConstellationEffect constellationInit) { - return new SettingsModel + var settingsModel = new SettingsModel { Auditors = constellationInit.Auditors.Select(a => a.Data).ToArray(), MinAccountBalance = constellationInit.MinAccountBalance, MinAllowedLotSize = constellationInit.MinAllowedLotSize, Vault = constellationInit.Vault.Data }; + + if (constellationInit.RequestRateLimits != null) + settingsModel.RequestRateLimits = new RequestRateLimitsModel + { + HourLimit = constellationInit.RequestRateLimits.HourLimit, + MinuteLimit = constellationInit.RequestRateLimits.MinuteLimit + }; + + return settingsModel; } /// diff --git a/Centaurus.Domain/WebSockets/Alpha/AlphaWebSocketConnection.cs b/Centaurus.Domain/WebSockets/Alpha/AlphaWebSocketConnection.cs index 864248a0..e6035ebf 100644 --- a/Centaurus.Domain/WebSockets/Alpha/AlphaWebSocketConnection.cs +++ b/Centaurus.Domain/WebSockets/Alpha/AlphaWebSocketConnection.cs @@ -43,7 +43,7 @@ private async void InvalidationTimer_Elapsed(object sender, System.Timers.Elapse public HandshakeData HandshakeData { get; } - public bool IsAuditor { get; set; } + public AccountWrapper Account { get; set; } private QuantumSyncWorker quantumWorker; diff --git a/Centaurus.Domain/accounts/AccountStorage.cs b/Centaurus.Domain/accounts/AccountStorage.cs index f59db2cd..90ab0cb6 100644 --- a/Centaurus.Domain/accounts/AccountStorage.cs +++ b/Centaurus.Domain/accounts/AccountStorage.cs @@ -10,28 +10,33 @@ namespace Centaurus.Domain public class AccountStorage { public AccountStorage(IEnumerable accounts) + :this(accounts.Select(a => new AccountWrapper(a))) + { + + } + public AccountStorage(IEnumerable accounts) { if (accounts == null) - accounts = new Account[] { }; + accounts = new AccountWrapper[] { }; - this.accounts = new Dictionary(accounts.ToDictionary(m => m.Pubkey)); + this.accounts = new Dictionary(accounts.ToDictionary(m => m.Account.Pubkey)); } - Dictionary accounts = new Dictionary(); + Dictionary accounts = new Dictionary(); /// /// Retrieve account record by its public key. /// /// Account public key /// Account record, or null if not found - public Account GetAccount(RawPubKey pubkey) + public AccountWrapper GetAccount(RawPubKey pubkey) { if (pubkey == null) throw new ArgumentNullException(nameof(pubkey)); return accounts.GetValueOrDefault(pubkey); } - public Account CreateAccount(RawPubKey pubkey) + public AccountWrapper CreateAccount(RawPubKey pubkey) { if (pubkey == null) throw new ArgumentNullException(nameof(pubkey)); @@ -39,11 +44,11 @@ public Account CreateAccount(RawPubKey pubkey) if (accounts.ContainsKey(pubkey)) throw new InvalidOperationException($"Account with public key {pubkey} already exists"); - var acc = new Account + var acc = new AccountWrapper(new Account { Pubkey = pubkey, Balances = new List() - }; + }); accounts.Add(pubkey, acc); return acc; @@ -61,7 +66,7 @@ public void RemoveAccount(RawPubKey pubkey) throw new Exception($"Unable to remove the account with public key {pubkey}"); } - public IEnumerable GetAll() + public IEnumerable GetAll() { return accounts.Values; } diff --git a/Centaurus.Domain/exchange/OrderMatcher.cs b/Centaurus.Domain/exchange/OrderMatcher.cs index 0e2b654b..4b666fcd 100644 --- a/Centaurus.Domain/exchange/OrderMatcher.cs +++ b/Centaurus.Domain/exchange/OrderMatcher.cs @@ -14,7 +14,7 @@ public OrderMatcher(OrderRequest orderRequest, EffectProcessorsContainer effects takerOrder = new Order() { OrderId = OrderIdConverter.Encode(unchecked((ulong)effectsContainer.Apex), orderRequest.Asset, orderRequest.Side), - Account = Global.AccountStorage.GetAccount(orderRequest.Account), + Account = Global.AccountStorage.GetAccount(orderRequest.Account).Account, Amount = orderRequest.Amount, Price = orderRequest.Price }; diff --git a/Centaurus.Models/Account/Account.cs b/Centaurus.Models/Account/Account.cs index 875e5b7a..f6153fc9 100644 --- a/Centaurus.Models/Account/Account.cs +++ b/Centaurus.Models/Account/Account.cs @@ -14,5 +14,8 @@ public class Account [XdrField(2)] public List Balances { get; set; } + + [XdrField(3, Optional = true)] + public RequestRateLimits RequestRateLimits { get; set; } } } diff --git a/Centaurus.Models/Common/RequestRateLimits.cs b/Centaurus.Models/Common/RequestRateLimits.cs new file mode 100644 index 00000000..dd8fa0c5 --- /dev/null +++ b/Centaurus.Models/Common/RequestRateLimits.cs @@ -0,0 +1,15 @@ +using Centaurus.Xdr; + +namespace Centaurus.Models +{ + [XdrContract] + public class RequestRateLimits + { + + [XdrField(0)] + public uint MinuteLimit { get; set; } + + [XdrField(1)] + public uint HourLimit { get; set; } + } +} diff --git a/Centaurus.Models/Effects/Accounts/RequestRateLimitUpdateEffect.cs b/Centaurus.Models/Effects/Accounts/RequestRateLimitUpdateEffect.cs new file mode 100644 index 00000000..3c5a8e8d --- /dev/null +++ b/Centaurus.Models/Effects/Accounts/RequestRateLimitUpdateEffect.cs @@ -0,0 +1,16 @@ +using Centaurus.Xdr; + +namespace Centaurus.Models +{ + [XdrContract] + public class RequestRateLimitUpdateEffect : Effect + { + public override EffectTypes EffectType => EffectTypes.RequestRateLimitUpdate; + + [XdrField(0)] + public RequestRateLimits RequestRateLimits { get; set; } + + [XdrField(1, Optional = true)] + public RequestRateLimits PrevRequestRateLimits { get; set; } + } +} diff --git a/Centaurus.Models/Effects/Effect.cs b/Centaurus.Models/Effects/Effect.cs index 19fb3740..2d67a346 100644 --- a/Centaurus.Models/Effects/Effect.cs +++ b/Centaurus.Models/Effects/Effect.cs @@ -21,6 +21,7 @@ namespace Centaurus.Models [XdrUnion((int)EffectTypes.ConstellationUpdate, typeof(ConstellationUpdateEffect))] [XdrUnion((int)EffectTypes.WithdrawalCreate, typeof(WithdrawalCreateEffect))] [XdrUnion((int)EffectTypes.WithdrawalRemove, typeof(WithdrawalRemoveEffect))] + [XdrUnion((int)EffectTypes.RequestRateLimitUpdate, typeof(RequestRateLimitUpdateEffect))] public class Effect { public virtual EffectTypes EffectType => throw new InvalidOperationException(); diff --git a/Centaurus.Models/Effects/EffectTypes.cs b/Centaurus.Models/Effects/EffectTypes.cs index c35b931a..d24c7b7f 100644 --- a/Centaurus.Models/Effects/EffectTypes.cs +++ b/Centaurus.Models/Effects/EffectTypes.cs @@ -16,6 +16,7 @@ public enum EffectTypes BalanceUpdate = 4, LockLiabilities = 5, UnlockLiabilities = 6, + RequestRateLimitUpdate = 7, OrderPlaced = 10, OrderRemoved = 11, diff --git a/Centaurus.Models/Effects/Settings/ConstellationEffect.cs b/Centaurus.Models/Effects/Settings/ConstellationEffect.cs index 573a3a99..5ce8d53b 100644 --- a/Centaurus.Models/Effects/Settings/ConstellationEffect.cs +++ b/Centaurus.Models/Effects/Settings/ConstellationEffect.cs @@ -22,5 +22,8 @@ public abstract class ConstellationEffect : Effect [XdrField(4)] public List Assets { get; set; } + + [XdrField(5, Optional = true)] + public RequestRateLimits RequestRateLimits { get; set; } } } diff --git a/Centaurus.Models/InternalMessages/ConstellationSettingsQuantum.cs b/Centaurus.Models/InternalMessages/ConstellationSettingsQuantum.cs index dbeffb3c..06721901 100644 --- a/Centaurus.Models/InternalMessages/ConstellationSettingsQuantum.cs +++ b/Centaurus.Models/InternalMessages/ConstellationSettingsQuantum.cs @@ -21,5 +21,8 @@ public abstract class ConstellationSettingsQuantum : Quantum [XdrField(4)] public List Assets { get; set; } + + [XdrField(5, Optional = true)] + public RequestRateLimits RequestRateLimits { get; set; } } } diff --git a/Centaurus.Models/Settings/ConstellationSettings.cs b/Centaurus.Models/Settings/ConstellationSettings.cs index 219c30f0..2fb29d34 100644 --- a/Centaurus.Models/Settings/ConstellationSettings.cs +++ b/Centaurus.Models/Settings/ConstellationSettings.cs @@ -24,5 +24,8 @@ public class ConstellationSettings [XdrField(5)] public List Assets { get; set; } + + [XdrField(6, Optional = true)] + public RequestRateLimits RequestRateLimits { get; set; } } } diff --git a/Centaurus.Test.Domain/AlphaMessageHandlersTests.cs b/Centaurus.Test.Domain/AlphaMessageHandlersTests.cs index 1d5be345..041d2ba3 100644 --- a/Centaurus.Test.Domain/AlphaMessageHandlersTests.cs +++ b/Centaurus.Test.Domain/AlphaMessageHandlersTests.cs @@ -228,5 +228,45 @@ public async Task AccountDataRequestTest(ConnectionState state, Type excpectedEx await AssertMessageHandling(clientConnection, envelope, excpectedException); } + + static object[] AccountRequestRateLimitsCases = + { + new object[] { TestEnvironment.Client1KeyPair, null }, + new object[] { TestEnvironment.Client2KeyPair, 10 } + }; + + [Test] + [TestCaseSource(nameof(AccountRequestRateLimitsCases))] + public async Task AccountRequestRateLimitTest(KeyPair clientKeyPair, int? requestLimit) + { + Global.AppState.State = ApplicationState.Ready; + + var account = Global.AccountStorage.GetAccount(clientKeyPair); + if (requestLimit.HasValue) + account.Account.RequestRateLimits = new RequestRateLimits { HourLimit = (uint)requestLimit.Value, MinuteLimit = (uint)requestLimit.Value }; + + var clientConnection = new AlphaWebSocketConnection(new FakeWebSocket()) + { + ClientPubKey = clientKeyPair, + ConnectionState = ConnectionState.Ready, + Account = account + }; + + var minuteLimit = (account.Account.RequestRateLimits ?? Global.Constellation.RequestRateLimits).MinuteLimit; + var minuteIterCount = minuteLimit + 1; + for (var i = 0; i < minuteIterCount; i++) + { + var envelope = new AccountDataRequest + { + Account = clientKeyPair, + Nonce = (ulong)(i + 1) + }.CreateEnvelope(); + envelope.Sign(clientKeyPair); + if (i + 1 > minuteLimit) + await AssertMessageHandling(clientConnection, envelope, typeof(TooManyRequests)); + else + await AssertMessageHandling(clientConnection, envelope); + } + } } } diff --git a/Centaurus.Test.Domain/AlphaQuantumHandlerTests.cs b/Centaurus.Test.Domain/AlphaQuantumHandlerTests.cs index 15ff926a..d124aa13 100644 --- a/Centaurus.Test.Domain/AlphaQuantumHandlerTests.cs +++ b/Centaurus.Test.Domain/AlphaQuantumHandlerTests.cs @@ -17,32 +17,10 @@ public void Setup() GlobalInitHelper.DefaultAlphaSetup(); } - //[Test] - //public async Task SnapshotQuantumTest() - //{ - // var snapshot = new SnapshotQuantum(); - // var envelope = snapshot.CreateEnvelope(); - // await Global.QuantumHandler.HandleAsync(envelope); - - // //emulate quorum - // var res = envelope.CreateResult(ResultStatusCodes.Success).CreateEnvelope(); - // res.Sign(TestEnvironment.Auditor1KeyPair); - - // Global.SnapshotManager.SetResult(res); - - // Assert.AreEqual(Global.SnapshotManager.LastSnapshot.Id, 2); - //} - - //[Test] - //public async Task SnapshotFailQuantumTest() - //{ - // var snapshot = new SnapshotQuantum(); - // var envelope = snapshot.CreateEnvelope(); - // await Global.QuantumHandler.HandleAsync(envelope); - - // snapshot = new SnapshotQuantum(); - // envelope = snapshot.CreateEnvelope(); - // Assert.ThrowsAsync(async () => await Global.QuantumHandler.HandleAsync(envelope)); - //} + static object[] AccountRequestRateLimitsCases = + { + new object[] { TestEnvironment.Client1KeyPair, null }, + new object[] { TestEnvironment.Client2KeyPair, 10 } + }; } } diff --git a/Centaurus.Test.Domain/AuditorQuantumHandlerTests.cs b/Centaurus.Test.Domain/AuditorQuantumHandlerTests.cs index 2854792f..91dd34f4 100644 --- a/Centaurus.Test.Domain/AuditorQuantumHandlerTests.cs +++ b/Centaurus.Test.Domain/AuditorQuantumHandlerTests.cs @@ -17,27 +17,10 @@ public void Setup() GlobalInitHelper.DefaultAuditorSetup(); } - //[Test] - //public async Task SnapshotQuantumTest() - //{ - // var snapshot = Global.SnapshotManager.InitSnapshot(); - // Global.SnapshotManager.AbortPendingSnapshot(); - - // var snapshotQuantum = new SnapshotQuantum() { Hash = snapshot.ComputeHash(), Apex = 1 }; - // var envelope = snapshotQuantum.CreateEnvelope(); - // await Global.QuantumHandler.HandleAsync(envelope); - - // Assert.AreEqual(Global.SnapshotManager.LastSnapshot.Apex, 1); - //} - - //[Test] - //public void SnapshotFailedQuantumTest() - //{ - // var snapshot = new SnapshotQuantum { Apex = 1 }; - // var envelope = snapshot.CreateEnvelope(); - - // //TODO: it should throw BadRequestExcetiop - // Assert.ThrowsAsync(async () => await Global.QuantumHandler.HandleAsync(envelope)); - //} + static object[] AccountRequestRateLimitsCases = + { + new object[] { TestEnvironment.Client1KeyPair, null }, + new object[] { TestEnvironment.Client2KeyPair, 10 } + }; } } diff --git a/Centaurus.Test.Domain/BaseQuantumHandlerTests.cs b/Centaurus.Test.Domain/BaseQuantumHandlerTests.cs index c4b917de..070c0141 100644 --- a/Centaurus.Test.Domain/BaseQuantumHandlerTests.cs +++ b/Centaurus.Test.Domain/BaseQuantumHandlerTests.cs @@ -22,7 +22,10 @@ public async Task LedgerQuantumTest(int ledgerFrom, int ledgerTo, int amount, in long apex = Global.QuantumStorage.CurrentApex; var client1StartBalanceAmount = (long)0; - var clientAccountBalance = Global.AccountStorage.GetAccount(TestEnvironment.Client1KeyPair).GetBalance(asset); + + var account1 = Global.AccountStorage.GetAccount(TestEnvironment.Client1KeyPair).Account; + + var clientAccountBalance = account1.GetBalance(asset); var withdrawalDest = KeyPair.Random(); var txHash = new byte[] { }; @@ -101,8 +104,6 @@ public async Task LedgerQuantumTest(int ledgerFrom, int ledgerTo, int amount, in { Assert.AreEqual(Global.LedgerManager.Ledger, ledgerNotification.LedgerTo); - var account1 = Global.AccountStorage.GetAccount(TestEnvironment.Client1KeyPair); - Assert.AreEqual(account1.GetBalance(asset).Liabilities, 0); Assert.AreEqual(account1.GetBalance(asset).Amount, client1StartBalanceAmount - amount + depositeAmount); //acc balance + deposit - withdrawal } @@ -175,13 +176,52 @@ public async Task AccountDataRequestTest(int nonce, Type excpectedException) Assert.IsInstanceOf(res); } + [Test] + [TestCaseSource("AccountRequestRateLimitsCases")] + public async Task AccountRequestRateLimitTest(KeyPair clientKeyPair, int? requestLimit) + { + Global.AppState.State = ApplicationState.Ready; + + var account = Global.AccountStorage.GetAccount(clientKeyPair); + if (requestLimit.HasValue) + account.Account.RequestRateLimits = new RequestRateLimits { HourLimit = (uint)requestLimit.Value, MinuteLimit = (uint)requestLimit.Value }; + + var minuteLimit = (account.Account.RequestRateLimits ?? Global.Constellation.RequestRateLimits).MinuteLimit; + var minuteIterCount = minuteLimit + 1; + for (var i = 0; i < minuteIterCount; i++) + { + var envelope = new AccountDataRequest + { + Account = clientKeyPair, + Nonce = (ulong)(i + 1) + }.CreateEnvelope(); + envelope.Sign(clientKeyPair); + if (!Global.IsAlpha) + { + var quantum = new RequestQuantum { Apex = Global.QuantumStorage.CurrentApex + 1, RequestEnvelope = envelope }; + envelope = quantum.CreateEnvelope(); + envelope.Sign(TestEnvironment.AlphaKeyPair); + } + + if (i + 1 > minuteLimit) + await AssertQuantumHandling(envelope, typeof(TooManyRequests)); + else + await AssertQuantumHandling(envelope, null); + } + } + protected async Task AssertQuantumHandling(MessageEnvelope quantum, Type excpectedException = null) { - if (excpectedException == null) + try + { return await Global.QuantumHandler.HandleAsync(quantum); - - Assert.ThrowsAsync(excpectedException, async () => await Global.QuantumHandler.HandleAsync(quantum)); - return null; + } + catch (Exception exc) + { + if (excpectedException == null || excpectedException.FullName != exc.GetType().FullName) + throw; + return null; + } } } } diff --git a/Centaurus.Test.Utils/GlobalInitHelper.cs b/Centaurus.Test.Utils/GlobalInitHelper.cs index b1ea35c7..d5891940 100644 --- a/Centaurus.Test.Utils/GlobalInitHelper.cs +++ b/Centaurus.Test.Utils/GlobalInitHelper.cs @@ -92,7 +92,8 @@ public static void Setup(List clients, List auditors, BaseSett Vault = settings is AlphaSettings ? settings.KeyPair.PublicKey : ((AuditorSettings)settings).AlphaKeyPair.PublicKey, VaultSequence = 1, Ledger = 1, - PrevHash = new byte[] { } + PrevHash = new byte[] { }, + RequestRateLimits = new RequestRateLimits { HourLimit = 1000, MinuteLimit = 100 } }; @@ -138,33 +139,6 @@ public static void Setup(List clients, List auditors, BaseSett depositeQuantum.Source.Sign(TestEnvironment.Auditor1KeyPair); Global.QuantumHandler.HandleAsync(depositeQuantum.CreateEnvelope()).Wait(); - - //var accountUpdates = new List(); - //var balances = new List(); - //for (var i = 0; i < clients.Count; i++) - //{ - // accountUpdates.Add(new DiffObject.Account { IsInserted = true, PubKey = clients[i].PublicKey }); - // balances.Add(new DiffObject.Balance { IsInserted = true, PubKey = clients[i].PublicKey, Amount = 10000, AssetId = 0 }); - // for (var c = 0; c < assets.Count; c++) - // { - // balances.Add(new DiffObject.Balance { IsInserted = true, PubKey = clients[i].PublicKey, Amount = 10000, AssetId = assets[c].Id }); - // } - //} - - //Global.PermanentStorage.Update(new DiffObject - //{ - // Accounts = accountUpdates, - // Balances = balances, - // Assets = new List(), - // Effects = new List(), - // Orders = new List(), - // Quanta = new List(), - // Widthrawals = new List() - //}).Wait(); - - //var snapshot = SnapshotManager.GetSnapshot().Result; - - //Global.Setup(snapshot); } } } diff --git a/Centaurus.Test.Utils/MockStorage.cs b/Centaurus.Test.Utils/MockStorage.cs index b795fc4b..74ab2661 100644 --- a/Centaurus.Test.Utils/MockStorage.cs +++ b/Centaurus.Test.Utils/MockStorage.cs @@ -211,11 +211,16 @@ private void UpdateAccount(List accounts) var pubKey = acc.PubKey; var currentAcc = accountsCollection.FirstOrDefault(a => ByteArrayPrimitives.Equals(a.PubKey, pubKey)); if (acc.IsInserted) - accountsCollection.Add(new AccountModel { PubKey = pubKey, Nonce = (long)acc.Nonce }); + accountsCollection.Add(new AccountModel { PubKey = pubKey, Nonce = (long)acc.Nonce, RequestRateLimits = acc.RequestRateLimits }); else if (acc.IsDeleted) accountsCollection.Remove(currentAcc); else - currentAcc.Nonce = (long)acc.Nonce; + { + if (acc.Nonce != 0) + currentAcc.Nonce = (long)acc.Nonce; + if (acc.RequestRateLimits != null) + currentAcc.RequestRateLimits = acc.RequestRateLimits; + } } }