Skip to content

Commit

Permalink
feat: wallet tx management (#8246)
Browse files Browse the repository at this point in the history
This PR introduces the concept of cancellable transactions. It is now
possible to send a TX as `cancellable`, which will output an extra
nullifier in the entrypoint function using the tx nonce and a dedicated
generator.

A subsequent transaction with the same nonce but an empty payload and a
higher fee should then picked up by the sequencer, making the original
one fail due to the duplicate nullifier. This is not yet implemented in
the sequencer and kinda difficult to test at the moment.

## New commands

- `get-tx [txHash]`: Poor's man tx inspector/history. Provides a list of
recent transactions (supports pagination), their aliases and status.
Providing a txHash will inspect it in more detail.

<img width="783" alt="Screenshot 2024-08-29 at 12 22 13"
src="https://github.com/user-attachments/assets/ad52bb7b-4349-4049-9ca6-bcaa63bbb837">

- `cancel-tx <txHash>`: Cancels a previous transaction (provided it was
done via the same wallet and we have the fee info) via its txHash (or
alias), using a multiplier to the `inclusionFee` of the original tx.

## Fixes

- Should fix earthly compilation for devnet builds
  • Loading branch information
Thunkar authored Aug 29, 2024
1 parent cc12558 commit 2cfe7cd
Show file tree
Hide file tree
Showing 30 changed files with 331 additions and 91 deletions.
9 changes: 7 additions & 2 deletions noir-projects/aztec-nr/authwit/src/account.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use dep::aztec::{
context::PrivateContext, protocol_types::constants::GENERATOR_INDEX__COMBINED_PAYLOAD,
context::PrivateContext,
protocol_types::constants::{GENERATOR_INDEX__COMBINED_PAYLOAD, GENERATOR_INDEX__TX_NULLIFIER},
hash::poseidon2_hash_with_separator
};

Expand Down Expand Up @@ -34,7 +35,7 @@ impl AccountActions<&mut PrivateContext> {
* @param fee_payload The payload that contains the calls to be executed in the setup phase.
*/
// docs:start:entrypoint
pub fn entrypoint(self, app_payload: AppPayload, fee_payload: FeePayload) {
pub fn entrypoint(self, app_payload: AppPayload, fee_payload: FeePayload, cancellable: bool) {
let valid_fn = self.is_valid_impl;

let combined_payload_hash = poseidon2_hash_with_separator(
Expand All @@ -46,6 +47,10 @@ impl AccountActions<&mut PrivateContext> {
fee_payload.execute_calls(self.context);
self.context.end_setup();
app_payload.execute_calls(self.context);
if cancellable {
let tx_nullifier = poseidon2_hash_with_separator([app_payload.nonce], GENERATOR_INDEX__TX_NULLIFIER);
self.context.push_nullifier(tx_nullifier);
}
}
// docs:end:entrypoint

Expand Down
1 change: 0 additions & 1 deletion noir-projects/noir-contracts/aztec

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ contract EcdsaKAccount {
storage.public_key.initialize(&mut pub_key_note).emit(encode_and_encrypt_note_with_keys(&mut context, this_keys.ovpk_m, this_keys.ivpk_m, this));
}

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
// Note: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload, cancellable: bool) {
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
actions.entrypoint(app_payload, fee_payload, cancellable);
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ contract EcdsaRAccount {
storage.public_key.initialize(&mut pub_key_note).emit(encode_and_encrypt_note_with_keys(&mut context, this_keys.ovpk_m, this_keys.ivpk_m, this));
}

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
// Note: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload, cancellable: bool) {
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
actions.entrypoint(app_payload, fee_payload, cancellable);
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ contract SchnorrAccount {
// docs:end:initialize
}

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts file
// Note: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts file
#[aztec(private)]
#[aztec(noinitcheck)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload, cancellable: bool) {
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
actions.entrypoint(app_payload, fee_payload, cancellable);
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ contract SchnorrHardcodedAccount {
global public_key_x: Field = 0x16b93f4afae55cab8507baeb8e7ab4de80f5ab1e9e1f5149bf8cd0d375451d90;
global public_key_y: Field = 0x208d44b36eb6e73b254921134d002da1a90b41131024e3b1d721259182106205;

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
// Note: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload, cancellable: bool) {
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
actions.entrypoint(app_payload, fee_payload, cancellable);
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ contract SchnorrSingleKeyAccount {

use crate::{util::recover_address, auth_oracle::get_auth_witness};

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
// Note: If you globally change the entrypoint signature don't forget to update account_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload, cancellable: bool) {
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
actions.entrypoint(app_payload, fee_payload, cancellable);
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ global GENERATOR_INDEX__BLOCK_HASH: u32 = 28;
global GENERATOR_INDEX__SIDE_EFFECT: u32 = 29;
global GENERATOR_INDEX__FEE_PAYLOAD: u32 = 30;
global GENERATOR_INDEX__COMBINED_PAYLOAD: u32 = 31;
global GENERATOR_INDEX__TX_NULLIFIER: u32 = 32;
// Indices with size ≤ 16
global GENERATOR_INDEX__TX_REQUEST: u32 = 33;
global GENERATOR_INDEX__SIGNATURE_PAYLOAD: u32 = 34;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class DeployAccountMethod extends DeployMethod {
exec.calls.push({
name: this.#feePaymentArtifact.name,
to: address,
args: encodeArguments(this.#feePaymentArtifact, [emptyAppPayload, feePayload]),
args: encodeArguments(this.#feePaymentArtifact, [emptyAppPayload, feePayload, false]),
selector: FunctionSelector.fromNameAndParameters(
this.#feePaymentArtifact.name,
this.#feePaymentArtifact.parameters,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Tx, type TxExecutionRequest } from '@aztec/circuit-types';
import { GasSettings } from '@aztec/circuits.js';
import { type Fr, GasSettings } from '@aztec/circuits.js';
import { createDebugLogger } from '@aztec/foundation/log';

import { type Wallet } from '../account/wallet.js';
Expand All @@ -18,6 +18,10 @@ export type SendMethodOptions = {
fee?: FeeOptions;
/** Whether to run an initial simulation of the tx with high gas limit to figure out actual gas settings (will default to true later down the road). */
estimateGas?: boolean;
/** Custom nonce to inject into the app payload of the transaction. Useful when trying to cancel an ongoing transaction by creating a new one with a higher fee */
nonce?: Fr;
/** Whether the transaction can be cancelled. If true, an extra nullifier will be emitted: H(nonce, GENERATOR_INDEX__TX_NULLIFIER) */
cancellable?: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ export class ContractFunctionInteraction extends BaseContractInteraction {
if (!this.txRequest) {
const calls = [this.request()];
const fee = opts?.estimateGas ? await this.getFeeOptionsFromEstimatedGas({ calls, fee: opts?.fee }) : opts?.fee;
this.txRequest = await this.wallet.createTxExecutionRequest({ calls, fee });
this.txRequest = await this.wallet.createTxExecutionRequest({
calls,
fee,
nonce: opts?.nonce,
cancellable: opts?.cancellable,
});
}
return this.txRequest;
}
Expand Down
5 changes: 5 additions & 0 deletions yarn-project/aztec.js/src/entrypoint/entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type AuthWitness, type FunctionCall, type PackedValues, type TxExecutionRequest } from '@aztec/circuit-types';
import { type Fr } from '@aztec/circuits.js';

import { EntrypointPayload, type FeeOptions, computeCombinedPayloadHash } from './payload.js';

Expand All @@ -17,6 +18,10 @@ export type ExecutionRequestInit = {
packedArguments?: PackedValues[];
/** How the fee is going to be payed */
fee?: FeeOptions;
/** An optional nonce. Used to repeat a previous tx with a higher fee so that the first one is cancelled */
nonce?: Fr;
/** Whether the transaction can be cancelled. If true, an extra nullifier will be emitted: H(nonce, GENERATOR_INDEX__TX_NULLIFIER) */
cancellable?: boolean;
};

/** Creates transaction execution requests out of a set of function calls. */
Expand Down
10 changes: 6 additions & 4 deletions yarn-project/aztec.js/src/entrypoint/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ type EncodedFunctionCall = {
export abstract class EntrypointPayload {
#packedArguments: PackedValues[] = [];
#functionCalls: EncodedFunctionCall[] = [];
#nonce = Fr.random();
#nonce: Fr;
#generatorIndex: number;

protected constructor(functionCalls: FunctionCall[], generatorIndex: number) {
protected constructor(functionCalls: FunctionCall[], generatorIndex: number, nonce = Fr.random()) {
for (const call of functionCalls) {
this.#packedArguments.push(PackedValues.fromValues(call.args));
}
Expand All @@ -62,6 +62,7 @@ export abstract class EntrypointPayload {
/* eslint-enable camelcase */

this.#generatorIndex = generatorIndex;
this.#nonce = nonce;
}

/* eslint-disable camelcase */
Expand Down Expand Up @@ -126,14 +127,15 @@ export abstract class EntrypointPayload {
/**
* Creates an execution payload for the app-portion of a transaction from a set of function calls
* @param functionCalls - The function calls to execute
* @param nonce - The nonce for the payload, used to emit a nullifier identifying the call
* @returns The execution payload
*/
static fromAppExecution(functionCalls: FunctionCall[] | Tuple<FunctionCall, 4>) {
static fromAppExecution(functionCalls: FunctionCall[] | Tuple<FunctionCall, 4>, nonce = Fr.random()) {
if (functionCalls.length > APP_MAX_CALLS) {
throw new Error(`Expected at most ${APP_MAX_CALLS} function calls, got ${functionCalls.length}`);
}
const paddedCalls = padArrayEnd(functionCalls, FunctionCall.empty(), APP_MAX_CALLS);
return new AppEntrypointPayload(paddedCalls, GeneratorIndex.SIGNATURE_PAYLOAD);
return new AppEntrypointPayload(paddedCalls, GeneratorIndex.SIGNATURE_PAYLOAD, nonce);
}

/**
Expand Down
1 change: 1 addition & 0 deletions yarn-project/cli-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@aztec/ethereum": "workspace:^",
"@aztec/foundation": "workspace:^",
"@aztec/kv-store": "workspace:^",
"@aztec/noir-contracts.js": "workspace:^",
"commander": "^12.1.0",
"inquirer": "^10.1.8",
"source-map-support": "^0.5.21",
Expand Down
52 changes: 52 additions & 0 deletions yarn-project/cli-wallet/src/cmds/cancel_tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { type AccountWalletWithSecretKey, type FeePaymentMethod, SentTx, type TxHash, TxStatus } from '@aztec/aztec.js';
import { type FeeOptions } from '@aztec/aztec.js/entrypoint';
import { Fr, type GasSettings } from '@aztec/circuits.js';
import { type LogFn } from '@aztec/foundation/log';

export async function cancelTx(
wallet: AccountWalletWithSecretKey,
{
txHash,
gasSettings,
nonce,
cancellable,
}: { txHash: TxHash; gasSettings: GasSettings; nonce: Fr; cancellable: boolean },
paymentMethod: FeePaymentMethod,
log: LogFn,
) {
const receipt = await wallet.getTxReceipt(txHash);
if (receipt.status !== TxStatus.PENDING || !cancellable) {
log(`Transaction is in status ${receipt.status} and cannot be cancelled`);
return;
}

const fee: FeeOptions = {
paymentMethod,
gasSettings,
};

gasSettings.inclusionFee.mul(new Fr(2));

const txRequest = await wallet.createTxExecutionRequest({
calls: [],
fee,
nonce,
cancellable: true,
});

const txPromise = await wallet.proveTx(txRequest, true);
const tx = new SentTx(wallet, wallet.sendTx(txPromise));
try {
await tx.wait();

log('Transaction has been cancelled');

const cancelReceipt = await tx.getReceipt();
log(` Tx fee: ${cancelReceipt.transactionFee}`);
log(` Status: ${cancelReceipt.status}`);
log(` Block number: ${cancelReceipt.blockNumber}`);
log(` Block hash: ${cancelReceipt.blockHash?.toString('hex')}`);
} catch (err: any) {
log(`Could not cancel transaction\n ${err.message}`);
}
}
12 changes: 12 additions & 0 deletions yarn-project/cli-wallet/src/cmds/check_tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type PXE, type TxHash } from '@aztec/aztec.js';
import { inspectTx } from '@aztec/cli/utils';
import { type LogFn } from '@aztec/foundation/log';

export async function checkTx(client: PXE, txHash: TxHash, statusOnly: boolean, log: LogFn) {
if (statusOnly) {
const receipt = await client.getTxReceipt(txHash);
return receipt.status;
} else {
await inspectTx(client, txHash, log, { includeBlockInfo: true });
}
}
Loading

0 comments on commit 2cfe7cd

Please sign in to comment.