@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
188 lines • 8.88 kB
JavaScript
import { joinSignature, splitSignature } from 'ethers/lib/utils.js';
import { MerkleTreeHook__factory } from '@hyperlane-xyz/core';
import { assert, bytes32ToAddress, chunk, ensure0x, eqAddress, eqAddressEvm, fromHexString, rootLogger, strip0x, toHexString, } from '@hyperlane-xyz/utils';
import { S3Validator } from '../../aws/validator.js';
import { IsmType } from '../types.js';
const MerkleTreeInterface = MerkleTreeHook__factory.createInterface();
const SIGNATURE_LENGTH = 65;
export class MultisigMetadataBuilder {
core;
logger;
validatorCache = {};
constructor(core, logger = rootLogger.child({
module: 'MultisigMetadataBuilder',
})) {
this.core = core;
this.logger = logger;
}
async s3Validators(originChain, validators) {
this.validatorCache[originChain] ??= {};
const toFetch = validators.filter((v) => !(v in this.validatorCache[originChain]));
if (toFetch.length > 0) {
const validatorAnnounce = this.core.getContracts(originChain).validatorAnnounce;
const storageLocations = await validatorAnnounce.getAnnouncedStorageLocations(toFetch);
this.logger.debug({ storageLocations }, 'Fetched storage locations');
const s3Validators = await Promise.all(storageLocations.map((locations) => {
const latestLocation = locations.slice(-1)[0];
return S3Validator.fromStorageLocation(latestLocation);
}));
this.logger.debug({ s3Validators }, 'Fetched validators');
toFetch.forEach((validator, index) => {
this.validatorCache[originChain][validator] = s3Validators[index];
});
}
return validators.map((v) => this.validatorCache[originChain][v]);
}
async getS3Checkpoints(validators, match) {
this.logger.debug({ match, validators }, 'Fetching checkpoints');
const originChain = this.core.multiProvider.getChainName(match.origin);
const s3Validators = await this.s3Validators(originChain, validators);
const results = await Promise.allSettled(s3Validators.map((v) => v.getCheckpoint(match.index)));
results
.filter((r) => r.status === 'rejected')
.forEach((r) => {
this.logger.error({ error: r }, 'Failed to fetch checkpoint');
});
const checkpoints = results
.filter((result) => result.status === 'fulfilled' && result.value !== undefined)
.map((result) => result.value);
this.logger.debug({ checkpoints }, 'Fetched checkpoints');
if (checkpoints.length < validators.length) {
this.logger.debug({ checkpoints, validators, match }, `Found ${checkpoints.length} checkpoints out of ${validators.length} validators`);
}
const matchingCheckpoints = checkpoints.filter(({ value }) => eqAddress(bytes32ToAddress(value.checkpoint.merkle_tree_hook_address), match.merkleTree) &&
value.message_id === match.messageId &&
value.checkpoint.index === match.index &&
value.checkpoint.mailbox_domain === match.origin);
if (matchingCheckpoints.length !== checkpoints.length) {
this.logger.warn({ matchingCheckpoints, checkpoints, match }, 'Mismatched checkpoints');
}
return matchingCheckpoints;
}
async build(context) {
assert(context.ism.type === IsmType.MESSAGE_ID_MULTISIG, 'Merkle proofs are not yet supported');
const merkleTree = context.hook.address;
const matchingInsertion = context.dispatchTx.logs
.filter((log) => eqAddressEvm(log.address, merkleTree))
.map((log) => MerkleTreeInterface.parseLog(log))
.find((event) => event.args.messageId === context.message.id);
assert(matchingInsertion, `No merkle tree insertion of ${context.message.id} to ${merkleTree} found in dispatch tx`);
this.logger.debug({ matchingInsertion }, 'Found matching insertion event');
const checkpoints = await this.getS3Checkpoints(context.ism.validators, {
origin: context.message.parsed.origin,
messageId: context.message.id,
merkleTree,
index: matchingInsertion.args.index,
});
assert(checkpoints.length >= context.ism.threshold, `Only ${checkpoints.length} of ${context.ism.threshold} required checkpoints found`);
this.logger.debug({ checkpoints }, `Found ${checkpoints.length} checkpoints for message ${context.message.id}`);
const signatures = checkpoints
.map((checkpoint) => checkpoint.signature)
.slice(0, context.ism.threshold);
this.logger.debug({ signatures, ism: context.ism }, `Taking ${signatures.length} (threshold) signatures for message ${context.message.id}`);
const metadata = {
type: IsmType.MESSAGE_ID_MULTISIG,
checkpoint: checkpoints[0].value.checkpoint,
signatures,
};
return MultisigMetadataBuilder.encode(metadata);
}
static encodeSimplePrefix(metadata) {
const checkpoint = metadata.checkpoint;
const buf = Buffer.alloc(68);
buf.write(strip0x(checkpoint.merkle_tree_hook_address), 0, 32, 'hex');
buf.write(strip0x(checkpoint.root), 32, 32, 'hex');
buf.writeUInt32BE(checkpoint.index, 64);
return toHexString(buf);
}
static decodeSimplePrefix(metadata) {
const buf = fromHexString(metadata);
const merkleTree = toHexString(buf.subarray(0, 32));
const root = toHexString(buf.subarray(32, 64));
const index = buf.readUint32BE(64);
const checkpoint = {
root,
index,
merkle_tree_hook_address: merkleTree,
};
return {
signatureOffset: 68,
type: IsmType.MESSAGE_ID_MULTISIG,
checkpoint,
};
}
static encodeProofPrefix(metadata) {
const checkpoint = metadata.checkpoint;
const buf = Buffer.alloc(1096);
buf.write(strip0x(checkpoint.merkle_tree_hook_address), 0, 32, 'hex');
buf.writeUInt32BE(metadata.proof.index, 32);
buf.write(strip0x(metadata.proof.leaf.toString()), 36, 32, 'hex');
const branchEncoded = metadata.proof.branch
.map((b) => strip0x(b.toString()))
.join('');
buf.write(branchEncoded, 68, 32 * 32, 'hex');
buf.writeUint32BE(checkpoint.index, 1092);
return toHexString(buf);
}
static decodeProofPrefix(metadata) {
const buf = fromHexString(metadata);
const merkleTree = toHexString(buf.subarray(0, 32));
const messageIndex = buf.readUint32BE(32);
const signedMessageId = toHexString(buf.subarray(36, 68));
const branchEncoded = buf.subarray(68, 1092).toString('hex');
const branch = chunk(branchEncoded, 32 * 2).map((v) => ensure0x(v));
const signedIndex = buf.readUint32BE(1092);
const checkpoint = {
root: '',
index: messageIndex,
merkle_tree_hook_address: merkleTree,
};
const proof = {
branch,
leaf: signedMessageId,
index: signedIndex,
};
return {
signatureOffset: 1096,
type: IsmType.MERKLE_ROOT_MULTISIG,
checkpoint,
proof,
};
}
static encode(metadata) {
let encoded = metadata.type === IsmType.MESSAGE_ID_MULTISIG
? this.encodeSimplePrefix(metadata)
: this.encodeProofPrefix(metadata);
metadata.signatures.forEach((signature) => {
const encodedSignature = joinSignature(signature);
assert(fromHexString(encodedSignature).byteLength === SIGNATURE_LENGTH, 'Invalid signature length');
encoded += strip0x(encodedSignature);
});
return encoded;
}
static signatureAt(metadata, offset, index) {
const buf = fromHexString(metadata);
const start = offset + index * SIGNATURE_LENGTH;
const end = start + SIGNATURE_LENGTH;
if (end > buf.byteLength) {
return undefined;
}
return toHexString(buf.subarray(start, end));
}
static decode(metadata, type) {
const prefix = type === IsmType.MERKLE_ROOT_MULTISIG
? this.decodeProofPrefix(metadata)
: this.decodeSimplePrefix(metadata);
const { signatureOffset: offset, ...values } = prefix;
const signatures = [];
for (let i = 0; this.signatureAt(metadata, offset, i); i++) {
const { r, s, v } = splitSignature(this.signatureAt(metadata, offset, i));
signatures.push({ r, s, v });
}
return {
signatures,
...values,
};
}
}
//# sourceMappingURL=multisig.js.map