Skip to content

Commit 03e693a

Browse files
authored
feat: decoded return values (#5762)
Fixes #5450. Alters the typescript contract artifact slightly to have the proper return types of functions such that it can be used when simulating the contract interaction and return the decoded values. Allows us to get rid of some of the `unconstrained` function calls as we can simply use the constrained version instead, this is very interesting for the tokens or anything that have values that is expected to be read from multiple domains as it limits the code. ```rust #[aztec(private)] fn get_shared_immutable_constrained_private() -> pub Leader { storage.shared_immutable.read_private() } ``` ```typescript const a = await contract.methods.get_shared_immutable_constrained_private().simulate(); const b = await contract.methods.get_shared_immutable().simulate(); expect(a).toEqual(b); ```
1 parent 23d0070 commit 03e693a

File tree

18 files changed

+113
-122
lines changed

18 files changed

+113
-122
lines changed

noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,13 @@ contract DocsExample {
115115
// and returns the response.
116116
// Used to test that we can retrieve values through calls and
117117
// correctly return them in the simulation
118-
context.call_private_function_no_args(
118+
let mut leader: Leader = context.call_private_function_no_args(
119119
context.this_address(),
120120
FunctionSelector::from_signature("get_shared_immutable_constrained_private()")
121-
).unpack_into()
121+
).unpack_into();
122+
123+
leader.points += 1;
124+
leader
122125
}
123126

124127
#[aztec(public)]
@@ -127,17 +130,26 @@ contract DocsExample {
127130
// and returns the response.
128131
// Used to test that we can retrieve values through calls and
129132
// correctly return them in the simulation
130-
context.call_public_function_no_args(
133+
let mut leader: Leader = context.call_public_function_no_args(
131134
context.this_address(),
132135
FunctionSelector::from_signature("get_shared_immutable_constrained_public()")
133-
).deserialize_into()
136+
).deserialize_into();
137+
138+
leader.points += 1;
139+
leader
134140
}
135141

136142
#[aztec(public)]
137143
fn get_shared_immutable_constrained_public() -> pub Leader {
138144
storage.shared_immutable.read_public()
139145
}
140146

147+
#[aztec(public)]
148+
fn get_shared_immutable_constrained_public_multiple() -> pub [Leader; 5] {
149+
let a = storage.shared_immutable.read_public();
150+
[a, a, a, a, a]
151+
}
152+
141153
#[aztec(private)]
142154
fn get_shared_immutable_constrained_private() -> pub Leader {
143155
storage.shared_immutable.read_private()

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ export class AztecNodeService implements AztecNode {
671671
throw reverted[0].revertReason;
672672
}
673673
this.log.info(`Simulated tx ${tx.getTxHash()} succeeds`);
674-
return returns;
674+
return returns[0];
675675
}
676676

677677
public setConfig(config: Partial<SequencerConfig>): Promise<void> {

yarn-project/aztec.js/src/contract/contract_function_interaction.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type FunctionCall, PackedValues, TxExecutionRequest } from '@aztec/circuit-types';
22
import { type AztecAddress, FunctionData, GasSettings, TxContext } from '@aztec/circuits.js';
3-
import { type FunctionAbi, FunctionType, encodeArguments } from '@aztec/foundation/abi';
3+
import { type FunctionAbi, FunctionType, decodeReturnValues, encodeArguments } from '@aztec/foundation/abi';
44

55
import { type Wallet } from '../account/wallet.js';
66
import { BaseContractInteraction, type SendMethodOptions } from './base_contract_interaction.js';
@@ -73,7 +73,6 @@ export class ContractFunctionInteraction extends BaseContractInteraction {
7373
* 2. It supports `unconstrained`, `private` and `public` functions
7474
* 3. For `private` execution it:
7575
* 3.a SKIPS the entrypoint and starts directly at the function
76-
* 3.b SKIPS public execution entirely
7776
* 4. For `public` execution it:
7877
* 4.a Removes the `txRequest` value after ended simulation
7978
* 4.b Ignores the `from` in the options
@@ -86,10 +85,6 @@ export class ContractFunctionInteraction extends BaseContractInteraction {
8685
return this.wallet.viewTx(this.functionDao.name, this.args, this.contractAddress, options.from);
8786
}
8887

89-
// TODO: If not unconstrained, we return a size 4 array of fields.
90-
// TODO: It should instead return the correctly decoded value
91-
// TODO: The return type here needs to be fixed! @LHerskind
92-
9388
if (this.functionDao.functionType == FunctionType.SECRET) {
9489
const nodeInfo = await this.wallet.getNodeInfo();
9590
const packedArgs = PackedValues.fromValues(encodeArguments(this.functionDao, this.args));
@@ -103,13 +98,15 @@ export class ContractFunctionInteraction extends BaseContractInteraction {
10398
authWitnesses: [],
10499
gasSettings: options.gasSettings ?? GasSettings.simulation(),
105100
});
106-
const simulatedTx = await this.pxe.simulateTx(txRequest, false, options.from ?? this.wallet.getAddress());
107-
return simulatedTx.privateReturnValues?.[0];
101+
const simulatedTx = await this.pxe.simulateTx(txRequest, true, options.from ?? this.wallet.getAddress());
102+
const flattened = simulatedTx.privateReturnValues;
103+
return flattened ? decodeReturnValues(this.functionDao, flattened) : [];
108104
} else {
109105
const txRequest = await this.create();
110106
const simulatedTx = await this.pxe.simulateTx(txRequest, true);
111107
this.txRequest = undefined;
112-
return simulatedTx.publicReturnValues?.[0];
108+
const flattened = simulatedTx.publicReturnValues;
109+
return flattened ? decodeReturnValues(this.functionDao, flattened) : [];
113110
}
114111
}
115112
}

yarn-project/circuit-types/src/interfaces/aztec-node.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
type PUBLIC_DATA_TREE_HEIGHT,
88
} from '@aztec/circuits.js';
99
import { type L1ContractAddresses } from '@aztec/ethereum';
10-
import { type ProcessReturnValues } from '@aztec/foundation/abi';
1110
import { type AztecAddress } from '@aztec/foundation/aztec-address';
1211
import { type Fr } from '@aztec/foundation/fields';
1312
import { type ContractClassPublic, type ContractInstanceWithAddress } from '@aztec/types/contracts';
@@ -22,7 +21,7 @@ import {
2221
} from '../logs/index.js';
2322
import { type MerkleTreeId } from '../merkle_tree_id.js';
2423
import { type SiblingPath } from '../sibling_path/index.js';
25-
import { type Tx, type TxHash, type TxReceipt } from '../tx/index.js';
24+
import { type ProcessReturnValues, type Tx, type TxHash, type TxReceipt } from '../tx/index.js';
2625
import { type TxEffect } from '../tx_effect.js';
2726
import { type SequencerConfig } from './configs.js';
2827
import { type L2BlockNumber } from './l2_block_number.js';
@@ -283,7 +282,7 @@ export interface AztecNode {
283282
* This currently just checks that the transaction execution succeeds.
284283
* @param tx - The transaction to simulate.
285284
**/
286-
simulatePublicCalls(tx: Tx): Promise<ProcessReturnValues[]>;
285+
simulatePublicCalls(tx: Tx): Promise<ProcessReturnValues>;
287286

288287
/**
289288
* Updates the configuration of this node.

yarn-project/circuit-types/src/mocks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
getContractClassFromArtifact,
1212
} from '@aztec/circuits.js';
1313
import { makePublicCallRequest } from '@aztec/circuits.js/testing';
14-
import { type ContractArtifact, type DecodedReturn } from '@aztec/foundation/abi';
14+
import { type ContractArtifact } from '@aztec/foundation/abi';
1515
import { makeTuple } from '@aztec/foundation/array';
1616
import { times } from '@aztec/foundation/collection';
1717
import { randomBytes } from '@aztec/foundation/crypto';
@@ -21,7 +21,7 @@ import { type ContractInstanceWithAddress, SerializableContractInstance } from '
2121
import { EncryptedL2Log } from './logs/encrypted_l2_log.js';
2222
import { EncryptedFunctionL2Logs, EncryptedTxL2Logs, Note, UnencryptedTxL2Logs } from './logs/index.js';
2323
import { ExtendedNote } from './notes/index.js';
24-
import { SimulatedTx, Tx, TxHash } from './tx/index.js';
24+
import { type ProcessReturnValues, SimulatedTx, Tx, TxHash } from './tx/index.js';
2525

2626
/**
2727
* Testing utility to create empty logs composed from a single empty log.
@@ -94,7 +94,7 @@ export const mockTxForRollup = (seed = 1, { hasLogs = false }: { hasLogs?: boole
9494

9595
export const mockSimulatedTx = (seed = 1, hasLogs = true) => {
9696
const tx = mockTx(seed, { hasLogs });
97-
const dec: DecodedReturn = [1n, 2n, 3n, 4n];
97+
const dec: ProcessReturnValues = [new Fr(1n), new Fr(2n), new Fr(3n), new Fr(4n)];
9898
return new SimulatedTx(tx, dec, dec);
9999
};
100100

yarn-project/circuit-types/src/tx/simulated_tx.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,11 @@ describe('simulated_tx', () => {
66
const simulatedTx = mockSimulatedTx();
77
expect(SimulatedTx.fromJSON(simulatedTx.toJSON())).toEqual(simulatedTx);
88
});
9+
10+
it('convert undefined effects to and from json', () => {
11+
const simulatedTx = mockSimulatedTx();
12+
simulatedTx.privateReturnValues = undefined;
13+
simulatedTx.publicReturnValues = undefined;
14+
expect(SimulatedTx.fromJSON(simulatedTx.toJSON())).toEqual(simulatedTx);
15+
});
916
});

yarn-project/circuit-types/src/tx/simulated_tx.ts

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { AztecAddress } from '@aztec/circuits.js';
2-
import { type ProcessReturnValues } from '@aztec/foundation/abi';
1+
import { Fr } from '@aztec/circuits.js';
32

43
import { Tx } from './tx.js';
54

5+
export type ProcessReturnValues = Fr[] | undefined;
6+
67
export class SimulatedTx {
78
constructor(
89
public tx: Tx,
@@ -15,17 +16,11 @@ export class SimulatedTx {
1516
* @returns A plain object with SimulatedTx properties.
1617
*/
1718
public toJSON() {
18-
const returnToJson = (data: ProcessReturnValues): string => {
19-
const replacer = (key: string, value: any): any => {
20-
if (typeof value === 'bigint') {
21-
return value.toString() + 'n'; // Indicate bigint with "n"
22-
} else if (value instanceof AztecAddress) {
23-
return value.toString();
24-
} else {
25-
return value;
26-
}
27-
};
28-
return JSON.stringify(data, replacer);
19+
const returnToJson = (data: ProcessReturnValues | undefined): string => {
20+
if (data === undefined) {
21+
return JSON.stringify(data);
22+
}
23+
return JSON.stringify(data.map(fr => fr.toString()));
2924
};
3025

3126
return {
@@ -41,22 +36,11 @@ export class SimulatedTx {
4136
* @returns A Tx class object.
4237
*/
4338
public static fromJSON(obj: any) {
44-
const returnFromJson = (json: string): ProcessReturnValues => {
45-
if (json == undefined) {
39+
const returnFromJson = (json: string): ProcessReturnValues | undefined => {
40+
if (json === undefined) {
4641
return json;
4742
}
48-
const reviver = (key: string, value: any): any => {
49-
if (typeof value === 'string') {
50-
if (value.match(/\d+n$/)) {
51-
// Detect bigint serialization
52-
return BigInt(value.slice(0, -1));
53-
} else if (value.match(/^0x[a-fA-F0-9]{64}$/)) {
54-
return AztecAddress.fromString(value);
55-
}
56-
}
57-
return value;
58-
};
59-
return JSON.parse(json, reviver);
43+
return JSON.parse(json).map(Fr.fromString);
6044
};
6145

6246
const tx = Tx.fromJSON(obj.tx);

yarn-project/end-to-end/src/e2e_avm_simulator.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ describe('e2e_avm_simulator', () => {
8080
}, 50_000);
8181

8282
it('Can execute ACVM function among AVM functions', async () => {
83-
expect(await avmContract.methods.constant_field_acvm().simulate()).toEqual([123456n]);
83+
expect(await avmContract.methods.constant_field_acvm().simulate()).toEqual(123456n);
8484
});
8585

8686
it('Can call AVM function from ACVM', async () => {
87-
expect(await avmContract.methods.call_avm_from_acvm().simulate()).toEqual([123456n]);
87+
expect(await avmContract.methods.call_avm_from_acvm().simulate()).toEqual(123456n);
8888
});
8989

9090
it('Can call ACVM function from AVM', async () => {
91-
expect(await avmContract.methods.call_acvm_from_avm().simulate()).toEqual([123456n]);
91+
expect(await avmContract.methods.call_acvm_from_avm().simulate()).toEqual(123456n);
9292
});
9393

9494
it('AVM sees settled nullifiers by ACVM', async () => {
@@ -146,7 +146,7 @@ describe('e2e_avm_simulator', () => {
146146

147147
describe('Storage', () => {
148148
it('Read immutable (initialized) storage (Field)', async () => {
149-
expect(await avmContract.methods.read_storage_immutable().simulate()).toEqual([42n]);
149+
expect(await avmContract.methods.read_storage_immutable().simulate()).toEqual(42n);
150150
});
151151
});
152152
});

yarn-project/end-to-end/src/e2e_state_vars.test.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,40 +38,45 @@ describe('e2e_state_vars', () => {
3838
// checking the return values with:
3939
// 1. A constrained private function that reads it directly
4040
// 2. A constrained private function that calls another private function that reads.
41+
// The indirect, adds 1 to the point to ensure that we are returning the correct value.
4142

4243
await contract.methods.initialize_shared_immutable(1).send().wait();
4344

4445
const a = await contract.methods.get_shared_immutable_constrained_private().simulate();
4546
const b = await contract.methods.get_shared_immutable_constrained_private_indirect().simulate();
4647
const c = await contract.methods.get_shared_immutable().simulate();
4748

48-
expect((a as any)[0]).toEqual((c as any)['account'].toBigInt());
49-
expect((a as any)[1]).toEqual((c as any)['points']);
50-
expect((b as any)[0]).toEqual((c as any)['account'].toBigInt());
51-
expect((b as any)[1]).toEqual((c as any)['points']);
52-
53-
expect(a).toEqual(b);
49+
expect(a).toEqual(c);
50+
expect(b).toEqual({ account: c.account, points: c.points + 1n });
5451
await contract.methods.match_shared_immutable(c.account, c.points).send().wait();
5552
});
5653

5754
it('public read of SharedImmutable', async () => {
5855
// Reads the value using an unconstrained function checking the return values with:
5956
// 1. A constrained public function that reads it directly
6057
// 2. A constrained public function that calls another public function that reads.
58+
// The indirect, adds 1 to the point to ensure that we are returning the correct value.
6159

6260
const a = await contract.methods.get_shared_immutable_constrained_public().simulate();
6361
const b = await contract.methods.get_shared_immutable_constrained_public_indirect().simulate();
6462
const c = await contract.methods.get_shared_immutable().simulate();
6563

66-
expect((a as any)[0]).toEqual((c as any)['account'].toBigInt());
67-
expect((a as any)[1]).toEqual((c as any)['points']);
68-
expect((b as any)[0]).toEqual((c as any)['account'].toBigInt());
69-
expect((b as any)[1]).toEqual((c as any)['points']);
64+
expect(a).toEqual(c);
65+
expect(b).toEqual({ account: c.account, points: c.points + 1n });
7066

71-
expect(a).toEqual(b);
7267
await contract.methods.match_shared_immutable(c.account, c.points).send().wait();
7368
});
7469

70+
it('public multiread of SharedImmutable', async () => {
71+
// Reads the value using an unconstrained function checking the return values with:
72+
// 1. A constrained public function that reads 5 times directly (going beyond the previous 4 Field return value)
73+
74+
const a = await contract.methods.get_shared_immutable_constrained_public_multiple().simulate();
75+
const c = await contract.methods.get_shared_immutable().simulate();
76+
77+
expect(a).toEqual([c, c, c, c, c]);
78+
});
79+
7580
it('initializing SharedImmutable the second time should fail', async () => {
7681
// Jest executes the tests sequentially and the first call to initialize_shared_immutable was executed
7782
// in the previous test, so the call bellow should fail.

yarn-project/foundation/src/abi/decoder.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { AztecAddress } from '../aztec-address/index.js';
22
import { type Fr } from '../fields/index.js';
3-
import { type ABIParameter, type ABIVariable, type AbiType, type FunctionArtifact } from './abi.js';
3+
import { type ABIParameter, type ABIVariable, type AbiType, type FunctionAbi } from './abi.js';
44
import { isAztecAddressStruct } from './utils.js';
55

66
/**
77
* The type of our decoded ABI.
88
*/
99
export type DecodedReturn = bigint | boolean | AztecAddress | DecodedReturn[] | { [key: string]: DecodedReturn };
10-
export type ProcessReturnValues = (DecodedReturn | undefined)[] | undefined;
1110

1211
/**
1312
* Decodes return values from a function call.
1413
* Missing support for integer and string.
1514
*/
1615
class ReturnValuesDecoder {
17-
constructor(private artifact: FunctionArtifact, private flattened: Fr[]) {}
16+
constructor(private artifact: FunctionAbi, private flattened: Fr[]) {}
1817

1918
/**
2019
* Decodes a single return value from field to the given type.
@@ -97,7 +96,7 @@ class ReturnValuesDecoder {
9796
* @param returnValues - The decoded return values.
9897
* @returns
9998
*/
100-
export function decodeReturnValues(abi: FunctionArtifact, returnValues: Fr[]) {
99+
export function decodeReturnValues(abi: FunctionAbi, returnValues: Fr[]) {
101100
return new ReturnValuesDecoder(abi, returnValues.slice()).decode();
102101
}
103102

0 commit comments

Comments
 (0)