Skip to content

Commit

Permalink
initial: lookup the validity of authwits
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Mar 19, 2024
1 parent 09b2b7c commit d734cfc
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 16 deletions.
9 changes: 8 additions & 1 deletion noir-projects/aztec-nr/aztec/src/hash.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use dep::protocol_types::{constants::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET, hash::pedersen_hash};
use dep::protocol_types::{
address::AztecAddress,
constants::GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET,
hash::{pedersen_hash, silo_nullifier}};

pub fn compute_secret_hash(secret: Field) -> Field {
// TODO(#1205) This is probably not the right index to use
pedersen_hash([secret], GENERATOR_INDEX__L1_TO_L2_MESSAGE_SECRET)
}

pub fn compute_siloed_nullifier(address: AztecAddress, nullifier: Field) -> Field {
silo_nullifier(address, nullifier)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ contract SchnorrAccount {
use dep::std;

use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, PrivateContext, PrivateImmutable};

use dep::aztec::state_vars::{Map, PublicMutable};
use dep::aztec::{context::Context, oracle::get_public_key::get_public_key};
use dep::authwit::{
entrypoint::{app::AppPayload, fee::FeePayload}, account::AccountActions,
auth_witness::get_auth_witness
};
use dep::aztec::hash::compute_siloed_nullifier;
use dep::aztec::oracle::get_nullifier_membership_witness::get_low_nullifier_membership_witness;

use crate::public_key_note::{PublicKeyNote, PUBLIC_KEY_NOTE_LEN};

Expand Down Expand Up @@ -92,4 +94,55 @@ contract SchnorrAccount {
// docs:end:entrypoint
true
}

/**
* @notice Helper function to check the existing and validity of authwitnesses
* @dev TODO: myself and block_number should be removed and passed from a context
* @param myself The address of the contract
* @param block_number The block number to check the nullifier against
* @param message_hash The message hash of the message to check the validity
* @return An array of two booleans, the first is the validity of the authwitness in the private state,
* the second is the validity of the authwitness in the public state
* Both values will be `false` if the nullifier is spent
*/
unconstrained fn lookup_validity(myself: AztecAddress, block_number: u32, check_private: bool, message_hash: Field) -> pub [bool; 2] {
let storage = Storage::init(Context::none());

let valid_in_private = if check_private {
let public_key = storage.signing_public_key.view_note();
let witness: [Field; 64] = get_auth_witness(message_hash);
let mut signature: [u8; 64] = [0; 64];
for i in 0..64 {
signature[i] = witness[i] as u8;
}
std::schnorr::verify_signature(
public_key.x,
public_key.y,
signature,
message_hash.to_be_bytes(32)
)
} else {
false
};

let public_state: Map<Field, PublicMutable<bool>> = Map::new(
Context::none(),
ACCOUNT_ACTIONS_STORAGE_SLOT,
|context, slot| { PublicMutable::new(context, slot)}
);
let valid_in_public = public_state.at(message_hash).read();

// Compute the nullifier and check if it is spent
// This will BLINDLY TRUST the oracle, but the oracle is us, and
// it is not as part of execution of the contract, so we are good.
let siloed_nullifier = compute_siloed_nullifier(myself, message_hash);
let lower_wit = get_low_nullifier_membership_witness(block_number, siloed_nullifier);
let is_spent = lower_wit.leaf_preimage.nullifier == siloed_nullifier;

if is_spent {
[false, false]
} else {
[valid_in_private, valid_in_public]
}
}
}
66 changes: 52 additions & 14 deletions yarn-project/aztec.js/src/wallet/account_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ export class AccountWallet extends BaseWallet {
}

/**
* Returns a function interaction to set a message hash as authorized or revoked in this account.
* Public calls can then consume this authorization.
* @param messageHashOrIntent - The message or the caller and action to authorize/revoke
* @param authorized - True to authorize, false to revoke authorization.
* @returns - A function interaction.
* Lookup the validity of an authwit in private and public contexts.
* If the authwit have been consumed already (nullifier spent), will return false in both contexts.
* @param target - The target contract address
* @param messageHashOrIntent - The message hash or the caller and action to authorize/revoke
* @returns - A struct containing the validity of the authwit in private and public contexts.
*/
public setPublicAuthWit(
async lookupValidity(
target: AztecAddress,
messageHashOrIntent:
| Fr
| Buffer
Expand All @@ -86,14 +87,24 @@ export class AccountWallet extends BaseWallet {
/** The action to approve */
action: ContractFunctionInteraction | FunctionCall;
},
authorized: boolean,
): ContractFunctionInteraction {
const message = this.getMessageHash(messageHashOrIntent);
if (authorized) {
return new ContractFunctionInteraction(this, this.getAddress(), this.getApprovePublicAuthwitAbi(), [message]);
} else {
return this.cancelAuthWit(message);
}
): Promise<{
/** boolean flag indicating if the authwit is valid in private context */
isValidInPrivate: boolean;
/** boolean flag indicating if the authwit is valid in public context */
isValidInPublic: boolean;
}> {
const messageHash = this.getMessageHash(messageHashOrIntent);
const witness = await this.getAuthWitness(messageHash);
const blockNumber = await this.getBlockNumber();
const interaction = new ContractFunctionInteraction(this, target, this.getLookupValidityAbi(), [
target,
blockNumber,
witness != undefined,
messageHash,
]);

const [isValidInPrivate, isValidInPublic] = await interaction.view();
return { isValidInPrivate, isValidInPublic };
}

/**
Expand Down Expand Up @@ -160,4 +171,31 @@ export class AccountWallet extends BaseWallet {
returnTypes: [],
};
}

private getLookupValidityAbi(): FunctionAbi {
return {
name: 'lookup_validity',
isInitializer: false,
functionType: FunctionType.UNCONSTRAINED,
isInternal: false,
parameters: [
{
name: 'myself',
type: {
kind: 'struct',
path: 'authwit::aztec::protocol_types::address::aztec_address::AztecAddress',
fields: [{ name: 'inner', type: { kind: 'field' } }],
},
visibility: 'private' as ABIParameterVisibility,
},
{
name: 'block_number',
type: { kind: 'integer', sign: 'unsigned', width: 32 },
visibility: 'private' as ABIParameterVisibility,
},
{ name: 'message_hash', type: { kind: 'field' }, visibility: 'private' as ABIParameterVisibility },
],
returnTypes: [{ kind: 'array', length: 2, type: { kind: 'boolean' } }],
};
}
}
3 changes: 3 additions & 0 deletions yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export abstract class BaseWallet implements Wallet {
addAuthWitness(authWitness: AuthWitness) {
return this.pxe.addAuthWitness(authWitness);
}
getAuthWitness(messageHash: Fr) {
return this.pxe.getAuthWitness(messageHash);
}
isContractClassPubliclyRegistered(id: Fr): Promise<boolean> {
return this.pxe.isContractClassPubliclyRegistered(id);
}
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export interface PXE {
*/
addAuthWitness(authWitness: AuthWitness): Promise<void>;

// @todo @LHerskind
getAuthWitness(messageHash: Fr): Promise<Fr[] | undefined>;

/**
* Adding a capsule to the capsule dispenser.
* @param capsule - An array of field elements representing the capsule.
Expand Down
67 changes: 67 additions & 0 deletions yarn-project/end-to-end/src/e2e_authwit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,58 @@ describe('e2e_authwit_tests', () => {
const witness = await wallets[0].createAuthWit(outerHash);
await wallets[1].addAuthWitness(witness);

// Check that the authwit is valid in private for wallets[0]
expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: true,
isValidInPublic: false,
});

// Check that the authwit is NOT valid in private for wallets[1]
expect(await wallets[0].lookupValidity(wallets[1].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

const c = await SchnorrAccountContract.at(wallets[0].getAddress(), wallets[0]);
await c.withWallet(wallets[1]).methods.spend_private_authwit(innerHash).send().wait();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});
});

describe('failure case', () => {
it('cancel before usage', async () => {
const innerHash = computeInnerAuthWitHash([Fr.fromString('0xdead'), Fr.fromString('0xbeef')]);
const outerHash = computeOuterAuthWitHash(wallets[1].getAddress(), innerHash);

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

const witness = await wallets[0].createAuthWit(outerHash);
await wallets[1].addAuthWitness(witness);
expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: true,
isValidInPublic: false,
});
await wallets[0].cancelAuthWit(outerHash).send().wait();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

const c = await SchnorrAccountContract.at(wallets[0].getAddress(), wallets[0]);
const txCancelledAuthwit = c.withWallet(wallets[1]).methods.spend_private_authwit(innerHash).send();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

// The transaction should be dropped because of a cancelled authwit (duplicate nullifier)
await expect(txCancelledAuthwit.wait()).rejects.toThrow('Transaction ');
});
Expand All @@ -55,21 +92,51 @@ describe('e2e_authwit_tests', () => {
const innerHash = computeInnerAuthWitHash([Fr.fromString('0xdead'), Fr.fromString('0x01')]);
const outerHash = computeOuterAuthWitHash(wallets[1].getAddress(), innerHash);

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

await wallets[0].setPublicAuthWit(outerHash, true).send().wait();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: true,
});

const c = await SchnorrAccountContract.at(wallets[0].getAddress(), wallets[0]);
await c.withWallet(wallets[1]).methods.spend_public_authwit(innerHash).send().wait();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});
});

describe('failure case', () => {
it('cancel before usage', async () => {
const innerHash = computeInnerAuthWitHash([Fr.fromString('0xdead'), Fr.fromString('0x02')]);
const outerHash = computeOuterAuthWitHash(wallets[1].getAddress(), innerHash);

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

await wallets[0].setPublicAuthWit(outerHash, true).send().wait();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: true,
});

await wallets[0].cancelAuthWit(outerHash).send().wait();

expect(await wallets[0].lookupValidity(wallets[0].getAddress(), outerHash)).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

const c = await SchnorrAccountContract.at(wallets[0].getAddress(), wallets[0]);
const txCancelledAuthwit = c.withWallet(wallets[1]).methods.spend_public_authwit(innerHash).send();
// The transaction should be dropped because of a cancelled authwit (duplicate nullifier)
Expand Down
20 changes: 20 additions & 0 deletions yarn-project/end-to-end/src/e2e_token_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,23 @@ describe('e2e_token_contract', () => {
.withWallet(wallets[1])
.methods.transfer_public(accounts[0].address, accounts[1].address, amount, nonce);

expect(
await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }),
).toEqual({
isValidInPrivate: false,
isValidInPublic: false,
});

// We need to compute the message we want to sign and add it to the wallet as approved
await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait();

expect(
await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }),
).toEqual({
isValidInPrivate: false,
isValidInPublic: true,
});

// Perform the transfer
await expect(action.simulate()).rejects.toThrow(U128_UNDERFLOW_ERROR);

Expand Down Expand Up @@ -567,6 +581,12 @@ describe('e2e_token_contract', () => {

const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action });
await wallets[1].addAuthWitness(witness);
expect(
await wallets[0].lookupValidity(wallets[0].getAddress(), { caller: accounts[1].address, action }),
).toEqual({
isValidInPrivate: true,
isValidInPublic: false,
});
// docs:end:authwit_transfer_example

// Perform the transfer
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/pxe/src/pxe_service/pxe_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ export class PXEService implements PXE {
return this.db.addAuthWitness(witness.requestHash, witness.witness);
}

public getAuthWitness(messageHash: Fr): Promise<Fr[] | undefined> {
return this.db.getAuthWitness(messageHash);
}

public addCapsule(capsule: Fr[]) {
return this.db.addCapsule(capsule);
}
Expand Down

0 comments on commit d734cfc

Please sign in to comment.