# Improving adoption with Ethereum-wallet-based Aztec wallets with clear signing

**URL:** <https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422>\
**Category:** Aztec\
**Created:** [January 27, 2026, 7:21pm UTC](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422 "2026-01-27T19:21:29Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![wonderland](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wonderland/32/2437_2.png) [@wonderland](https://forum.aztec.network/u/wonderland)\
**Post date:** [January 27, 2026, 7:21pm UTC](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422/1 "2026-01-27T19:21:29Z")

</div>

Aztec wallets demand domain-specific functionality from its wallet software. Namely, handling the protocol-level keys (including discovery, encryption, and decryption of logs), as well as the specific mechanisms defined for each class of account. Hence the need for Aztec-specific wallets, that take into account Aztec’s specific security and privacy requirements into account.  
There currently exist a few Aztec-specific wallets, such as the [Azguard wallet](https://azguardwallet.io/).

However, it’s useful to allow newcomers to try Aztec without the need to download specialized software. Namely, using wallets that work for widespread blockchains such as Ethereum. This also offloads secret handling, which allows for the use of hardware wallets, with the caveat that it only covers authorization secrets.

This post outlines the approach that we’re taking in the development of such an account contract for Ethereum wallets like Metamask or Rabby. By leveraging [EIP-712](https://eips.ethereum.org/EIPS/eip-712), we can produce structured authorization requests that are compatible with such wallets while showing clear signing of what’s actually being authorized.

Work on a fully working version is still ongoing. We’ve currently benchmarked the entrypoint function for the most simple version of this, obtaining a gatecount on the order of a million.

It is assumed that the reader is familiar with how [account contracts](https://docs.aztec.network/developers/docs/foundational-topics/accounts) work on Aztec.

## Approach overview

Contracts are defined by specifying

- Contract code
- If initialized, initialization data: constructor and its arguments
- Nullifying and viewing keys

The contract code is going to contain the EIP-712 authorization mechanism for the connected Ethereum wallet’s ECDSA signing key, which is gonna be the only constructor argument.

### Secret keys derivation

As per the nullifying and viewing keys, we want to define them to be:

- Deterministically derivable from an Ethereum wallet, so that even if the user’s computer gets crushed by a raging meteorite, recovering the Ethereum wallet allows the recovery of the corresponding Aztec wallet.
- Not derivable from publicly available data, such as the Ethereum address. Note that even the complete ECDSA public key can be [ecrecover](https://docs.soliditylang.org/en/latest/cheatsheet.html#mathematical-and-cryptographic-functions)’d from any `(payload, signature)` pair, such as any Ethereum TX.

To this end, we use a [personal\_sign](https://docs.metamask.io/wallet/how-to/sign-data#use-personal_sign) signature of a message that should explicitly describe its effects, the most important of which is allowing the holder of the signature to inspect all of the wallets state. Something like

```auto
Sign to allow this website to inspect your Aztec EVM-based wallet's state.

```

This signature can only be forged by someone holding the secret key, and thus works as a valid secret for the wallet. It is worth noting that any website can request such signature, and afterwards inspect the account contract’s state.

As far as we’re aware of, there aren’t currently any mitigations for this that don’t require website-scoped account contracts, which in turn make DApp interoperability awful.

### Authorization

Standard account contract authorization is divided into two:

- Authorization for the [entrypoint](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/account.nr#L42-L83)
- Authorization for [specific actions](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/account.nr#L85-L106)

Both of those expose a single `Field` that acts as a commitment to what we’re gonna authorize.

For the entrypoint, this is a commitment to each function call, composed of:

- Function selector
- Arguments hash
- Target\_address
- Whether the function call is public
- Whether to hide the sender
- Whether the call should be static

Contract developers leverage the [`assert_current_call_valid_authwit`](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/auth.nr#L243-L274) helper function and its [public](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/auth.nr#L305-L326) counterpart for authorization, which also compute a commitment to said fields.

These commitments are usually [domain-separated](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/account.nr#L97-L102) to a specific chain ID and version as well. We’ll refer to said commitments as “Aztec message hashes”.

We can use these fields as commitment to the following human-readable data:

- Function _signature_
- Arguments _list_
- The remaining members

For instance, a call to the standard token contract’s [`transfer_private_to_private`](https://github.com/defi-wonderland/aztec-standards/blob/1ae323c9601148fb9db79c207bcb85fa1c49b4fd/src/token_contract/src/main.nr#L210-L234) function would expose the following values:

- Function signature: `transfer_private_to_private((Field),(Field),u256,Field)`
- Arguments: `[0x123123..., 0x321321..., 100, 5233126]`
- Contract address: `0x0303456...`
- Is public: `false`
- Hide message sender: `false` (this must be `false` for private calls)
- Static call: `false`

To this end, we define EIP-712 structures that represent function calls (and batched function calls).

In the account contract, authorization is done through the following steps:

1. Fetch the human-readable data and the EIP-712 signature via [capsules](https://docs.aztec.network/developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules), as well as any other data that may be required for the payload reconstruction steps below.
2. Reconstruct the EIP-712 payload.
3. Validate the received EIP-712 signature against the reconstructed payload and the stored ECDSA public key.
4. Reconstruct the Aztec message hash.
5. Ensure the reconstructed message hash equals the one the authorization function is meant to validate.

This way, we constrain the received payload to include a valid EIP-712 signature of a clear message for the action that the Aztec account contract is made to check.

## Specifics

Throughout this section, we assume the reader knows the [EIP-712 specification](https://eips.ethereum.org/EIPS/eip-712) to a good degree. Feel free however to read on at your own peril.

We’ll start by specifying an implementation where the EIP-712 structs for entrypoint and function call authorizations are known at compile time. Throughout all of this analysis we will, however, restrict ourselves to functions whose arguments each fit in 32 bytes.

EIP-712 defines the payload as a commitment to an instance of a typed struct, as well as a domain separator. The domain separator separator is itself the following EIP-712 struct:

```rust
struct EIP712Domain {
    string name;
    string version;
    uint256 chainId;
    address verifyingContract;
    bytes32 salt;
}

```

Which can optionally be stripped of some fields.

### The EIP-712 domain

This domain refers to the one the wallet is doing a signature on, and is EVM-specific (as evidenced by `address verifyingContract`, and EIP-712’s remark that “The user-agent should refuse signing if it does not match the currently active chain.”). We propose using this as a domain separator for the Aztec rollup contract that the message is meant to. Something like

```rust
EIP712Domain {
    name: "Aztec",
    version: "devnet.20251212-ultimate.patch.1-v3",
    chainId: 1,
    verifyingContract: 0x5772156...,
}

```

Here, `chainId` refers to the L1 where Aztec is being sequenced. But the remaining fields are bound to the specific Aztec version that the message is purported to, so this domain-separator already acts as a domain-separator for different Aztec versions.

We’re not gonna implement a similar domain to refer to the applications that are being called, as they will not have `name` and `version` in their code. Instead, contract address will have to suffice.

### Representing function calls

```rust
struct FunctionCallAuthorization {
    bytes32 contract;
    string functionSignature;
    uint256[] arguments;
    bool isPublic;
}

```

> ❕We’re limiting the arguments types to `uint256` here for simplicity

### Representing entrypoint calls

```rust
struct EntrypointAuthorization {
    AccountData accountData;
    FunctionCall[ACCOUNT_MAX_CALLS] functionCalls;
    TxMetadata txMetadata;
}

struct AccountData {
    bytes32 address;
    string walletName;
    string version;
}

struct FunctionCall {
    bytes32 contract;
    string functionSignature;
    uint256[] arguments;
    bool isPublic;
    bool hideMessageSender;
    bool isStatic;
}

struct TxMetadata {
    uint8 feePaymentMethod;
    bool cancellable;
    uint256 txNonce;
}

```

The constant `ACCOUNT_MAX_CALLS` is already defined for standard accounts and currently [takes the value of 5](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/entrypoint/app.nr#L9).

### Fetching the structs’ contents

The EIP-712 domain is a deploy-time constant, and thus needs no fetching. Instead, we’ll compute what we need from it and define those values at either deploy-time or compile-time. The same applies for `AccountData.{walletName, version}`.

Everything else will be fetched from [capsules](https://docs.aztec.network/developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules) as arrays of `u8`. For variable-size members such as `FunctionCall.{arguments, functionSignature}`, we’ll fetch a compile-time bound array (e.g. `[u8; MAX_ARGS * 4]`), and an extra `u8` value with the run-time known size.

### EIP-712 payload reconstruction

First, let’s go through a _quick_ recap of how EIP-712 works.

Every struct _instance_ is committed to via `hashStruct` defined as follows

```rust
hashStruct(s) = keccak256(typeHash(s) || encodeData(s))
typeHash(s) = keccak256(encodeType(typeOf(s)))

```

`encodeType` is a concise description of a struct’s shape. For example,

```rust
struct Mail {
    address from;
    address to;
    string contents;
};

encodeType(Mail) == "Mail(address from,address to,string contents)"`.

```

Meanwhile, `encodeData(s)` is defined as the `keccak256` of the concatenation of the following encoding of the members of `s`:

- for a struct: its `hashStruct`
- for an atomic type (e.g. `uint32`, `bytes8`, etc): a non-surprising 32-bytes representation of it
- for `bytes` and `string`: the `keccak256` of its contents
- for `type[]`: the `keccak256` of the concatenation of the encodings for each of the array’s items

Finally, the EIP-712 payload is `"\\x19\\x01 || domainSeparator || hashStruct(message)`, where `domainSeparator` is the `hashStruct` of an instance of the aforementioned `EIP712Domain` struct.

Now let’s go back to our specific use case.

The `domainSeparator` is gonna be a deploy-time constant that depends on the specific version of the rollup. Each struct’s `typeHash` is a compile-time constant that will be hardcoded.

For arguments whose length is known at run-time, we leverage `keccak256`’s [hash function](https://github.com/noir-lang/keccak256/blob/v0.1.2/src/keccak256.nr#L14)’s `message_size` argument to produce the required subarray.

### Aztec message hash reconstruction

> ❕We implicitly assume the `u8` arrays are converted to their corresponding Aztec types for this section.

### Entrypoint

In the case of the `entrypoint` function, rather than reconstructing the message hash that’s usually fed to the authorization function, we can instead check many of the values directly. This means unlike the standard `entrypoint` function, our `entrypoint` function won’t perform any call to `is_valid_impl`.

As a remainder, `entrypoint` is defined as:

```rust
pub fn entrypoint(self, app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { ... }

#[derive(Serialize)]
pub struct AppPayload {
    function_calls: [FunctionCall; ACCOUNT_MAX_CALLS],
    pub tx_nonce: Field,
}

#[derive(Serialize)]
pub struct FunctionCall {
    pub args_hash: Field,
    pub function_selector: FunctionSelector,
    pub target_address: AztecAddress,
    pub is_public: bool,
    pub hide_msg_sender: bool,
    pub is_static: bool,
}

```

Thus, the following checks must be performed from the fetched values:

- `AccountData`’s `address` must match `context.msg_sender()`.
- `TxMetadata`’s members must match the `entrypoint`’s `fee_payment_method` and `cancellable` arguments, as well as `AppPayload`’s `tx_nonce`.
- For each `FunctionCall` instance, we must:
  - Reconstruct `args_hash` from the arguments and their size
  - Reconstruct `function_selector` from the function signature
  - Check that every remaining component from the `entrypoint`’s `AppPayload` matches what’s fed from the oracle

The reconstruction of `args_hash` and `function_selector` should be done the same way that Aztec does it:

- The `function_selector` is computed [here](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/noir-protocol-circuits/crates/types/src/abis/function_selector.nr#L33-L39) as the `poseidon2` hash of the signature (expressed as bytes).
- The `args_hash` is computed differently according to `is_public`:
  - If `true`, this is actually the `calldata`’s `poseidon2`, where `calldata` is an array with the function selector followed by the arguments. And the hash is [domain-separated](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/hash.nr#L69-L71) to public function calls
  - If `false`, this is the `poseidon2` hash of the arguments’ hash, [domain-separated](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/hash.nr#L59-L66) to private function calls

As with EIP-712, computing function selectors and arguments hashes requires doing a hash for an array of run-time length. As with the `keccak256` crate, Noir’s `poseidon` crate’s [hash function](https://github.com/noir-lang/poseidon/blob/v0.2.2/src/poseidon2.nr#L14-L17) allows the slicing of its input. However, unlike with EIP-712, the way those commitments are computed may change in further versions of Aztec.

There are two ways to deal with this:

- Use Aztec’s functions as black boxes, to avoid code repetition. This would be the tidiest option. However, these functions don’t currently support run-time known length for the hashes they perform.
- Copy-paste those functions and add the `message_size` parameter that the hash functions take. This is maintenance-heavier, as the development team should _remember_ to update those functions if the ones they’re copying ever change.

### Specific actions

In the case of specific actions, which end up calling [`verify_private_authwit`](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/account.nr#L85-L106), our account contract receives an `inner_hash` from some calling contract. This `inner_hash` has been computed by `assert_current_call_valid_authwit` as the `poseidon2` hash of `[msg_sender, function_selector, args_hash]`, [domain-separated](https://github.com/AztecProtocol/aztec-packages/blob/v3.0.0-nightly.20251212/noir-projects/aztec-nr/aztec/src/authwit/auth.nr#L390-L392) to inner message hashes.

### Allowing argument types other than `uint256`

We want to loosen the type of the `arguments` member of both the `FunctionCall` and the `FunctionCallAuthorization` structs. Let’s first approach the later, which involves just _one_ function call.

We’ll replace the `uint256[]` type with an `Arguments` struct. We could potentially leave this later struct unconstrained, by taking as oracle input a string for the return value of `encodeType(FuncionCallAuthorization)`, and only constraining the first part (which is gonna be the definition of `FunctionCallAuthorization` without the definition of `Authorization`.

This would have the advantage that a DApp that uses a token could e.g. define

```rust
struct Arguments {
    bytes32 from;
    bytes32 to;
    uint256 value;
    uint256 _nonce;
}

```

And it would work _for this_, but there are two caveats.

1. It wouldn’t be performant to do it for the `EntrypointAuthorization` struct, because its dependencies must be sorted in alphabetical order, and so the `Arguments1` (and so forth) structs would go in the middle of the string. We’d have to use string concatenation at run-time. And not doing so could be confusing for the user, because they’d see clear messages for other account’s authorizations but not for theirs.
2. Most importantly, it lends itself to phishing attacks. By switching the position of `from` and `to` in the same example, a phishing attack could convince a user that they’re getting a reward from the sender, while in reality they’d be getting rugged.

Instead, we propose whitelisting a set of structs, where every member is sequentially named as `argument1`, `argument2`, and so forth, and their type could be one of `uint256`, `int256`, and `bytes32`. The contract would expect the `typeHash` corresponding to one of the whitelisted structs, along of a proof of inclusion to the whitelist.

This can be quite performant because the _encoding_ of the contents of such a struct is precisely the same, given that the encoding of said struct would just be `typeHash || keccak(arg1 || arg2 || ...)`.

In order to implement the whitelist, there should be a Merkle tree where the leaves represent the `typeHash` of every whitelisted possibility. It suffices to constrain the `typeHash` of the `Arguments` struct, since finding a second `typeHash` for the encoding of `FunctionCallAuthorization` that yields the same payload would be akin to finding a hash’s second preimage.

There are `(3^{n+1}-1)/2` different possible such structs that allow for up to `n` arguments, so a binary tree of size `ceil((n+1) log(3)/log(2)) - 1` would suffice to store the whitelist. For the case of up to 8 arguments, this is a depth of 13, which is reasonably small. Constructing the tree would be non-trivial for too big of an `n`.

In the case of `EntrypointAuthorization`, each function call should have its own `Arguments` struct, sequentially named as `Arguments1`, `Arguments2`, and so forth. Storing a whitelist for the whole `EntrypointAuthorization` would be impractical anyways, so we’d have to take one `typeHash` and its corresponding merkle proof for each of the function calls.

## Appendix 1: alleviating privacy exposure

As mentioned in [#secret-keys-derivation], any website can request the signature that’s used for the viewing and nullifying secrets. A potential alleviation to this would be using [WebAuthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API), whose signatures are domain-scoped.

This would, however, require one account per website, which is arguably bad UX.

This siloing could in turn be mitigated by having a “central” account that uses the ECDSA signature for secret generation, and adding a “transfer” function that requires signing a sufficiently clear message.

But this wouldn’t be enough make DApp interoperability UX reasonable.

## Appendix 2: Benchmarks

Development of the whole EIP-712 account contract is ongoing. We’ve benchmarked the `entrypoint` with this PoC, along with other account contract’s entrypoint. All of them consume the same amount of gas. Regarding the gates of the `entrypoint` function, the following results were found

| Account contract | Entrypoint gates |
| --- | --- |
| EIP-712 | 1 076 959 |
| ECDSA | 121 844 |
| Schnorr | 54 441 |

In the current PoC, there’s an 8x and a 19x increase in gatecount with respect to the conventional ECDSA and Schnorr accounts respectively, which is a lot.

---

<div class="post-metadata">

**Author:** ![porco](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/porco/32/147_2.png) [@porco](https://forum.aztec.network/u/porco)\
**Post date:** [January 28, 2026, 9:45am UTC](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422/2 "2026-01-28T09:45:04Z")

</div>

great article!

> [@wonderland](#):
>
> Appendix 1: alleviating privacy exposure

on webauthn, as far as i know, it doesn’t give you the same signature over the same message deterministically, as nocne is randomly generated by webauthn internally. Workarounds might be new webauthn extensions but these are still not well-supported by major browsers.

refs :

- [https://stackoverflow.com/questions/75067617/producing-the-same-signature-with-webauthn](https://stackoverflow.com/questions/75067617/producing-the-same-signature-with-webauthn)

- [https://x.com/gakonst/status/1942500378278338833?s=20](https://x.com/gakonst/status/1942500378278338833?s=20)

---

<div class="post-metadata">

**Author:** ![jasonxh](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/jasonxh/32/2507_2.png) [@jasonxh](https://forum.aztec.network/u/jasonxh)\
**Post date:** [January 29, 2026, 7:00pm UTC](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422/3 "2026-01-29T19:00:11Z")

</div>

This is a great writeup! We’ve been exploring EIP-712 based Aztec account governance as well at [nyx.money](https://docs.nyx.money/docs/technical-overview), because we too believe bringing your own wallet is the best way to draw existing Ethereum users in to Aztec!

A couple of quite interesting points I noticed in this design:

- Using `personal_sign` signatures to derive privacy keys, leveraging the deterministic nature of Ethereum signatures. This achieves the goal of consolidating all of Aztec account governance with the EVM wallet. However, I wonder if there’s any precedence in using Ethereum message signing as a key derivation scheme? The biggest mental leap for me is treating message signatures as permanent (not time-bound) secrets. You already mentioned the possibility of phishing attacks (all it takes is signing one message (not tx) from a malicious website to permanently leak your Aztec account’s privacy). I also wonder if most EVM wallet providers follow the same mental model of treating message signatures as secrets (we know tx signatures are not secrets) so that they don’t unintentionally leak them to their own backends or 3rd parties.  
In the nyx prototype, we chose the path of generating random account secrets on client-side, and encrypt them using WebAuthn credentials before syncing to the nyx backend. This has the obvious downside of fragmenting the Aztec account governance: your privacy is governed by WebAuthn credentials, while tx authorization is done by EVM wallet. It also relies on the nyx backend for encrypted storage and is domain-bound to the nyx app (which is not a problem for nyx, but limits the account’s portability).
- Using capsules to pass `FunctionCall` members’ preimages needed for message reconstruction. This seems quite neat as it could potentially minimize the interface change. We went down the path of forking the entrypoint and authwit, which resulted in a lot of code forking in both Noir and typescript SDK. Curious if you know any downsides of using capsules in this case.

~~Another thing that puzzles us is, while we have observed the similar gate count jump with our account contract implementation, we don’t seem to observe significant jump in tx proving times on both devnet and local (with proving enabled). We’re looking to do more profiling in this area, but I’m curious if you have any insights.~~ See @shiqicao 's post for more accurate observations: [Improving adoption with Ethereum-wallet-based Aztec wallets with clear signing - #5 by shiqicao](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422/5)

---

<div class="post-metadata">

**Author:** ![wei3erHase](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wei3erhase/32/2263_2.png) [@wei3erHase](https://forum.aztec.network/u/wei3erHase)\
**Post date:** [January 29, 2026, 7:42pm UTC](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422/4 "2026-01-29T19:42:31Z")

</div>

> [@jasonxh](#):
>
> I wonder if there’s any precedence in using Ethereum message signing as a key derivation scheme?

yes, that’s how [PrivacyPools](https://privacypools.com/) currently derive their secrets, in the “action” field they’re declaring “Derive Seed”, tho i agree that it should have a bigger warning saying “You’re signing your keys derivation for X protocol” (that wasn’t developed by us btw) in plain text, but yes, the User should always be aware of what they’re signing and where.

> [@jasonxh](#):
>
> I also wonder if most EVM wallet providers follow the same mental model of treating message signatures as secrets

I don’t know about “most wallets” nowadays, but we do have a signing derivation scheme standard that avoids leaking which address of your enclave is actually signing that derivation, so 3rd parties shouldn’t be able to even request you that signature to begin with. ([writeup](https://docs.fileverse.io/0xb4f70982129fe6f8e83c41ec777edd5edaed2bf3/0#key=wA_Ln2kh-XwYZn8jBudTgyEb5Hag0in88WwmjbZbFunwHHNY_jLaw0ge4-iQg_Bp) & Kohaku implementation [A(L329,L829,850)](https://github.com/ethereum/kohaku-commons/blob/demo/devconnect/src/controllers/privacyPools/privacyPools.ts) & [B](https://github.com/ethereum/kohaku-commons/blob/demo/devconnect/src/consts/derivation.ts))

About using both Passkey and Wallet, it does seem like a good tradeoff, but something to take into account is that because these “wallets” aren’t native, we’ll need to pass the viewing keys to the app in order to detect messages, as well as the nullifier keys to generate the proof when spending keys, so malicious apps always have this vector of attack. In contrast, Aztec specific wallets do the note discovery natively, without ever requiring to share the keys to begin with.

> [@jasonxh](#):
>
> Another thing that puzzles us is, while we have observed the similar gate count jump with our account contract implementation, we don’t seem to observe significant jump in tx proving times on both devnet and local

Interesting, perhaps the gate-count isn’t the actual proving time bottleneck, we’ll run benchmarks on our end, we’re polishing a GH workflow to track proving times across PRs.

> [@jasonxh](#):
>
> Using capsules to pass `FunctionCall` members’ preimages needed for message reconstruction

We’ll take a look into it.

Thanks for your responses! I hope the information here results helpful!

---

<div class="post-metadata">

**Author:** ![shiqicao](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/shiqicao/32/2646_2.png) [@shiqicao](https://forum.aztec.network/u/shiqicao)\
**Post date:** [February 4, 2026, 11:16am UTC](https://forum.aztec.network/t/improving-adoption-with-ethereum-wallet-based-aztec-wallets-with-clear-signing/8422/5 "2026-02-04T11:16:59Z")

</div>

We(nyx.money) also observed **very strong positive correlation** between the number of gates and the execution duration. We measured 3 cases, run 10 times for each case.

Case 1, signing tx hash with domain info (141,958 gates)  
Case 2, signing 2 function detials but one hash for all args (255,835 gates)  
Case 3, signing 5 function details but one hash for all args (490,144 gates)

| **Gates** | **Average Duration** | **Standard Deviation** | **Sample Size (n)** |
| --- | --- | --- | --- |
| **141,958** | ~11,270 - 11,430 | ~491 - 761 | 21 |
| **255,835** | ~13,569 | ~237 | 10 |
| **490,144** | ~15,642 | ~459 | 10 |
