-
Notifications
You must be signed in to change notification settings - Fork 362
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,628 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.', | ||
}); | ||
} |
Oops, something went wrong.