Skip to content

Commit

Permalink
Merge pull request #1676 from api3dao/oev-gw-fixes-2
Browse files Browse the repository at this point in the history
OEV GW timestamp calculation fix
  • Loading branch information
amarthadan authored Mar 20, 2023
2 parents a75741b + 8a302ae commit acaea82
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 43 deletions.
2 changes: 2 additions & 0 deletions .changeset/nine-schools-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
6 changes: 5 additions & 1 deletion packages/airnode-deployer/src/handlers/aws/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ export async function processSignOevDataRequest(
}
logger.debug(`Sign OEV data request passed request verification`);

const [err, result] = await handlers.signOevData(rawSignOevDataRequestBody, verificationResult.validUpdateValues);
const [err, result] = await handlers.signOevData(
rawSignOevDataRequestBody,
verificationResult.validUpdateValues,
verificationResult.validUpdateTimestamps
);
if (err) {
// Returning 500 because failure here means something went wrong internally with a valid request
logger.error(`Sign OEV data request processing error`);
Expand Down
6 changes: 5 additions & 1 deletion packages/airnode-deployer/src/handlers/gcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,11 @@ export async function processSignOevDataRequest(req: Request, res: Response) {
}
logger.debug(`Sign OEV data request passed request verification`);

const [err, result] = await handlers.signOevData(rawSignOevDataRequestBody, verificationResult.validUpdateValues);
const [err, result] = await handlers.signOevData(
rawSignOevDataRequestBody,
verificationResult.validUpdateValues,
verificationResult.validUpdateTimestamps
);
if (err) {
// Returning 500 because failure here means something went wrong internally with a valid request
logger.error(`Sign OEV data request processing error`);
Expand Down
69 changes: 37 additions & 32 deletions packages/airnode-node/src/handlers/sign-oev-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import map from 'lodash/map';
import { BigNumber } from 'ethers';
import { calculateMedian, deriveBeaconId, deriveBeaconSetId, signOevData } from './sign-oev-data';
import {
calculateMedian,
calculateUpdateTimestamp,
deriveBeaconId,
deriveBeaconSetId,
signOevData,
} from './sign-oev-data';
import * as fixtures from '../../test/fixtures';

describe('deriveBeaconId', () => {
Expand Down Expand Up @@ -50,6 +57,18 @@ describe('calculateMedian', () => {
});
});

describe('calculateUpdateTimestamp', () => {
it('calculates beacon set timestamp', () => {
const beaconSetBeaconTimestamps = ['1555711223', '1556229645', '1555020018', '1556402497'];
expect(calculateUpdateTimestamp(beaconSetBeaconTimestamps)).toEqual(1555840845);
});

it('calculates beacon set timestamp from just one timestamp', () => {
const beaconSetBeaconTimestamps = ['1555711223'];
expect(calculateUpdateTimestamp(beaconSetBeaconTimestamps)).toEqual(1555711223);
});
});

// The commented values below (e.g. beaconId, templateId, ...) are there so we have easier time
// changing the tests in the future as the values in the tests must be derived correctly in order to pass
describe('signOevData', () => {
Expand Down Expand Up @@ -105,54 +124,40 @@ describe('signOevData', () => {
],
};
// median: 0x0000000000000000000000000000000000000000000000000000000000000096
const validUpdateValues = [BigNumber.from(100), BigNumber.from(150), BigNumber.from(200)];
const timestamp = 1677753822;
// oevUpdateHash: 0xf29974946b92c9a49b09c6d290b2073de9d932665a567c13e13f1e3ea716df55
const validUpdateValues = [100, 150, 200].map(BigNumber.from);
const validUpdateTimestamps = map(requestBody.signedData, 'timestamp');
// oevUpdateHash: 0xcd5bb3b413773277e8b03a10c3213fb752ad9d7c4d286ce8826b73905ba0672c
const signatures = [
'0x2a20562ff1c0fa985eea09e33e1902140acf8f17c2c4bbbcaf98ad4ba2417c4a18d0bc293750bc195e45b0d33a8b71628abbdcc7296a550aaeb8c7c2cc29fbb11b',
'0xcd29b94e503107fed316363e27a94eb55829c09b9d89f6594de8b24b05e2e44e4a365f1f8890acb36a003d00c091aef6c2288f72e0ebb96ec7ee307828fb64e81b',
'0x81b1512a67848c0d46ce6957f7b377dc43e9444a57f602353e5c9ab41a24c68d3a2c5a261f7d59e0bca0e72bdc7353bb20e2c4f801452ddd95c43c4f9c7e56581b',
'0xc01e7da0e6e2a7057f5c95aefc327d34d85faf812876340b48b052a611d32ec61cae4fc1443d364bc4ee819eae00ff032dd904aaa8e169c52144b9c81a9c3fba1b',
];

let dateNowSpy: jest.SpyInstance;

beforeAll(() => {
dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => timestamp * 1000);
});

afterAll(() => {
dateNowSpy.mockRestore();
});

it('signs the OEV data for one beacon', async () => {
// median: 0x0000000000000000000000000000000000000000000000000000000000000064
// oevUpdateHash: 0xc3789fe6050b1b9a77e15920048a1eaa4a84b80f3d09804f4a5ad7ed5d7e27af
// oevUpdateHash: 0xf6675230e0269c8308f219e520f91a3a665e0b48b408047445e218369642d5af
const oneBeaconSignedData = [requestBody.signedData[0]];
const oneBeaconUpdateValues = [validUpdateValues[0]];
const signedOevData = await signOevData({ ...requestBody, signedData: oneBeaconSignedData }, oneBeaconUpdateValues);
const oneBeaconUpdateTimestamps = [requestBody.signedData[0].timestamp];
const signedOevData = await signOevData(
{ ...requestBody, signedData: oneBeaconSignedData },
oneBeaconUpdateValues,
oneBeaconUpdateTimestamps
);
const [err, res] = signedOevData;
const [signedData] = res!.data;

expect(err).toBeNull();
expect(res!.success).toBeTruthy();
expect(signedData.encodedValue).toEqual('0x0000000000000000000000000000000000000000000000000000000000000064');
expect(signedData.signature).toEqual(
'0x31a5fa78a05e619f304e8949f65e084c9d368b282d3d7e270178749bb072c7e92e52c722bc604120cd3a2d81071318bc6da17376b23721d088d75737072a3e3b1c'
);
expect(signedData.timestamp).toEqual(`${timestamp}`);
expect(res!.data).toEqual([
'0xa8339565d47b5a80ae35702df0a4656809dfc0152c9bbd22a8a94ce6501690e077ad3b5d1fa7f9198eff3db0b74b8196c30c0f931677e2a09ba5e2c96621b08b1b',
]);
});

it('signs the OEV data for beacon set', async () => {
const signedOevData = await signOevData(requestBody, validUpdateValues);
const signedOevData = await signOevData(requestBody, validUpdateValues, validUpdateTimestamps);
const [err, res] = signedOevData;
const [signedData1, signedData2] = res!.data;

expect(err).toBeNull();
expect(res!.success).toBeTruthy();
expect(signedData1.encodedValue).toEqual('0x0000000000000000000000000000000000000000000000000000000000000096');
expect(signedData1.signature).toEqual(signatures[0]);
expect(signedData1.timestamp).toEqual(`${timestamp}`);
expect(signedData2.encodedValue).toEqual('0x0000000000000000000000000000000000000000000000000000000000000096');
expect(signedData2.signature).toEqual(signatures[1]);
expect(signedData2.timestamp).toEqual(`${timestamp}`);
expect(res!.data).toEqual(signatures);
});
});
12 changes: 9 additions & 3 deletions packages/airnode-node/src/handlers/sign-oev-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ export function calculateMedian(arr: ethers.BigNumber[]) {
return arr.length % 2 !== 0 ? nums[mid] : nums[mid - 1].add(nums[mid]).div(2);
}

export const calculateUpdateTimestamp = (timestamps: string[]) => {
const accumulatedTimestamp = timestamps.reduce((total, next) => total + parseInt(next, 10), 0);
return Math.floor(accumulatedTimestamp / timestamps.length);
};

export async function signOevData(
requestBody: ProcessSignOevDataRequestBody,
validUpdateValues: ethers.BigNumber[]
validUpdateValues: ethers.BigNumber[],
validUpdateTimestamps: string[]
): Promise<[Error, null] | [null, SignOevDataResponse]> {
const { chainId, dapiServerAddress, oevProxyAddress, updateId, bidderAddress, bidAmount, signedData } = requestBody;
const airnodeWallet = getAirnodeWalletFromPrivateKey();
const airnodeAddress = airnodeWallet.address;
const timestamp = Math.floor(Date.now() / 1000).toString();

const beaconsWithTemplateId = signedData.map((beacon) => {
const templateId = getExpectedTemplateIdV1({
Expand All @@ -44,6 +49,7 @@ export async function signOevData(
const beaconIds = beaconsWithTemplateId.map((beacon) => deriveBeaconId(beacon.airnodeAddress, beacon.templateId));
// We are computing both update value and data feed ID in Airnode otherwise it would be possible to spoof the signature.
const dataFeedId = beaconIds.length === 1 ? beaconIds[0] : deriveBeaconSetId(beaconIds);
const timestamp = calculateUpdateTimestamp(validUpdateTimestamps);
const updateValue = calculateMedian(validUpdateValues);
const encodedUpdateValue = ethers.utils.defaultAbiCoder.encode(['int256'], [updateValue]);
const oevUpdateHash = ethers.utils.solidityKeccak256(
Expand Down Expand Up @@ -80,7 +86,7 @@ export async function signOevData(
null,
{
success: true,
data: signatures.map((signature) => ({ signature, timestamp, encodedValue: encodedUpdateValue })),
data: signatures,
},
];
}
2 changes: 1 addition & 1 deletion packages/airnode-node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export interface HttpSignedDataApiCallSuccessResponse {

export interface SignOevDataResponse {
success: true;
data: { timestamp: string; encodedValue: string; signature: string }[];
data: string[];
}

export interface ApiCallErrorResponse {
Expand Down
6 changes: 5 additions & 1 deletion packages/airnode-node/src/workers/local-gateways/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,11 @@ export function startGatewayServer(config: Config, enabledGateways: GatewayName[
}
logger.debug(`OEV gateway request passed request verification`);

const [err, result] = await signOevData(rawSignOevDataRequestBody, verificationResult.validUpdateValues);
const [err, result] = await signOevData(
rawSignOevDataRequestBody,
verificationResult.validUpdateValues,
verificationResult.validUpdateTimestamps
);
if (err) {
// Returning 500 because failure here means something went wrong internally with a valid request
logger.error(`OEV gateway request processing error`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from 'path';
import { subMinutes } from 'date-fns';
import { ethers } from 'ethers';
import omit from 'lodash/omit';
import map from 'lodash/map';
import { Config } from '@api3/airnode-node';
import {
verifyHttpRequest,
Expand Down Expand Up @@ -282,7 +283,8 @@ describe('verifySignOevDataRequest', () => {
'0xc60d89ab00348cced9e1daa050694ac01ba50b3608dcf6ee556d625bf56fdd54697e76358742c0845f1dcf1930e1f612235291572c7445cabccaf167e2ee95511c',
};

const expectedDecodedValues = [ethers.BigNumber.from(1000), ethers.BigNumber.from(1001), ethers.BigNumber.from(1002)];
const expectedDecodedValues = [1000, 1001, 1002].map(ethers.BigNumber.from);
const expectedTimestamps = map(validBeacons, 'timestamp');

const currentTimestamp = 1677790659;
beforeAll(() => {
Expand All @@ -295,7 +297,11 @@ describe('verifySignOevDataRequest', () => {
});

it('verifies beacon data for the request', () => {
expect(verifySignOevDataRequest(validBeacons)).toEqual({ success: true, validUpdateValues: expectedDecodedValues });
expect(verifySignOevDataRequest(validBeacons)).toEqual({
success: true,
validUpdateValues: expectedDecodedValues,
validUpdateTimestamps: expectedTimestamps,
});
});

it('fails if majority of beacons are missing data', () => {
Expand Down Expand Up @@ -358,6 +364,7 @@ describe('verifySignOevDataRequest', () => {
expect(verifySignOevDataRequest([validBeacons[0]])).toEqual({
success: true,
validUpdateValues: [expectedDecodedValues[0]],
validUpdateTimestamps: [expectedTimestamps[0]],
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export function allBeaconsConsistent(beacons: BeaconDecoded[]) {

export function verifySignOevDataRequest(
beacons: Beacon[]
): VerificationResult<{ validUpdateValues: ethers.BigNumber[] }> {
): VerificationResult<{ validUpdateValues: ethers.BigNumber[]; validUpdateTimestamps: string[] }> {
const majority = Math.floor(beacons.length / 2) + 1;
const beaconsWithData = beacons.filter((beacon) => beacon.encodedValue && beacon.timestamp && beacon.signature);

Expand Down Expand Up @@ -250,7 +250,11 @@ export function verifySignOevDataRequest(
};
}

return { success: true, validUpdateValues: map(validDecodedBeacons, 'decodedValue') };
return {
success: true,
validUpdateValues: map(validDecodedBeacons, 'decodedValue'),
validUpdateTimestamps: map(validDecodedBeacons, 'timestamp'),
};
}

export const checkRequestOrigin = (allowedOrigins: string[], origin?: string) =>
Expand Down

0 comments on commit acaea82

Please sign in to comment.