From f0aacfadd9c76d44f6d5f550c6875cdd71530c40 Mon Sep 17 00:00:00 2001 From: Michal Kimle Date: Thu, 16 Mar 2023 15:14:15 +0100 Subject: [PATCH 1/3] Fix OEV gw timestamp calculation --- .../src/handlers/aws/index.ts | 6 +- .../src/handlers/gcp/index.ts | 6 +- .../src/handlers/sign-oev-data.test.ts | 87 ++++++++++++------- .../src/handlers/sign-oev-data.ts | 16 +++- .../src/workers/local-gateways/server.ts | 6 +- .../workers/local-gateways/validation.test.ts | 11 ++- .../src/workers/local-gateways/validation.ts | 8 +- 7 files changed, 98 insertions(+), 42 deletions(-) diff --git a/packages/airnode-deployer/src/handlers/aws/index.ts b/packages/airnode-deployer/src/handlers/aws/index.ts index 2f0f0f74ba..eb511974a8 100644 --- a/packages/airnode-deployer/src/handlers/aws/index.ts +++ b/packages/airnode-deployer/src/handlers/aws/index.ts @@ -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`); diff --git a/packages/airnode-deployer/src/handlers/gcp/index.ts b/packages/airnode-deployer/src/handlers/gcp/index.ts index 6bc95c5319..3eb0db4a4b 100644 --- a/packages/airnode-deployer/src/handlers/gcp/index.ts +++ b/packages/airnode-deployer/src/handlers/gcp/index.ts @@ -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`); diff --git a/packages/airnode-node/src/handlers/sign-oev-data.test.ts b/packages/airnode-node/src/handlers/sign-oev-data.test.ts index 5ccf7b0ecd..0717a6aae8 100644 --- a/packages/airnode-node/src/handlers/sign-oev-data.test.ts +++ b/packages/airnode-node/src/handlers/sign-oev-data.test.ts @@ -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', () => { @@ -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', () => { @@ -105,54 +124,58 @@ 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(); - }); + const encodedOevUpdateValue = '0x0000000000000000000000000000000000000000000000000000000000000096'; + const oevUpdateTimestamp = '1677747314'; 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([ + { + signature: + '0xa8339565d47b5a80ae35702df0a4656809dfc0152c9bbd22a8a94ce6501690e077ad3b5d1fa7f9198eff3db0b74b8196c30c0f931677e2a09ba5e2c96621b08b1b', + encodedValue: requestBody.signedData[0].encodedValue, + timestamp: requestBody.signedData[0].timestamp, + }, + ]); }); 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([ + { + signature: signatures[0], + encodedValue: encodedOevUpdateValue, + timestamp: oevUpdateTimestamp, + }, + { + signature: signatures[1], + encodedValue: encodedOevUpdateValue, + timestamp: oevUpdateTimestamp, + }, + ]); }); }); diff --git a/packages/airnode-node/src/handlers/sign-oev-data.ts b/packages/airnode-node/src/handlers/sign-oev-data.ts index c04f549a2a..7db4ecd0c9 100644 --- a/packages/airnode-node/src/handlers/sign-oev-data.ts +++ b/packages/airnode-node/src/handlers/sign-oev-data.ts @@ -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({ @@ -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( @@ -80,7 +86,11 @@ export async function signOevData( null, { success: true, - data: signatures.map((signature) => ({ signature, timestamp, encodedValue: encodedUpdateValue })), + data: signatures.map((signature) => ({ + signature, + timestamp: timestamp.toString(), + encodedValue: encodedUpdateValue, + })), }, ]; } diff --git a/packages/airnode-node/src/workers/local-gateways/server.ts b/packages/airnode-node/src/workers/local-gateways/server.ts index dc36be14be..6b95ff8f5b 100644 --- a/packages/airnode-node/src/workers/local-gateways/server.ts +++ b/packages/airnode-node/src/workers/local-gateways/server.ts @@ -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`); diff --git a/packages/airnode-node/src/workers/local-gateways/validation.test.ts b/packages/airnode-node/src/workers/local-gateways/validation.test.ts index 7fcf139bef..29605ecf81 100644 --- a/packages/airnode-node/src/workers/local-gateways/validation.test.ts +++ b/packages/airnode-node/src/workers/local-gateways/validation.test.ts @@ -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, @@ -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(() => { @@ -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', () => { @@ -358,6 +364,7 @@ describe('verifySignOevDataRequest', () => { expect(verifySignOevDataRequest([validBeacons[0]])).toEqual({ success: true, validUpdateValues: [expectedDecodedValues[0]], + validUpdateTimestamps: [expectedTimestamps[0]], }); }); }); diff --git a/packages/airnode-node/src/workers/local-gateways/validation.ts b/packages/airnode-node/src/workers/local-gateways/validation.ts index 30a175529a..75ab4a3d5e 100644 --- a/packages/airnode-node/src/workers/local-gateways/validation.ts +++ b/packages/airnode-node/src/workers/local-gateways/validation.ts @@ -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); @@ -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) => From d9a8f2eff5f3baa6268f8fc9f8ba8009c680de96 Mon Sep 17 00:00:00 2001 From: Michal Kimle Date: Thu, 16 Mar 2023 15:41:40 +0100 Subject: [PATCH 2/3] Remove unnecessary return values from the OEV gw --- .changeset/nine-schools-raise.md | 2 ++ packages/airnode-node/src/handlers/sign-oev-data.test.ts | 8 -------- packages/airnode-node/src/handlers/sign-oev-data.ts | 2 -- packages/airnode-node/src/types.ts | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) create mode 100644 .changeset/nine-schools-raise.md diff --git a/.changeset/nine-schools-raise.md b/.changeset/nine-schools-raise.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/nine-schools-raise.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/airnode-node/src/handlers/sign-oev-data.test.ts b/packages/airnode-node/src/handlers/sign-oev-data.test.ts index 0717a6aae8..5101784483 100644 --- a/packages/airnode-node/src/handlers/sign-oev-data.test.ts +++ b/packages/airnode-node/src/handlers/sign-oev-data.test.ts @@ -131,8 +131,6 @@ describe('signOevData', () => { '0x81b1512a67848c0d46ce6957f7b377dc43e9444a57f602353e5c9ab41a24c68d3a2c5a261f7d59e0bca0e72bdc7353bb20e2c4f801452ddd95c43c4f9c7e56581b', '0xc01e7da0e6e2a7057f5c95aefc327d34d85faf812876340b48b052a611d32ec61cae4fc1443d364bc4ee819eae00ff032dd904aaa8e169c52144b9c81a9c3fba1b', ]; - const encodedOevUpdateValue = '0x0000000000000000000000000000000000000000000000000000000000000096'; - const oevUpdateTimestamp = '1677747314'; it('signs the OEV data for one beacon', async () => { // median: 0x0000000000000000000000000000000000000000000000000000000000000064 @@ -153,8 +151,6 @@ describe('signOevData', () => { { signature: '0xa8339565d47b5a80ae35702df0a4656809dfc0152c9bbd22a8a94ce6501690e077ad3b5d1fa7f9198eff3db0b74b8196c30c0f931677e2a09ba5e2c96621b08b1b', - encodedValue: requestBody.signedData[0].encodedValue, - timestamp: requestBody.signedData[0].timestamp, }, ]); }); @@ -168,13 +164,9 @@ describe('signOevData', () => { expect(res!.data).toEqual([ { signature: signatures[0], - encodedValue: encodedOevUpdateValue, - timestamp: oevUpdateTimestamp, }, { signature: signatures[1], - encodedValue: encodedOevUpdateValue, - timestamp: oevUpdateTimestamp, }, ]); }); diff --git a/packages/airnode-node/src/handlers/sign-oev-data.ts b/packages/airnode-node/src/handlers/sign-oev-data.ts index 7db4ecd0c9..16985dadc4 100644 --- a/packages/airnode-node/src/handlers/sign-oev-data.ts +++ b/packages/airnode-node/src/handlers/sign-oev-data.ts @@ -88,8 +88,6 @@ export async function signOevData( success: true, data: signatures.map((signature) => ({ signature, - timestamp: timestamp.toString(), - encodedValue: encodedUpdateValue, })), }, ]; diff --git a/packages/airnode-node/src/types.ts b/packages/airnode-node/src/types.ts index 05210ea0b1..6c951046f0 100644 --- a/packages/airnode-node/src/types.ts +++ b/packages/airnode-node/src/types.ts @@ -247,7 +247,7 @@ export interface HttpSignedDataApiCallSuccessResponse { export interface SignOevDataResponse { success: true; - data: { timestamp: string; encodedValue: string; signature: string }[]; + data: { signature: string }[]; } export interface ApiCallErrorResponse { From 8a302ae8589a4c8306d84de82fce40166d1246c9 Mon Sep 17 00:00:00 2001 From: Michal Kimle Date: Fri, 17 Mar 2023 15:18:14 +0100 Subject: [PATCH 3/3] Simplify OEV GW response format --- .../src/handlers/sign-oev-data.test.ts | 14 ++------------ .../airnode-node/src/handlers/sign-oev-data.ts | 4 +--- packages/airnode-node/src/types.ts | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/airnode-node/src/handlers/sign-oev-data.test.ts b/packages/airnode-node/src/handlers/sign-oev-data.test.ts index 5101784483..e3b561e473 100644 --- a/packages/airnode-node/src/handlers/sign-oev-data.test.ts +++ b/packages/airnode-node/src/handlers/sign-oev-data.test.ts @@ -148,10 +148,7 @@ describe('signOevData', () => { expect(err).toBeNull(); expect(res!.success).toBeTruthy(); expect(res!.data).toEqual([ - { - signature: - '0xa8339565d47b5a80ae35702df0a4656809dfc0152c9bbd22a8a94ce6501690e077ad3b5d1fa7f9198eff3db0b74b8196c30c0f931677e2a09ba5e2c96621b08b1b', - }, + '0xa8339565d47b5a80ae35702df0a4656809dfc0152c9bbd22a8a94ce6501690e077ad3b5d1fa7f9198eff3db0b74b8196c30c0f931677e2a09ba5e2c96621b08b1b', ]); }); @@ -161,13 +158,6 @@ describe('signOevData', () => { expect(err).toBeNull(); expect(res!.success).toBeTruthy(); - expect(res!.data).toEqual([ - { - signature: signatures[0], - }, - { - signature: signatures[1], - }, - ]); + expect(res!.data).toEqual(signatures); }); }); diff --git a/packages/airnode-node/src/handlers/sign-oev-data.ts b/packages/airnode-node/src/handlers/sign-oev-data.ts index 16985dadc4..bd59b980e7 100644 --- a/packages/airnode-node/src/handlers/sign-oev-data.ts +++ b/packages/airnode-node/src/handlers/sign-oev-data.ts @@ -86,9 +86,7 @@ export async function signOevData( null, { success: true, - data: signatures.map((signature) => ({ - signature, - })), + data: signatures, }, ]; } diff --git a/packages/airnode-node/src/types.ts b/packages/airnode-node/src/types.ts index 6c951046f0..e870e8bc50 100644 --- a/packages/airnode-node/src/types.ts +++ b/packages/airnode-node/src/types.ts @@ -247,7 +247,7 @@ export interface HttpSignedDataApiCallSuccessResponse { export interface SignOevDataResponse { success: true; - data: { signature: string }[]; + data: string[]; } export interface ApiCallErrorResponse {