Skip to content

Commit a088668

Browse files
committed
flesh out implementation for simulate with gas estimation
1 parent 93ada9c commit a088668

File tree

1 file changed

+217
-72
lines changed

1 file changed

+217
-72
lines changed

in-progress/6973-refactor-base-contract-interaction.md

Lines changed: 217 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const { request: transferRequest } = await aliceWallet.simulate({
9090
value: privateBalance,
9191
nonce: 0n
9292
},
93-
paymentMethod
93+
paymentMethod,
9494
});
9595

9696

@@ -171,17 +171,6 @@ This would clear up concerns like:
171171
// derived ContractInteractions is confusing. We should unify the flow of all ContractInteractions.
172172

173173

174-
175-
176-
177-
178-
Implementing this in the current API would be difficult
179-
180-
The complexity in the current API is shown elsewhere
181-
182-
183-
Further, it is presently difficult to estimate the gas cost of a transaction.
184-
185174
### The old UML
186175

187176
```mermaid
@@ -277,63 +266,60 @@ export interface AuthWitnessProvider {
277266
): Promise<AuthWitness>;
278267
}
279268

280-
export interface EntrypointInterface {
281-
/**
282-
* Generates an execution request out of set of function calls.
283-
* @param execution - The execution intents to be run.
284-
* @returns The authenticated transaction execution request.
285-
*/
286-
createTxExecutionRequest(execution: ExecutionRequestInit): Promise<TxExecutionRequest>;
287-
}
288-
289269
export class TxExecutionRequest {
290270
constructor(
291-
/**
292-
* Sender.
293-
*/
271+
// All these are the same:
294272
public origin: AztecAddress,
295-
/**
296-
* Selector of the function to call.
297-
*/
298273
public functionSelector: FunctionSelector,
299-
/**
300-
* The hash of arguments of first call to be executed (usually account entrypoint).
301-
* @dev This hash is a pointer to `argsOfCalls` unordered array.
302-
*/
303274
public firstCallArgsHash: Fr,
304-
/**
305-
* Transaction context.
306-
*/
307275
public txContext: TxContext,
308-
/**
309-
* An unordered array of packed arguments for each call in the transaction.
310-
* @dev These arguments are accessed in Noir via oracle and constrained against the args hash. The length of
311-
* the array is equal to the number of function calls in the transaction (1 args per 1 call).
312-
*/
313276
public argsOfCalls: PackedValues[],
277+
public authWitnesses: AuthWitness[],
278+
// Add:
314279
/**
315-
* Transient authorization witnesses for authorizing the execution of one or more actions during this tx.
316-
* These witnesses are not expected to be stored in the local witnesses database of the PXE.
280+
* Transient capsules needed for this execution.
317281
*/
318-
public authWitnesses: AuthWitness[],
282+
public capsules: Fr[][],
319283
) {}
320284
// ...
321285
}
322286

323-
export type ExecutionRequestInit = {
324-
/** The function calls to be executed. */
325-
calls: FunctionCall[];
326-
/** Any transient auth witnesses needed for this execution */
327-
authWitnesses?: AuthWitness[];
328-
/** Any transient packed arguments for this execution */
329-
packedArguments?: PackedValues[];
330-
/** Any transient capsules needed for this execution */
331-
capsules?: Fr[][];
332-
/** The fee payment method to use */
333-
paymentMethod: FeePaymentMethod;
334-
/** The gas settings */
335-
gasSettings: GasSettings;
336-
};
287+
export type TxExecutionRequestEntrypoint = Pick<TxExecutionRequest,
288+
| 'functionSelector'
289+
| 'firstCallArgsHash'
290+
| 'argsOfCalls'
291+
| 'authWitnesses' >
292+
293+
export interface ExecutionRequestInit {
294+
contractInstance: ContractInstanceWithAddress;
295+
functionName: string;
296+
args: any;
297+
paymentMethod?: FeePaymentMethod;
298+
contractArtifact?: ContractArtifact;
299+
functionAbi?: FunctionAbi;
300+
from?: AztecAddress;
301+
simulatePublicFunctions?: boolean;
302+
}
303+
304+
export interface FeePaymentMethod {
305+
getSetup(gasSettings: GasSettings): Promise<{
306+
functionCalls: FunctionCall[],
307+
authWitnesses: AuthWitness[],
308+
}>;
309+
310+
getEquivalentAztBalance(): Promise<Fr>;
311+
}
312+
313+
314+
export interface EntrypointInterface {
315+
createTxExecutionRequestEntrypoint(
316+
functionCalls: {
317+
appFunctionCalls: FunctionCall[];
318+
setupFunctionCalls: FunctionCall[];
319+
},
320+
authWitnessProvider: AuthWitnessProvider
321+
): TxExecutionRequestEntrypoint;
322+
}
337323

338324
```
339325

@@ -377,32 +363,191 @@ Assume we're dealing with schnorr.
377363

378364
Get the contract artifact out of the PXE using the contractInstance's contractClassId.
379365

380-
Scan its `FunctionAbi`s for the one with the given name.
366+
```ts
367+
368+
function findFunctionAbi(contractArtifact: ContractArtifact, functionName: string): FunctionAbi {
369+
const functionAbi = contractArtifact.abi.find((abi) => abi.name === functionName);
370+
if (!functionAbi) {
371+
throw new Error(`Function ${functionName} not found in contract artifact`);
372+
}
373+
return functionAbi;
374+
}
375+
376+
function makeFunctionCall(
377+
functionAbi: FunctionAbi,
378+
instanceAddress: AztecAddress,
379+
args: any,
380+
): FunctionCall {
381+
return FunctionCall.from({
382+
name: functionAbi.name,
383+
args: mapArgsObjectToArray(functionAbi.parameters, args),
384+
selector: FunctionSelector.fromNameAndParameters(functionAbi.name, functionAbi.parameters),
385+
type: functionAbi.functionType,
386+
to: instanceAddress,
387+
isStatic: functionAbi.isStatic,
388+
returnTypes: functionAbi.returnTypes,
389+
});
390+
}
391+
392+
393+
```
394+
395+
Note that since `gasSettings` was not provided, we need to determine them.
396+
397+
In order to do this, we need to simulate the function call without the FPC to get the gas cost of the app logic, then simulate with the FPC to get the gas cost of the entire transaction.
398+
399+
#### Get the `TxExecutionRequestEntrypoint`
400+
401+
```ts
402+
createTxExecutionRequestAccountEntrypoint(
403+
functionCalls: {
404+
appFunctionCalls: FunctionCall[];
405+
setupFunctionCalls: FunctionCall[];
406+
},
407+
authWitnessProvider: AuthWitnessProvider
408+
): TxExecutionRequestEntrypoint {
409+
const appPayload = EntrypointPayload.fromFunctionCalls(functionCalls.appFunctionCalls);
410+
const setupPayload = EntrypointPayload.fromFunctionCalls(functionCalls.setupFunctionCalls);
411+
const abi = this.getEntrypointAbi();
412+
const entrypointPackedArgs = PackedValues.fromValues(encodeArguments(abi, [appPayload, setupPayload]));
413+
const firstCallArgsHash = entrypointPackedArgs.hash();
414+
const argsOfCalls = [...appPayload.packedArguments, ...feePayload.packedArguments, entrypointPackedArgs];
415+
const functionSelector = FunctionSelector.fromNameAndParameters(abi.name, abi.parameters);
416+
417+
// Does not insert into PXE
418+
const appAuthWit = await authWitnessProvider.createAuthWit(appPayload.hash());
419+
const setupAuthWit = await authWitnessProvider.createAuthWit(setupPayload.hash());
420+
421+
return {
422+
functionSelector,
423+
firstCallArgsHash,
424+
argsOfCalls,
425+
authWitnesses: [appAuthWit, setupAuthWit],
426+
}
427+
}
428+
```
429+
430+
#### Fill in the `TxExecutionRequest`
381431

382-
Construct the `FunctionCall` as:
383432
```ts
384-
{
385-
name: functionAbi.name,
386-
args,
387-
selector: FunctionSelector.fromNameAndParameters(functionAbi.name, functionAbi.parameters),
388-
type: functionAbi.functionType,
389-
to: contractInstance.address,
390-
isStatic: functionAbi.isStatic,
391-
returnTypes: functionAbi.returnTypes,
433+
// within alice wallet.
434+
// This is a low level call that requires us to have resolved the contract artifact and function abi.
435+
// It is intentionally synchronous.
436+
#getTxExecutionRequest(requestInit: ExecutionRequestInit) {
437+
if (!requestInit.functionAbi) {
438+
throw new Error('Function ABI must be provided');
439+
}
440+
441+
const builder = new TxExecutionRequestBuilder();
442+
builder.setOrigin(this.getAddress());
443+
444+
builder.setTxContext({
445+
chainId: this.getChainId(),
446+
version: this.getVersion(),
447+
gasSettings: requestInit.gasSettings,
448+
});
449+
450+
const setup = requestInit.paymentMethod.getSetup(requestInit.gasSettings);
451+
452+
builder.addAuthWitnesses(setup.authWitnesses);
453+
454+
// Could also allow users to pass the artifact and short-circuit this
455+
const appFunctionCall = makeFunctionCall(
456+
requestInit.functionAbi,
457+
requestInit.contractInstance.address,
458+
requestInit.args
459+
);
460+
const entrypointInfo = createTxExecutionRequestAccountEntrypoint({
461+
appFunctionCalls: [appFunctionCall],
462+
setupFunctionCalls: setup.functionCalls,
463+
}, this.account);
464+
465+
builder.setFunctionSelector(entrypointInfo.functionSelector);
466+
builder.setFirstCallArgsHash(entrypointInfo.firstCallArgsHash);
467+
builder.setArgsOfCalls(entrypointInfo.argsOfCalls);
468+
builder.addAuthWitnesses(entrypointInfo.authWitnesses);
469+
470+
return builder.build();
471+
392472
}
393473
```
394474

395475

396476

397477

478+
#### Define top-level `Simulate`
479+
480+
```ts
481+
// helpers somewhere
398482

399-
1. Extract the "app" EntrypointPayload
483+
function decodeSimulatedTx(simulatedTx: SimulatedTx, functionAbi: FunctionAbi): DecodedReturn | [] {
484+
const rawReturnValues =
485+
functionAbi.functionType == FunctionType.PRIVATE
486+
? simulatedTx.privateReturnValues?.nested?.[0].values
487+
: simulatedTx.publicOutput?.publicReturnValues?.[0].values;
400488

401-
2. Collect all authwits
402-
1. For the app payload
403-
2. For the fee payment
404-
3. For anything needed by the fee payment method
405-
3.
489+
return rawReturnValues ? decodeReturnValues(functionAbi.returnTypes, rawReturnValues) : [];
490+
}
491+
492+
// within alice wallet
493+
494+
async #simulateInner(requestInit: ExecutionRequestInit): {
495+
tx: SimulatedTx,
496+
result: DecodedReturn | [],
497+
request: ExecutionRequestInit,
498+
} {
499+
const txExecutionRequest = this.getTxExecutionRequest(initRequest);
500+
// Call the PXE
501+
const simulatedTx = await this.simulateTx(txExecutionRequest, builder.simulatePublicFunctions, builder.from);
502+
const decodedReturn = decodeSimulatedTx(simulatedTx, builder.functionAbi);
503+
return {
504+
tx: simulatedTx,
505+
result: decodedReturn,
506+
request: initRequest,
507+
};
508+
}
509+
510+
async simulate(requestInit: ExecutionRequestInit): {
511+
const builder = new ExecutionRequestInitBuilder(requestInit);
512+
513+
if (!builder.functionAbi) {
514+
const contractArtifact = builder.contractArtifact ?? await pxe.getContractArtifact(builder.contractInstance.contractClassId);
515+
builder.setContractArtifact(contractArtifact);
516+
const functionAbi = findFunctionAbi(builder.contractArtifact, builder.functionName);
517+
builder.setFunctionAbi(functionAbi);
518+
}
519+
520+
// If we're not paying, e.g. this is just a read that we don't intend to submit as a TX,
521+
// set the gas settings to default
522+
if (!builder.paymentMethod){
523+
builder.setFeePaymentMethod(new NoFeePaymentMethod());
524+
builder.setGasSettings(GasSettings.default());
525+
return this.#simulateInner(builder.build());
526+
}
527+
if (builder.gasSettings) {
528+
return this.#simulateInner(builder.build());
529+
}
530+
531+
// If we're paying, e.g. in bananas, figure out how much AZT that is.
532+
// Note: this may call simulate recursively,
533+
// but it *should* set the payment method to a NoFeePaymentMethod or undefined.
534+
// perhaps we need a `read` method on the wallet.
535+
const equivalentAztBalance = await builder.paymentMethod.getEquivalentAztBalance();
536+
gasEstimator = GasEstimator.fromAztBalance(equivalentAztBalance);
537+
builder.setGasSettings(gasEstimator.proposeGasSettings());
538+
539+
while (!gasEstimator.isConverged()) {
540+
const result = await this.#simulateInner(builder.build());
541+
gasEstimator.update(result);
542+
builder.setGasSettings(gasEstimator.proposeGasSettings());
543+
}
544+
545+
return result;
546+
547+
}
548+
549+
550+
```
406551

407552

408553

0 commit comments

Comments
 (0)