Skip to content

Commit

Permalink
chore(e2e): Test multiple decryption keys on the same tx (#1110)
Browse files Browse the repository at this point in the history
Fixes #1010
  • Loading branch information
spalladino authored Jul 31, 2023
1 parent 7ffa84e commit 8717827
Show file tree
Hide file tree
Showing 18 changed files with 458 additions and 56 deletions.
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);

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);
}, 120_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

0 comments on commit 8717827

Please sign in to comment.