Skip to content

Process on-chain-received DApi batches #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 60 additions & 20 deletions src/condition-check/condition-check.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BigNumber, ethers } from 'ethers';
import { ethers } from 'ethers';

import { getUnixTimestamp } from '../../test/utils';
import { HUNDRED_PERCENT } from '../constants';

import {
Expand All @@ -12,52 +11,71 @@ import {
checkUpdateConditions,
} from './condition-check';

const getUnixTimestamp = (dateString: string) => Math.floor(Date.parse(dateString) / 1000);

const getDeviationThresholdAsBigNumber = (input: number) =>
ethers.BigNumber.from(Math.trunc(input * HUNDRED_PERCENT)).div(ethers.BigNumber.from(100));

describe('checkUpdateCondition', () => {
const onChainValue = ethers.BigNumber.from(500);

it('returns true when api value is higher and deviation threshold is reached', () => {
const shouldUpdate = checkDeviationThresholdExceeded(onChainValue, 10, ethers.BigNumber.from(560));
const shouldUpdate = checkDeviationThresholdExceeded(
onChainValue,
getDeviationThresholdAsBigNumber(10),
ethers.BigNumber.from(560)
);

expect(shouldUpdate).toBe(true);
});

it('returns true when api value is lower and deviation threshold is reached', () => {
const shouldUpdate = checkDeviationThresholdExceeded(onChainValue, 10, ethers.BigNumber.from(440));
const shouldUpdate = checkDeviationThresholdExceeded(
onChainValue,
getDeviationThresholdAsBigNumber(10),
ethers.BigNumber.from(440)
);

expect(shouldUpdate).toBe(true);
});

it('returns false when deviation threshold is not reached', () => {
const shouldUpdate = checkDeviationThresholdExceeded(onChainValue, 10, ethers.BigNumber.from(480));
const shouldUpdate = checkDeviationThresholdExceeded(
onChainValue,
getDeviationThresholdAsBigNumber(10),
ethers.BigNumber.from(480)
);

expect(shouldUpdate).toBe(false);
});

it('handles correctly bad JS math', () => {
expect(() => checkDeviationThresholdExceeded(onChainValue, 0.14, ethers.BigNumber.from(560))).not.toThrow();
expect(() =>
checkDeviationThresholdExceeded(onChainValue, getDeviationThresholdAsBigNumber(0.14), ethers.BigNumber.from(560))
).not.toThrow();
});

it('checks all update conditions | heartbeat exceeded', () => {
const result = checkUpdateConditions(
BigNumber.from(10),
ethers.BigNumber.from(10),
Date.now() / 1000 - 60 * 60 * 24,
BigNumber.from(10),
ethers.BigNumber.from(10),
Date.now() / 1000,
60 * 60 * 23,
2
getDeviationThresholdAsBigNumber(2)
);

expect(result).toBe(true);
});

it('checks all update conditions | no update', () => {
const result = checkUpdateConditions(
BigNumber.from(10),
ethers.BigNumber.from(10),
Date.now() / 1000,
BigNumber.from(10),
ethers.BigNumber.from(10),
Date.now() + 60 * 60 * 23,
86_400,
2
getDeviationThresholdAsBigNumber(2)
);

expect(result).toBe(false);
Expand Down Expand Up @@ -150,25 +168,47 @@ describe('calculateUpdateInPercentage', () => {
describe('calculateMedian', () => {
describe('for array with odd number of elements', () => {
it('calculates median for sorted array', () => {
const arr = [BigNumber.from(10), BigNumber.from(11), BigNumber.from(24), BigNumber.from(30), BigNumber.from(47)];
expect(calculateMedian(arr)).toStrictEqual(BigNumber.from(24));
const arr = [
ethers.BigNumber.from(10),
ethers.BigNumber.from(11),
ethers.BigNumber.from(24),
ethers.BigNumber.from(30),
ethers.BigNumber.from(47),
];
expect(calculateMedian(arr)).toStrictEqual(ethers.BigNumber.from(24));
});

it('calculates median for unsorted array', () => {
const arr = [BigNumber.from(24), BigNumber.from(11), BigNumber.from(10), BigNumber.from(47), BigNumber.from(30)];
expect(calculateMedian(arr)).toStrictEqual(BigNumber.from(24));
const arr = [
ethers.BigNumber.from(24),
ethers.BigNumber.from(11),
ethers.BigNumber.from(10),
ethers.BigNumber.from(47),
ethers.BigNumber.from(30),
];
expect(calculateMedian(arr)).toStrictEqual(ethers.BigNumber.from(24));
});
});

describe('for array with even number of elements', () => {
it('calculates median for sorted array', () => {
const arr = [BigNumber.from(10), BigNumber.from(11), BigNumber.from(24), BigNumber.from(30)];
expect(calculateMedian(arr)).toStrictEqual(BigNumber.from(17));
const arr = [
ethers.BigNumber.from(10),
ethers.BigNumber.from(11),
ethers.BigNumber.from(24),
ethers.BigNumber.from(30),
];
expect(calculateMedian(arr)).toStrictEqual(ethers.BigNumber.from(17));
});

it('calculates median for unsorted array', () => {
const arr = [BigNumber.from(24), BigNumber.from(11), BigNumber.from(10), BigNumber.from(30)];
expect(calculateMedian(arr)).toStrictEqual(BigNumber.from(17));
const arr = [
ethers.BigNumber.from(24),
ethers.BigNumber.from(11),
ethers.BigNumber.from(10),
ethers.BigNumber.from(30),
];
expect(calculateMedian(arr)).toStrictEqual(ethers.BigNumber.from(17));
});
});
});
18 changes: 11 additions & 7 deletions src/condition-check/condition-check.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ethers } from 'ethers';
import { type BigNumber, ethers } from 'ethers';

import { HUNDRED_PERCENT } from '../constants';
import { logger } from '../logger';
Expand All @@ -25,17 +25,21 @@ export const calculateMedian = (arr: ethers.BigNumber[]) => {
return arr.length % 2 === 0 ? nums[mid - 1]!.add(nums[mid]!).div(2) : nums[mid];
};

/**
* Checks if the deviation threshold has been exceeded.
*
* @param onChainValue
* @param deviationThreshold Refer to getDeviationThresholdAsBigNumber()
* @param apiValue
*/
export const checkDeviationThresholdExceeded = (
onChainValue: ethers.BigNumber,
deviationThreshold: number,
deviationThreshold: ethers.BigNumber,
apiValue: ethers.BigNumber
) => {
const updateInPercentage = calculateUpdateInPercentage(onChainValue, apiValue);
const threshold = ethers.BigNumber.from(Math.trunc(deviationThreshold * HUNDRED_PERCENT)).div(
ethers.BigNumber.from(100)
);

return updateInPercentage.gt(threshold);
return updateInPercentage.gt(deviationThreshold);
};

/**
Expand All @@ -60,7 +64,7 @@ export const checkUpdateConditions = (
offChainValue: ethers.BigNumber,
offChainTimestamp: number,
heartbeatInterval: number,
deviationThreshold: number
deviationThreshold: BigNumber
): boolean => {
// Check that fulfillment data is newer than on chain data
const isFulfillmentDataFresh = checkFulfillmentDataTimestamp(onChainTimestamp, offChainTimestamp);
Expand Down
9 changes: 8 additions & 1 deletion src/gas-price/gas-price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@ export const clearSponsorLastUpdateTimestampMs = (
sponsorWalletAddress: string
) =>
updateState((draft) => {
delete draft.gasPriceStore[chainId]![providerName]!.sponsorLastUpdateTimestampMs[sponsorWalletAddress];
const gasPriceStorePerChain = draft?.gasPriceStore[chainId] ?? {};

const sponsorLastUpdateTimestampMs =
gasPriceStorePerChain[providerName]?.sponsorLastUpdateTimestampMs[sponsorWalletAddress];

if (sponsorLastUpdateTimestampMs) {
delete draft.gasPriceStore[chainId]![providerName]!.sponsorLastUpdateTimestampMs[sponsorWalletAddress];
}
});

export const getPercentile = (percentile: number, array: ethers.BigNumber[]) => {
Expand Down
6 changes: 4 additions & 2 deletions src/signed-api-fetch/data-fetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ describe('data fetcher', () => {
produce(getState(), (draft) => {
draft.signedApiUrlStore = {
'31337': {
'0xbF3137b0a7574563a23a8fC8badC6537F98197CC': 'http://127.0.0.1:8090/',
'0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4': 'https://pool.nodary.io',
hardhat: [
'http://127.0.0.1:8090/0xbF3137b0a7574563a23a8fC8badC6537F98197CC',
'https://pool.nodary.io/0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4',
],
},
};
})
Expand Down
6 changes: 3 additions & 3 deletions src/signed-api-fetch/data-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ export const runDataFetcher = async () => {
}

const urls = uniq(
Object.keys(config.chains).flatMap((chainId) =>
Object.entries(signedApiUrlStore[chainId]!).flatMap(([airnodeAddress, baseUrl]) => `${baseUrl}/${airnodeAddress}`)
)
Object.values(signedApiUrlStore)
.flatMap((urlsPerProvider) => Object.values(urlsPerProvider))
.flat()
);

return Promise.allSettled(
Expand Down
16 changes: 8 additions & 8 deletions src/signed-data-store/signed-data-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { BigNumber, ethers } from 'ethers';
import { ethers } from 'ethers';

import { init } from '../../test/fixtures/mock-config';
import { generateRandomBytes32, signData } from '../../test/utils';
import type { SignedData } from '../types';
import { deriveBeaconId } from '../utils';

import { verifySignedDataIntegrity } from './signed-data-store';
import * as localDataStore from './signed-data-store';
Expand All @@ -16,7 +17,7 @@ describe('datastore', () => {
const templateId = generateRandomBytes32();
const timestamp = Math.floor((Date.now() - 25 * 60 * 60 * 1000) / 1000).toString();
const airnode = signer.address;
const encodedValue = ethers.utils.defaultAbiCoder.encode(['int256'], [BigNumber.from(1)]);
const encodedValue = ethers.utils.defaultAbiCoder.encode(['int256'], [ethers.BigNumber.from(1)]);

testDataPoint = {
airnode,
Expand All @@ -34,18 +35,17 @@ describe('datastore', () => {
const promisedStorage = localDataStore.setStoreDataPoint(testDataPoint);
expect(promisedStorage).toBeFalsy();

const datapoint = localDataStore.getStoreDataPoint(testDataPoint.airnode, testDataPoint.templateId);
const dataFeedId = deriveBeaconId(testDataPoint.airnode, testDataPoint.templateId)!;
const datapoint = localDataStore.getStoreDataPoint(dataFeedId);

const { encodedValue, signature, timestamp } = testDataPoint;

expect(datapoint).toStrictEqual({ encodedValue, signature, timestamp });
expect(datapoint).toStrictEqual(testDataPoint);
});

it('checks that the timestamp on signed data is not in the future', async () => {
const templateId = generateRandomBytes32();
const timestamp = Math.floor((Date.now() + 61 * 60 * 1000) / 1000).toString();
const airnode = signer.address;
const encodedValue = ethers.utils.defaultAbiCoder.encode(['int256'], [BigNumber.from(1)]);
const encodedValue = ethers.utils.defaultAbiCoder.encode(['int256'], [ethers.BigNumber.from(1)]);

const futureTestDataPoint = {
airnode,
Expand All @@ -63,7 +63,7 @@ describe('datastore', () => {
const templateId = generateRandomBytes32();
const timestamp = Math.floor((Date.now() + 60 * 60 * 1000) / 1000).toString();
const airnode = ethers.Wallet.createRandom().address;
const encodedValue = ethers.utils.defaultAbiCoder.encode(['int256'], [BigNumber.from(1)]);
const encodedValue = ethers.utils.defaultAbiCoder.encode(['int256'], [ethers.BigNumber.from(1)]);

const badTestDataPoint = {
airnode,
Expand Down
20 changes: 11 additions & 9 deletions src/signed-data-store/signed-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { ethers } from 'ethers';

import { logger } from '../logger';
import { getState, updateState } from '../state';
import type { SignedData, AirnodeAddress, TemplateId } from '../types';
import type { SignedData, AirnodeAddress, TemplateId, DataFeedId } from '../types';
import { deriveBeaconId } from '../utils';

export const verifySignedData = ({ airnode, templateId, timestamp, signature, encodedValue }: SignedData) => {
// Verification is wrapped in goSync, because ethers methods can potentially throw on invalid input.
Expand Down Expand Up @@ -65,7 +66,10 @@ export const setStoreDataPoint = (signedData: SignedData) => {
}

const state = getState();
const existingValue = state.signedApiStore[airnode]?.[templateId];

const dataFeedId = deriveBeaconId(airnode, templateId)!;

const existingValue = state.signedApiStore[dataFeedId];
if (existingValue && existingValue.timestamp >= timestamp) {
logger.debug('Skipping store update. The existing store value is fresher.');
return;
Expand All @@ -79,16 +83,14 @@ export const setStoreDataPoint = (signedData: SignedData) => {
encodedValue,
});
updateState((draft) => {
if (!draft.signedApiStore[airnode]) {
draft.signedApiStore[airnode] = {};
}

draft.signedApiStore[airnode]![templateId] = { signature, timestamp, encodedValue };
draft.signedApiStore[dataFeedId] = signedData;
});
};

export const getStoreDataPoint = (airnode: AirnodeAddress, templateId: TemplateId) =>
getState().signedApiStore[airnode]?.[templateId];
export const getStoreDataPointByAirnodeAndTemplate = (airnode: AirnodeAddress, template: TemplateId) =>
getState().signedApiStore[deriveBeaconId(airnode, template)!];

export const getStoreDataPoint = (dataFeedId: DataFeedId) => getState().signedApiStore[dataFeedId];

export const clear = () => {
updateState((draft) => {
Expand Down
Loading