Skip to content

Commit

Permalink
feat: simulate, aliases, ECDSA R account contract + touchid wallet (#…
Browse files Browse the repository at this point in the history
…7725)

# TLDR;

A bunch of improvements for `aztec-wallet`. Try this!

```bash 
cd ~/repos/aztec-packages/noir-projects/noir-contracts
aztec-wallet create-account -a main
aztec-wallet deploy token_contract@Token --args accounts:main Test TST 18 -ac main
aztec-wallet send mint_public -ca contracts:last --args accounts:main 42 -ac main
aztec-wallet simulate balance_of_public -ca contracts:last --args accounts:main -ac main
```

## Aliases (explicit and automatic)

Options on the wallet were getting out of hand. The ability to alias
accounts partially alleviated the issue, but even moderately complex
requests required +5 parameters that had to be copied all over the
place.

To solve this problem, the wallet now has a more general concept of
aliases, plus autogeneration of some of them. For example:

```bash
# set an explicit alias on an account
aztec-wallet create-account -a main

# Used the `accounts:main` alias in an argument and `main` alias in the `-ac` option. 
# The wallet is smart enough to deduce you're probably referring to an account in the latter. 
# Finally, set an explicit alias on the deployed contract, `my_contract`
aztec-wallet deploy ../target/contract_artifact-MyContract.json --args accounts:main -ac main -a my_contract
``` 
But wait! There's more. You could have done:

```bash 
# `last` is an automatic alias on the latest object you created. In this case, the latest account!
aztec-wallet deploy ../target/contract_artifact-MyContract.json --args accounts:last -ac last -a my_contract
``` 

So now, it is possible to do:

```bash 
# Used the `contracts:last` alias to refer to the address of the last contract that was deployed. 
# Used the `artifacts:last` alias to refer to the path of the artifact of the last contract that was deployed.
azctec-wallet send my_function -c artifacts:last -ac contracts:last --args accounts:main -ac main

# But we can do better!
# Notice how the `-ca` argument (contract artifact) is omitted, 
# as the wallet is smart enough to assume that you're probably referring to the path of the artifact of the contract that was deployed at `contracts:last`
azctec-wallet send my_function -ca contracts:last --args accounts:main -ac main

# in this case, this is equivalent to:
azctec-wallet send my_function -ca contracts:my_contract --args accounts:main -ac main
```

## Nargo project detection

For the `deploy` subcommand argument and in every instance you would
provide a `-c` (contract artifact) option you could just execute the
wallet from the contract folder (where `Nargo.toml` is located)

```bash 
cd to/the/folder/that/contains/your/contract
# The wallet can detect you're in a nargo workspace and pick the artifact path automatically
aztec-wallet deploy --args accounts:main -ac last
```

If it's not an isolated contract but a workspace (much like
`noir-projects/noir-contracts`)

```bash 
cd to/the/nargo/workspace/that/contains/your/contract
# It is possible to just provide the package and omit the contract name, but any ambiguity will result on the command failing
aztec-wallet deploy contract_package@ContractName --args accounts:main -ac last
```

## ECDSA secp256r1 and secp256k1 account contracts + SSH_AGENT and
TouchID support

It is now possible to create accounts with other than
`SchnorrAccountContract` as backend (even though it's still the
default). Particularly interesting is the ECDSA secp256r1 + SSH agent
account, which will use the default SSH_SOCK_AUTH env variable to
establish connection with a running SSH agent to perform authentication.
On a Mac this can be combined with
[secretive](https://github.com/maxgoedjen/secretive) for a secure
enclave hardware wallet!

```bash 
# After installing secretive, this env variable is provided in a wizard. It can also be checked using its "help" menu

export SSH_AUTH_SOCK=/Users/<myUser>/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh 

aztec-wallet create-account -a main -t ecdsasecp256r1ssh
```

You will be prompted to use one of the stored SSH keys inside the agent
(identified by their public key), and once selected, your account will
use TouchID to sign any requests the wallet might create.

## Simulate 

Wallet is now able to simulate functions (including unconstrained!)

```bash 
aztec-wallet simulate balance_of_private -c contracts:token --args accounts:main -ac main
```
  • Loading branch information
Thunkar authored Aug 8, 2024
1 parent 78ae6b4 commit 811d62f
Show file tree
Hide file tree
Showing 37 changed files with 1,174 additions and 194 deletions.
2 changes: 1 addition & 1 deletion aztec-up/bin/.aztec-run
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ docker run \
-ti \
--rm \
--workdir "$PWD" \
-v $HOME:$HOME -v cache:/cache \
-v $HOME:$HOME -v cache:/cache -v $(readlink $SSH_AUTH_SOCK):$SSH_AUTH_SOCK \
$port_assignment \
${DOCKER_ENV:-} \
${DOCKER_HOST_BINDS:-} \
Expand Down
3 changes: 1 addition & 2 deletions aztec-up/bin/aztec-wallet
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail

export SKIP_NET=1
export SKIP_PORT_ASSIGNMENT=1
export WALLET_DATA_DIRECTORY=$(dirname $0)/wallet-data
export ENV_VARS_TO_INJECT=WALLET_DATA_DIRECTORY
export ENV_VARS_TO_INJECT="WALLET_DATA_DIRECTORY SSH_AUTH_SOCK"

mkdir -p $WALLET_DATA_DIRECTORY

Expand Down
3 changes: 2 additions & 1 deletion noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ members = [
"contracts/docs_example_contract",
"contracts/easy_private_token_contract",
"contracts/easy_private_voting_contract",
"contracts/ecdsa_account_contract",
"contracts/ecdsa_k_account_contract",
"contracts/ecdsa_r_account_contract",
"contracts/escrow_contract",
"contracts/fee_juice_contract",
"contracts/import_test_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
[package]
name = "ecdsa_account_contract"
name = "ecdsa_k_account_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
authwit = { path = "../../../aztec-nr/authwit" }
ecdsa_public_key_note = { path = "../ecdsa_public_key_note" }
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
mod ecdsa_public_key_note;

// Account contract that uses ECDSA signatures for authentication on the same curve as Ethereum.
// The signing key is stored in an immutable private note and should be different from the signing key.
contract EcdsaAccount {
contract EcdsaKAccount {
use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PrivateContext, PrivateImmutable};
use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note;

Expand All @@ -14,7 +12,7 @@ contract EcdsaAccount {
auth_witness::get_auth_witness
};

use crate::ecdsa_public_key_note::EcdsaPublicKeyNote;
use dep::ecdsa_public_key_note::EcdsaPublicKeyNote;

#[aztec(storage)]
struct Storage {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "ecdsa_public_key_note"
authors = ["aztec-labs"]
compiler_version = ">=0.25.0"
type = "lib"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "ecdsa_r_account_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
authwit = { path = "../../../aztec-nr/authwit" }
ecdsa_public_key_note = { path = "../ecdsa_public_key_note" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Account contract that uses ECDSA signatures for authentication on random version of the p256 curve (to use with touchID).
contract EcdsaRAccount {
use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, NoteGetterOptions, PrivateContext, PrivateImmutable};
use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note;

use dep::aztec::protocol_types::abis::call_context::CallContext;
use dep::std;

use dep::authwit::{
entrypoint::{app::AppPayload, fee::FeePayload}, account::AccountActions,
auth_witness::get_auth_witness
};

use dep::ecdsa_public_key_note::EcdsaPublicKeyNote;

#[aztec(storage)]
struct Storage {
public_key: PrivateImmutable<EcdsaPublicKeyNote>,
}

// Creates a new account out of an ECDSA public key to use for signature verification
#[aztec(private)]
#[aztec(initializer)]
fn constructor(signing_pub_key_x: [u8; 32], signing_pub_key_y: [u8; 32]) {
let this = context.this_address();
let header = context.get_header();
let this_npk_m_hash = header.get_npk_m_hash(&mut context, this);
// Not emitting outgoing for msg_sender here to not have to register keys for the contract through which we
// deploy this (typically MultiCallEntrypoint). I think it's ok here as I feel the outgoing here is not that
// important.

let mut pub_key_note = EcdsaPublicKeyNote::new(signing_pub_key_x, signing_pub_key_y, this_npk_m_hash);
storage.public_key.initialize(&mut pub_key_note).emit(encode_and_encrypt_note(&mut context, this, this));
}

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

#[aztec(private)]
#[aztec(noinitcheck)]
#[aztec(view)]
fn verify_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.verify_private_authwit(inner_hash)
}

#[contract_library_method]
fn is_valid_impl(context: &mut PrivateContext, outer_hash: Field) -> bool {
// Load public key from storage
let storage = Storage::init(context);
let public_key = storage.public_key.get_note();

// Load auth witness
let witness: [Field; 64] = get_auth_witness(outer_hash);
let mut signature: [u8; 64] = [0; 64];
for i in 0..64 {
signature[i] = witness[i] as u8;
}

// Verify payload signature using Ethereum's signing scheme
// Note that noir expects the hash of the message/challenge as input to the ECDSA verification.
let outer_hash_bytes: [u8; 32] = outer_hash.to_be_bytes(32).as_array();
let hashed_message: [u8; 32] = std::hash::sha256(outer_hash_bytes);
let verification = std::ecdsa_secp256r1::verify_signature(public_key.x, public_key.y, signature, hashed_message);
assert(verification == true);

true
}
}
3 changes: 2 additions & 1 deletion yarn-project/accounts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"./ecdsa": "./dest/ecdsa/index.js",
"./schnorr": "./dest/schnorr/index.js",
"./single_key": "./dest/single_key/index.js",
"./testing": "./dest/testing/index.js"
"./testing": "./dest/testing/index.js",
"./utils": "./dest/utils/index.js"
},
"typedocOptions": {
"entryPoints": [
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/accounts/scripts/copy-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail
mkdir -p ./artifacts

contracts=(schnorr_account_contract-SchnorrAccount ecdsa_account_contract-EcdsaAccount schnorr_single_key_account_contract-SchnorrSingleKeyAccount)
contracts=(schnorr_account_contract-SchnorrAccount ecdsa_k_account_contract-EcdsaKAccount ecdsa_r_account_contract-EcdsaRAccount schnorr_single_key_account_contract-SchnorrSingleKeyAccount)

decl=$(cat <<EOF
import { type NoirCompiledContract } from '@aztec/types/noir';
Expand Down
5 changes: 0 additions & 5 deletions yarn-project/accounts/src/ecdsa/artifact.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { Ecdsa } from '@aztec/circuits.js/barretenberg';
import { type ContractArtifact } from '@aztec/foundation/abi';
import { type Fr } from '@aztec/foundation/fields';

import { DefaultAccountContract } from '../defaults/account_contract.js';
import { EcdsaAccountContractArtifact } from './artifact.js';
import { DefaultAccountContract } from '../../defaults/account_contract.js';
import { EcdsaKAccountContractArtifact } from './artifact.js';

/**
* Account contract that authenticates transactions using ECDSA signatures
* verified against a secp256k1 public key stored in an immutable encrypted note.
*/
export class EcdsaAccountContract extends DefaultAccountContract {
export class EcdsaKAccountContract extends DefaultAccountContract {
constructor(private signingPrivateKey: Buffer) {
super(EcdsaAccountContractArtifact as ContractArtifact);
super(EcdsaKAccountContractArtifact as ContractArtifact);
}

getDeploymentArgs() {
Expand All @@ -22,12 +22,12 @@ export class EcdsaAccountContract extends DefaultAccountContract {
}

getAuthWitnessProvider(_address: CompleteAddress): AuthWitnessProvider {
return new EcdsaAuthWitnessProvider(this.signingPrivateKey);
return new EcdsaKAuthWitnessProvider(this.signingPrivateKey);
}
}

/** Creates auth witnesses using ECDSA signatures. */
class EcdsaAuthWitnessProvider implements AuthWitnessProvider {
class EcdsaKAuthWitnessProvider implements AuthWitnessProvider {
constructor(private signingPrivateKey: Buffer) {}

createAuthWit(messageHash: Fr): Promise<AuthWitness> {
Expand Down
5 changes: 5 additions & 0 deletions yarn-project/accounts/src/ecdsa/ecdsa_k/artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js';

import EcdsaKAccountContractJson from '../../../artifacts/EcdsaKAccount.json' assert { type: 'json' };

export const EcdsaKAccountContractArtifact = loadContractArtifact(EcdsaKAccountContractJson as NoirCompiledContract);
37 changes: 37 additions & 0 deletions yarn-project/accounts/src/ecdsa/ecdsa_k/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* The `@aztec/accounts/ecdsa` export provides an ECDSA account contract implementation, that uses an ECDSA private key for authentication, and a Grumpkin key for encryption.
* Consider using this account type when working with integrations with Ethereum wallets.
*
* @packageDocumentation
*/
import { AccountManager, type Salt } from '@aztec/aztec.js/account';
import { type AccountWallet, getWallet } from '@aztec/aztec.js/wallet';
import { type PXE } from '@aztec/circuit-types';
import { type AztecAddress, type Fr } from '@aztec/circuits.js';

import { EcdsaKAccountContract } from './account_contract.js';

export { EcdsaKAccountContractArtifact } from './artifact.js';
export { EcdsaKAccountContract };

/**
* Creates an Account that relies on an ECDSA signing key for authentication.
* @param pxe - An PXE server instance.
* @param secretKey - Secret key used to derive all the keystore keys.
* @param signingPrivateKey - Secp256k1 key used for signing transactions.
* @param salt - Deployment salt.
*/
export function getEcdsaKAccount(pxe: PXE, secretKey: Fr, signingPrivateKey: Buffer, salt?: Salt): AccountManager {
return new AccountManager(pxe, secretKey, new EcdsaKAccountContract(signingPrivateKey), salt);
}

/**
* Gets a wallet for an already registered account using ECDSA signatures.
* @param pxe - An PXE server instance.
* @param address - Address for the account.
* @param signingPrivateKey - ECDSA key used for signing transactions.
* @returns A wallet for this account that can be used to interact with a contract instance.
*/
export function getEcdsaKWallet(pxe: PXE, address: AztecAddress, signingPrivateKey: Buffer): Promise<AccountWallet> {
return getWallet(pxe, address, new EcdsaKAccountContract(signingPrivateKey));
}
39 changes: 2 additions & 37 deletions yarn-project/accounts/src/ecdsa/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,2 @@
/**
* The `@aztec/accounts/ecdsa` export provides an ECDSA account contract implementation, that uses an ECDSA private key for authentication, and a Grumpkin key for encryption.
* Consider using this account type when working with integrations with Ethereum wallets.
*
* @packageDocumentation
*/
import { AccountManager, type Salt } from '@aztec/aztec.js/account';
import { type AccountWallet, getWallet } from '@aztec/aztec.js/wallet';
import { type PXE } from '@aztec/circuit-types';
import { type AztecAddress, type Fr } from '@aztec/circuits.js';

import { EcdsaAccountContract } from './account_contract.js';

export { EcdsaAccountContractArtifact } from './artifact.js';
export { EcdsaAccountContract };

/**
* Creates an Account that relies on an ECDSA signing key for authentication.
* @param pxe - An PXE server instance.
* @param secretKey - Secret key used to derive all the keystore keys.
* @param signingPrivateKey - Secp256k1 key used for signing transactions.
* @param salt - Deployment salt.
*/
export function getEcdsaAccount(pxe: PXE, secretKey: Fr, signingPrivateKey: Buffer, salt?: Salt): AccountManager {
return new AccountManager(pxe, secretKey, new EcdsaAccountContract(signingPrivateKey), salt);
}

/**
* Gets a wallet for an already registered account using ECDSA signatures.
* @param pxe - An PXE server instance.
* @param address - Address for the account.
* @param signingPrivateKey - ECDSA key used for signing transactions.
* @returns A wallet for this account that can be used to interact with a contract instance.
*/
export function getEcdsaWallet(pxe: PXE, address: AztecAddress, signingPrivateKey: Buffer): Promise<AccountWallet> {
return getWallet(pxe, address, new EcdsaAccountContract(signingPrivateKey));
}
export * from './ecdsa_k/index.js';
export * from './ssh_ecdsa_r/index.js';
88 changes: 88 additions & 0 deletions yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/account_contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { type AuthWitnessProvider } from '@aztec/aztec.js/account';
import { AuthWitness, type CompleteAddress } from '@aztec/circuit-types';
import { EcdsaSignature } from '@aztec/circuits.js/barretenberg';
import { type ContractArtifact } from '@aztec/foundation/abi';
import { type Fr } from '@aztec/foundation/fields';

import { DefaultAccountContract } from '../../defaults/account_contract.js';
import { signWithAgent } from '../../utils/ssh_agent.js';
import { EcdsaRAccountContractArtifact } from './artifact.js';

const secp256r1N = 115792089210356248762697446949407573529996955224135760342422259061068512044369n;
/**
* Account contract that authenticates transactions using ECDSA signatures
* verified against a secp256r1 public key stored in an immutable encrypted note.
* Since this implementation relays signatures to an SSH agent, we provide the
* public key here not for signature verification, but to identify actual identity
* that will be used to sign authwitnesses.
*/
export class EcdsaRSSHAccountContract extends DefaultAccountContract {
constructor(private signingPublicKey: Buffer) {
super(EcdsaRAccountContractArtifact as ContractArtifact);
}

getDeploymentArgs() {
return [this.signingPublicKey.subarray(0, 32), this.signingPublicKey.subarray(32, 64)];
}

getAuthWitnessProvider(_address: CompleteAddress): AuthWitnessProvider {
return new SSHEcdsaRAuthWitnessProvider(this.signingPublicKey);
}
}

/** Creates auth witnesses using ECDSA signatures. */
class SSHEcdsaRAuthWitnessProvider implements AuthWitnessProvider {
constructor(private signingPublicKey: Buffer) {}

#parseECDSASignature(data: Buffer) {
// Extract ECDSA signature components
let offset = 0;
const sigTypeLen = data.readUInt32BE(offset);
offset += 4;
const sigType = data.subarray(offset, offset + sigTypeLen).toString();
offset += sigTypeLen;

if (sigType !== 'ecdsa-sha2-nistp256') {
throw new Error(`Unexpected signature type: ${sigType}`);
}

offset += 4;
const rLen = data.readUInt32BE(offset);
offset += 4;
let r = data.subarray(offset, offset + rLen);
offset += rLen;

const sLen = data.readUInt32BE(offset);
offset += 4;
let s = data.subarray(offset, offset + sLen);

// R and S are encoded using ASN.1 DER format, which may include a leading zero byte to avoid interpreting the value as negative
if (r.length > 32) {
r = Buffer.from(Uint8Array.prototype.slice.call(r, 1));
}

if (s.length > 32) {
s = Buffer.from(Uint8Array.prototype.slice.call(s, 1));
}

const maybeHighS = BigInt(`0x${s.toString('hex')}`);

// ECDSA signatures must have a low S value so they can be used as a nullifier. BB forces a value of 27 for v, so
// only one PublicKey can verify the signature (and not its negated counterpart) https://ethereum.stackexchange.com/a/55728
if (maybeHighS > secp256r1N / 2n + 1n) {
s = Buffer.from((secp256r1N - maybeHighS).toString(16), 'hex');
}

return new EcdsaSignature(r, s, Buffer.from([0]));
}

async createAuthWit(messageHash: Fr): Promise<AuthWitness> {
// Key type and curve name
const keyType = Buffer.from('ecdsa-sha2-nistp256');
const curveName = Buffer.from('nistp256');
const data = await signWithAgent(keyType, curveName, this.signingPublicKey, messageHash.toBuffer());
const signature = this.#parseECDSASignature(data);

return new AuthWitness(messageHash, [...signature.r, ...signature.s]);
}
}
5 changes: 5 additions & 0 deletions yarn-project/accounts/src/ecdsa/ssh_ecdsa_r/artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js';

import EcdsaRAccountContractJson from '../../../artifacts/EcdsaRAccount.json' assert { type: 'json' };

export const EcdsaRAccountContractArtifact = loadContractArtifact(EcdsaRAccountContractJson as NoirCompiledContract);
Loading

0 comments on commit 811d62f

Please sign in to comment.