diff --git a/packages/cre-sdk/src/sdk/don-info.ts b/packages/cre-sdk/src/sdk/don-info.ts
new file mode 100644
index 00000000..a1c02224
--- /dev/null
+++ b/packages/cre-sdk/src/sdk/don-info.ts
@@ -0,0 +1,20 @@
+export type Environment = {
+ chainSelector: bigint
+ registryAddress: string
+}
+
+export type Zone = {
+ environment: Environment
+ donID: number
+}
+
+export function productionEnvironment(): Environment {
+ return {
+ chainSelector: 5009297550715157269n,
+ registryAddress: '0x76c9cf548b4179F8901cda1f8623568b58215E62',
+ }
+}
+
+export function zoneFromEnvironment(environment: Environment, donID: number): Zone {
+ return { environment, donID }
+}
diff --git a/packages/cre-sdk/src/sdk/errors.ts b/packages/cre-sdk/src/sdk/errors.ts
index 9dab9a28..02fa2186 100644
--- a/packages/cre-sdk/src/sdk/errors.ts
+++ b/packages/cre-sdk/src/sdk/errors.ts
@@ -1,4 +1,4 @@
-import { Mode, type SecretRequest } from '@cre/generated/sdk/v1alpha/sdk_pb'
+import type { SecretRequest } from '@cre/generated/sdk/v1alpha/sdk_pb'
export class DonModeError extends Error {
constructor() {
@@ -25,3 +25,55 @@ export class SecretsError extends Error {
this.name = 'SecretsError'
}
}
+
+export class NullReportError extends Error {
+ constructor() {
+ super('null report')
+ this.name = 'NullReportError'
+ }
+}
+
+export class WrongSignatureCountError extends Error {
+ constructor() {
+ super('wrong number of signatures')
+ this.name = 'WrongSignatureCountError'
+ }
+}
+
+export class ParseSignatureError extends Error {
+ constructor() {
+ super('failed to parse signature')
+ this.name = 'ParseSignatureError'
+ }
+}
+
+export class RecoverSignerError extends Error {
+ constructor() {
+ super('failed to recover signer address from signature')
+ this.name = 'RecoverSignerError'
+ }
+}
+
+export class UnknownSignerError extends Error {
+ constructor() {
+ super('invalid signature')
+ this.name = 'UnknownSignerError'
+ }
+}
+
+export class DuplicateSignerError extends Error {
+ constructor() {
+ super('duplicate signer')
+ this.name = 'DuplicateSignerError'
+ }
+}
+
+export class RawReportTooShortError extends Error {
+ constructor(
+ public readonly need: number,
+ public readonly got: number,
+ ) {
+ super(`raw report too short to contain metadata header: need ${need} bytes, got ${got}`)
+ this.name = 'RawReportTooShortError'
+ }
+}
diff --git a/packages/cre-sdk/src/sdk/index.ts b/packages/cre-sdk/src/sdk/index.ts
index 65d659dc..9ca2643e 100644
--- a/packages/cre-sdk/src/sdk/index.ts
+++ b/packages/cre-sdk/src/sdk/index.ts
@@ -1,4 +1,6 @@
export * from './cre'
+export * from './don-info'
+export * from './errors'
export * from './report'
export type * from './runtime'
export * from './runtime'
diff --git a/packages/cre-sdk/src/sdk/report-internals.ts b/packages/cre-sdk/src/sdk/report-internals.ts
new file mode 100644
index 00000000..01e5fc13
--- /dev/null
+++ b/packages/cre-sdk/src/sdk/report-internals.ts
@@ -0,0 +1,13 @@
+import type { Address } from 'viem'
+
+// Do not re-export in index.ts to avoid exposing it to the public API.
+// This is public to allow the cache to be cleared in the tests.
+
+export type DonInfo = {
+ f: number
+ signers: Map
+}
+
+// Do not re-export in index.ts to avoid exposing it to the public API.
+// donInfoCache exists outside Report to allow testing to clear the cache.
+export var donInfoCache = new Map()
diff --git a/packages/cre-sdk/src/sdk/report.test.ts b/packages/cre-sdk/src/sdk/report.test.ts
new file mode 100644
index 00000000..e2b3c5b3
--- /dev/null
+++ b/packages/cre-sdk/src/sdk/report.test.ts
@@ -0,0 +1,600 @@
+import { beforeEach, describe, expect } from 'bun:test'
+import { create } from '@bufbuild/protobuf'
+import { AnySchema } from '@bufbuild/protobuf/wkt'
+import { newTestRuntime, registerTestCapability, test } from '@cre/sdk/test'
+import { hexToBytes } from 'viem'
+import {
+ DuplicateSignerError,
+ type Environment,
+ ParseSignatureError,
+ productionEnvironment,
+ RawReportTooShortError,
+ RecoverSignerError,
+ Report,
+ UnknownSignerError,
+ WrongSignatureCountError,
+ zoneFromEnvironment,
+} from '.'
+import { donInfoCache } from './report-internals'
+
+// From a real report
+const reportContext = decodeHex(
+ '000e8ce31db48e5e44619d24d9dadfc5f22a34db8205b2b25cd831eab02244c500000000000000000000000000000000000000000000000000000001612bcb000000000000000000000000000000000000000000000000000000000000000000',
+)
+const rawReport = decodeHex(
+ '010d46e9e05b28a322cd96c94f1f60c236a1b52bd446d98396f1a84afae388836a69c96faa000000010000000100a3bb3b17a1053fe409147c2b170af4866c182070905e5a0f314be04c8296a336613263393832646464b0f2d38245dd6d397ebbdb5a814b753d56c30715000148656c6c6f2c20576f726c6421',
+)
+
+const reportBody = 'Hello, World!'
+const reportExecID = '0d46e9e05b28a322cd96c94f1f60c236a1b52bd446d98396f1a84afae388836a'
+const reportSeqNr = 0n
+const reportDigest = reportContext.subarray(0, 32)
+const reportDONID = 1
+const reportDONCfgVer = 1
+const reportWfID = '00a3bb3b17a1053fe409147c2b170af4866c182070905e5a0f314be04c8296a3'
+const reportWfName = '6a2c982ddd'
+const reportWfOwner = 'b0f2d38245dd6d397ebbdb5a814b753d56c30715'
+const reportTimestampUnix = 1774809002
+const reportReportID = '0001'
+const reportVersion = 1
+
+const reportSigs = sigsToBytes([
+ '452d9e1fef501946b5e0a52e32dd4ec48a796c8c30a4f62332006a1fb5a7d4b578b6ad0b7a15f3c4ef46bffb891884b50ac23f5fcfa5cf75e7a7329d6fed7a6400',
+ '3b48aa3db3d6398f1d69f649e266d4388d3d176b76ff58de72ae4ca30e27559b5789a491b8f94578f793bc5781ad5c50dbf48bb79e61b929f625162a4c79172300',
+ 'a0fa2428e44e7ffa9452433b3275bd79819db0620b7d2090fea21065a1dfed1a7c5135540c6e44c20be4d36d18c1859253eca53476f45ea3f8a54ddc97d4b42c01',
+ 'f868744afd82e657110ec1e214fe91e6abc10ea84edcf69b5076ee5bf9126e056ba50eb078d1b594140ddaac11aa0b52fe9ca4f629b334b243e38a345e0d519501',
+])
+
+const extraValidSig = decodeHex(
+ 'c6ad1f99caadc9438d3ac14a821d523f2716c908286224e0a558e864cfa51a07aa3d8707818c47bd324e4053c29ea046b0073343d48186ae3bc71f5ee85a6401',
+)
+const notASignerSig = decodeHex(
+ 'ec644867408acbfbc51f30731e1ebfb6395a943c500f824c23db57aec71642777b1a143319ad3b5b9d2c8bda616ea98cd3c6c469e76da9482abb6afb0d3b06c200',
+)
+
+const reportSigIds = [6, 1, 5, 8]
+
+// callReplay holds the expected request and canned response for one capability call.
+interface CallReplay {
+ RequestValue: Uint8Array
+ ResponseValue: Uint8Array
+}
+
+// workflowReplay holds a sequence of expected calls for one chain.
+// The mock advances through Calls in order, matching each by RequestValue.
+interface WorkflowReplay {
+ Calls: CallReplay[]
+ ChainSelector: bigint
+}
+
+const productionEnvironmentReplay: WorkflowReplay = {
+ Calls: [
+ {
+ RequestValue: decodeHex(
+ '120e0a010310ffffffffffffffffff010a3c121476c9cf548b4179f8901cda1f8623568b58215e621a24235374050000000000000000000000000000000000000000000000000000000000000001',
+ ),
+ ResponseValue: decodeHex(
+ '0ae018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000000a2f2ef1df488df1fa0481e5e9029b140e06b8b451fd50f63af35e78daa8e77a9b1b151301a950a6e7349df023c6a61c2f836425b4219d819781d59930205314d379b4573667de33b549e999c1318fa183a8965ecb29aa6f9b5076470a097da30c43de846c124a064722dbfe87260bcf7f268bb8b6a95812c00316ba58ace153c9274238b4c0afa52061cc0bac29b3002bc90568e002426fb697658d70bf3f10fe79e8ca1a676b07039ac3e1f5fa70d89e4a58487a3b54ecbb53900f5f2d85afb20ebd0742865703034f8a357db566837dde300357e0a6b3a0982d622495bdb2bb044432b7c8ee9d14059569595a1020a3b4598574aa3a502708a6d96a1881cc0c196ea7403b385b02634647717a096a71fb4d577a11bc37bc3794fb2d599b3c5d4ec86a31d8865abe7704e2059a58ec1d5e177325b0fc09c5ca9b2ff6fc5158710000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067a6f6e652d6100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011776f726b666c6f775f315f7a6f6e652d610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000005a00000000000000000000000000000000000000000000000000000000000000660000000000000000000000000000000000000000000000000000000000000072000000000000000000000000000000000000000000000000000000000000007e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000004500acd080aca080a08456e636c6176657312bd082aba081299042296040a170a11456e636c6176654175746848656164657212020a000a160a10456e636c61766545787472614461746112021a000a2f0a09456e636c617665494412221a2066338ab3ebe1d246feb5849330e92022b9d66b0c0753b1f2ac6e356acbff20fc0a160a0b456e636c6176655479706512070a054e4954524f0a3d0a0a456e636c61766555524c122f0a2d68747470733a2f2f636f6e666964656e7469616c2d687474702e656e636c617665732e636861696e2e6c696e6b0ada020a0d5472757374656456616c75657312c8022ac50212c2021abf027b2270637230223a22336130646135323235666439323134323666633864613065376139316534323631623431333137336239626530303237616462326666626231616661653430616462613662663431626133663066653537386339303032646134653565613237222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22643938306538363964333066303333353130356563373236623234393430633636633130383130376362386636653661363638363561323562646464303537303363393231373964303334386661323936636166336332663633313036393631227d129b042298040a160a10456e636c61766545787472614461746112021a000a2f0a09456e636c617665494412221a206401f4177859d87613cdc14bd3841049840abaf6d0163cf77080227b8866ebe20a160a0b456e636c6176655479706512070a054e4954524f0a3f0a0a456e636c61766555524c12310a2f68747470733a2f2f636f6e666964656e7469616c2d687474702d322e656e636c617665732e636861696e2e6c696e6b0ada020a0d5472757374656456616c75657312c8022ac50212c2021abf027b2270637230223a22336130646135323235666439323134323666633864613065376139316534323631623431333137336239626530303237616462326666626231616661653430616462613662663431626133663066653537386339303032646134653565613237222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22643938306538363964333066303333353130356563373236623234393430633636633130383130376362386636653661363638363561323562646464303537303363393231373964303334386661323936636166336332663633313036393631227d0a170a11456e636c6176654175746848656164657212020a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000000000002400100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000000000000024001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000000000000024001000000000000000000000000000000000000000000000000000000000000',
+ ),
+ },
+ {
+ RequestValue: decodeHex(
+ '120e0a010310ffffffffffffffffff010a9d03121476c9cf548b4179f8901cda1f8623568b58215e621a840305a519660000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a2f2ef1df488df1fa0481e5e9029b140e06b8b451fd50f63af35e78daa8e77a9b1b151301a950a6e7349df023c6a61c2f836425b4219d819781d59930205314d379b4573667de33b549e999c1318fa183a8965ecb29aa6f9b5076470a097da30c43de846c124a064722dbfe87260bcf7f268bb8b6a95812c00316ba58ace153c9274238b4c0afa52061cc0bac29b3002bc90568e002426fb697658d70bf3f10fe79e8ca1a676b07039ac3e1f5fa70d89e4a58487a3b54ecbb53900f5f2d85afb20ebd0742865703034f8a357db566837dde300357e0a6b3a0982d622495bdb2bb044432b7c8ee9d14059569595a1020a3b4598574aa3a502708a6d96a1881cc0c196ea7403b385b02634647717a096a71fb4d577a11bc37bc3794fb2d599b3c5d4ec86a31d8865abe7704e2059a58ec1d5e177325b0fc09c5ca9b2ff6fc515871',
+ ),
+ ResponseValue: decodeHex(
+ '0a80440000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000007c00000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000e40000000000000000000000000000000000000000000000000000000000000118000000000000000000000000000000000000000000000000000000000000014c000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001b400000000000000000000000000000000000000000000000000000000000001e80000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001ff9b062fccb2f042311343048b9518068370f8370000000000000000000000002f2ef1df488df1fa0481e5e9029b140e06b8b451fd50f63af35e78daa8e77a9b3ab22f16cd99e40a0961c1fe754957995ad9342876935903f98748b4679ad14461faf5857337f5bd2286b73385d5a14d2fa7a06cb412a60c48c0ab8b6a48d29500000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001e55fcaf921e76c6bbcf9415bba12b1236f07b0c30000000000000000000000001b151301a950a6e7349df023c6a61c2f836425b4219d819781d59930205314d38fd8e4f29d73296f22a7b3f5fc9202575ce22f0f2d00958535933317e10bed7931d7553b6a9b0f19ad9b2135005e17ac43b2d64156fbc40189016a02edca0e2600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000014d6cfd44f94408a39fb1af94a53c107a730ba16100000000000000000000000079b4573667de33b549e999c1318fa183a8965ecb29aa6f9b5076470a097da30cdcef5ac750843e491047b36a967b21cd1808b89150b649fb52026f9d4fbd3d7d42f5c6de0835861a4851a6a28b9701bb6b7f3334dba723723766faeb9179587000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001de5cd1dd4300a0b4854f8223add60d20e1dfe21b00000000000000000000000043de846c124a064722dbfe87260bcf7f268bb8b6a95812c00316ba58ace153c9bf27f741368396516a3eddadb25d6155aa826f49a803339a8d26ad414011ae10fd55f6f19ddac740310fc1e11df087af1204edb720d8087b1ac81ed7526305c200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001f3baa9a99b5ad64f50779f449bac83baac8bfdb6000000000000000000000000274238b4c0afa52061cc0bac29b3002bc90568e002426fb697658d70bf3f10fec2d55147082f081d11eeec32a377e78d61413e8ed3324fc580f908159a6f8d2f44191e378823030493e5005bfb9eab10d36f480d7aab96a3a7342cd582e870aa00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001d7f22fb5382ff477d2ff5c702cab0ef8abf1823300000000000000000000000079e8ca1a676b07039ac3e1f5fa70d89e4a58487a3b54ecbb53900f5f2d85afb2062ba100224f908adf57d63a93833df318624d88d8a196e3944be333e35c4033638ae4ff0cf5a5b1644363fa9c2cdbd6dcc68385b6e743a4fc35428106ec48c200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001cdf20f8ffd41b02c680988b20e68735cc8c1ca170000000000000000000000000ebd0742865703034f8a357db566837dde300357e0a6b3a0982d622495bdb2bb0204c67585b82a51088c1f206e9cf70d915b5216b19002805ac2ab0a8e8a36688d1df6c27959c6526fd563063ad3257736f965df9c1001262a5aeaadee20a9fb00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000014d7d71c7e584cfa1f5c06275e5d283b9d3176924000000000000000000000000044432b7c8ee9d14059569595a1020a3b4598574aa3a502708a6d96a1881cc0c642af6c11171c08a776eadcf58582f4cd7e47d9d14677cc46e2809db3fc4dd64ea95a3cd860cac9afa6833ef3c690fb0000bb95ddf5690471c0602d99b54858f00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000011a89c98e75983ec384ad8e83eaf7d0176eeaf155000000000000000000000000196ea7403b385b02634647717a096a71fb4d577a11bc37bc3794fb2d599b3c5d236ad77c8868ef28a0cfc7911f93b0dbfa2bfb0aea0aede56a84cffc7ecdee421ad7ab8dfda546fe13432afdddec94ff44d18c5307bd8e95dddccef8a8671d0200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000014f99b550623e77b807df7cbed9c79d55e1163b480000000000000000000000004ec86a31d8865abe7704e2059a58ec1d5e177325b0fc09c5ca9b2ff6fc515871eeeb764510e9f4f6159bcbb8750e99f7ef8d8611c503adb79a415adea3c6ef68924645a472462338ba1d39a83bb08be8de8cb63716e0f54caae9475208f3619400000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000',
+ ),
+ },
+ ],
+ ChainSelector: 5009297550715157269n,
+}
+
+const otherEnvironmentReplay: WorkflowReplay = {
+ Calls: [
+ {
+ RequestValue: decodeHex(
+ '120e0a010310ffffffffffffffffff010a3c12147f3191eaf73429177bab3bac5c36ed2d5e39985f1a24235374050000000000000000000000000000000000000000000000000000000000000001',
+ ),
+ ResponseValue: decodeHex(
+ '0a803c0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000000717d46e26bc3ff8f63ee9de35237e545a44371b08506fe5ef271e94916a4dc63e7c396cae7f777e6619a187d797f7f3636818777d88bbac532782ebc1a44baccd439c4912deb85b489723f72e58e5ba50ac39f8225b30dd8ab9dd5521d867702f5886de6eb4c1c4283dacbd678a1da430ea8bf1b7da7e767afb2ecd2ea6d930dbba27b0482262260c9c377c666405b0cdbecfc54c1a59b2ba9ecff04c9ed3a6c5df142f783baab355fdc201e24d999fa3f65ee2f7461b3a944e0c0c742599018a578a1b91f504453e42470255b6adffe81462809c5b7d83cb3a7b453ff9f41ed50000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067a6f6e652d6100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011776f726b666c6f775f315f7a6f6e652d610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000ae000000000000000000000000000000000000000000000000000000000000011a00000000000000000000000000000000000000000000000000000000000001260000000000000000000000000000000000000000000000000000000000000192000000000000000000000000000000000000000000000000000000000000019e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000009760af1120aee120a08456e636c6176657312e1122ade1212ac0922a9090a170a11456e636c6176654175746848656164657212020a000a160a10456e636c61766545787472614461746112021a000a2f0a09456e636c617665494412221a2066338ab3ebe1d246feb5849330e92022b9d66b0c0753b1f2ac6e356acbff20fc0a160a0b456e636c6176655479706512070a054e4954524f0a460a0a456e636c61766555524c12380a3668747470733a2f2f656e636c6176652d312e707269766163792e73746167652e65787465726e616c2e67726964646c652e73683a38300ae4070a0d5472757374656456616c75657312d2072acf0712c2021abf027b2270637230223a22353533383935656138373939373638373830346337666437363533303462613839663661643163656135623935333066393036616466616562373634376239326436623566623732656333633261303638356632333134623134323138623138222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22663361646365393734363332373531646362643131363532623035656630633666383133663931616638346562363430353033313336396530396435616464636139373436333431313162643039353563336637643237343939316364316638227d12c2021abf027b2270637230223a22376237643532376134336636363164396334633634623964393432333762623937383332343332373832633235633031633661343435333935303363303330383766613562616230323165396436663433643062663634396665653437343864222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22313363333837353839396431636239373136346464386132383165373431646538313966616236353337366262306139346335313636656631343930376339623166316161373433326234653639303138346339306433623738666663633161227d12c2021abf027b2270637230223a22336130646135323235666439323134323666633864613065376139316534323631623431333137336239626530303237616462326666626231616661653430616462613662663431626133663066653537386339303032646134653565613237222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22643938306538363964333066303333353130356563373236623234393430633636633130383130376362386636653661363638363561323562646464303537303363393231373964303334386661323936636166336332663633313036393631227d12ac0922a9090a170a11456e636c6176654175746848656164657212020a000a160a10456e636c61766545787472614461746112021a000a2f0a09456e636c617665494412221a206401f4177859d87613cdc14bd3841049840abaf6d0163cf77080227b8866ebe20a160a0b456e636c6176655479706512070a054e4954524f0a460a0a456e636c61766555524c12380a3668747470733a2f2f656e636c6176652d322e707269766163792e73746167652e65787465726e616c2e67726964646c652e73683a38300ae4070a0d5472757374656456616c75657312d2072acf0712c2021abf027b2270637230223a22353533383935656138373939373638373830346337666437363533303462613839663661643163656135623935333066393036616466616562373634376239326436623566623732656333633261303638356632333134623134323138623138222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22663361646365393734363332373531646362643131363532623035656630633666383133663931616638346562363430353033313336396530396435616464636139373436333431313162643039353563336637643237343939316364316638227d12c2021abf027b2270637230223a22376237643532376134336636363164396334633634623964393432333762623937383332343332373832633235633031633661343435333935303363303330383766613562616230323165396436663433643062663634396665653437343864222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22313363333837353839396431636239373136346464386132383165373431646538313966616236353337366262306139346335313636656631343930376339623166316161373433326234653639303138346339306433623738666663633161227d12c2021abf027b2270637230223a22336130646135323235666439323134323666633864613065376139316534323631623431333137336239626530303237616462326666626231616661653430616462613662663431626133663066653537386339303032646134653565613237222c2270637231223a22346234643562333636316233656663313239323039303063383065313236653463653738336335323264653663303261326135626637616633613262393332376238363737366631383865346265316331633430346131323964626461343933222c2270637232223a22643938306538363964333066303333353130356563373236623234393430633636633130383130376362386636653661363638363561323562646464303537303363393231373964303334386661323936636166336332663633313036393631227d400100000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000060a40014a850c0a0b5f5f64656661756c745f5f12f50b0a3a0114000a9dadc131e4e73c69a68881faf8e6a308cb3d8a0520008a138f84becd0d0b3cffd3e5c07f835d6fd6ec1c73c8897e566ee8e51e23a1100a3a0114004c691603d19278baf83a6fca9d6a19fb5fdbd4ec05200057a758ef576b2bc759d9e4099a9a2fdd9f36e38816594492c45b3621f698cf700a3a011400591be078e128b2393e748226659795281de0e8a705200026b2b8455ff0a7085a2629b8307cc22de5085e7b52a30a1bae61f91e2ae055c00a3a011400e75c048f6d663668a7973838fa2711221e50af94052000ebe25babaa9004d9c7395d4bb77c53de0914c769b50b1092a817c1789b2141450a3a0114002e93310b6b97e77e9ae25e4c365687c79f19ea1c05200053db2212eb20f2a03d440449ec86a92c6c943ef79ee30d7b74ca51f8358fcc2c0a3a0114004887166c424a781d043a2200db75e5d749c6020e052000794e4c8fddc426c659c0bd75a86322315ac83faf6958fb08f3ca89efcf50663e0a3a0114000369e9dd2c77dda3327b6f5809608105e9d3b4ca052000b6a50858ffd29bcdfafb0beb0e6a9591008789734c2176aa3ca33ab6ce79bb5a1214cb3549ac73ad2c1b551777cc9420c3d9ca9bcfbe12142f0287694abc561fe516106a3af22a317274d4ff1214ecdc973d36282505eabe963e45eca1d370a49f28121479936119db3bca54a6202efcf5eae306c12ffdab1214207574c9879bf6b3ffe30fe92dc21b64b5d1f493121411ff98c6da070010affb54d708047bbf92f0403b121436fdca6a6655a64db2f0e24dca32d978fd8694981802281e32ae07c80180a8d6b907d00180e497d012d80180cab5ee01e0018084af5fe80180d88ee16ff0010afa0101078202208af5e968a270655ff374d829e5e1bf88ff61b2a2945e15799d01ccbc1a7a045e8202204600a42ce908bb7c6d756165fef5db1e3152a83256e3783a525de103d09d7b888202201d6c9ae9a3058f3e653c71576cf33df497c688f3e461f931e907d749afd1b4608202208f81cd5b79c6f249e03866ed61aec09fe9b488d97012a5ceb8f1269843c68a768202204e9ce42151dd26209e2fca5dd4c3a580f7aa7a07ea82e6705357f10895abaf8b82022064413b48eba168c6667d27d2090a8e0728fb488638a5fcc945b14bc663e6fc8f82022062a0f487830c32db10c802e63df02967981ec504046003a36e1c892b7ab510c08a0234313244334b6f6f57425250524c3442466f73694644763172383333516177725537376f4e653942326d446171346f75697a5353528a0234313244334b6f6f574a42485758574b70504d6d784b354c6f414372776b6f65476545386d3356457a7352347864503334375157598a0234313244334b6f6f57454e48686f42525271676a764e776e66514d54435a683432316565586d78587a4164505a4b626157774b514a8a0234313244334b6f6f57466d774b6f4b695a5a466d687a42566842676d726e555a6e39467a59443432707051375456714d5a346747328a0234313244334b6f6f574e4d3336714c6d384b474e3674647a526361694750574e735a6458455871457935316246566f526f794c7a478a0234313244334b6f6f57517141774c325037516555516a637359573139734769466b6e356868786233516f41316b6d64564561764d508a0234313244334b6f6f574669356e486d4844366646594535486837575a71726d746832445064736e61484d77316632726a6e5a50544192021c0880804010808040188080c002208080c00228e80738901c42020828980280cab5ee01a00280cab5ee01a80280cab5ee01b00280cab5ee01ba02c2010a20e4f689f966fce6c5c7e4865310420bb4aa8fdee813050f00b9ba6c84b4a7fe721220f2246aaa3942288f1752820ebdf4e9783a71d894746e53992409bd6586b15ea31a10c9ed7748fbeda59af34a99ac7c963a371a109f4d4a36fd9af719d9dcbd24854093af1a1020e18ae075ca840807c3c68cf5e581081a10c3b6de0de744c5eb37aa2c74536be0421a10a56f33982b187226f17658861b06d2611a102680ffceddc5f45271a917ced0ddbbaf1a10fb2218a8ef6e42e17626a0cddcc82f5ac00280cab5ee01c8028094ebdc0338050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002400100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060740014a820c0a0b5f5f64656661756c745f5f12f20b0a3a0114000a9dadc131e4e73c69a68881faf8e6a308cb3d8a0520008a138f84becd0d0b3cffd3e5c07f835d6fd6ec1c73c8897e566ee8e51e23a1100a3a0114004c691603d19278baf83a6fca9d6a19fb5fdbd4ec05200057a758ef576b2bc759d9e4099a9a2fdd9f36e38816594492c45b3621f698cf700a3a011400591be078e128b2393e748226659795281de0e8a705200026b2b8455ff0a7085a2629b8307cc22de5085e7b52a30a1bae61f91e2ae055c00a3a011400e75c048f6d663668a7973838fa2711221e50af94052000ebe25babaa9004d9c7395d4bb77c53de0914c769b50b1092a817c1789b2141450a3a0114002e93310b6b97e77e9ae25e4c365687c79f19ea1c05200053db2212eb20f2a03d440449ec86a92c6c943ef79ee30d7b74ca51f8358fcc2c0a3a0114004887166c424a781d043a2200db75e5d749c6020e052000794e4c8fddc426c659c0bd75a86322315ac83faf6958fb08f3ca89efcf50663e0a3a0114000369e9dd2c77dda3327b6f5809608105e9d3b4ca052000b6a50858ffd29bcdfafb0beb0e6a9591008789734c2176aa3ca33ab6ce79bb5a1214cb3549ac73ad2c1b551777cc9420c3d9ca9bcfbe12142f0287694abc561fe516106a3af22a317274d4ff1214ecdc973d36282505eabe963e45eca1d370a49f28121479936119db3bca54a6202efcf5eae306c12ffdab1214207574c9879bf6b3ffe30fe92dc21b64b5d1f493121411ff98c6da070010affb54d708047bbf92f0403b121436fdca6a6655a64db2f0e24dca32d978fd8694981802281e32ab07c80180a8d6b907d00180e497d012e0018084af5fe80180d88ee16ff0010afa0101078202208af5e968a270655ff374d829e5e1bf88ff61b2a2945e15799d01ccbc1a7a045e8202204600a42ce908bb7c6d756165fef5db1e3152a83256e3783a525de103d09d7b888202201d6c9ae9a3058f3e653c71576cf33df497c688f3e461f931e907d749afd1b4608202208f81cd5b79c6f249e03866ed61aec09fe9b488d97012a5ceb8f1269843c68a768202204e9ce42151dd26209e2fca5dd4c3a580f7aa7a07ea82e6705357f10895abaf8b82022064413b48eba168c6667d27d2090a8e0728fb488638a5fcc945b14bc663e6fc8f82022062a0f487830c32db10c802e63df02967981ec504046003a36e1c892b7ab510c08a0234313244334b6f6f57425250524c3442466f73694644763172383333516177725537376f4e653942326d446171346f75697a5353528a0234313244334b6f6f574a42485758574b70504d6d784b354c6f414372776b6f65476545386d3356457a7352347864503334375157598a0234313244334b6f6f57454e48686f42525271676a764e776e66514d54435a683432316565586d78587a4164505a4b626157774b514a8a0234313244334b6f6f57466d774b6f4b695a5a466d687a42566842676d726e555a6e39467a59443432707051375456714d5a346747328a0234313244334b6f6f574e4d3336714c6d384b474e3674647a526361694750574e735a6458455871457935316246566f526f794c7a478a0234313244334b6f6f57517141774c325037516555516a637359573139734769466b6e356868786233516f41316b6d64564561764d508a0234313244334b6f6f574669356e486d4844366646594535486837575a71726d746832445064736e61484d77316632726a6e5a5054419202200880804010808040188080c002208080c002280130904e38c0843d4203088407980280cab5ee01a00280cab5ee01a80280cab5ee01b00280cab5ee01ba02c2010a20e4f689f966fce6c5c7e4865310420bb4aa8fdee813050f00b9ba6c84b4a7fe721220f2246aaa3942288f1752820ebdf4e9783a71d894746e53992409bd6586b15ea31a10c9ed7748fbeda59af34a99ac7c963a371a109f4d4a36fd9af719d9dcbd24854093af1a1020e18ae075ca840807c3c68cf5e581081a10c3b6de0de744c5eb37aa2c74536be0421a10a56f33982b187226f17658861b06d2611a102680ffceddc5f45271a917ced0ddbbaf1a10fb2218a8ef6e42e17626a0cddcc82f5ac00280cab5ee01c8028094ebdc03380200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000000000000024001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000000000000024001000000000000000000000000000000000000000000000000000000000000',
+ ),
+ },
+ {
+ RequestValue: decodeHex(
+ '120e0a010310ffffffffffffffffff010abd0212147f3191eaf73429177bab3bac5c36ed2d5e39985f1aa40205a519660000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000717d46e26bc3ff8f63ee9de35237e545a44371b08506fe5ef271e94916a4dc63e7c396cae7f777e6619a187d797f7f3636818777d88bbac532782ebc1a44baccd439c4912deb85b489723f72e58e5ba50ac39f8225b30dd8ab9dd5521d867702f5886de6eb4c1c4283dacbd678a1da430ea8bf1b7da7e767afb2ecd2ea6d930dbba27b0482262260c9c377c666405b0cdbecfc54c1a59b2ba9ecff04c9ed3a6c5df142f783baab355fdc201e24d999fa3f65ee2f7461b3a944e0c0c742599018a578a1b91f504453e42470255b6adffe81462809c5b7d83cb3a7b453ff9f41ed5',
+ ),
+ ResponseValue: decodeHex(
+ '0aa03a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000008e00000000000000000000000000000000000000000000000000000000000000ce000000000000000000000000000000000000000000000000000000000000010e000000000000000000000000000000000000000000000000000000000000014e000000000000000000000000000000000000000000000000000000000000018e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000010a9dadc131e4e73c69a68881faf8e6a308cb3d8a00000000000000000000000017d46e26bc3ff8f63ee9de35237e545a44371b08506fe5ef271e94916a4dc63e896803ee748795154f4d43c781f6ff91c23d9e10697ff8fdb195581b6a2db8524240b57854dd1f21c10353ea458eecd8593624d0e0a7cca07c62a4b58df8c258000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000014c691603d19278baf83a6fca9d6a19fb5fdbd4ec0000000000000000000000007c396cae7f777e6619a187d797f7f3636818777d88bbac532782ebc1a44baccd69b6edd515be8ca5acc9c94515b55bf6c635f6fafc452df3fc26098f7c179f0407d5475a1d4e14340fa6c94bcc81e77717314630d469689370f35dc7517dbed8000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001591be078e128b2393e748226659795281de0e8a7000000000000000000000000439c4912deb85b489723f72e58e5ba50ac39f8225b30dd8ab9dd5521d867702f9d2d902db7c2d75ffc3dd4cff4a70f93e7ace5f8f9f4fff6f9e512239a2a29351c2d617ecc06b241b89dea2eaeeafe9da9b19a80e8c03e8e9c0b9c732b48255a000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001e75c048f6d663668a7973838fa2711221e50af940000000000000000000000005886de6eb4c1c4283dacbd678a1da430ea8bf1b7da7e767afb2ecd2ea6d930dbf32436e8554e1ad99f3751682bea0c7058c3079295df2fc252c122c6a6256566099865d05106cee289724c3bdaefd435238dc39194493aab978ff3ee59d62d58000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000012e93310b6b97e77e9ae25e4c365687c79f19ea1c000000000000000000000000ba27b0482262260c9c377c666405b0cdbecfc54c1a59b2ba9ecff04c9ed3a6c5deb8e63fb276addfcb987956da035d97b2f2279c8b367c349fe8174530a1822095263cf5c220fad97babc2967b311f7ba463353d56e78c410d3d0e8fcc798343000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000014887166c424a781d043a2200db75e5d749c6020e000000000000000000000000df142f783baab355fdc201e24d999fa3f65ee2f7461b3a944e0c0c742599018aaf546e571c7ffa311382a263a6ba92a908d2e0b918010079bbce610a859da12c57335620009298b5705b7c1bb9b346cdc01e39b8241d6fda6163deecaea58721000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000010369e9dd2c77dda3327b6f5809608105e9d3b4ca000000000000000000000000578a1b91f504453e42470255b6adffe81462809c5b7d83cb3a7b453ff9f41ed59dcbb68288a52c3439afd223c4414b678733b7667ff20e21d6261d1be5bc8c61837ec6a79c0e790244c4b14c93adb988db6dfc3e2042886785c3ae5871f43d68000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000003e0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000186f6666636861696e5f7265706f7274696e6740312e302e300000000000000000000000000000000000000000000000000000000000000000000000000000001263726f6e2d7472696767657240312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d616374696f6e7340312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000018687474702d7472696767657240312e302e302d616c70686100000000000000000000000000000000000000000000000000000000000000000000000000000015636f6e73656e73757340312e302e302d616c7068610000000000000000000000000000000000000000000000000000000000000000000000000000000000001d636f6e666964656e7469616c2d6874747040312e302e302d616c706861000000000000000000000000000000000000000000000000000000000000000000000d646f6e74696d6540312e302e30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
+ ),
+ },
+ ],
+ ChainSelector: 16015286601757825753n,
+}
+
+function decodeHex(s: string): Uint8Array {
+ return hexToBytes(`0x${s}`)
+}
+
+function sigsToBytes(sigsHex: string[]): Uint8Array[] {
+ return sigsHex.map((s) => decodeHex(s))
+}
+
+function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
+ return a.length === b.length && a.every((v, i) => v === b[i])
+}
+
+const CALL_CONTRACT_REQUEST_TYPE_URL =
+ 'type.googleapis.com/capabilities.blockchain.evm.v1alpha.CallContractRequest'
+const CALL_CONTRACT_REPLY_TYPE_URL =
+ 'type.googleapis.com/capabilities.blockchain.evm.v1alpha.CallContractReply'
+
+function setupDonSettingRead(replay: WorkflowReplay): { actualCalls: number } {
+ const state = { actualCalls: 0 }
+ const capId = `evm:ChainSelector:${replay.ChainSelector}@1.0.0`
+ registerTestCapability(capId, (req) => {
+ const callIdx = state.actualCalls++
+ if (req.payload.typeUrl !== CALL_CONTRACT_REQUEST_TYPE_URL) {
+ return {
+ response: {
+ case: 'error',
+ value: `expected type url ${CALL_CONTRACT_REQUEST_TYPE_URL} got ${req.payload.typeUrl}`,
+ },
+ }
+ }
+ if (callIdx >= replay.Calls.length) {
+ return {
+ response: {
+ case: 'error',
+ value: `unexpected call ${callIdx} (only ${replay.Calls.length} calls expected)`,
+ },
+ }
+ }
+ const expected = replay.Calls[callIdx]
+ if (!bytesEqual(expected.RequestValue, req.payload.value)) {
+ return {
+ response: {
+ case: 'error',
+ value: `call ${callIdx}: request bytes mismatch`,
+ },
+ }
+ }
+ return {
+ response: {
+ case: 'payload',
+ value: create(AnySchema, {
+ typeUrl: CALL_CONTRACT_REPLY_TYPE_URL,
+ value: expected.ResponseValue,
+ }),
+ },
+ }
+ })
+ return state
+}
+
+// isErrorType mirrors Go's errors.Is: checks if err or any error in an
+// AggregateError is an instance of the given class.
+function isErrorType(
+ err: unknown,
+ cls: abstract new (...args: unknown[]) => T,
+): boolean {
+ if (err instanceof cls) return true
+ if (err instanceof AggregateError) return err.errors.some((e) => isErrorType(e, cls))
+ return false
+}
+
+async function getReport(): Promise {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const sigsWithExtra: Uint8Array[] = []
+ sigsWithExtra.push(reportSigs[0])
+ sigsWithExtra.push(notASignerSig)
+ sigsWithExtra.push(...reportSigs.slice(1))
+ sigsWithExtra.push(extraValidSig)
+ return Report.parse(runtime, rawReport, sigsWithExtra, reportContext)
+}
+
+beforeEach(() => {
+ donInfoCache.clear()
+})
+
+test('SeqNr', async () => {
+ const report = await getReport()
+ expect(report.seqNr()).toBe(reportSeqNr)
+})
+
+test('ConfigDigest', async () => {
+ const report = await getReport()
+ expect(report.configDigest()).toEqual(reportDigest)
+})
+
+test('ReportContext', async () => {
+ const report = await getReport()
+ expect(report.reportContext()).toEqual(reportContext)
+})
+
+test('Version', async () => {
+ const report = await getReport()
+ expect(report.version()).toBe(reportVersion)
+})
+
+test('ExecutionID', async () => {
+ const report = await getReport()
+ expect(report.executionId()).toBe(reportExecID)
+})
+
+test('Timestamp', async () => {
+ const report = await getReport()
+ expect(report.timestamp()).toBe(reportTimestampUnix)
+})
+
+test('DONID', async () => {
+ const report = await getReport()
+ expect(report.donId()).toBe(reportDONID)
+})
+
+test('DONConfigVersion', async () => {
+ const report = await getReport()
+ expect(report.donConfigVersion()).toBe(reportDONCfgVer)
+})
+
+test('WorkflowID', async () => {
+ const report = await getReport()
+ expect(report.workflowId()).toBe(reportWfID)
+})
+
+test('WorkflowName', async () => {
+ const report = await getReport()
+ expect(report.workflowName()).toBe(reportWfName)
+})
+
+test('WorkflowOwner', async () => {
+ const report = await getReport()
+ expect(report.workflowOwner()).toBe(reportWfOwner)
+})
+
+test('ReportID', async () => {
+ const report = await getReport()
+ expect(report.reportId()).toBe(reportReportID)
+})
+
+test('Body', async () => {
+ const report = await getReport()
+ expect(new TextDecoder().decode(report.body())).toBe(reportBody)
+})
+
+test('Signatures includes signatures and ids of F+1 valid entries', async () => {
+ const report = await getReport()
+ const actual = report.x_generatedCodeOnly_unwrap().sigs
+ for (let i = 0; i < reportSigs.length; i++) {
+ expect(actual[i].signature).toEqual(reportSigs[i])
+ expect(actual[i].signerId).toEqual(reportSigIds[i])
+ }
+})
+
+describe('Report Parse', () => {
+ test('valid report', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const report = await Report.parse(runtime, rawReport, reportSigs, reportContext)
+ expect(report).not.toBeNull()
+ })
+
+ test('DON signatures are cached', async () => {
+ const mock = setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ await Report.parse(runtime, rawReport, reportSigs, reportContext)
+ await Report.parse(runtime, rawReport, reportSigs, reportContext)
+ // Two capability calls (getDON + getNodesByP2PIds) on the first parse;
+ // the second parse should hit the cache and make zero additional calls.
+ expect(mock.actualCalls).toBe(2)
+ })
+
+ test('valid report on specific DON', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedZones: [zoneFromEnvironment(productionEnvironment(), reportDONID)],
+ }
+
+ const report = await Report.parse(runtime, rawReport, reportSigs, reportContext, config)
+
+ expect(report).not.toBeNull()
+ })
+
+ test('invalid DON does not fetch signatures on chain', async () => {
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedZones: [zoneFromEnvironment(productionEnvironment(), reportDONID + 1)],
+ }
+ await expect(
+ Report.parse(runtime, rawReport, reportSigs, reportContext, config),
+ ).rejects.toThrow(/DON ID 1 is not in accepted zones/)
+ })
+
+ test('zones not fetched across environments', async () => {
+ setupDonSettingRead(otherEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedZones: [
+ zoneFromEnvironment(productionEnvironment(), reportDONID + 1),
+ {
+ environment: {
+ chainSelector: otherEnvironmentReplay.ChainSelector,
+ registryAddress: '0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f',
+ },
+ donID: reportDONID,
+ },
+ ],
+ }
+
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, reportSigs, reportContext, config)
+ } catch (e) {
+ err = e
+ }
+
+ expect(isErrorType(err, UnknownSignerError)).toBe(true)
+ })
+
+ test('DON id in multiple environments', async () => {
+ setupDonSettingRead(otherEnvironmentReplay)
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedZones: [
+ {
+ environment: {
+ chainSelector: otherEnvironmentReplay.ChainSelector,
+ registryAddress: '0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f',
+ },
+ donID: reportDONID,
+ },
+ zoneFromEnvironment(productionEnvironment(), reportDONID),
+ ],
+ }
+ const report = await Report.parse(runtime, rawReport, reportSigs, reportContext, config)
+ expect(report).not.toBeNull()
+ })
+
+ test('All fetch errors surfaced if zones cannot be checked', async () => {
+ setupDonSettingRead(otherEnvironmentReplay)
+ // Unexpected input will simulate a failure from the read:
+ // same Calls as otherEnvironmentReplay but different ChainSelector,
+ // so the request bytes won't match and the mock returns an error.
+ const unexpectedInput: WorkflowReplay = {
+ ...otherEnvironmentReplay,
+ ChainSelector: 17015286601757825753n,
+ }
+ setupDonSettingRead(unexpectedInput)
+ // Production (5009...) is intentionally NOT registered.
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedZones: [
+ {
+ environment: {
+ chainSelector: otherEnvironmentReplay.ChainSelector,
+ registryAddress: '0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f',
+ },
+ donID: reportDONID,
+ },
+ {
+ environment: {
+ chainSelector: unexpectedInput.ChainSelector,
+ registryAddress: '0x89c9cf548b4179F8901cda1f8623568b58215E62',
+ },
+ donID: reportDONID,
+ },
+ {
+ environment: {
+ chainSelector: 18015286601757825753n,
+ registryAddress: '0x99c9cf548b4179F8901cda1f8623568b58215E62',
+ },
+ // not included, not intended to read from
+ donID: reportDONID + 1,
+ },
+ ],
+ acceptedEnvironments: [productionEnvironment()],
+ }
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, reportSigs, reportContext, config)
+ } catch (e) {
+ err = e
+ }
+ const msg = err instanceof Error ? err.message : String(err)
+ expect(msg).toContain('17015286601757825753')
+ expect(msg).toContain('0x89c9cf548b4179F8901cda1f8623568b58215E62')
+ expect(msg).toContain('5009297550715157269')
+ expect(msg).toContain('0x76c9cf548b4179F8901cda1f8623568b58215E62')
+ expect(msg).not.toContain('18015286601757825753')
+ })
+
+ describe('DON id in zone and environment', () => {
+ const cases: Array<{
+ name: string
+ environment: Environment
+ zone: { environment: Environment; donID: number }
+ }> = [
+ {
+ name: 'zone',
+ environment: {
+ chainSelector: otherEnvironmentReplay.ChainSelector,
+ registryAddress: '0x76c9cf548b4179F8901cda1f8623568b58215E62',
+ },
+ zone: zoneFromEnvironment(productionEnvironment(), reportDONID),
+ },
+ {
+ name: 'environment',
+ environment: productionEnvironment(),
+ zone: {
+ environment: {
+ chainSelector: otherEnvironmentReplay.ChainSelector,
+ registryAddress: '0x76c9cf548b4179F8901cda1f8623568b58215E62',
+ },
+ donID: reportDONID,
+ },
+ },
+ ]
+
+ for (const tt of cases) {
+ test(`Report in ${tt.name}`, async () => {
+ setupDonSettingRead(otherEnvironmentReplay)
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedEnvironments: [tt.environment],
+ acceptedZones: [tt.zone],
+ }
+ const report = await Report.parse(runtime, rawReport, reportSigs, reportContext, config)
+ expect(report).not.toBeNull()
+ })
+ }
+ })
+
+ test('too few signatures fails validation', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ await expect(
+ Report.parse(runtime, rawReport, reportSigs.slice(0, 2), reportContext),
+ ).rejects.toThrow(WrongSignatureCountError)
+ })
+
+ test('invalid signature length fails validation', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const invalidSigs = [...reportSigs]
+ invalidSigs[1] = new Uint8Array(32)
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, invalidSigs, reportContext)
+ } catch (e) {
+ err = e
+ }
+ expect(isErrorType(err, ParseSignatureError)).toBe(true)
+ })
+
+ test('unrecoverable signature fails validation', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const invalidSigs = [...reportSigs]
+ invalidSigs[1] = new Uint8Array(65)
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, invalidSigs, reportContext)
+ } catch (e) {
+ err = e
+ }
+ expect(isErrorType(err, RecoverSignerError)).toBe(true)
+ })
+
+ test('invalid signatures fails validation', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const invalidSigs = [...reportSigs]
+ invalidSigs[1] = notASignerSig
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, invalidSigs, reportContext)
+ } catch (e) {
+ err = e
+ }
+ expect(isErrorType(err, UnknownSignerError)).toBe(true)
+ })
+
+ test('duplicated signatures fails validation', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const invalidSigs = [...reportSigs]
+ invalidSigs[2] = invalidSigs[1]
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, invalidSigs, reportContext)
+ } catch (e) {
+ err = e
+ }
+ expect(isErrorType(err, DuplicateSignerError)).toBe(true)
+ })
+
+ test('multiple signature failures all reported', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ // sigs[0]: too short → ParseSignatureError
+ // sigs[1]: valid
+ // sigs[2]: 65 zeros → RecoverSignerError
+ // sigs[3]: valid
+ // Only 2 valid sigs, need 4 (f+1), so all skip errors must be surfaced.
+ const invalidSigs = [...reportSigs]
+ invalidSigs[0] = new Uint8Array(32) // wrong length
+ invalidSigs[2] = new Uint8Array(65) // unrecoverable
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, invalidSigs, reportContext)
+ } catch (e) {
+ err = e
+ }
+ expect(isErrorType(err, ParseSignatureError)).toBe(true)
+ expect(isErrorType(err, RecoverSignerError)).toBe(true)
+ })
+
+ describe('invalid report format returns errors without reading from chain', () => {
+ for (const skipSignatureVerification of [false, true]) {
+ test(`skipSignatureVerification=${skipSignatureVerification}`, async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const config = {
+ acceptedEnvironments: [productionEnvironment()],
+ skipSignatureVerification,
+ }
+ await expect(
+ Report.parse(
+ runtime,
+ new TextEncoder().encode('Not a report'),
+ reportSigs,
+ new Uint8Array(0),
+ config,
+ ),
+ ).rejects.toThrow(RawReportTooShortError)
+ })
+ }
+ })
+
+ test('skip signature validation invalid signatures', async () => {
+ setupDonSettingRead(productionEnvironmentReplay)
+ const runtime = newTestRuntime()
+ const invalidSigs = [...reportSigs]
+ invalidSigs[1] = new Uint8Array(32)
+ invalidSigs[1].set(reportSigs[1].subarray(0, 32))
+ invalidSigs[1][2] = 0
+ const config = { skipSignatureVerification: true }
+ const report = await Report.parse(runtime, rawReport, invalidSigs, reportContext, config)
+ expect(report).not.toBeNull()
+ })
+
+ test('invalid registry address returns an error', async () => {
+ const runtime = newTestRuntime()
+ const invalidAddress = 'Not real'
+ const config = {
+ acceptedEnvironments: [
+ {
+ chainSelector: 1324n,
+ registryAddress: invalidAddress,
+ } satisfies Environment,
+ ],
+ }
+ let err: unknown
+ try {
+ await Report.parse(runtime, rawReport, reportSigs, reportContext, config)
+ } catch (e) {
+ err = e
+ }
+ const allMessages =
+ err instanceof AggregateError
+ ? err.errors.map((e) => (e instanceof Error ? e.message : String(e))).join(' ')
+ : err instanceof Error
+ ? err.message
+ : String(err)
+ expect(allMessages).toContain(`invalid registry address`)
+ expect(allMessages).toContain(invalidAddress)
+ })
+})
diff --git a/packages/cre-sdk/src/sdk/report.ts b/packages/cre-sdk/src/sdk/report.ts
index d07ca1da..92719b9e 100644
--- a/packages/cre-sdk/src/sdk/report.ts
+++ b/packages/cre-sdk/src/sdk/report.ts
@@ -1,18 +1,654 @@
-import { fromJson } from '@bufbuild/protobuf'
+import { create, fromJson } from '@bufbuild/protobuf'
+import { BinaryReader, WireType } from '@bufbuild/protobuf/wire'
+import { AnySchema } from '@bufbuild/protobuf/wkt'
import {
+ AttributedSignatureSchema,
+ AwaitCapabilitiesRequestSchema,
+ CapabilityRequestSchema,
type ReportResponse,
type ReportResponseJson,
ReportResponseSchema,
} from '@cre/generated/sdk/v1alpha/sdk_pb'
+import type { Address } from 'viem'
+import { concatHex, getAddress, hexToBytes, keccak256, recoverAddress, toHex } from 'viem'
+import { type Environment, productionEnvironment, type Zone } from './don-info'
+import {
+ DuplicateSignerError,
+ NullReportError,
+ ParseSignatureError,
+ RawReportTooShortError,
+ RecoverSignerError,
+ UnknownSignerError,
+ WrongSignatureCountError,
+} from './errors'
+import { type DonInfo, donInfoCache } from './report-internals'
+import type { BaseRuntime } from './runtime'
+
+const GET_DON_SELECTOR = new Uint8Array([0x23, 0x53, 0x74, 0x05])
+const GET_NODES_BY_P2P_IDS_SELECTOR = new Uint8Array([0x05, 0xa5, 0x19, 0x66])
+
+function cacheKey(env: Environment, donID: number): string {
+ return `${env.chainSelector.toString()}:${donID}`
+}
+
+function normalizeRegistryHex(addr: string): string {
+ return addr
+ .trim()
+ .replace(/^0[xX]/, '')
+ .toLowerCase()
+}
+
+function isProductionEnvironmentForReport(env: Environment): boolean {
+ const pe = productionEnvironment()
+ return (
+ env.chainSelector === pe.chainSelector &&
+ normalizeRegistryHex(env.registryAddress) === normalizeRegistryHex(pe.registryAddress)
+ )
+}
+
+function decodeRegistryAddress(registryAddress: string): Uint8Array {
+ const hex = normalizeRegistryHex(registryAddress)
+ if (hex.length !== 40) {
+ throw new Error(`invalid registry address ${JSON.stringify(registryAddress)}`)
+ }
+ return hexToBytes(`0x${hex}`)
+}
+
+function padUint256(v: bigint | number): Uint8Array {
+ const n = typeof v === 'bigint' ? v : BigInt(v)
+ const b = new Uint8Array(32)
+ const view = new DataView(b.buffer)
+ view.setBigUint64(24, n, false)
+ return b
+}
+
+function bytesToBigIntBE(word: Uint8Array): bigint {
+ let x = 0n
+ for (let i = 0; i < word.length; i++) {
+ x = (x << 8n) | BigInt(word[i])
+ }
+ return x
+}
+
+function readUint256AsInt(word: Uint8Array): number {
+ const b = bytesToBigIntBE(word)
+ if (b > BigInt(Number.MAX_SAFE_INTEGER)) {
+ throw new Error('ABI uint256 value too large for Number')
+ }
+ return Number(b)
+}
+
+function protoVarint(v: number | bigint): number[] {
+ const out: number[] = []
+ let n = typeof v === 'bigint' ? v : BigInt(v)
+ while (n >= 128n) {
+ out.push(Number(n & 0x7fn) | 0x80)
+ n >>= 7n
+ }
+ out.push(Number(n))
+ return out
+}
+
+function protoTag(field: number, wireType: 0 | 2): number[] {
+ return protoVarint((field << 3) | wireType)
+}
+
+function protoLenBytes(data: Uint8Array): number[] {
+ return [...protoVarint(data.length), ...data]
+}
+
+function buildCallContractRequestBytes(registryAddr: Uint8Array, callData: Uint8Array): Uint8Array {
+ const callMsg = new Uint8Array([
+ ...protoTag(2, 2),
+ ...protoLenBytes(registryAddr),
+ ...protoTag(3, 2),
+ ...protoLenBytes(callData),
+ ])
+ const bigInt = new Uint8Array([
+ ...protoTag(1, 2),
+ ...protoLenBytes(new Uint8Array([0x03])),
+ ...protoTag(2, 0),
+ ...protoVarint(0xffffffffffffffffn),
+ ])
+ return new Uint8Array([
+ ...protoTag(2, 2),
+ ...protoLenBytes(bigInt),
+ ...protoTag(1, 2),
+ ...protoLenBytes(callMsg),
+ ])
+}
+
+function decodeCallContractReplyData(bytes: Uint8Array): Uint8Array {
+ const reader = new BinaryReader(bytes)
+ while (reader.pos < reader.len) {
+ const [fieldNo, wireType] = reader.tag()
+ if (fieldNo === 1 && wireType === WireType.LengthDelimited) {
+ return reader.bytes()
+ }
+ reader.skip(wireType)
+ }
+ throw new Error('data field not found in CallContractReply')
+}
+
+const CALL_CONTRACT_REQUEST_TYPE_URL =
+ 'type.googleapis.com/capabilities.blockchain.evm.v1alpha.CallContractRequest'
+
+function callContract(
+ runtime: BaseRuntime,
+ capID: string,
+ registryAddr: Uint8Array,
+ callData: Uint8Array,
+): Uint8Array {
+ const reqBytes = buildCallContractRequestBytes(registryAddr, callData)
+ const anyPayload = create(AnySchema, {
+ typeUrl: CALL_CONTRACT_REQUEST_TYPE_URL,
+ value: reqBytes,
+ })
+ // biome-ignore lint/suspicious/noExplicitAny: intentional bypass of typed callCapability to avoid EVM schema imports
+ const rt = runtime as any
+ const callbackId: number = rt.nextCallId++
+ const req = create(CapabilityRequestSchema, {
+ id: capID,
+ method: 'CallContract',
+ payload: anyPayload,
+ callbackId,
+ })
+
+ if (!rt.helpers.call(req)) {
+ throw new Error(`EVM capability '${capID}' not found`)
+ }
+
+ // Must use create() — the real WASM bridge serializes this as a protobuf message and
+ // requires a proper typed message (with $typeName). A plain { ids: [...] } object works
+ // in the test runtime but silently breaks in simulation.
+ const awaitResp = rt.helpers.await(
+ create(AwaitCapabilitiesRequestSchema, { ids: [callbackId] }),
+ rt.maxResponseSize,
+ )
+ const capResp = awaitResp?.responses?.[callbackId]
+ if (!capResp) {
+ throw new Error(`no response from EVM capability '${capID}'`)
+ }
+ const response = capResp.response
+ if (response.case === 'error') {
+ throw new Error(response.value as string)
+ }
+ if (response.case !== 'payload') {
+ throw new Error(`unexpected response '${response.case}' from EVM capability '${capID}'`)
+ }
+ return decodeCallContractReplyData((response.value as { value: Uint8Array }).value)
+}
+
+function encodeGetDONCalldata(donID: number): Uint8Array {
+ const padded = new Uint8Array(32)
+ const view = new DataView(padded.buffer)
+ view.setUint32(28, donID >>> 0, false)
+ const out = new Uint8Array(4 + 32)
+ out.set(GET_DON_SELECTOR, 0)
+ out.set(padded, 4)
+ return out
+}
+
+function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
+ const total = parts.reduce((n, p) => n + p.length, 0)
+ const r = new Uint8Array(total)
+ let o = 0
+ for (const p of parts) {
+ r.set(p, o)
+ o += p.length
+ }
+ return r
+}
+
+function encodeGetNodesByP2PIdsCalldata(p2pIds: Uint8Array[]): Uint8Array {
+ const chunks: Uint8Array[] = [
+ GET_NODES_BY_P2P_IDS_SELECTOR,
+ padUint256(32),
+ padUint256(p2pIds.length),
+ ]
+ for (const id of p2pIds) {
+ if (id.length !== 32) {
+ throw new Error('p2p id must be 32 bytes')
+ }
+ chunks.push(new Uint8Array(id))
+ }
+ return concatBytes(chunks)
+}
+
+const NODE_TUPLE_HEAD_SIZE = 288
+
+function fetchDONInfo(runtime: BaseRuntime, env: Environment, donID: number): DonInfo {
+ const key = cacheKey(env, donID)
+ const hit = donInfoCache.get(key)
+ if (hit) {
+ return hit
+ }
+
+ const registryAddr = decodeRegistryAddress(env.registryAddress)
+ const capID = `evm:ChainSelector:${env.chainSelector.toString()}@1.0.0`
+
+ const getDONABI = callContract(runtime, capID, registryAddr, encodeGetDONCalldata(donID))
+
+ if (getDONABI.length < 224) {
+ throw new Error(`getDON ABI response too short: ${getDONABI.length} bytes`)
+ }
+
+ const f = readUint256AsInt(getDONABI.subarray(96, 128))
+
+ const tupleStart = 32
+ const nodeP2PIdsPtr = readUint256AsInt(getDONABI.subarray(192, 224))
+ const nodeCountOff = tupleStart + nodeP2PIdsPtr
+ if (nodeCountOff + 32 > getDONABI.length) {
+ throw new Error('getDON ABI: nodeP2PIds pointer out of range')
+ }
+ const nodeCount = readUint256AsInt(getDONABI.subarray(nodeCountOff, nodeCountOff + 32))
+ if (nodeCountOff + 32 + nodeCount * 32 > getDONABI.length) {
+ throw new Error('getDON ABI: nodeP2PIds data out of range')
+ }
+
+ const nodeP2PIds: Uint8Array[] = []
+ for (let i = 0; i < nodeCount; i++) {
+ const start = nodeCountOff + 32 + i * 32
+ nodeP2PIds.push(getDONABI.subarray(start, start + 32))
+ }
+
+ if (nodeCount === 0) {
+ const info: DonInfo = { f, signers: new Map() }
+ donInfoCache.set(key, info)
+ return info
+ }
+
+ const getNodesABI = callContract(
+ runtime,
+ capID,
+ registryAddr,
+ encodeGetNodesByP2PIdsCalldata(nodeP2PIds),
+ )
+
+ if (getNodesABI.length < 64) {
+ throw new Error(`getNodesByP2PIds ABI response too short: ${getNodesABI.length} bytes`)
+ }
+
+ const outerPtr = readUint256AsInt(getNodesABI.subarray(0, 32))
+ if (outerPtr + 32 > getNodesABI.length) {
+ throw new Error('getNodesByP2PIds ABI: outer pointer out of range')
+ }
+ const returnedCount = readUint256AsInt(getNodesABI.subarray(outerPtr, outerPtr + 32))
+
+ const signers = new Map()
+ for (let i = 0; i < returnedCount; i++) {
+ const elemPtrOff = outerPtr + 32 + i * 32
+ if (elemPtrOff + 32 > getNodesABI.length) {
+ break
+ }
+ const elemPtr = readUint256AsInt(getNodesABI.subarray(elemPtrOff, elemPtrOff + 32))
+ const tupleBase = outerPtr + 32 + elemPtr
+ if (tupleBase + NODE_TUPLE_HEAD_SIZE > getNodesABI.length) {
+ break
+ }
+ const nodeOperatorId = Number(
+ bytesToBigIntBE(getNodesABI.subarray(tupleBase, tupleBase + 32)) & 0xffffffffn,
+ )
+ const signerSlot = tupleBase + 3 * 32
+ const addrBytes = getNodesABI.subarray(signerSlot, signerSlot + 20)
+ const addr = getAddress(toHex(addrBytes))
+ signers.set(addr, nodeOperatorId)
+ }
+
+ const info: DonInfo = { f, signers }
+ donInfoCache.set(key, info)
+ return info
+}
+
+function computeReportHash(rawReport: Uint8Array, reportContext: Uint8Array): `0x${string}` {
+ const innerHash = keccak256(toHex(rawReport))
+ return keccak256(concatHex([innerHash, toHex(reportContext)]))
+}
+
+function addressKeyNo0x(addr: `0x${string}`): string {
+ return addr.slice(2).toLowerCase()
+}
+
+async function verifySigs(
+ report: ReportResponse,
+ f: number,
+ signers: Map,
+): Promise {
+ const required = f + 1
+ const sigs = report.sigs
+ if (sigs.length < required) {
+ throw new WrongSignatureCountError()
+ }
+
+ const reportHash = computeReportHash(report.rawReport, report.reportContext)
+ const seen = new Set()
+ const accepted: typeof sigs = []
+ const skipErrs: Error[] = []
+
+ for (let i = 0; i < sigs.length; i++) {
+ if (accepted.length === required) {
+ break
+ }
+
+ const attrSig = sigs[i]
+ const sigBytes = new Uint8Array(attrSig.signature)
+
+ if (sigBytes.length !== 65) {
+ skipErrs.push(new ParseSignatureError())
+ continue
+ }
+
+ const normalized = new Uint8Array(sigBytes)
+ if (normalized[64] === 27 || normalized[64] === 28) {
+ normalized[64] -= 27
+ }
+
+ let recovered: `0x${string}`
+ try {
+ recovered = await recoverAddress({
+ hash: reportHash,
+ signature: toHex(normalized),
+ })
+ } catch {
+ skipErrs.push(new RecoverSignerError())
+ continue
+ }
+
+ const key = addressKeyNo0x(recovered)
+ if (seen.has(key)) {
+ skipErrs.push(new DuplicateSignerError())
+ continue
+ }
+ seen.add(key)
+
+ const nodeOperatorId = signers.get(key)
+ if (nodeOperatorId === undefined) {
+ skipErrs.push(new UnknownSignerError())
+ continue
+ }
+
+ attrSig.signerId = nodeOperatorId
+ accepted.push(attrSig)
+ }
+
+ if (accepted.length < required) {
+ if (skipErrs.length > 0) {
+ throw new AggregateError(skipErrs)
+ }
+ throw new WrongSignatureCountError()
+ }
+
+ report.sigs = accepted
+}
+
+export type ReportParseConfig = {
+ acceptedZones?: Zone[]
+ acceptedEnvironments?: Environment[]
+ skipSignatureVerification?: boolean
+}
+
+function mergeReportParseConfig(
+ overrides?: ReportParseConfig,
+): Required<
+ Pick
+> {
+ return {
+ acceptedZones: overrides?.acceptedZones ?? [],
+ acceptedEnvironments:
+ overrides !== undefined ? (overrides.acceptedEnvironments ?? []) : [productionEnvironment()],
+ skipSignatureVerification: overrides?.skipSignatureVerification ?? false,
+ }
+}
+
+function normalizeDonSigners(signers: Map): Map {
+ const out = new Map()
+ for (const [addr, id] of signers) {
+ out.set(addr.slice(2).toLowerCase(), id)
+ }
+ return out
+}
+
+export const REPORT_METADATA_HEADER_LENGTH = 109
+
+const REPORT_METADATA_OFFSETS = {
+ version: 0,
+ versionSize: 1,
+ executionId: 1,
+ executionIdSize: 32,
+ timestamp: 33,
+ timestampSize: 4,
+ donId: 37,
+ donIdSize: 4,
+ donConfigVersion: 41,
+ donConfigVersionSize: 4,
+ workflowId: 45,
+ workflowIdSize: 32,
+ workflowName: 77,
+ workflowNameSize: 10,
+ workflowOwner: 87,
+ workflowOwnerSize: 20,
+ reportId: 107,
+ reportIdSize: 2,
+ bodyStart: 109,
+} as const
+
+export type ReportMetadataHeader = {
+ version: number
+ executionId: string
+ timestamp: number
+ donId: number
+ donConfigVersion: number
+ workflowId: string
+ workflowName: string
+ workflowOwner: string
+ reportId: string
+ body: Uint8Array
+}
+
+function encodeHexLower(bytes: Uint8Array): string {
+ let out = ''
+ for (let i = 0; i < bytes.length; i++) {
+ out += bytes[i].toString(16).padStart(2, '0')
+ }
+ return out
+}
+
+function readUint32BE(raw: Uint8Array, offset: number): number {
+ return new DataView(raw.buffer, raw.byteOffset + offset, 4).getUint32(0, false)
+}
+
+function parseReportMetadataHeader(raw: Uint8Array | null | undefined): ReportMetadataHeader {
+ if (raw === undefined || raw === null) {
+ throw new NullReportError()
+ }
+ if (raw.length < REPORT_METADATA_HEADER_LENGTH) {
+ throw new RawReportTooShortError(REPORT_METADATA_HEADER_LENGTH, raw.length)
+ }
+
+ const o = REPORT_METADATA_OFFSETS
+ const workflowNameBytes = raw.subarray(o.workflowName, o.workflowName + o.workflowNameSize)
+
+ return {
+ version: raw[o.version],
+ executionId: encodeHexLower(raw.subarray(o.executionId, o.executionId + o.executionIdSize)),
+ timestamp: readUint32BE(raw, o.timestamp),
+ donId: readUint32BE(raw, o.donId),
+ donConfigVersion: readUint32BE(raw, o.donConfigVersion),
+ workflowId: encodeHexLower(raw.subarray(o.workflowId, o.workflowId + o.workflowIdSize)),
+ workflowName: new TextDecoder('utf-8', { fatal: false }).decode(workflowNameBytes),
+ workflowOwner: encodeHexLower(
+ raw.subarray(o.workflowOwner, o.workflowOwner + o.workflowOwnerSize),
+ ),
+ reportId: encodeHexLower(raw.subarray(o.reportId, o.reportId + o.reportIdSize)),
+ body: raw.subarray(REPORT_METADATA_HEADER_LENGTH),
+ }
+}
export class Report {
private readonly report: ReportResponse
+ private cachedHeader: ReportMetadataHeader | undefined
+
public constructor(report: ReportResponse | ReportResponseJson) {
this.report = (report as unknown as { $typeName?: string }).$typeName
? (report as ReportResponse)
: fromJson(ReportResponseSchema, report as ReportResponseJson)
}
+ public static async parse(
+ runtime: BaseRuntime,
+ rawReport: Uint8Array,
+ signatures: Uint8Array[],
+ reportContext: Uint8Array,
+ config?: ReportParseConfig,
+ ): Promise {
+ const configDigest =
+ reportContext.length >= 32 ? reportContext.slice(0, 32) : new Uint8Array(32)
+ const seqNr =
+ reportContext.length >= 40
+ ? new DataView(reportContext.buffer, reportContext.byteOffset + 32, 8).getBigUint64(
+ 0,
+ false,
+ )
+ : 0n
+ const reportResponse = create(ReportResponseSchema, {
+ configDigest,
+ seqNr,
+ reportContext,
+ rawReport,
+ sigs: signatures.map((signature) =>
+ create(AttributedSignatureSchema, { signature, signerId: 0 }),
+ ),
+ })
+
+ const merged = mergeReportParseConfig(config)
+ const report = new Report(reportResponse)
+ if (merged.skipSignatureVerification) {
+ report.donId()
+ return report
+ }
+ await report.verifySignaturesWithConfig(runtime, merged)
+ return report
+ }
+
+ private parseHeader(): ReportMetadataHeader {
+ if (this.cachedHeader !== undefined) {
+ return this.cachedHeader
+ }
+ this.cachedHeader = parseReportMetadataHeader(this.report.rawReport)
+ return this.cachedHeader
+ }
+
+ private async verifySignaturesWithConfig(
+ runtime: BaseRuntime,
+ config: Required<
+ Pick<
+ ReportParseConfig,
+ 'acceptedZones' | 'acceptedEnvironments' | 'skipSignatureVerification'
+ >
+ >,
+ ): Promise {
+ const donId = this.donId()
+ const candidates: Environment[] = []
+ for (const z of config.acceptedZones) {
+ if (z.donID === donId) {
+ candidates.push(z.environment)
+ }
+ }
+ candidates.push(...config.acceptedEnvironments)
+
+ if (candidates.length === 0) {
+ throw new Error(`DON ID ${donId} is not in accepted zones`)
+ }
+
+ const fetchFailures: Error[] = []
+ let lastVerifyErr: Error | null = null
+
+ for (const env of candidates) {
+ let info: DonInfo
+ try {
+ info = fetchDONInfo(runtime, env, donId)
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err)
+ fetchFailures.push(
+ new Error(
+ `could not read from chain ${env.chainSelector} contract ${env.registryAddress}: ${msg}`,
+ ),
+ )
+ continue
+ }
+
+ try {
+ await verifySigs(this.report, info.f, normalizeDonSigners(info.signers))
+ return
+ } catch (err) {
+ lastVerifyErr = err instanceof Error ? err : new Error(String(err))
+ }
+ }
+
+ if (fetchFailures.length > 0) {
+ throw new AggregateError(fetchFailures, fetchFailures.map((e) => e.message).join('\n'))
+ }
+ if (lastVerifyErr !== null) {
+ throw lastVerifyErr
+ }
+ }
+
+ public seqNr(): bigint {
+ return this.report.seqNr
+ }
+
+ public configDigest(): Uint8Array {
+ return this.report.configDigest
+ }
+
+ public reportContext(): Uint8Array {
+ return this.report.reportContext
+ }
+
+ public rawReport(): Uint8Array {
+ return this.report.rawReport
+ }
+
+ public version(): number {
+ return this.parseHeader().version
+ }
+
+ public executionId(): string {
+ return this.parseHeader().executionId
+ }
+
+ public timestamp(): number {
+ return this.parseHeader().timestamp
+ }
+
+ public donId(): number {
+ return this.parseHeader().donId
+ }
+
+ public donConfigVersion(): number {
+ return this.parseHeader().donConfigVersion
+ }
+
+ public workflowId(): string {
+ return this.parseHeader().workflowId
+ }
+
+ public workflowName(): string {
+ return this.parseHeader().workflowName
+ }
+
+ public workflowOwner(): string {
+ return this.parseHeader().workflowOwner
+ }
+
+ public reportId(): string {
+ return this.parseHeader().reportId
+ }
+
+ public body(): Uint8Array {
+ return this.parseHeader().body
+ }
+
// x_generatedCodeOnly_unwrap is meant to be used by the code generator only.
x_generatedCodeOnly_unwrap(): ReportResponse {
return this.report
diff --git a/packages/cre-sdk/src/sdk/testutils/test-runtime.ts b/packages/cre-sdk/src/sdk/testutils/test-runtime.ts
index 01844874..94e94882 100644
--- a/packages/cre-sdk/src/sdk/testutils/test-runtime.ts
+++ b/packages/cre-sdk/src/sdk/testutils/test-runtime.ts
@@ -9,6 +9,7 @@ import { create, toBinary } from '@bufbuild/protobuf'
import type { Any } from '@bufbuild/protobuf/wkt'
import { anyPack, anyUnpack } from '@bufbuild/protobuf/wkt'
import type {
+ AwaitCapabilitiesRequest,
AwaitCapabilitiesResponse,
AwaitSecretsResponse,
CapabilityResponse,
@@ -237,7 +238,18 @@ function createTestRuntimeHelpers(
return true
},
- await(request: { ids: number[] }, maxResponseSizeBytes: bigint): AwaitCapabilitiesResponse {
+ await(
+ request: AwaitCapabilitiesRequest,
+ maxResponseSizeBytes: bigint,
+ ): AwaitCapabilitiesResponse {
+ if (
+ (request as unknown as { $typeName?: string }).$typeName !==
+ 'sdk.v1alpha.AwaitCapabilitiesRequest'
+ ) {
+ throw new Error(
+ 'await: expected a typed AwaitCapabilitiesRequest (created via create(AwaitCapabilitiesRequestSchema, ...)); got a plain object. The real WASM bridge serializes this to binary and will fail with a plain object.',
+ )
+ }
const responses: Record = {}
for (const id of request.ids) {
const resp = pendingCalls.get(id)