1- import { type NoirCompiledContract , loadContractArtifact } from '@aztec/aztec.js/abi' ;
2- import { AztecAddress } from '@aztec/aztec.js/addresses' ;
3- import {
4- type ContractInstanceWithAddress ,
5- getContractInstanceFromInstantiationParams ,
6- } from '@aztec/aztec.js/contracts' ;
71import { Fr } from '@aztec/aztec.js/fields' ;
8- import { PublicKeys , deriveKeys } from '@aztec/aztec.js/keys' ;
92import type { Logger } from '@aztec/foundation/log' ;
103import { cloneEphemeralStoreFrom } from '@aztec/kv-store/lmdb-v2' ;
114import type { ProtocolContractName } from '@aztec/protocol-contracts' ;
125import { ContractStore } from '@aztec/pxe/client/lazy' ;
13- import { computeArtifactHash } from '@aztec/stdlib/contract' ;
14- import type { ContractArtifactWithHash } from '@aztec/stdlib/contract' ;
156import type { ApiSchemaFor } from '@aztec/stdlib/schemas' ;
167import { zodFor } from '@aztec/stdlib/schemas' ;
178
18- import { createHash } from 'crypto' ;
19- import { createReadStream } from 'fs' ;
20- import { readFile , readdir } from 'fs/promises' ;
21- import { join , parse } from 'path' ;
9+ import { join } from 'path' ;
2210import { z } from 'zod' ;
2311
2412// Side-effect import: registers the msgpackr Fr extension for the bundled `Fr` class. Must
@@ -28,64 +16,17 @@ import { type TXEOracleFunctionName, TXESession } from './txe_session.js';
2816import {
2917 type ForeignCallArgs ,
3018 ForeignCallArgsSchema ,
31- type ForeignCallArray ,
3219 type ForeignCallResult ,
3320 ForeignCallResultSchema ,
34- type ForeignCallSingle ,
35- addressFromSingle ,
36- fromArray ,
37- fromSingle ,
38- toSingle ,
3921} from './utils/encoding.js' ;
22+ import { TXEArtifactResolver } from './utils/txe_artifact_resolver.js' ;
4023
4124// Protocol contracts TXE registers in its contract store. Only AuthRegistry is needed for the
4225// current test suites; add a contract here if a lookup against a `0x000…00X` address fails.
4326export const TXE_REQUIRED_PROTOCOL_CONTRACTS : ProtocolContractName [ ] = [ ] ;
4427
4528const sessions = new Map < number , TXESession > ( ) ;
4629
47- /**
48- * Cache + in-flight map pair. Lookup hits the cache, then awaits an in-flight `compute()` if one
49- * exists, otherwise starts one and stores it. Guarantees `compute()` runs at most once per `key`
50- * across concurrent callers, which matters because `computeArtifactHash` is expensive.
51- */
52- class AsyncCache < K , V > {
53- private readonly cache = new Map < K , V > ( ) ;
54- private readonly inFlight = new Map < K , Promise < V > > ( ) ;
55-
56- getOrCompute ( key : K , compute : ( ) => Promise < V > ) : Promise < V > {
57- const cached = this . cache . get ( key ) ;
58- if ( cached !== undefined ) {
59- return Promise . resolve ( cached ) ;
60- }
61- let pending = this . inFlight . get ( key ) ;
62- if ( ! pending ) {
63- pending = ( async ( ) => {
64- try {
65- const value = await compute ( ) ;
66- this . cache . set ( key , value ) ;
67- return value ;
68- } finally {
69- this . inFlight . delete ( key ) ;
70- }
71- } ) ( ) ;
72- this . inFlight . set ( key , pending ) ;
73- }
74- return pending ;
75- }
76- }
77-
78- // Full deploys (artifact + computed instance), keyed by the full deploy context (contract +
79- // constructor args + publicKeys + salt + deployer). Hits on repeated identical deploys.
80- const TXEDeploymentsCache = new AsyncCache <
81- string ,
82- { artifact : ContractArtifactWithHash ; instance : ContractInstanceWithAddress }
83- > ( ) ;
84-
85- // Loaded + hashed contract artifact, keyed by compiled-bytecode hash. Hits across deploys of the
86- // same contract when constructor args / salt / deployer differ.
87- const TXEArtifactsCache = new AsyncCache < string , ContractArtifactWithHash > ( ) ;
88-
8930export type TXEForeignCallInput = {
9031 session_id : number ;
9132 function : TXEOracleFunctionName ;
@@ -119,15 +60,16 @@ export interface TXEDispatcherOptions {
11960 */
12061 contractStoreSourceDir : string ;
12162 /**
122- * Class id (hex) of the SchnorrAccount artifact pre-registered in the shared LMDB. When set,
123- * `#processAddAccountInputs` looks the artifact up from the cloned store instead of
124- * recomputing it via `getSchnorrAccountContractArtifact()` + `computeArtifactHash()`.
63+ * Class id (hex) of the SchnorrAccount artifact pre-registered in the shared LMDB. The
64+ * { @link TXEArtifactResolver} looks the artifact up from the cloned store via this class id
65+ * instead of recomputing it via `getSchnorrAccountContractArtifact()` + `computeArtifactHash()`.
12566 */
12667 schnorrClassId : string ;
12768}
12869
12970export class TXEDispatcher {
13071 private contractStore ! : ContractStore ;
72+ private artifactResolver ! : TXEArtifactResolver ;
13173 private readonly contractStoreSourceDir : string ;
13274 private readonly schnorrClassId : Fr ;
13375
@@ -156,146 +98,25 @@ export class TXEDispatcher {
15698 2 ,
15799 ) ;
158100 this . contractStore = new ContractStore ( kvStore ) ;
101+ this . artifactResolver = new TXEArtifactResolver ( this . contractStore , this . schnorrClassId ) ;
159102 this . logger . debug ( 'Cloned shared protocol-contracts store' , { totalMs : Date . now ( ) - t0 } ) ;
160103 }
161104
162- private fastHashFile ( path : string ) : Promise < string > {
163- return new Promise ( resolve => {
164- const fd = createReadStream ( path ) ;
165- const hash = createHash ( 'sha1' ) ;
166- hash . setEncoding ( 'hex' ) ;
167-
168- fd . on ( 'end' , function ( ) {
169- hash . end ( ) ;
170- resolve ( hash . read ( ) as string ) ;
171- } ) ;
172-
173- fd . pipe ( hash ) ;
174- } ) ;
175- }
176-
177- async #processDeployInputs( { inputs, root_path : rootPath , package_name : packageName } : TXEForeignCallInput ) {
178- const [ contractPath , initializer ] = inputs . slice ( 0 , 2 ) . map ( input =>
179- fromArray ( input as ForeignCallArray )
180- . map ( char => String . fromCharCode ( char . toNumber ( ) ) )
181- . join ( '' ) ,
182- ) ;
183-
184- const decodedArgs = fromArray ( inputs [ 3 ] as ForeignCallArray ) ;
185- const secret = fromSingle ( inputs [ 4 ] as ForeignCallSingle ) ;
186- const salt = fromSingle ( inputs [ 5 ] as ForeignCallSingle ) ;
187- const deployer = addressFromSingle ( inputs [ 6 ] as ForeignCallSingle ) ;
188- const publicKeys = secret . equals ( Fr . ZERO ) ? PublicKeys . default ( ) : ( await deriveKeys ( secret ) ) . publicKeys ;
189- const publicKeysHash = await publicKeys . hash ( ) ;
190-
191- let artifactPath = '' ;
192- const { dir : contractDirectory , base : contractFilename } = parse ( contractPath ) ;
193- if ( contractDirectory ) {
194- if ( contractDirectory . includes ( '@' ) ) {
195- // We're deploying a contract that belongs in a workspace
196- // env.deploy("../path/to/workspace/root@packageName/contractName")
197- const [ workspace , pkg ] = contractDirectory . split ( '@' ) ;
198- const targetPath = join ( rootPath , workspace , '/target' ) ;
199- this . logger . debug ( `Looking for compiled artifact in workspace ${ targetPath } ` ) ;
200- artifactPath = join ( targetPath , `${ pkg } -${ contractFilename } .json` ) ;
201- } else {
202- // We're deploying a standalone external contract
203- // env.deploy("../path/to/contract/root/contractName")
204- const targetPath = join ( rootPath , contractDirectory , '/target' ) ;
205- this . logger . debug ( `Looking for compiled artifact in ${ targetPath } ` ) ;
206- [ artifactPath ] = ( await readdir ( targetPath ) ) . filter ( file => file . endsWith ( `-${ contractFilename } .json` ) ) ;
207- }
208- } else {
209- // We're deploying a local contract
210- // env.deploy("contractName")
211- artifactPath = join ( rootPath , './target' , `${ packageName } -${ contractFilename } .json` ) ;
212- }
213-
214- const fileHash = await this . fastHashFile ( artifactPath ) ;
215-
216- const cacheKey = `${ contractDirectory ?? '' } -${ contractFilename } -${ initializer } -${ decodedArgs
217- . map ( arg => arg . toString ( ) )
218- . join ( '-' ) } -${ publicKeysHash } -${ salt } -${ deployer } -${ fileHash } `;
219-
220- const { artifact, instance } = await TXEDeploymentsCache . getOrCompute ( cacheKey , async ( ) => {
221- this . logger . debug ( `Loading compiled artifact ${ artifactPath } ` ) ;
222- // Inner cache: artifact load + hash depends only on the compiled bytecode (`fileHash`), so
223- // subsequent deploys of the same contract — regardless of constructor args / deployer /
224- // salt — reuse the same `ContractArtifactWithHash`.
225- const computedArtifact = await TXEArtifactsCache . getOrCompute ( fileHash , async ( ) => {
226- const artifactJSON = JSON . parse ( await readFile ( artifactPath , 'utf-8' ) ) as NoirCompiledContract ;
227- const artifactWithoutHash = loadContractArtifact ( artifactJSON ) ;
228- return { ...artifactWithoutHash , artifactHash : await computeArtifactHash ( artifactWithoutHash ) } ;
229- } ) ;
230- this . logger . debug (
231- `Deploy ${ computedArtifact . name } with initializer ${ initializer } (${ decodedArgs } ) and public keys hash ${ publicKeysHash . toString ( ) } ` ,
232- ) ;
233- const computedInstance = await getContractInstanceFromInstantiationParams ( computedArtifact , {
234- constructorArgs : decodedArgs ,
235- skipArgsDecoding : true ,
236- salt,
237- publicKeys,
238- constructorArtifact : initializer ? initializer : undefined ,
239- deployer,
240- } ) ;
241- return { artifact : computedArtifact , instance : computedInstance } ;
242- } ) ;
243-
244- inputs . splice ( 0 , 1 , artifact , instance , toSingle ( secret ) ) ;
245- }
246-
247- async #processAddAccountInputs( { inputs } : TXEForeignCallInput ) {
248- const secret = fromSingle ( inputs [ 0 ] as ForeignCallSingle ) ;
249-
250- const cacheKey = `SchnorrAccountContract-${ secret } ` ;
251-
252- const { artifact, instance } = await TXEDeploymentsCache . getOrCompute ( cacheKey , async ( ) => {
253- const [ artifactFromStore , classWithPreimage ] = await Promise . all ( [
254- this . contractStore . getContractArtifact ( this . schnorrClassId ) ,
255- this . contractStore . getContractClassWithPreimage ( this . schnorrClassId ) ,
256- ] ) ;
257- if ( ! artifactFromStore || ! classWithPreimage ) {
258- throw new Error (
259- `SchnorrAccount not found in shared contract store at class id ${ this . schnorrClassId . toString ( ) } ` ,
260- ) ;
261- }
262- const computedArtifact = { ...artifactFromStore , artifactHash : classWithPreimage . artifactHash } ;
263- const keys = await deriveKeys ( secret ) ;
264- const args = [ keys . publicKeys . ivpkM . x , keys . publicKeys . ivpkM . y ] ;
265- const computedInstance = await getContractInstanceFromInstantiationParams ( computedArtifact , {
266- constructorArgs : args ,
267- skipArgsDecoding : true ,
268- salt : Fr . ONE ,
269- publicKeys : keys . publicKeys ,
270- constructorArtifact : 'constructor' ,
271- deployer : AztecAddress . ZERO ,
272- } ) ;
273- return { artifact : computedArtifact , instance : computedInstance } ;
274- } ) ;
275-
276- inputs . splice ( 0 , 0 , artifact , instance ) ;
277- }
278-
279105 // eslint-disable-next-line camelcase
280106 async resolve_foreign_call ( callData : TXEForeignCallInput ) : Promise < ForeignCallResult > {
281- const { session_id : sessionId , function : functionName , inputs } = callData ;
107+ const {
108+ session_id : sessionId ,
109+ function : functionName ,
110+ inputs,
111+ root_path : rootPath ,
112+ package_name : packageName ,
113+ } = callData ;
282114 this . logger . debug ( `Calling ${ functionName } on session ${ sessionId } ` ) ;
283115
284116 if ( ! sessions . has ( sessionId ) ) {
285117 this . logger . debug ( `Creating new session ${ sessionId } ` ) ;
286118 await this . warmUp ( ) ;
287- sessions . set ( sessionId , await TXESession . init ( this . contractStore ) ) ;
288- }
289-
290- switch ( functionName ) {
291- case 'aztec_txe_deploy' : {
292- await this . #processDeployInputs( callData ) ;
293- break ;
294- }
295- case 'aztec_txe_addAccount' : {
296- await this . #processAddAccountInputs( callData ) ;
297- break ;
298- }
119+ sessions . set ( sessionId , await TXESession . init ( this . contractStore , this . artifactResolver , rootPath , packageName ) ) ;
299120 }
300121
301122 return await sessions . get ( sessionId ) ! . processFunction ( functionName , inputs ) ;
0 commit comments