From 2e18bfeff0448ce6621fcee5112e3e961acd1a7a Mon Sep 17 00:00:00 2001 From: Aaron Scheiner Date: Tue, 15 Nov 2022 09:49:53 +0000 Subject: [PATCH 1/4] Smplifies heartbeat scheme to remove binary packing --- .../airnode-node/src/reporting/heartbeat.test.ts | 14 ++++++-------- packages/airnode-node/src/reporting/heartbeat.ts | 9 ++------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/airnode-node/src/reporting/heartbeat.test.ts b/packages/airnode-node/src/reporting/heartbeat.test.ts index 432ab04c1e..5aa11408e8 100644 --- a/packages/airnode-node/src/reporting/heartbeat.test.ts +++ b/packages/airnode-node/src/reporting/heartbeat.test.ts @@ -60,7 +60,7 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - ...heartbeatPayload, + payload: JSON.stringify(heartbeatPayload), signature, timestamp, }, @@ -93,7 +93,7 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - ...heartbeatPayload, + payload: JSON.stringify(heartbeatPayload), signature, timestamp, }, @@ -181,7 +181,7 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - ...heartbeatPayload, + payload: JSON.stringify(heartbeatPayload), signature, timestamp, }, @@ -215,7 +215,7 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - ...heartbeatPayload, + payload: JSON.stringify(heartbeatPayload), signature, timestamp, }, @@ -236,14 +236,12 @@ describe('reportHeartbeat', () => { it('signs verifiable heartbeat', async () => { const signature = await heartbeat.signHeartbeat(heartbeatPayload, timestamp); const signerAddress = ethers.utils.verifyMessage( - ethers.utils.arrayify( - ethers.utils.solidityKeccak256(['uint256', 'string'], [timestamp, JSON.stringify(heartbeatPayload)]) - ), + JSON.stringify({ payload: JSON.stringify(heartbeatPayload), timestamp }), signature ); expect(signature).toEqual( - '0x3d63a776155b1ea16eff0a8399fe16ed25e1f207300136918979074e80bd2cab75ee200bb4148e294a0867a42520eeed1a2214f190e3369914b5d2116c2cde141b' + '0x6bb2fe4b89e57f63e7bf4942125a026dff7c97626cacdcefb24cae8bfd9ba275744cf0ee9afa68c16380aae1a563c9c0ccf53d4786f7595051794a89dc812d361c' ); expect(signerAddress).toEqual(airnodeAddress); }); diff --git a/packages/airnode-node/src/reporting/heartbeat.ts b/packages/airnode-node/src/reporting/heartbeat.ts index 50254c794c..0b4349068c 100644 --- a/packages/airnode-node/src/reporting/heartbeat.ts +++ b/packages/airnode-node/src/reporting/heartbeat.ts @@ -1,4 +1,3 @@ -import { ethers } from 'ethers'; import { execute } from '@api3/airnode-adapter'; import { logger, PendingLog } from '@api3/airnode-utilities'; import { go } from '@api3/promise-utils'; @@ -34,11 +33,7 @@ export const signHeartbeat = ( ) => { const airnodeWallet = getAirnodeWalletFromPrivateKey(); - return airnodeWallet.signMessage( - ethers.utils.arrayify( - ethers.utils.solidityKeccak256(['uint256', 'string'], [timestamp, JSON.stringify(heartbeatPayload)]) - ) - ); + return airnodeWallet.signMessage(JSON.stringify({ payload: JSON.stringify(heartbeatPayload), timestamp })); }; export async function reportHeartbeat(state: CoordinatorState): Promise { @@ -78,7 +73,7 @@ export async function reportHeartbeat(state: CoordinatorState): Promise Date: Tue, 15 Nov 2022 11:02:50 +0000 Subject: [PATCH 2/4] Further simplify signature scheme --- .../src/reporting/heartbeat.test.ts | 54 +++++++++---------- .../airnode-node/src/reporting/heartbeat.ts | 26 ++++----- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/packages/airnode-node/src/reporting/heartbeat.test.ts b/packages/airnode-node/src/reporting/heartbeat.test.ts index 5aa11408e8..da9507174e 100644 --- a/packages/airnode-node/src/reporting/heartbeat.test.ts +++ b/packages/airnode-node/src/reporting/heartbeat.test.ts @@ -40,13 +40,14 @@ describe('reportHeartbeat', () => { const config = fixtures.buildConfig(); const coordinatorId = randomHexString(16); const state = coordinatorState.create(config, coordinatorId); - const heartbeatPayload = { + const heartbeatPayload = JSON.stringify({ + timestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, http_gateway_url: 'http://localhost:3000/http-data', http_signed_data_gateway_url: 'http://localhost:3000/http-signed-data', - }; - const signature = await heartbeat.signHeartbeat(heartbeatPayload, timestamp); + }); + const signature = await heartbeat.signHeartbeat(heartbeatPayload); const res = await heartbeat.reportHeartbeat(state); expect(res).toEqual([ { level: 'INFO', message: 'Sending heartbeat...' }, @@ -60,9 +61,8 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - payload: JSON.stringify(heartbeatPayload), + payload: heartbeatPayload, signature, - timestamp, }, timeout: 5_000, }); @@ -73,13 +73,14 @@ describe('reportHeartbeat', () => { const config = fixtures.buildConfig(); const coordinatorId = randomHexString(16); const state = coordinatorState.create(config, coordinatorId); - const heartbeatPayload = { + const heartbeatPayload = JSON.stringify({ + timestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, http_gateway_url: 'http://localhost:3000/http-data', http_signed_data_gateway_url: 'http://localhost:3000/http-signed-data', - }; - const signature = await heartbeat.signHeartbeat(heartbeatPayload, timestamp); + }); + const signature = await heartbeat.signHeartbeat(heartbeatPayload); const logs = await heartbeat.reportHeartbeat(state); expect(logs).toEqual([ { level: 'INFO', message: 'Sending heartbeat...' }, @@ -93,9 +94,8 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - payload: JSON.stringify(heartbeatPayload), + payload: heartbeatPayload, signature, - timestamp, }, timeout: 5_000, }); @@ -159,14 +159,15 @@ describe('reportHeartbeat', () => { const region = 'us-east1'; config.nodeSettings.cloudProvider = { type: 'aws', disableConcurrencyReservations: false, region }; const state = coordinatorState.create(config, 'coordinatorId'); - const heartbeatPayload = { + const heartbeatPayload = JSON.stringify({ + timestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, region, http_gateway_url: httpGatewayUrl, http_signed_data_gateway_url: httpSignedDataGatewayUrl, - }; - const signature = await heartbeat.signHeartbeat(heartbeatPayload, timestamp); + }); + const signature = await heartbeat.signHeartbeat(heartbeatPayload); const logs = await heartbeat.reportHeartbeat(state); expect(logs).toEqual([ @@ -181,9 +182,8 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - payload: JSON.stringify(heartbeatPayload), + payload: heartbeatPayload, signature, - timestamp, }, timeout: 5_000, }); @@ -194,13 +194,14 @@ describe('reportHeartbeat', () => { const config = cloneDeep(baseConfig); config.nodeSettings.cloudProvider = { type: 'local', gatewayServerPort: 8765 }; const state = coordinatorState.create(config, 'coordinatorId'); - const heartbeatPayload = { + const heartbeatPayload = JSON.stringify({ + timestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, http_gateway_url: 'http://localhost:8765/http-data', http_signed_data_gateway_url: 'http://localhost:8765/http-signed-data', - }; - const signature = await heartbeat.signHeartbeat(heartbeatPayload, timestamp); + }); + const signature = await heartbeat.signHeartbeat(heartbeatPayload); const logs = await heartbeat.reportHeartbeat(state); expect(logs).toEqual([ @@ -215,9 +216,8 @@ describe('reportHeartbeat', () => { 'airnode-heartbeat-api-key': '3a7af83f-6450-46d3-9937-5f9773ce2849', }, data: { - payload: JSON.stringify(heartbeatPayload), + payload: heartbeatPayload, signature, - timestamp, }, timeout: 5_000, }); @@ -226,22 +226,20 @@ describe('reportHeartbeat', () => { describe('signHearbeat', () => { const airnodeAddress = fixtures.getAirnodeWallet().address; - const heartbeatPayload = { + const heartbeatPayload = JSON.stringify({ + timestamp, stage: 'test', cloud_provider: 'local', http_gateway_url: httpGatewayUrl, http_signed_data_gateway_url: httpSignedDataGatewayUrl, - }; + }); it('signs verifiable heartbeat', async () => { - const signature = await heartbeat.signHeartbeat(heartbeatPayload, timestamp); - const signerAddress = ethers.utils.verifyMessage( - JSON.stringify({ payload: JSON.stringify(heartbeatPayload), timestamp }), - signature - ); + const signature = await heartbeat.signHeartbeat(heartbeatPayload); + const signerAddress = ethers.utils.verifyMessage(heartbeatPayload, signature); expect(signature).toEqual( - '0x6bb2fe4b89e57f63e7bf4942125a026dff7c97626cacdcefb24cae8bfd9ba275744cf0ee9afa68c16380aae1a563c9c0ccf53d4786f7595051794a89dc812d361c' + '0x5b68956fde8b3c529d0e49c6b13c60183834416843c508d02cbaff8770101beb429d953b080e64b4269ea0bf68b91bc7dcb198da75ffe39ead46a0f84afedac21c' ); expect(signerAddress).toEqual(airnodeAddress); }); diff --git a/packages/airnode-node/src/reporting/heartbeat.ts b/packages/airnode-node/src/reporting/heartbeat.ts index 0b4349068c..9fcf6a0ff4 100644 --- a/packages/airnode-node/src/reporting/heartbeat.ts +++ b/packages/airnode-node/src/reporting/heartbeat.ts @@ -21,19 +21,10 @@ export function getHttpSignedDataGatewayUrl(config: Config) { return getEnvValue('HTTP_SIGNED_DATA_GATEWAY_URL'); } -export const signHeartbeat = ( - heartbeatPayload: { - cloud_provider: string; - stage: string; - region?: string; - http_gateway_url?: string; - httpSignedDataGatewayUrl?: string; - }, - timestamp: number -) => { +export const signHeartbeat = (heartbeatPayload: string) => { const airnodeWallet = getAirnodeWalletFromPrivateKey(); - return airnodeWallet.signMessage(JSON.stringify({ payload: JSON.stringify(heartbeatPayload), timestamp })); + return airnodeWallet.signMessage(heartbeatPayload); }; export async function reportHeartbeat(state: CoordinatorState): Promise { @@ -51,16 +42,18 @@ export async function reportHeartbeat(state: CoordinatorState): Promise signHeartbeat(heartbeatPayload, timestamp)); + const goSignHeartbeat = await go(() => signHeartbeat(heartbeatPayload)); if (!goSignHeartbeat.success) { const log = logger.pend('ERROR', 'Failed to sign heartbeat', goSignHeartbeat.error); return [log]; @@ -73,9 +66,8 @@ export async function reportHeartbeat(state: CoordinatorState): Promise Date: Tue, 15 Nov 2022 11:04:08 +0000 Subject: [PATCH 3/4] Add changeset --- .changeset/shaggy-points-collect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaggy-points-collect.md diff --git a/.changeset/shaggy-points-collect.md b/.changeset/shaggy-points-collect.md new file mode 100644 index 0000000000..804075a62b --- /dev/null +++ b/.changeset/shaggy-points-collect.md @@ -0,0 +1,5 @@ +--- +'@api3/airnode-node': minor +--- + +Simplify heartbeat scheme From 145a8eafdbb1ea44c68fdc047fc14a50ecf34c03 Mon Sep 17 00:00:00 2001 From: Aaron Scheiner Date: Mon, 28 Nov 2022 16:21:36 +0000 Subject: [PATCH 4/4] Add explanation of marshaling scheme and transmit timestamp in seconds vs milliseconds. --- .../src/reporting/heartbeat.test.ts | 17 +++++++++-------- .../airnode-node/src/reporting/heartbeat.ts | 6 +++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/airnode-node/src/reporting/heartbeat.test.ts b/packages/airnode-node/src/reporting/heartbeat.test.ts index da9507174e..34e500874b 100644 --- a/packages/airnode-node/src/reporting/heartbeat.test.ts +++ b/packages/airnode-node/src/reporting/heartbeat.test.ts @@ -19,10 +19,11 @@ describe('reportHeartbeat', () => { HTTP_SIGNED_DATA_GATEWAY_URL: httpSignedDataGatewayUrl, AIRNODE_WALLET_PRIVATE_KEY: fixtures.getAirnodeWalletPrivateKey(), }); - const timestamp = 1661582890984; + const systemTimestamp = 1661582890984; + const expectedTimestamp = 1661582891; beforeEach(() => { - jest.spyOn(Date, 'now').mockImplementation(() => timestamp); + jest.spyOn(Date, 'now').mockImplementation(() => systemTimestamp); }); it('does nothing if the heartbeat is disabled', async () => { @@ -41,7 +42,7 @@ describe('reportHeartbeat', () => { const coordinatorId = randomHexString(16); const state = coordinatorState.create(config, coordinatorId); const heartbeatPayload = JSON.stringify({ - timestamp, + timestamp: expectedTimestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, http_gateway_url: 'http://localhost:3000/http-data', @@ -74,7 +75,7 @@ describe('reportHeartbeat', () => { const coordinatorId = randomHexString(16); const state = coordinatorState.create(config, coordinatorId); const heartbeatPayload = JSON.stringify({ - timestamp, + timestamp: expectedTimestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, http_gateway_url: 'http://localhost:3000/http-data', @@ -160,7 +161,7 @@ describe('reportHeartbeat', () => { config.nodeSettings.cloudProvider = { type: 'aws', disableConcurrencyReservations: false, region }; const state = coordinatorState.create(config, 'coordinatorId'); const heartbeatPayload = JSON.stringify({ - timestamp, + timestamp: expectedTimestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, region, @@ -195,7 +196,7 @@ describe('reportHeartbeat', () => { config.nodeSettings.cloudProvider = { type: 'local', gatewayServerPort: 8765 }; const state = coordinatorState.create(config, 'coordinatorId'); const heartbeatPayload = JSON.stringify({ - timestamp, + timestamp: expectedTimestamp, stage: state.config.nodeSettings.stage, cloud_provider: state.config.nodeSettings.cloudProvider.type, http_gateway_url: 'http://localhost:8765/http-data', @@ -227,7 +228,7 @@ describe('reportHeartbeat', () => { describe('signHearbeat', () => { const airnodeAddress = fixtures.getAirnodeWallet().address; const heartbeatPayload = JSON.stringify({ - timestamp, + timestamp: expectedTimestamp, stage: 'test', cloud_provider: 'local', http_gateway_url: httpGatewayUrl, @@ -239,7 +240,7 @@ describe('reportHeartbeat', () => { const signerAddress = ethers.utils.verifyMessage(heartbeatPayload, signature); expect(signature).toEqual( - '0x5b68956fde8b3c529d0e49c6b13c60183834416843c508d02cbaff8770101beb429d953b080e64b4269ea0bf68b91bc7dcb198da75ffe39ead46a0f84afedac21c' + '0x941f0ed9f7990f26c9b98434cc3dc094d35607a9087f0e6a71f58659d3383dce6d19c64a85a11714287cbb2cec8e1718e6fe40e4cd81d5658b000b40fa75c0a91c' ); expect(signerAddress).toEqual(airnodeAddress); }); diff --git a/packages/airnode-node/src/reporting/heartbeat.ts b/packages/airnode-node/src/reporting/heartbeat.ts index 9fcf6a0ff4..067225a837 100644 --- a/packages/airnode-node/src/reporting/heartbeat.ts +++ b/packages/airnode-node/src/reporting/heartbeat.ts @@ -42,8 +42,12 @@ export async function reportHeartbeat(state: CoordinatorState): Promise