Merge pull request #2706 from 0xProject/feat/asset-swapper/punish-latent-rfqt-makers

asset-swapper: Punish latent RFQT makers
This commit is contained in:
F. Eugene Aumson 2020-09-18 18:06:12 -04:00 committed by GitHub
commit 32d11d1ba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 4 deletions

View File

@ -20,6 +20,8 @@ const NULL_ERC20_ASSET_DATA = '0xf47261b0000000000000000000000000000000000000000
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const MAINNET_CHAIN_ID = 1;
const ONE_SECOND_MS = 1000;
const ONE_MINUTE_SECS = 60;
const ONE_MINUTE_MS = ONE_SECOND_MS * ONE_MINUTE_SECS;
const DEFAULT_PER_PAGE = 1000;
const ZERO_AMOUNT = new BigNumber(0);
@ -97,6 +99,7 @@ export const constants = {
ETHER_TOKEN_DECIMALS: 18,
ONE_AMOUNT: new BigNumber(1),
ONE_SECOND_MS,
ONE_MINUTE_MS,
DEFAULT_SWAP_QUOTER_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,

View File

@ -11,6 +11,7 @@ import { constants } from '../constants';
import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types';
import { ONE_SECOND_MS } from './market_operation_utils/constants';
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
// tslint:disable-next-line: custom-no-magic-numbers
const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS;
@ -20,6 +21,10 @@ export const quoteRequestorHttpClient: AxiosInstance = Axios.create({
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
});
const MAKER_TIMEOUT_STREAK_LENGTH = 10;
const MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES = 10;
const rfqMakerBlacklist = new RfqMakerBlacklist(MAKER_TIMEOUT_STREAK_LENGTH, MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES);
/**
* Request quotes from RFQ-T providers
*/
@ -334,7 +339,10 @@ export class QuoteRequestor {
const result: Array<{ response: ResponseT; makerUri: string }> = [];
await Promise.all(
Object.keys(this._rfqtAssetOfferings).map(async url => {
if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) {
if (
this._makerSupportsPair(url, makerAssetData, takerAssetData) &&
!rfqMakerBlacklist.isMakerBlacklisted(url)
) {
const requestParamsWithBigNumbers = {
takerAddress: options.takerAddress,
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
@ -354,6 +362,10 @@ export class QuoteRequestor {
const partialLogEntry = { url, quoteType, requestParams };
const timeBeforeAwait = Date.now();
const maxResponseTimeMs =
options.makerEndpointMaxResponseTimeMs === undefined
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
: options.makerEndpointMaxResponseTimeMs;
try {
const quotePath = (() => {
switch (quoteType) {
@ -368,8 +380,9 @@ export class QuoteRequestor {
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
headers: { '0x-api-key': options.apiKey },
params: requestParams,
timeout: options.makerEndpointMaxResponseTimeMs,
timeout: maxResponseTimeMs,
});
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({
rfqtMakerInteraction: {
...partialLogEntry,
@ -378,12 +391,14 @@ export class QuoteRequestor {
apiKey: options.apiKey,
takerAddress: requestParams.takerAddress,
statusCode: response.status,
latencyMs: Date.now() - timeBeforeAwait,
latencyMs,
},
},
});
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs);
result.push({ response: response.data, makerUri: url });
} catch (err) {
const latencyMs = Date.now() - timeBeforeAwait;
this._infoLogger({
rfqtMakerInteraction: {
...partialLogEntry,
@ -392,10 +407,11 @@ export class QuoteRequestor {
apiKey: options.apiKey,
takerAddress: requestParams.takerAddress,
statusCode: err.response ? err.response.status : undefined,
latencyMs: Date.now() - timeBeforeAwait,
latencyMs,
},
},
});
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs);
this._warningLogger(
convertIfAxiosError(err),
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${

View File

@ -0,0 +1,33 @@
/**
* Tracks a maker's history of timely responses, and manages whether a given
* maker should be avoided for being too latent.
*/
import { constants } from '../constants';
export class RfqMakerBlacklist {
private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {};
private readonly _makerBlacklistedUntilDate: { [makerUrl: string]: number } = {};
constructor(private readonly _blacklistDurationMinutes: number, private readonly _timeoutStreakThreshold: number) {}
public logTimeoutOrLackThereof(makerUrl: string, didTimeout: boolean): void {
if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) {
this._makerTimeoutStreakLength[makerUrl] = 0;
}
if (didTimeout) {
this._makerTimeoutStreakLength[makerUrl] += 1;
if (this._makerTimeoutStreakLength[makerUrl] === this._timeoutStreakThreshold) {
this._makerBlacklistedUntilDate[makerUrl] =
Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS;
}
} else {
this._makerTimeoutStreakLength[makerUrl] = 0;
}
}
public isMakerBlacklisted(makerUrl: string): boolean {
const now = Date.now();
if (now > this._makerBlacklistedUntilDate[makerUrl]) {
delete this._makerBlacklistedUntilDate[makerUrl];
}
return this._makerBlacklistedUntilDate[makerUrl] > now;
}
}

View File

@ -0,0 +1,40 @@
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { RfqMakerBlacklist } from '../src/utils/rfq_maker_blacklist';
import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
describe('RfqMakerBlacklist', () => {
it('does blacklist', async () => {
const blacklistDurationMinutes = 1;
const timeoutStreakThreshold = 3;
const blacklist = new RfqMakerBlacklist(blacklistDurationMinutes, timeoutStreakThreshold);
blacklist.logTimeoutOrLackThereof('makerA', true);
blacklist.logTimeoutOrLackThereof('makerA', true);
expect(blacklist.isMakerBlacklisted('makerA')).to.be.false();
blacklist.logTimeoutOrLackThereof('makerA', true);
const sleepTimeMs = 10;
await new Promise<void>(r => {
setTimeout(r, sleepTimeMs);
});
expect(blacklist.isMakerBlacklisted('makerA')).to.be.true();
});
it('does unblacklist', async () => {
const blacklistDurationMinutes = 0.1;
const timeoutStreakThreshold = 3;
const blacklist = new RfqMakerBlacklist(blacklistDurationMinutes, timeoutStreakThreshold);
blacklist.logTimeoutOrLackThereof('makerA', true);
blacklist.logTimeoutOrLackThereof('makerA', true);
blacklist.logTimeoutOrLackThereof('makerA', true);
expect(blacklist.isMakerBlacklisted('makerA')).to.be.true();
await new Promise<void>(r => {
setTimeout(r, blacklistDurationMinutes * constants.ONE_MINUTE_MS);
});
expect(blacklist.isMakerBlacklisted('makerA')).to.be.false();
});
});