Skip to content

Commit

Permalink
Merge branch 'main' into sdk-patch
Browse files Browse the repository at this point in the history
  • Loading branch information
yorhodes authored May 23, 2024
2 parents 5b10569 + b22a0f4 commit 24201d8
Show file tree
Hide file tree
Showing 11 changed files with 1,628 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-ducks-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---

Add hyperlane validator address command to retrieve validator address from AWS
5 changes: 5 additions & 0 deletions .github/workflows/monorepo-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
paths:
# For now, because this image is only used to use `infra`, we just build for infra changes
- 'typescript/infra/**'
- 'Dockerfile'
- '.dockerignore'
concurrency:
group: build-push-monorepo-${{ github.ref }}
cancel-in-progress: true
Expand Down Expand Up @@ -74,3 +76,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# To always fetch the latest registry, we use the date as the cache key
build-args: |
REGISTRY_CACHE=${{ steps.taggen.outputs.TAG_DATE }}
26 changes: 24 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ jobs:
cli-e2e:
runs-on: larger-runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main')
needs: [yarn-build]
needs: [yarn-build, checkout-registry]
strategy:
matrix:
include:
Expand Down Expand Up @@ -382,6 +382,17 @@ jobs:
!./rust
key: ${{ github.event.pull_request.head.sha || github.sha }}

# A workaround for relative paths not being supported by actions/cache.
# See https://github.com/actions/upload-artifact/issues/176#issuecomment-1367855630.
- run: echo "REGISTRY_URI_ABSOLUTE=$(realpath $REGISTRY_URI)" >> $GITHUB_ENV

- name: registry-cache
uses: actions/cache@v3
with:
path: |
${{ env.REGISTRY_URI_ABSOLUTE }}
key: hyperlane-registry-${{ github.event.pull_request.head.sha || github.sha }}

- name: cargo-cache
uses: actions/cache@v3
with:
Expand All @@ -394,7 +405,7 @@ jobs:

env-test:
runs-on: ubuntu-latest
needs: [yarn-build]
needs: [yarn-build, checkout-registry]
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -422,6 +433,17 @@ jobs:
!./rust
key: ${{ github.event.pull_request.head.sha || github.sha }}

# A workaround for relative paths not being supported by actions/cache.
# See https://github.com/actions/upload-artifact/issues/176#issuecomment-1367855630.
- run: echo "REGISTRY_URI_ABSOLUTE=$(realpath $REGISTRY_URI)" >> $GITHUB_ENV

- name: registry-cache
uses: actions/cache@v3
with:
path: |
${{ env.REGISTRY_URI_ABSOLUTE }}
key: hyperlane-registry-${{ github.event.pull_request.head.sha || github.sha }}

- name: Fork test ${{ matrix.environment }} ${{ matrix.module }} ${{ matrix.chain }} deployment
run: cd typescript/infra && ./fork.sh ${{ matrix.environment }} ${{ matrix.module }} ${{ matrix.chain }}

Expand Down
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ COPY typescript ./typescript
COPY solidity ./solidity

RUN yarn build

ENV REGISTRY_URI="/hyperlane-registry"
# To allow us to avoid caching the registry clone, we use a build-time arg to force
# the below steps to be re-run if this arg is changed.
ARG REGISTRY_CACHE="default"

RUN git clone https://github.com/hyperlane-xyz/hyperlane-registry.git "$REGISTRY_URI"
2 changes: 2 additions & 0 deletions typescript/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from './src/commands/options.js';
import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js';
import { validatorCommand } from './src/commands/validator.js';
import { contextMiddleware } from './src/context/context.js';
import { configureLogger, errorRed } from './src/logger.js';
import { checkVersion } from './src/utils/version-check.js';
Expand Down Expand Up @@ -55,6 +56,7 @@ try {
.command(ismCommand)
.command(sendCommand)
.command(statusCommand)
.command(validatorCommand)
.version(VERSION)
.demandCommand()
.strict()
Expand Down
3 changes: 3 additions & 0 deletions typescript/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
"version": "3.12.2",
"description": "A command-line utility for common Hyperlane operations",
"dependencies": {
"@aws-sdk/client-kms": "^3.577.0",
"@aws-sdk/client-s3": "^3.577.0",
"@hyperlane-xyz/registry": "^1.0.7",
"@hyperlane-xyz/sdk": "3.12.2",
"@hyperlane-xyz/utils": "3.12.2",
"@inquirer/prompts": "^3.0.0",
"asn1.js": "^5.4.1",
"bignumber.js": "^9.1.1",
"chalk": "^5.3.0",
"ethers": "^5.7.2",
Expand Down
32 changes: 32 additions & 0 deletions typescript/cli/src/commands/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,35 @@ export const addressCommandOption = (
description,
demandOption,
});

/* Validator options */
export const awsAccessKeyCommandOption: Options = {
type: 'string',
description: 'AWS access key of IAM user associated with validator',
default: ENV.AWS_ACCESS_KEY_ID,
defaultDescription: 'process.env.AWS_ACCESS_KEY_ID',
};

export const awsSecretKeyCommandOption: Options = {
type: 'string',
description: 'AWS secret access key of IAM user associated with validator',
default: ENV.AWS_SECRET_ACCESS_KEY,
defaultDescription: 'process.env.AWS_SECRET_ACCESS_KEY',
};

export const awsRegionCommandOption: Options = {
type: 'string',
describe: 'AWS region associated with validator',
default: ENV.AWS_REGION,
defaultDescription: 'process.env.AWS_REGION',
};

export const awsBucketCommandOption: Options = {
type: 'string',
describe: 'AWS S3 bucket containing validator signatures and announcement',
};

export const awsKeyIdCommandOption: Options = {
type: 'string',
describe: 'Key ID from AWS KMS',
};
51 changes: 51 additions & 0 deletions typescript/cli/src/commands/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { CommandModule } from 'yargs';

import { CommandModuleWithContext } from '../context/types.js';
import { log } from '../logger.js';
import { getValidatorAddress } from '../validator/address.js';

import {
awsAccessKeyCommandOption,
awsBucketCommandOption,
awsKeyIdCommandOption,
awsRegionCommandOption,
awsSecretKeyCommandOption,
} from './options.js';

// Parent command to help configure and set up Hyperlane validators
export const validatorCommand: CommandModule = {
command: 'validator',
describe: 'Configure and manage Hyperlane validators',
builder: (yargs) => yargs.command(addressCommand).demandCommand(),
handler: () => log('Command required'),
};

// If AWS access key needed for future validator commands, move to context
const addressCommand: CommandModuleWithContext<{
accessKey: string;
secretKey: string;
region: string;
bucket: string;
keyId: string;
}> = {
command: 'address',
describe: 'Get the validator address from S3 bucket or KMS key ID',
builder: {
'access-key': awsAccessKeyCommandOption,
'secret-key': awsSecretKeyCommandOption,
region: awsRegionCommandOption,
bucket: awsBucketCommandOption,
'key-id': awsKeyIdCommandOption,
},
handler: async ({ context, accessKey, secretKey, region, bucket, keyId }) => {
await getValidatorAddress({
context,
accessKey,
secretKey,
region,
bucket,
keyId,
});
process.exit(0);
},
};
3 changes: 3 additions & 0 deletions typescript/cli/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const envScheme = z.object({
HYP_KEY: z.string().optional(),
ANVIL_IP_ADDR: z.string().optional(),
ANVIL_PORT: z.number().optional(),
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
AWS_REGION: z.string().optional(),
});

const parsedEnv = envScheme.safeParse(process.env);
Expand Down
166 changes: 166 additions & 0 deletions typescript/cli/src/validator/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { GetPublicKeyCommand, KMSClient } from '@aws-sdk/client-kms';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { input } from '@inquirer/prompts';
// @ts-ignore
import asn1 from 'asn1.js';
import { ethers } from 'ethers';

import { assert } from '@hyperlane-xyz/utils';

import { CommandContext } from '../context/types.js';
import { log, logBlue } from '../logger.js';

export async function getValidatorAddress({
context,
accessKey,
secretKey,
region,
bucket,
keyId,
}: {
context: CommandContext;
accessKey?: string;
secretKey?: string;
region?: string;
bucket?: string;
keyId?: string;
}) {
if (!bucket && !keyId) {
throw new Error('Must provide either an S3 bucket or a KMS Key ID.');
}

// Query user for AWS parameters if not passed in or stored as .env variables
accessKey ||= await getAccessKeyId(context.skipConfirmation);
secretKey ||= await getSecretAccessKey(context.skipConfirmation);
region ||= await getRegion(context.skipConfirmation);

assert(accessKey, 'No access key ID set.');
assert(secretKey, 'No secret access key set.');
assert(region, 'No AWS region set.');

let validatorAddress;
if (bucket) {
validatorAddress = await getAddressFromBucket(
bucket,
accessKey,
secretKey,
region,
);
} else {
validatorAddress = await getAddressFromKey(
keyId!,
accessKey,
secretKey,
region,
);
}

logBlue('Validator address is: ');
log(validatorAddress);
}

/**
* Displays validator key address from
* validator announcement S3 bucket.
*/
async function getAddressFromBucket(
bucket: string,
accessKeyId: string,
secretAccessKey: string,
region: string,
) {
const s3Client = new S3Client({
region: region,
credentials: {
accessKeyId,
secretAccessKey,
},
});

const { Body } = await s3Client.send(
new GetObjectCommand({
Bucket: bucket,
Key: 'announcement.json',
}),
);

if (Body) {
const announcement = JSON.parse(await Body?.transformToString());
return announcement['value']['validator'];
} else {
throw new Error('Announcement file announcement.json not found in bucket');
}
}

/**
* Logs validator key address using AWS KMS key ID.
* Taken from github.com/tkporter/get-aws-kms-address/
*/
async function getAddressFromKey(
keyId: string,
accessKeyId: string,
secretAccessKey: string,
region: string,
) {
const client = new KMSClient({
region: region,
credentials: {
accessKeyId,
secretAccessKey,
},
});

const publicKeyResponse = await client.send(
new GetPublicKeyCommand({ KeyId: keyId }),
);

return getEthereumAddress(Buffer.from(publicKeyResponse.PublicKey!));
}

const EcdsaPubKey = asn1.define('EcdsaPubKey', function (this: any) {
this.seq().obj(
this.key('algo').seq().obj(this.key('a').objid(), this.key('b').objid()),
this.key('pubKey').bitstr(),
);
});

function getEthereumAddress(publicKey: Buffer): string {
// The public key is ASN1 encoded in a format according to
// https://tools.ietf.org/html/rfc5480#section-2
const res = EcdsaPubKey.decode(publicKey, 'der');
let pubKeyBuffer: Buffer = res.pubKey.data;

// The public key starts with a 0x04 prefix that needs to be removed
// more info: https://www.oreilly.com/library/view/mastering-ethereum/9781491971932/ch04.html
pubKeyBuffer = pubKeyBuffer.slice(1, pubKeyBuffer.length);

const address = ethers.utils.keccak256(pubKeyBuffer); // keccak256 hash of publicKey
return `0x${address.slice(-40)}`; // take last 20 bytes as ethereum address
}

async function getAccessKeyId(skipConfirmation: boolean) {
if (skipConfirmation) throw new Error('No AWS access key ID set.');
else
return await input({
message:
'Please enter AWS access key ID or use the AWS_ACCESS_KEY_ID environment variable.',
});
}

async function getSecretAccessKey(skipConfirmation: boolean) {
if (skipConfirmation) throw new Error('No AWS secret access key set.');
else
return await input({
message:
'Please enter AWS secret access key or use the AWS_SECRET_ACCESS_KEY environment variable.',
});
}

async function getRegion(skipConfirmation: boolean) {
if (skipConfirmation) throw new Error('No AWS region set.');
else
return await input({
message:
'Please enter AWS region or use the AWS_REGION environment variable.',
});
}
Loading

0 comments on commit 24201d8

Please sign in to comment.