Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(e2e): Test multiple decryption keys on the same tx #1110

Merged
merged 10 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,17 @@ jobs:
command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_account_contracts.test.ts
working_directory: yarn-project/end-to-end

e2e-escrow-contract:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_tests end-to-end e2e_escrow_contract.test.ts

e2e-pending-commitments-contract:
machine:
image: ubuntu-2004:202010-01
Expand Down Expand Up @@ -1150,6 +1161,7 @@ workflows:
- e2e-public-cross-chain-messaging: *e2e_test
- e2e-public-to-private-messaging: *e2e_test
- e2e-account-contracts: *e2e_test
- e2e-escrow-contract: *e2e_test
- e2e-pending-commitments-contract: *e2e_test
- uniswap-trade-on-l1-from-l2: *e2e_test
- integration-l1-publisher: *e2e_test
Expand All @@ -1172,6 +1184,7 @@ workflows:
- e2e-public-cross-chain-messaging
- e2e-public-to-private-messaging
- e2e-account-contracts
- e2e-escrow-contract
- e2e-pending-commitments-contract
- uniswap-trade-on-l1-from-l2
- integration-l1-publisher
Expand Down
10 changes: 10 additions & 0 deletions yarn-project/acir-simulator/src/client/client_execution_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CircuitsWasm, PrivateHistoricTreeRoots, ReadRequestMembershipWitness, T
import { computeCommitmentNonce } from '@aztec/circuits.js/abis';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr, Point } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';

import {
ACVMField,
Expand All @@ -23,6 +24,9 @@ export class ClientTxExecutionContext {
// output by an app circuit via public inputs.
private readRequestPartialWitnesses: ReadRequestMembershipWitness[] = [];

/** Logger instance */
private logger = createDebugLogger('aztec:simulator:execution_context');

constructor(
/** The database oracle. */
public db: DBOracle,
Expand Down Expand Up @@ -136,6 +140,12 @@ export class ClientTxExecutionContext {
offset,
});

this.logger(
`Returning ${notes.length} notes for ${contractAddress} at ${storageSlotField}: ${notes
.map(n => `${n.nonce.toString()}:[${n.preimage.map(i => i.toString()).join(',')}]`)
.join(', ')}`,
);

// Combine pending and db preimages into a single flattened array.
const preimages = notes.flatMap(({ nonce, preimage }) => [nonce, ...preimage]);

Expand Down
12 changes: 10 additions & 2 deletions yarn-project/aztec-rpc/src/note_processor/note_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,21 @@ export class NoteProcessor {
if (noteSpendingInfoDaosBatch.length) {
await this.db.addNoteSpendingInfoBatch(noteSpendingInfoDaosBatch);
noteSpendingInfoDaosBatch.forEach(noteSpendingInfo => {
this.log(`Added note spending info with nullifier ${noteSpendingInfo.nullifier.toString()}}`);
this.log(
`Added note spending info for contract ${noteSpendingInfo.contractAddress} at slot ${
noteSpendingInfo.storageSlot
} with nullifier ${noteSpendingInfo.nullifier.toString()}`,
);
});
}
if (txDaos.length) await this.db.addTxs(txDaos);
const removedNoteSpendingInfo = await this.db.removeNullifiedNoteSpendingInfo(newNullifiers, this.publicKey);
removedNoteSpendingInfo.forEach(noteSpendingInfo => {
this.log(`Removed note spending info with nullifier ${noteSpendingInfo.nullifier.toString()}}`);
this.log(
`Removed note spending info for contract ${noteSpendingInfo.contractAddress} at slot ${
noteSpendingInfo.storageSlot
} with nullifier ${noteSpendingInfo.nullifier.toString()}`,
);
});
}
}
6 changes: 3 additions & 3 deletions yarn-project/aztec.js/src/abis/ecdsa_account_contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"name": "flattened_args_hashes",
"type": {
"kind": "array",
"length": 2,
"length": 4,
"type": {
"kind": "field"
}
Expand All @@ -107,7 +107,7 @@
"name": "flattened_selectors",
"type": {
"kind": "array",
"length": 2,
"length": 4,
"type": {
"kind": "field"
}
Expand All @@ -117,7 +117,7 @@
"name": "flattened_targets",
"type": {
"kind": "array",
"length": 2,
"length": 4,
"type": {
"kind": "field"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"name": "flattened_args_hashes",
"type": {
"kind": "array",
"length": 2,
"length": 4,
"type": {
"kind": "field"
}
Expand All @@ -32,7 +32,7 @@
"name": "flattened_selectors",
"type": {
"kind": "array",
"length": 2,
"length": 4,
"type": {
"kind": "field"
}
Expand All @@ -42,7 +42,7 @@
"name": "flattened_targets",
"type": {
"kind": "array",
"length": 2,
"length": 4,
"type": {
"kind": "field"
}
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/aztec.js/src/account_impl/entrypoint_payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { padArrayEnd } from '@aztec/foundation/collection';
import { sha256 } from '@aztec/foundation/crypto';
import { ExecutionRequest, PackedArguments, emptyExecutionRequest } from '@aztec/types';

const ACCOUNT_MAX_PRIVATE_CALLS = 1;
const ACCOUNT_MAX_PUBLIC_CALLS = 1;
// These must match the values defined in yarn-project/noir-libs/noir-aztec/src/entrypoint.nr
const ACCOUNT_MAX_PRIVATE_CALLS = 2;
const ACCOUNT_MAX_PUBLIC_CALLS = 2;

/** Encoded payload for the account contract entrypoint */
export type EntrypointPayload = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ export class ContractFunctionInteraction {
return this.tx;
}

/**
* Returns an execution request that represents this operation. Useful as a building
* block for constructing batch requests.
* @param options - An optional object containing additional configuration for the transaction.
* @returns An execution request.
*/
public request(options: SendMethodOptions = {}): ExecutionRequest {
return this.getExecutionRequest(this.contractAddress, options.origin);
}

protected getExecutionRequest(to: AztecAddress, from?: AztecAddress): ExecutionRequest {
const flatArgs = encodeArguments(this.functionDao, this.args);
from = from ?? this.wallet.getAddress();
Expand Down
23 changes: 20 additions & 3 deletions yarn-project/aztec.js/src/contract/sent_tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AztecRPC, TxHash, TxReceipt, TxStatus } from '@aztec/types';
* its hash, receipt, and mining status.
*/
export class SentTx {
constructor(private arc: AztecRPC, private txHashPromise: Promise<TxHash>) {}
constructor(protected arc: AztecRPC, protected txHashPromise: Promise<TxHash>) {}

/**
* Retrieves the transaction hash of the SentTx instance.
Expand All @@ -30,6 +30,19 @@ export class SentTx {
return await this.arc.getTxReceipt(txHash);
}

/**
* Awaits for a tx to be mined and returns the receipt. Throws if tx is not mined.
* @param timeout - The maximum time (in seconds) to wait for the transaction to be mined. A value of 0 means no timeout.
* @param interval - The time interval (in seconds) between retries to fetch the transaction receipt.
* @returns The transaction receipt.
*/
public async wait(timeout = 0, interval = 1): Promise<TxReceipt> {
const receipt = await this.waitForReceipt(timeout, interval);
if (receipt.status !== TxStatus.MINED)
throw new Error(`Transaction ${await this.getTxHash()} was ${receipt.status}`);
return receipt;
}

/**
* Checks whether the transaction is mined or not within the specified timeout and retry interval.
* Resolves to true if the transaction status is 'MINED', false otherwise.
Expand All @@ -40,8 +53,13 @@ export class SentTx {
* @returns A Promise that resolves to a boolean indicating if the transaction is mined or not.
*/
public async isMined(timeout = 0, interval = 1): Promise<boolean> {
const receipt = await this.waitForReceipt(timeout, interval);
return receipt.status === TxStatus.MINED;
}

protected async waitForReceipt(timeout = 0, interval = 1): Promise<TxReceipt> {
const txHash = await this.getTxHash();
const receipt = await retryUntil(
return await retryUntil(
async () => {
const txReceipt = await this.arc.getTxReceipt(txHash);
return txReceipt.status != TxStatus.PENDING ? txReceipt : undefined;
Expand All @@ -50,6 +68,5 @@ export class SentTx {
timeout,
interval,
);
return receipt.status === TxStatus.MINED;
}
}
115 changes: 115 additions & 0 deletions yarn-project/end-to-end/src/e2e_escrow_contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { AztecNodeService } from '@aztec/aztec-node';
import { AztecRPCServer } from '@aztec/aztec-rpc';
import { AztecAddress, SentTx, Wallet, generatePublicKey } from '@aztec/aztec.js';
import { Fr, PrivateKey, TxContext, getContractDeploymentInfo } from '@aztec/circuits.js';
import { generateFunctionSelector } from '@aztec/foundation/abi';
import { toBufferBE } from '@aztec/foundation/bigint-buffer';
import { DebugLogger } from '@aztec/foundation/log';
import { retryUntil } from '@aztec/foundation/retry';
import { EscrowContractAbi, ZkTokenContractAbi } from '@aztec/noir-contracts/artifacts';
import { EscrowContract, ZkTokenContract } from '@aztec/noir-contracts/types';
import { AztecRPC, PublicKey } from '@aztec/types';

import { setup } from './utils.js';

describe('e2e_escrow_contract', () => {
let aztecNode: AztecNodeService | undefined;
let aztecRpcServer: AztecRPC;
let wallet: Wallet;
let accounts: AztecAddress[];
let logger: DebugLogger;

let zkTokenContract: ZkTokenContract;
let escrowContract: EscrowContract;
let owner: AztecAddress;
let recipient: AztecAddress;

let escrowPrivateKey: PrivateKey;
let escrowPublicKey: PublicKey;

beforeAll(() => {
// Validate transfer selector. If this fails, then make sure to change it in the escrow contract.
const transferAbi = ZkTokenContractAbi.functions.find(f => f.name === 'transfer')!;
const transferSelector = generateFunctionSelector(transferAbi.name, transferAbi.parameters);
expect(transferSelector).toEqual(toBufferBE(0xdcd4c318n, 4));
});

beforeEach(async () => {
// Setup environment
({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup(2));
[owner, recipient] = accounts;

// Generate private key for escrow contract, register key in rpc server, and deploy
// Note that we need to register it first if we want to emit an encrypted note for it in the constructor
// TODO: We need a nicer interface for deploying contracts!
escrowPrivateKey = PrivateKey.random();
escrowPublicKey = await generatePublicKey(escrowPrivateKey);
const salt = Fr.random();
const deployInfo = await getContractDeploymentInfo(EscrowContractAbi, [owner], salt, escrowPublicKey);
await aztecRpcServer.addAccount(escrowPrivateKey, deployInfo.address, deployInfo.partialAddress);
const escrowDeployTx = EscrowContract.deployWithPublicKey(aztecRpcServer, escrowPublicKey, owner);
await escrowDeployTx.send({ contractAddressSalt: salt }).wait();
escrowContract = new EscrowContract(escrowDeployTx.completeContractAddress!, wallet);
logger(`Escrow contract deployed at ${escrowContract.address}`);

// Deploy ZK token contract and mint funds for the escrow contract
zkTokenContract = await ZkTokenContract.deploy(aztecRpcServer, 100n, escrowContract.address)
.send()
.wait()
.then(r => new ZkTokenContract(r.contractAddress!, wallet));
logger(`Token contract deployed at ${zkTokenContract.address}`);
}, 100_000);

afterEach(async () => {
await aztecNode?.stop();
if (aztecRpcServer instanceof AztecRPCServer) await aztecRpcServer.stop();
}, 30_000);

const expectBalance = async (who: AztecAddress, expectedBalance: bigint) => {
const [balance] = await zkTokenContract.methods.getBalance(who).view({ from: who });
logger(`Account ${who} balance: ${balance}`);
expect(balance).toBe(expectedBalance);
};

it('withdraws funds from the escrow contract', async () => {
await expectBalance(owner, 0n);
await expectBalance(recipient, 0n);
await expectBalance(escrowContract.address, 100n);

logger(`Withdrawing funds from token contract to ${recipient}`);
await escrowContract.methods.withdraw(zkTokenContract.address, 30, recipient).send().wait();

await expectBalance(owner, 0n);
await expectBalance(recipient, 30n);
await expectBalance(escrowContract.address, 70n);
}, 60_000);

it('refuses to withdraw funds as a non-owner', async () => {
await expect(
escrowContract.methods.withdraw(zkTokenContract.address, 30, recipient).simulate({ origin: recipient }),
).rejects.toThrowError();
}, 60_000);
Comment on lines +87 to +91
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd've expected this test to fail with a cannot satisfy constraints, but I got this error instead. For context, I'm running an ensure_note_exists for a note which does not exist. Is it possible to catch this somehow? cc @LeilaWang

  wasm Error(2018): private data tree root mismatch at read_request[0]
  wasm 	expected root:    0x01834dbe674ccaa98f5aaafdcd8a42267503a0c141f582ad7f53564fab450c23
  wasm 	but got root*:    0x23b12bc80a094747ce77a30b71288abcb7cbf648a8891cfe907405db219c8549
  wasm 	read_request:     0x156409eef4798a1db5b3a8d73dfa047c3c0de0f06fc888209b1a191c31b5dde8
  wasm 	siloed-rr (leaf): 0x1115a070391204040030b3f5e4ab5d5e6fc5b3c82e16be7de43f2b089961e7c6
  wasm 	leaf_index: 0x0000000000000000000000000000000000000000000000000000000000000040
  wasm 	is_transient: 0
  wasm 	hint_to_commitment: 0x0000000000000000000000000000000000000000000000000000000000000000
  wasm 	* got root by siloing read_request (compressing with storage_contract_address to get leaf) and merkle-hashing to a root using membership witness (mem: 34.50MB) +30s


it('moves funds using multiple keys on the same tx (#1010)', async () => {
logger(`Minting funds in token contract to ${owner}`);
await zkTokenContract.methods.mint(50, owner).send().wait();
await expectBalance(owner, 50n);

const actions = [
zkTokenContract.methods.transfer(10, owner, recipient).request(),
escrowContract.methods.withdraw(zkTokenContract.address, 20, recipient).request(),
];

// TODO: We need a nicer interface for batch actions
const nodeInfo = await wallet.getNodeInfo();
const txContext = TxContext.empty(new Fr(nodeInfo.chainId), new Fr(nodeInfo.version));
const txRequest = await wallet.createAuthenticatedTxRequest(actions, txContext);
logger(`Executing batch transfer from ${wallet.getAddress()}`);
const tx = await wallet.simulateTx(txRequest);
const sentTx = new SentTx(aztecRpcServer, wallet.sendTx(tx));
await sentTx.isMined();

await retryUntil(() => aztecRpcServer.isAccountSynchronised(recipient), 'account sync', 30);
await expectBalance(recipient, 30n);
}, 90_000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ contract EcdsaAccount {
) -> distinct pub abi::PrivateCircuitPublicInputs {

// Initialise context
// 71 = ENTRYPOINT_PAYLOAD_SIZE(7) + 64
let mut args: BoundedVec<Field, 71> = BoundedVec::new(0);
// ENTRYPOINT_PAYLOAD_SIZE(13) + 64
let mut args: BoundedVec<Field, 77> = BoundedVec::new(0);
args.push_array(payload.serialize());
for byte in signature { args.push(byte as Field); }
let mut context = Context::new(inputs, abi::hash_args(args.storage));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "escrow-contract"
authors = [""]
compiler_version = "0.1"

[dependencies]
aztec = { path = "../../../../noir-libs/noir-aztec" }
Loading