Define gRPC api call rate constraints

The general rule is limit calls that change p2p data to 1/minute,
others to 1/second.  An exception is made to set/remove wallet password
methods (1/5s), due to the latency of writing wallet changes to disk.

This change may affect api testing in the future.  If that happens,
further changes to the call rate metering interceptor may be made to loosen
the constraints when running in regtest/dev mode.
This commit is contained in:
ghubstan 2021-01-22 13:45:12 -03:00
parent 334fbdbc86
commit 4eed44d350
No known key found for this signature in database
GPG Key ID: E35592D6800A861E
11 changed files with 264 additions and 11 deletions

View File

@ -6,12 +6,24 @@ import bisq.proto.grpc.DisputeAgentsGrpc;
import bisq.proto.grpc.RegisterDisputeAgentReply;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
@ -36,4 +48,22 @@ class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
// You can only register mainnet dispute agents in the UI.
// Do not limit devs' ability to register test agents.
put("registerDisputeAgent", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -7,12 +7,23 @@ import bisq.proto.grpc.GetTradeStatisticsGrpc;
import bisq.proto.grpc.GetTradeStatisticsReply;
import bisq.proto.grpc.GetTradeStatisticsRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import java.util.stream.Collectors;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
private final CoreApi coreApi;
@ -39,4 +50,20 @@ class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStati
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getTradeStatistics", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -23,12 +23,24 @@ import bisq.proto.grpc.GetMethodHelpReply;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.HelpGrpc;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
class GrpcHelpService extends HelpGrpc.HelpImplBase {
@ -53,4 +65,20 @@ class GrpcHelpService extends HelpGrpc.HelpImplBase {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getMethodHelp", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -36,16 +36,27 @@ import bisq.proto.grpc.GetOffersReply;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OffersGrpc;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.api.model.OfferInfo.toOfferInfo;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
class GrpcOffersService extends OffersGrpc.OffersImplBase {
@ -171,4 +182,25 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getOffer", new GrpcCallRateMeter(1, SECONDS));
put("getMyOffer", new GrpcCallRateMeter(1, SECONDS));
put("getOffers", new GrpcCallRateMeter(1, SECONDS));
put("getMyOffers", new GrpcCallRateMeter(1, SECONDS));
put("createOffer", new GrpcCallRateMeter(1, MINUTES));
put("cancelOffer", new GrpcCallRateMeter(1, MINUTES));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -31,12 +31,24 @@ import bisq.proto.grpc.GetPaymentMethodsReply;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.PaymentAccountsGrpc;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import java.util.stream.Collectors;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImplBase {
@ -110,4 +122,23 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("createPaymentAccount", new GrpcCallRateMeter(1, MINUTES));
put("getPaymentAccounts", new GrpcCallRateMeter(1, SECONDS));
put("getPaymentMethods", new GrpcCallRateMeter(1, SECONDS));
put("getPaymentAccountForm", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -23,12 +23,24 @@ import bisq.proto.grpc.MarketPriceReply;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.PriceGrpc;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
class GrpcPriceService extends PriceGrpc.PriceImplBase {
@ -55,4 +67,20 @@ class GrpcPriceService extends PriceGrpc.PriceImplBase {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getMarketPrice", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -60,15 +60,15 @@ public class GrpcServer {
GrpcWalletsService walletsService) {
this.server = ServerBuilder.forPort(config.apiPort)
.executor(UserThread.getExecutor())
.addService(disputeAgentsService)
.addService(helpService)
.addService(offersService)
.addService(paymentAccountsService)
.addService(priceService)
.addService(tradeStatisticsService)
.addService(tradesService)
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
.addService(interceptForward(helpService, helpService.interceptors()))
.addService(interceptForward(offersService, offersService.interceptors()))
.addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors()))
.addService(interceptForward(priceService, priceService.interceptors()))
.addService(interceptForward(tradeStatisticsService, tradeStatisticsService.interceptors()))
.addService(interceptForward(tradesService, tradesService.interceptors()))
.addService(interceptForward(versionService, versionService.interceptors()))
.addService(walletsService)
.addService(interceptForward(walletsService, walletsService.interceptors()))
.intercept(passwordAuthInterceptor)
.build();
coreContext.setApiUser(true);

View File

@ -35,13 +35,25 @@ import bisq.proto.grpc.TradesGrpc;
import bisq.proto.grpc.WithdrawFundsReply;
import bisq.proto.grpc.WithdrawFundsRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.api.model.TradeInfo.toTradeInfo;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
class GrpcTradesService extends TradesGrpc.TradesImplBase {
@ -142,4 +154,25 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getTrade", new GrpcCallRateMeter(1, SECONDS));
put("takeOffer", new GrpcCallRateMeter(1, MINUTES));
put("confirmPaymentStarted", new GrpcCallRateMeter(1, MINUTES));
put("confirmPaymentReceived", new GrpcCallRateMeter(1, MINUTES));
put("keepFunds", new GrpcCallRateMeter(1, MINUTES));
put("withdrawFunds", new GrpcCallRateMeter(1, MINUTES));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -74,13 +74,12 @@ public class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
@SuppressWarnings("unused") // Defined as a usage example.
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getVersion", new GrpcCallRateMeter(100, SECONDS));
put("getVersion", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(Optional::empty /* Optional.of(defaultCallRateMeteringInterceptor) */);
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -53,6 +53,7 @@ import bisq.proto.grpc.UnsetTxFeeRatePreferenceReply;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import org.bitcoinj.core.Transaction;
@ -61,7 +62,9 @@ import javax.inject.Inject;
import com.google.common.util.concurrent.FutureCallback;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@ -69,6 +72,14 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import static bisq.core.api.model.TxInfo.toTxInfo;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
@ -330,4 +341,37 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getBalances", new GrpcCallRateMeter(1, SECONDS));
put("getAddressBalance", new GrpcCallRateMeter(1, SECONDS));
put("getFundingAddresses", new GrpcCallRateMeter(1, SECONDS));
put("getUnusedBsqAddress", new GrpcCallRateMeter(1, SECONDS));
put("sendBsq", new GrpcCallRateMeter(1, MINUTES));
put("sendBtc", new GrpcCallRateMeter(1, MINUTES));
put("getTxFeeRate", new GrpcCallRateMeter(1, SECONDS));
put("setTxFeeRatePreference", new GrpcCallRateMeter(1, SECONDS));
put("unsetTxFeeRatePreference", new GrpcCallRateMeter(1, SECONDS));
put("getTransaction", new GrpcCallRateMeter(1, SECONDS));
// Trying to set or remove a wallet password several times before the 1st attempt has time to
// persist the change to disk may corrupt the wallet, so allow only 1 attempt per 5 seconds.
put("setWalletPassword", new GrpcCallRateMeter(1, SECONDS, 5));
put("removeWalletPassword", new GrpcCallRateMeter(1, SECONDS, 5));
put("lockWallet", new GrpcCallRateMeter(1, SECONDS));
put("unlockWallet", new GrpcCallRateMeter(1, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(defaultCallRateMeteringInterceptor));
}
}

View File

@ -76,6 +76,7 @@ public class GrpcServiceRateMeteringConfig {
this.methodRateMeters = methodRateMeters;
}
@SuppressWarnings("unused")
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
int maxCalls,
TimeUnit timeUnit) {