Skip to content
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

Replace Airnode's short address with deployment ID #1577

Merged
merged 4 commits into from
Dec 8, 2022
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
7 changes: 7 additions & 0 deletions .changeset/unlucky-eggs-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@api3/airnode-deployer': minor
'@api3/airnode-node': minor
'@api3/airnode-validator': minor
---

Replace Airnode's short address with deployment ID
1 change: 0 additions & 1 deletion packages/airnode-deployer/src/cli/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ jest.mock('../utils', () => ({
const gcpReceipt: receipt.Receipt = {
airnodeWallet: {
airnodeAddress: '0xF347ADEd76F7AC2013e379078738aBfF75780C2e',
airnodeAddressShort: 'f347ade',
airnodeXpub:
'xpub6CZqcAR5RtRPYYGbJe7MzFGbJkJ86ub9KtYvTtenPQRgxFXNCmR7woXjV8SdFPWrTBooAqWVLKe19KWBnaktkwUsvSEfH18HyxeNZQRJq8k',
},
Expand Down
333 changes: 292 additions & 41 deletions packages/airnode-deployer/src/infrastructure/index.test.ts

Large diffs are not rendered by default.

62 changes: 22 additions & 40 deletions packages/airnode-deployer/src/infrastructure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
Config,
evm,
availableCloudProviders,
deriveDeploymentId,
deriveDeploymentVersionId,
} from '@api3/airnode-node';
import { consoleLog } from '@api3/airnode-utilities';
import { go } from '@api3/promise-utils';
Expand All @@ -35,14 +37,8 @@ import {
Bucket,
} from '../utils/infrastructure';
import { version as nodeVersion } from '../../package.json';
import { deriveAirnodeAddress, shortenAirnodeAddress } from '../utils';
import {
airnodeAddressReadable,
cloudProviderReadable,
hashDeployment,
hashDeploymentVersion,
timestampReadable,
} from '../utils/cli';
import { deriveAirnodeAddress } from '../utils';
import { airnodeAddressReadable, cloudProviderReadable, timestampReadable } from '../utils/cli';

export const TF_STATE_FILENAME = 'default.tfstate';

Expand Down Expand Up @@ -192,8 +188,7 @@ export function prepareCloudProviderAirnodeApplyDestoryArguments(
}

export type AirnodeApplyDestroyVariables = {
airnodeAddressShort: string;
stage: string;
deploymentId: string;
configPath?: string;
secretsPath?: string;
handlerDir: string;
Expand All @@ -202,19 +197,11 @@ export type AirnodeApplyDestroyVariables = {
};

export function prepareAirnodeApplyDestroyArguments(variables: AirnodeApplyDestroyVariables): CommandArg[] {
const {
airnodeAddressShort,
stage,
configPath,
secretsPath,
handlerDir,
disableConcurrencyReservations,
airnodeWalletPrivateKey,
} = variables;
const { deploymentId, configPath, secretsPath, handlerDir, disableConcurrencyReservations, airnodeWalletPrivateKey } =
variables;

return [
['var', 'airnode_address_short', airnodeAddressShort],
['var', 'stage', stage],
['var', 'deployment_id', deploymentId],
['var', 'configuration_file', configPath ? path.resolve(configPath) : 'NULL'],
['var', 'secrets_file', secretsPath ? path.resolve(secretsPath) : 'NULL'],
['var', 'handler_dir', handlerDir],
Expand Down Expand Up @@ -249,17 +236,22 @@ export async function terraformAirnodeApply(
configPath: string,
secretsPath: string
) {
const { airnodeWalletMnemonic, stage, httpGateway, httpSignedDataGateway } = config.nodeSettings;
const {
airnodeWalletMnemonic,
stage,
httpGateway,
httpSignedDataGateway,
nodeVersion: configNodeVersion,
} = config.nodeSettings;
const cloudProvider = config.nodeSettings.cloudProvider as CloudProvider;
const airnodeAddressShort = shortenAirnodeAddress(deriveAirnodeAddress(airnodeWalletMnemonic));
const airnodeAddress = deriveAirnodeAddress(airnodeWalletMnemonic);
const airnodeWalletPrivateKey = evm.getAirnodeWallet(config).privateKey;
const maxConcurrency = config.chains.reduce((concurrency: number, chain) => concurrency + chain.maxConcurrency, 0);

await terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketDeploymentPath);

const commonArguments = prepareAirnodeApplyDestroyArguments({
airnodeAddressShort,
stage,
deploymentId: deriveDeploymentId(cloudProvider, airnodeAddress, stage, configNodeVersion),
configPath,
secretsPath,
handlerDir,
Expand Down Expand Up @@ -399,16 +391,14 @@ export const deployAirnode = async (config: Config, configPath: string, secretsP
export async function terraformAirnodeDestroy(
execOptions: child.ExecOptions,
cloudProvider: CloudProvider,
airnodeAddressShort: string,
stage: string,
deploymentId: string,
bucket: Bucket,
bucketDeploymentPath: string
) {
await terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketDeploymentPath);

const commonArguments = prepareAirnodeApplyDestroyArguments({
airnodeAddressShort,
stage,
deploymentId,
handlerDir,
disableConcurrencyReservations: cloudProvider.disableConcurrencyReservations,
});
Expand Down Expand Up @@ -486,10 +476,10 @@ async function fetchDeployments(cloudProviderType: CloudProvider['type'], deploy

const cloudProvider = interpolatedConfig.nodeSettings.cloudProvider as CloudProvider;
const airnodeVersion = interpolatedConfig.nodeSettings.nodeVersion;
const id = hashDeployment(cloudProvider, airnodeAddress, stage, airnodeVersion);
const id = deriveDeploymentId(cloudProvider, airnodeAddress, stage, airnodeVersion);

const deploymentVersions = Object.keys(stageDirectory.children).map((versionTimestamp) => ({
id: hashDeploymentVersion(cloudProvider, airnodeAddress, stage, airnodeVersion, versionTimestamp),
id: deriveDeploymentVersionId(cloudProvider, airnodeAddress, stage, airnodeVersion, versionTimestamp),
timestamp: versionTimestamp,
}));
const deployment = {
Expand Down Expand Up @@ -564,17 +554,9 @@ export async function removeAirnode(deploymentId: string) {
}

logger.debug('Removing Airnode via Terraform recipes');
const airnodeAddressShort = shortenAirnodeAddress(airnodeAddress);
const airnodeTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airnode'));
const execOptions = { cwd: airnodeTmpDir };
await terraformAirnodeDestroy(
execOptions,
cloudProvider,
airnodeAddressShort,
stage,
bucket,
bucketLatestDeploymentPath
);
await terraformAirnodeDestroy(execOptions, cloudProvider, deploymentId, bucket, bucketLatestDeploymentPath);
fs.rmSync(airnodeTmpDir, { recursive: true });

// Refreshing the bucket content because the source code archives were removed by Terraform
Expand Down
31 changes: 0 additions & 31 deletions packages/airnode-deployer/src/utils/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { CloudProvider } from '@api3/airnode-node';
import {
airnodeAddressReadable,
cloudProviderReadable,
hashDeployment,
timestampReadable,
longArguments,
printableArguments,
hashDeploymentVersion,
} from './cli';

describe('longArguments', () => {
Expand All @@ -31,35 +29,6 @@ describe('printableArguments', () => {
});
});

describe('hashDeployment', () => {
it('creates a unique hash from deployment details', () => {
const cloudProvider = {
type: 'aws',
region: 'us-east-1',
} as CloudProvider;
const airnodeAddress = '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace';
const stage = 'dev';
const airnodeVersion = '0.9.5';

expect(hashDeployment(cloudProvider, airnodeAddress, stage, airnodeVersion)).toEqual('aws521d7174');
});
});

describe('hashDeploymentVersion', () => {
it('creates a unique hash from deployment version details', () => {
const cloudProvider = {
type: 'aws',
region: 'us-east-1',
} as CloudProvider;
const airnodeAddress = '0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace';
const stage = 'dev';
const airnodeVersion = '0.9.5';
const timestamp = '1664256335137';

expect(hashDeploymentVersion(cloudProvider, airnodeAddress, stage, airnodeVersion, timestamp)).toEqual('e2d3286d');
});
});

describe('cloudProviderReadable', () => {
it('returns a human-readble cloud provider identification', () => {
const cloudProvider = {
Expand Down
37 changes: 0 additions & 37 deletions packages/airnode-deployer/src/utils/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as crypto from 'crypto';
import { format } from 'date-fns-tz';
import { CloudProvider } from '@api3/airnode-node';
import join from 'lodash/join';
Expand All @@ -15,42 +14,6 @@ export function printableArguments(args: string[]) {
);
}

function cloudProviderHashElements(cloudProvider: CloudProvider) {
const hashElements = [cloudProvider.type, cloudProvider.region];
if (cloudProvider.type === 'gcp') {
hashElements.push(cloudProvider.projectId);
}

return hashElements;
}

export function hashDeployment(
cloudProvider: CloudProvider,
airnodeAddress: string,
stage: string,
airnodeVersion: string
) {
return `${cloudProvider.type}${crypto
.createHash('sha256')
.update([...cloudProviderHashElements(cloudProvider), airnodeAddress, stage, airnodeVersion].join(''))
.digest('hex')
.substring(0, 8)}`;
}

export function hashDeploymentVersion(
cloudProvider: CloudProvider,
airnodeAddress: string,
stage: string,
airnodeVersion: string,
timestamp: string
) {
return crypto
.createHash('sha256')
.update([...cloudProviderHashElements(cloudProvider), airnodeAddress, stage, airnodeVersion, timestamp].join(''))
.digest('hex')
.substring(0, 8);
}

export function cloudProviderReadable(cloudProvider: CloudProvider) {
return `${cloudProvider.type.toUpperCase()} (${cloudProvider.region})`;
}
Expand Down
12 changes: 0 additions & 12 deletions packages/airnode-deployer/src/utils/evm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as ethers from 'ethers';
import { goSync } from '@api3/promise-utils';
import { logAndReturnError } from './infrastructure';
import * as logger from '../utils/logger';

export function deriveAirnodeAddress(mnemonic: string) {
Expand All @@ -14,13 +12,3 @@ export function deriveAirnodeXpub(mnemonic: string) {
const airnodeHdNode = ethers.utils.HDNode.fromMnemonic(mnemonic).derivePath("m/44'/60'/0'");
return airnodeHdNode.neuter().extendedKey;
}

export function shortenAirnodeAddress(airnodeAddress: string) {
logger.debug('Shortening Airnode Address');

const goGetAddress = goSync(() => ethers.utils.getAddress(airnodeAddress));
if (!goGetAddress.success) throw logAndReturnError('"airnodeAddress" is not a valid hex string');

// NOTE: AWS doesn't allow uppercase letters in S3 bucket and lambda function names
return airnodeAddress.substring(2, 9).toLowerCase();
}
9 changes: 3 additions & 6 deletions packages/airnode-deployer/src/utils/files.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as fs from 'fs';
import * as dotenv from 'dotenv';
import { CloudProvider, Config } from '@api3/airnode-node';
import { CloudProvider, Config, deriveDeploymentId } from '@api3/airnode-node';
import { parseReceipt, receipt } from '@api3/airnode-validator';
import { goSync } from '@api3/promise-utils';
import { logAndReturnError } from './infrastructure';
import { hashDeployment } from './cli';
import * as logger from '../utils/logger';
import { deriveAirnodeAddress, deriveAirnodeXpub, shortenAirnodeAddress } from '../utils';
import { deriveAirnodeAddress, deriveAirnodeXpub } from '../utils';

export function parseSecretsFile(secretsPath: string) {
logger.debug('Parsing secrets file');
Expand All @@ -20,17 +19,15 @@ export function parseSecretsFile(secretsPath: string) {
export function writeReceiptFile(receiptFilename: string, config: Config, timestamp: string, success: boolean) {
const mnemonic = config.nodeSettings.airnodeWalletMnemonic;
const airnodeAddress = deriveAirnodeAddress(mnemonic);
const airnodeAddressShort = shortenAirnodeAddress(airnodeAddress);
const { stage, nodeVersion } = config.nodeSettings;
const cloudProvider = config.nodeSettings.cloudProvider as CloudProvider;
const receipt: receipt.Receipt = {
airnodeWallet: {
airnodeAddress,
airnodeAddressShort,
airnodeXpub: deriveAirnodeXpub(mnemonic),
},
deployment: {
deploymentId: hashDeployment(cloudProvider, airnodeAddress, stage, nodeVersion),
deploymentId: deriveDeploymentId(cloudProvider, airnodeAddress, stage, nodeVersion),
cloudProvider,
stage,
nodeVersion,
Expand Down
18 changes: 6 additions & 12 deletions packages/airnode-deployer/terraform/aws/variables.tf
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
locals {
# Be aware when using `name-prefix` for naming resources
# as it can be up to 32 characters long:
# as it can be up to 19 characters long:
#
# infrastructure_name - "airnode" - 7 characters
# airnode_address_short - 7 characters
# stage - up to 16 characters
# dashes between - 2 characters
name_prefix = "${var.infrastructure_name}-${var.airnode_address_short}-${var.stage}"
# deployment_id - 11 characters
# dash between - 1 character
name_prefix = "${var.infrastructure_name}-${var.deployment_id}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we make the deployment ID a bit longer? The 8 characters length seems to me unreasoanbly low.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Are you worried about collisions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and I do not see a reason why not make it longer when the character limit allows it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reason would be so that the outputs of list and info deployer commands won't be that wide. But I'll take a look. Maybe 12 or even 16 would be doable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep the 8-character deployment IDs for now. The rows from the list and info commands are around 120 characters long now and I don't want to increase that more. Also, the probability of a collision is still quite low. From a quick googling the problem is not with the hash function creating a collision but rather running into a collision purely based on limited possibilities from the pool of strings of that length and composition (https://stackoverflow.com/a/30565118). But even that, gives us about 65k possible hashes for 8-character long substrings (https://www.bluebill.net/hash_collisions.html). No one account will have that.

}

variable "aws_region" {
description = "AWS region for deployment"
default = "us-east-1"
}

variable "stage" {
description = "Infrastructure environment"
default = "testing"
}

variable "infrastructure_name" {
description = "Infrastructure name"
default = "airnode"
}

variable "airnode_address_short" {
description = "Airnode address (short)"
variable "deployment_id" {
description = "ID of the deployment"
}

variable "configuration_file" {
Expand Down
18 changes: 6 additions & 12 deletions packages/airnode-deployer/terraform/gcp/variables.tf
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
locals {
# Be aware when using `name-prefix` for naming resources
# as it can be up to 32 characters long:
# as it can be up to 19 characters long:
#
# infrastructure_name - "airnode" - 7 characters
# airnode_address_short - 7 characters
# stage - up to 16 characters
# dashes between - 2 characters
name_prefix = "${var.infrastructure_name}-${var.airnode_address_short}-${var.stage}"
# deployment_id - 11 characters
# dash between - 1 character
name_prefix = "${var.infrastructure_name}-${var.deployment_id}"
}

variable "gcp_project" {
Expand All @@ -18,18 +17,13 @@ variable "gcp_region" {
default = "us-east1"
}

variable "stage" {
description = "Infrastructure environment"
default = "testing"
}

variable "infrastructure_name" {
description = "Infrastructure name"
default = "airnode"
}

variable "airnode_address_short" {
description = "Airnode address (short)"
variable "deployment_id" {
description = "ID of the deployment"
}

variable "configuration_file" {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"airnodeWallet": {
"airnodeAddress": "0xA30CA71Ba54E83127214D3271aEA8F5D6bD4Dace",
"airnodeAddressShort": "a30ca71",
"airnodeXpub": "xpub6C8tvRgYkjNVaGMtpyZf4deBcUQHf7vgWUraVxY6gYiZhBYbPkFkLLWJzUUeVFdkKpVtatmXHX8kB76xgfmTpVZWbVWdq1rneaAY6a8RtbY"
},
"deployment": {
Expand Down
Loading