wormhole/clients/js/vaa.ts

460 lines
14 KiB
TypeScript
Raw Normal View History

import { Parser } from "binary-parser"
import { ethers } from "ethers"
import { solidityKeccak256 } from "ethers/lib/utils"
import * as elliptic from "elliptic"
export interface Signature {
guardianSetIndex: number
signature: string
}
export interface VAA<T> {
version: number
guardianSetIndex: number
signatures: Signature[]
timestamp: number
nonce: number
emitterChain: number
emitterAddress: string
sequence: bigint
consistencyLevel: number
payload: T
}
class P<T> {
private parser: Parser
constructor(parser: Parser) {
this.parser = parser
}
// Try to parse a buffer with a parser, and return null if it failed due to an
// assertion error.
parse(buffer: Buffer): T | null {
try {
let result = this.parser.parse(buffer)
delete result['end']
return result
} catch (e: any) {
if (e.message?.includes("Assertion error")) {
return null
} else {
throw e
}
}
}
or<U>(other: P<U>): P<T | U> {
let p = new P<T | U>(other.parser);
p.parse = (buffer: Buffer): T | U | null => {
return this.parse(buffer) ?? other.parse(buffer)
}
return p
}
}
// All the different types of payloads
export type Payload =
GuardianSetUpgrade
| CoreContractUpgrade
| PortalContractUpgrade<"TokenBridge">
| PortalContractUpgrade<"NFTBridge">
| PortalRegisterChain<"TokenBridge">
| PortalRegisterChain<"NFTBridge">
// TODO: add other types of payloads
export function parse(buffer: Buffer): VAA<Payload | null> {
const vaa = parseEnvelope(buffer)
const parser = guardianSetUpgradeParser
.or(coreContractUpgradeParser)
.or(portalContractUpgradeParser("TokenBridge"))
.or(portalContractUpgradeParser("NFTBridge"))
.or(portalRegisterChainParser("TokenBridge"))
.or(portalRegisterChainParser("NFTBridge"))
const payload = parser.parse(vaa.payload)
var myVAA = { ...vaa, payload }
return myVAA
}
export function hasPayload(vaa: VAA<Payload | null>): vaa is VAA<Payload> {
return vaa.payload !== null
}
// Parse the VAA envelope without looking into the payload.
// If you want to parse the payload as well, use 'parse'.
export function parseEnvelope(buffer: Buffer): VAA<Buffer> {
var vaa = vaaParser.parse(buffer)
delete vaa['end']
delete vaa['signatureCount']
vaa.payload = Buffer.from(vaa.payload)
return vaa
}
// Parse a signature
const signatureParser = new Parser()
.endianess("big")
.uint8("guardianSetIndex")
.array("signature", {
type: "uint8",
lengthInBytes: 65,
formatter: arr => Buffer.from(arr).toString("hex")
})
function serialiseSignature(sig: Signature): string {
const body = [
encode("uint8", sig.guardianSetIndex),
sig.signature
]
return body.join("")
}
// Parse a vaa envelope. The payload is returned as a byte array.
const vaaParser = new Parser()
.endianess("big")
.uint8("version")
.uint32("guardianSetIndex")
.uint8("signatureCount")
.array("signatures", {
type: signatureParser,
length: "signatureCount"
})
.uint32("timestamp")
.uint32("nonce")
.uint16("emitterChain")
.array("emitterAddress", {
type: "uint8",
lengthInBytes: 32,
formatter: arr => Buffer.from(arr).toString("hex")
})
.uint64("sequence")
.uint8("consistencyLevel")
.array("payload", {
type: "uint8",
readUntil: "eof"
})
.string("end", {
greedy: true,
assert: str => str === ""
})
export function serialiseVAA(vaa: VAA<Payload>) {
const body = [
encode("uint8", vaa.version),
encode("uint32", vaa.guardianSetIndex),
encode("uint8", vaa.signatures.length),
...(vaa.signatures.map((sig) => serialiseSignature(sig))),
vaaBody(vaa)
]
return body.join("")
}
export function vaaDigest(vaa: VAA<Payload>) {
return solidityKeccak256(["bytes"], [solidityKeccak256(["bytes"], ["0x" + vaaBody(vaa)])])
}
function vaaBody(vaa: VAA<Payload>) {
let payload = vaa.payload
let payload_str: string
switch (payload.module) {
case "Core":
switch (payload.type) {
case "GuardianSetUpgrade":
payload_str = serialiseGuardianSetUpgrade(payload)
break
case "ContractUpgrade":
payload_str = serialiseCoreContractUpgrade(payload)
break
}
break
case "NFTBridge":
case "TokenBridge":
switch (payload.type) {
case "ContractUpgrade":
payload_str = serialisePortalContractUpgrade(payload)
break
case "RegisterChain":
payload_str = serialisePortalRegisterChain(payload)
break
}
break
}
const body = [
encode("uint32", vaa.timestamp),
encode("uint32", vaa.nonce),
encode("uint16", vaa.emitterChain),
encode("bytes32", Buffer.from(vaa.emitterAddress, "hex")),
encode("uint64", vaa.sequence),
encode("uint8", vaa.consistencyLevel),
payload_str
]
return body.join("")
}
export function sign(signers: string[], vaa: VAA<Payload>): Signature[] {
const hash = vaaDigest(vaa)
const ec = new elliptic.ec("secp256k1")
return signers.map((signer, i) => {
const key = ec.keyFromPrivate(signer)
const signature = key.sign(Buffer.from(hash.substr(2), "hex"), { canonical: true })
const packed = [
signature.r.toString("hex").padStart(64, "0"),
signature.s.toString("hex").padStart(64, "0"),
encode("uint8", signature.recoveryParam)
].join("")
return {
guardianSetIndex: i,
signature: packed
}
})
}
// Parse an address of given length, and render it as hex
const addressParser = (length: number) => new Parser()
.endianess("big")
.array("address", {
type: "uint8",
lengthInBytes: length,
formatter: (arr) => Buffer.from(arr).toString("hex")
})
////////////////////////////////////////////////////////////////////////////////
// Guardian set upgrade
export interface GuardianSetUpgrade {
module: "Core"
type: "GuardianSetUpgrade"
chain: number
newGuardianSetIndex: number
newGuardianSetLength: number
newGuardianSet: string[]
}
// Parse a guardian set upgrade payload
const guardianSetUpgradeParser: P<GuardianSetUpgrade> = new P(new Parser()
.endianess("big")
.string("module", {
length: 32,
encoding: "hex",
assert: Buffer.from("Core").toString("hex").padStart(64, "0"),
formatter: (_str) => "Core"
})
.uint8("type", {
assert: 2,
formatter: (_action) => "GuardianSetUpgrade"
})
.uint16("chain")
.uint32("newGuardianSetIndex")
.uint8("newGuardianSetLength")
.array("newGuardianSet", {
type: addressParser(20),
length: "newGuardianSetLength",
formatter: (arr: [{ address: string }]) => arr.map((addr) => addr.address)
})
.string("end", {
greedy: true,
assert: str => str === ""
}))
function serialiseGuardianSetUpgrade(payload: GuardianSetUpgrade): string {
const body = [
encode("bytes32", Buffer.from(Buffer.from(payload.module).toString("hex").padStart(64, "0"), "hex")),
encode("uint8", 2),
encode("uint16", payload.chain),
encode("uint32", payload.newGuardianSetIndex),
encode("uint8", payload.newGuardianSet.length),
...payload.newGuardianSet
]
return body.join("")
}
////////////////////////////////////////////////////////////////////////////////
// Contract upgrades
export interface CoreContractUpgrade {
module: "Core"
type: "ContractUpgrade"
chain: number
address: Uint8Array
}
// Parse a core contract upgrade payload
const coreContractUpgradeParser: P<CoreContractUpgrade> =
new P(new Parser()
.endianess("big")
.string("module", {
length: 32,
encoding: "hex",
assert: Buffer.from("Core").toString("hex").padStart(64, "0"),
formatter: (_str) => "Core"
})
.uint8("type", {
assert: 1,
formatter: (_action) => "ContractUpgrade"
})
.uint16("chain")
.array("address", {
type: "uint8",
lengthInBytes: 32,
// formatter: (arr) => Buffer.from(arr).toString("hex")
})
.string("end", {
greedy: true,
assert: str => str === ""
}))
function serialiseCoreContractUpgrade(payload: CoreContractUpgrade): string {
const body = [
encode("bytes32", encodeString(payload.module)),
encode("uint8", 1),
encode("uint16", payload.chain),
encode("bytes32", payload.address)
]
return body.join("")
}
export interface PortalContractUpgrade<Module extends "NFTBridge" | "TokenBridge"> {
module: Module
type: "ContractUpgrade"
chain: number
address: Uint8Array
}
// Parse a portal contract upgrade payload
function portalContractUpgradeParser<Module extends "NFTBridge" | "TokenBridge">(module: Module): P<PortalContractUpgrade<Module>> {
return new P(new Parser()
.endianess("big")
.string("module", {
length: 32,
encoding: "hex",
assert: Buffer.from(module).toString("hex").padStart(64, "0"),
formatter: (_str: string) => module
})
.uint8("type", {
assert: 2,
formatter: (_action: number) => "ContractUpgrade"
})
.uint16("chain")
.array("address", {
type: "uint8",
lengthInBytes: 32,
// formatter: (arr) => Buffer.from(arr).toString("hex")
})
.string("end", {
greedy: true,
assert: str => str === ""
}))
}
function serialisePortalContractUpgrade<Module extends "NFTBridge" | "TokenBridge">(payload: PortalContractUpgrade<Module>): string {
const body = [
encode("bytes32", encodeString(payload.module)),
encode("uint8", 2),
encode("uint16", payload.chain),
encode("bytes32", payload.address)
]
return body.join("")
}
////////////////////////////////////////////////////////////////////////////////
// Registrations
export interface PortalRegisterChain<Module extends "NFTBridge" | "TokenBridge"> {
module: Module
type: "RegisterChain"
chain: number
emitterChain: number
emitterAddress: Uint8Array
}
// Parse a portal chain registration payload
function portalRegisterChainParser<Module extends "NFTBridge" | "TokenBridge">(module: Module): P<PortalRegisterChain<Module>> {
return new P(new Parser()
.endianess("big")
.string("module", {
length: 32,
encoding: "hex",
assert: Buffer.from(module).toString("hex").padStart(64, "0"),
formatter: (_str) => module
})
.uint8("type", {
assert: 1,
formatter: (_action) => "RegisterChain"
})
.uint16("chain")
.uint16("emitterChain")
.array("emitterAddress", {
type: "uint8",
lengthInBytes: 32,
// formatter: (arr) => Buffer.from(arr).toString("hex")
})
.string("end", {
greedy: true,
assert: str => str === ""
})
)
}
function serialisePortalRegisterChain<Module extends "NFTBridge" | "TokenBridge">(payload: PortalRegisterChain<Module>): string {
const body = [
encode("bytes32", encodeString(payload.module)),
encode("uint8", 1),
encode("uint16", payload.chain),
encode("uint16", payload.emitterChain),
encode("bytes32", payload.emitterAddress)
]
return body.join("")
}
// This function should be called after pattern matching on all possible options
// of an enum (union) type, so that typescript can derive that no other options
// are possible. If (from JavaScript land) an unsupported argument is passed
// in, this function just throws. If the enum type is extended with new cases,
// the call to this function will then fail to compile, drawing attention to an
// unhandled case somewhere.
export function impossible(a: never): any {
throw new Error(`Impossible: ${a}`)
}
////////////////////////////////////////////////////////////////////////////////
// Encoder utils
export type Encoding
= "uint8"
| "uint16"
| "uint32"
| "uint64"
| "uint128"
| "uint256"
| "bytes32"
| "address"
export function typeWidth(type: Encoding): number {
switch (type) {
case "uint8": return 1
case "uint16": return 2
case "uint32": return 4
case "uint64": return 8
case "uint128": return 16
case "uint256": return 32
case "bytes32": return 32
case "address": return 20
}
}
// Couldn't find a satisfactory binary serialisation solution, so we just use
// the ethers library's encoding logic
export function encode(type: Encoding, val: any): string {
// ethers operates on hex strings (sigh) and left pads everything to 32
// bytes (64 characters). We take last 2*n characters where n is the width
// of the type being serialised in bytes (since a byte is represented as 2
// digits in hex).
return ethers.utils.defaultAbiCoder.encode([type], [val]).substr(-2 * typeWidth(type))
}
// Encode a string as binary left-padded to 32 bytes, represented as a hex
// string (64 chars long)
export function encodeString(str: string): Buffer {
return Buffer.from(Buffer.from(str).toString("hex").padStart(64, "0"), "hex")
}