Skip to content

Commit

Permalink
Add support for new signed checkpoint types in verify-validators (#2516)
Browse files Browse the repository at this point in the history
### Description

This makes it easy to see who is running the latest version of the
validator.
  • Loading branch information
asaj authored Jul 12, 2023
1 parent f413a66 commit 2c4b861
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 73 deletions.
1 change: 0 additions & 1 deletion typescript/infra/scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export function withModuleAndFork<T>(args: yargs.Argv<T>) {
.choices('module', Object.values(Modules))
.demandOption('module')
.alias('m', 'module')

.describe('fork', 'network to fork')
.choices('fork', Object.values(Chains))
.alias('f', 'fork');
Expand Down
9 changes: 7 additions & 2 deletions typescript/infra/scripts/verify-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { deployEnvToSdkEnv } from '../src/config/environment';
import { getArgs, getEnvironmentConfig, getValidatorsByChain } from './utils';

async function main() {
const { environment } = await getArgs().argv;
const { environment, withMessageId } = await getArgs()
.boolean('with-message-id')
.default('with-message-id', true).argv;
const config = getEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider();
const core = HyperlaneCore.fromEnvironment(
Expand All @@ -33,7 +35,10 @@ async function main() {
const address = prospectiveValidator.address;
const bucket = prospectiveValidator.s3Bucket.bucket;
try {
const metrics = await prospectiveValidator.compare(controlValidator);
const metrics = await prospectiveValidator.compare(
controlValidator,
withMessageId,
);
const valid =
metrics.filter((metric) => metric.status !== CheckpointStatus.VALID)
.length === 0;
Expand Down
112 changes: 73 additions & 39 deletions typescript/infra/src/agents/aws/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,18 @@ interface CheckpointMetric {
index: number;
}

// TODO: merge with types.Checkpoint
/**
* Shape of a checkpoint in S3 as published by the agent.
*/
interface S3Checkpoint {
value: {
outbox_domain: number;
root: string;
index: number;
};
signature: {
r: string;
s: string;
v: number;
};
interface SignedCheckpoint {
checkpoint: types.Checkpoint;
messageId?: types.HexString;
signature: types.SignatureLike;
}

type CheckpointReceipt = S3Receipt<types.Checkpoint>;
type S3CheckpointReceipt = S3Receipt<SignedCheckpoint>;

const checkpointKey = (checkpointIndex: number) =>
`checkpoint_${checkpointIndex}.json`;
const checkpointWithMessageIdKey = (checkpointIndex: number) =>
`checkpoint_${checkpointIndex}_with_id.json`;
const LATEST_KEY = 'checkpoint_latest_index.json';
const ANNOUNCEMENT_KEY = 'announcement.json';
const LOCATION_PREFIX = 's3://';
Expand Down Expand Up @@ -99,7 +90,11 @@ export class S3Validator extends BaseValidator {
return latestCheckpointIndex.data;
}

async compare(other: S3Validator, count = 20): Promise<CheckpointMetric[]> {
async compare(
other: S3Validator,
withId = false,
count = 5,
): Promise<CheckpointMetric[]> {
const latestCheckpointIndex = await this.s3Bucket.getS3Obj<number>(
LATEST_KEY,
);
Expand Down Expand Up @@ -136,8 +131,11 @@ export class S3Validator extends BaseValidator {
const stop = Math.max(maxIndex - count, 0);

for (; checkpointIndex > stop; checkpointIndex--) {
const expected = await other.getCheckpointReceipt(checkpointIndex);
const actual = await this.getCheckpointReceipt(checkpointIndex);
const expected = await other.getCheckpointReceipt(
checkpointIndex,
withId,
);
const actual = await this.getCheckpointReceipt(checkpointIndex, withId);

const metric: CheckpointMetric = {
status: CheckpointStatus.MISSING,
Expand All @@ -146,18 +144,42 @@ export class S3Validator extends BaseValidator {

if (actual) {
metric.status = CheckpointStatus.VALID;
if (!this.matchesSigner(actual.data)) {
const signerAddress = this.recoverAddressFromCheckpoint(actual.data);
if (
!this.matchesSigner(
actual.data.checkpoint,
actual.data.signature,
actual.data.messageId,
)
) {
const signerAddress = this.recoverAddressFromCheckpoint(
actual.data.checkpoint,
actual.data.signature,
actual.data.messageId,
);
metric.violation = `signer mismatch: expected ${this.address}, received ${signerAddress}`;
}

if (expected) {
metric.delta =
actual.modified.getSeconds() - expected.modified.getSeconds();
if (expected.data.root !== actual.data.root) {
metric.violation = `root mismatch: expected ${expected.data.root}, received ${actual.data.root}`;
} else if (expected.data.index !== actual.data.index) {
metric.violation = `index mismatch: expected ${expected.data.index}, received ${actual.data.index}`;
if (expected.data.checkpoint.root !== actual.data.checkpoint.root) {
metric.violation = `root mismatch: expected ${expected.data.checkpoint.root}, received ${actual.data.checkpoint.root}`;
} else if (
expected.data.checkpoint.index !== actual.data.checkpoint.index
) {
metric.violation = `index mismatch: expected ${expected.data.checkpoint.index}, received ${actual.data.checkpoint.index}`;
} else if (
expected.data.checkpoint.mailbox_address !==
actual.data.checkpoint.mailbox_address
) {
metric.violation = `mailbox address mismatch: expected ${expected.data.checkpoint.mailbox_address}, received ${actual.data.checkpoint.mailbox_address}`;
} else if (
expected.data.checkpoint.mailbox_domain !==
actual.data.checkpoint.mailbox_domain
) {
metric.violation = `mailbox domain mismatch: expected ${expected.data.checkpoint.mailbox_domain}, received ${actual.data.checkpoint.mailbox_domain}`;
} else if (expected.data.messageId !== actual.data.messageId) {
metric.violation = `message id mismatch: expected ${expected.data.messageId}, received ${actual.data.messageId}`;
}
}

Expand All @@ -178,24 +200,36 @@ export class S3Validator extends BaseValidator {

private async getCheckpointReceipt(
index: number,
): Promise<CheckpointReceipt | undefined> {
const key = checkpointKey(index);
const s3Object = await this.s3Bucket.getS3Obj<S3Checkpoint>(key);
withId = false,
): Promise<S3CheckpointReceipt | undefined> {
const key = withId
? checkpointWithMessageIdKey(index)
: checkpointKey(index);
const s3Object = await this.s3Bucket.getS3Obj<
types.S3Checkpoint | types.S3CheckpointWithId
>(key);
if (!s3Object) {
return;
}
const checkpoint: types.Checkpoint = {
signature: s3Object.data.signature,
// @ts-ignore Old checkpoints might still be in this format
...s3Object.data.checkpoint,
...s3Object.data.value,
};
if (!utils.isCheckpoint(checkpoint)) {
if (utils.isS3Checkpoint(s3Object.data)) {
return {
data: {
checkpoint: s3Object.data.value,
signature: s3Object.data.signature,
},
modified: s3Object.modified,
};
} else if (utils.isS3CheckpointWithId(s3Object.data)) {
return {
data: {
checkpoint: s3Object.data.value.checkpoint,
messageId: s3Object.data.value.message_id,
signature: s3Object.data.signature,
},
modified: s3Object.modified,
};
} else {
throw new Error('Failed to parse checkpoint');
}
return {
data: checkpoint,
modified: s3Object.modified,
};
}
}
4 changes: 2 additions & 2 deletions typescript/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * as logging from './src/logging';
export * as types from './src/types';
export * as utils from './src/utils';
export { Validator, BaseValidator } from './src/validator';
export * as logging from './src/logging';
export { error, warn, log, debug, trace } from './src/logging';
export { BaseValidator, Validator } from './src/validator';
17 changes: 17 additions & 0 deletions typescript/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ export type MerkleProof = {
export type Checkpoint = {
root: string;
index: number; // safe because 2 ** 32 leaves < Number.MAX_VALUE
mailbox_domain: Domain;
mailbox_address: Address;
};

/**
* Shape of a checkpoint in S3 as published by the agent.
*/
export type S3CheckpointWithId = {
value: {
checkpoint: Checkpoint;
message_id: HexString;
};
signature: SignatureLike;
};

export type S3Checkpoint = {
value: Checkpoint;
signature: SignatureLike;
};

Expand Down
34 changes: 26 additions & 8 deletions typescript/utils/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
HexString,
ParsedLegacyMultisigIsmMetadata,
ParsedMessage,
S3Checkpoint,
S3CheckpointWithId,
SignatureLike,
} from './types';

export function exclude<T>(item: T, list: T[]) {
Expand Down Expand Up @@ -276,17 +279,32 @@ export function streamToString(stream: NodeJS.ReadableStream): Promise<string> {
});
}

export function isCheckpoint(obj: any): obj is Checkpoint {
const isValidSignature =
typeof obj.signature === 'string'
? ethers.utils.isHexString(obj.signature)
: ethers.utils.isHexString(obj.signature.r) &&
ethers.utils.isHexString(obj.signature.s) &&
Number.isSafeInteger(obj.signature.v);
function isValidSignature(signature: any): signature is SignatureLike {
return typeof signature === 'string'
? ethers.utils.isHexString(signature)
: ethers.utils.isHexString(signature.r) &&
ethers.utils.isHexString(signature.s) &&
Number.isSafeInteger(signature.v);
}

export function isS3Checkpoint(obj: any): obj is S3Checkpoint {
return isValidSignature(obj.signature) && isCheckpoint(obj.value);
}

export function isS3CheckpointWithId(obj: any): obj is S3CheckpointWithId {
return (
isValidSignature(obj.signature) &&
isCheckpoint(obj.value.checkpoint) &&
ethers.utils.isHexString(obj.value.message_id)
);
}

export function isCheckpoint(obj: any): obj is Checkpoint {
const isValidRoot = ethers.utils.isHexString(obj.root);
const isValidIndex = Number.isSafeInteger(obj.index);
return isValidIndex && isValidRoot && isValidSignature;
const isValidMailbox = ethers.utils.isHexString(obj.mailbox_address);
const isValidDomain = Number.isSafeInteger(obj.mailbox_domain);
return isValidIndex && isValidRoot && isValidMailbox && isValidDomain;
}

/**
Expand Down
65 changes: 44 additions & 21 deletions typescript/utils/src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ethers } from 'ethers';

import { Address, Checkpoint, Domain, HexString } from './types';
import {
Address,
Checkpoint,
Domain,
HexString,
S3Checkpoint,
SignatureLike,
} from './types';
import { domainHash } from './utils';

/**
Expand All @@ -21,34 +28,45 @@ export class BaseValidator {
return domainHash(this.localDomain, this.mailbox);
}

message(root: HexString, index: number) {
return ethers.utils.solidityPack(
['bytes32', 'bytes32', 'uint32'],
[this.domainHash(), root, index],
);
message(checkpoint: Checkpoint, messageId?: HexString) {
let types = ['bytes32', 'bytes32', 'uint32'];
let values = [this.domainHash(), checkpoint.root, checkpoint.index];
if (!!messageId) {
types.push('bytes32');
values.push(messageId);
}
return ethers.utils.solidityPack(types, values);
}

messageHash(root: HexString, index: number) {
const message = this.message(root, index);
messageHash(checkpoint: Checkpoint, messageId?: HexString) {
const message = this.message(checkpoint, messageId);
return ethers.utils.arrayify(ethers.utils.keccak256(message));
}

recoverAddressFromCheckpoint(checkpoint: Checkpoint): Address {
const msgHash = this.messageHash(checkpoint.root, checkpoint.index);
return ethers.utils.verifyMessage(msgHash, checkpoint.signature);
recoverAddressFromCheckpoint(
checkpoint: Checkpoint,
signature: SignatureLike,
messageId?: HexString,
): Address {
const msgHash = this.messageHash(checkpoint, messageId);
return ethers.utils.verifyMessage(msgHash, signature);
}

matchesSigner(checkpoint: Checkpoint) {
matchesSigner(
checkpoint: Checkpoint,
signature: SignatureLike,
messageId?: HexString,
) {
return (
this.recoverAddressFromCheckpoint(checkpoint).toLowerCase() ===
this.address.toLowerCase()
this.recoverAddressFromCheckpoint(
checkpoint,
signature,
messageId,
).toLowerCase() === this.address.toLowerCase()
);
}
}

/**
* Extension of BaseValidator that includes ethers signing utilities.
*/
export class Validator extends BaseValidator {
constructor(
protected signer: ethers.Signer,
Expand All @@ -72,12 +90,17 @@ export class Validator extends BaseValidator {
);
}

async signCheckpoint(root: HexString, index: number): Promise<Checkpoint> {
const msgHash = this.messageHash(root, index);
const signature = await this.signer.signMessage(msgHash);
return {
async signCheckpoint(root: HexString, index: number): Promise<S3Checkpoint> {
const checkpoint = {
root,
index,
mailbox_address: this.mailbox,
mailbox_domain: this.localDomain,
};
const msgHash = this.messageHash(checkpoint);
const signature = await this.signer.signMessage(msgHash);
return {
value: checkpoint,
signature,
};
}
Expand Down

0 comments on commit 2c4b861

Please sign in to comment.