Skip to content

Commit

Permalink
feat: initial is_valid eip1271 style wallet + minimal test changes
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Sep 1, 2023
1 parent 3fe5508 commit 7b20d0e
Show file tree
Hide file tree
Showing 23 changed files with 629 additions and 67 deletions.
1 change: 1 addition & 0 deletions yarn-project/acir-simulator/src/acvm/acvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const ONE_ACVM_FIELD: ACVMField = `0x${'00'.repeat(Fr.SIZE_IN_BYTES - 1)}
*/
type ORACLE_NAMES =
| 'packArguments'
| 'getEip1271Witness'
| 'getSecretKey'
| 'getNote'
| 'getNotes'
Expand Down
7 changes: 7 additions & 0 deletions yarn-project/acir-simulator/src/client/db_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ export interface DBOracle extends CommitmentsDB {
*/
getCompleteAddress(address: AztecAddress): Promise<CompleteAddress>;

/**
* Retrieve the eip-1271 witness for a given message hash.
* @param message_hash - The message hash.
* @returns A Promise that resolves to an array of field elements representing the eip-1271 witness.
*/
getEip1271Witness(message_hash: Fr): Promise<Fr[]>;

/**
* Retrieve the secret key associated with a specific public key.
* The function only allows access to the secret keys of the transaction creator,
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/acir-simulator/src/client/private_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export class PrivateFunctionExecution {
packArguments: async args => {
return toACVMField(await this.context.packedArgsCache.pack(args.map(fromACVMField)));
},
getEip1271Witness: async ([messageHash]) => {
return (await this.context.db.getEip1271Witness(fromACVMField(messageHash))).map(toACVMField);
},
getSecretKey: ([ownerX], [ownerY]) => this.context.getSecretKey(this.contractAddress, ownerX, ownerY),
getPublicKey: async ([acvmAddress]) => {
const address = frToAztecAddress(fromACVMField(acvmAddress));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export class AztecRPCServer implements AztecRPC {
this.clientInfo = `${name.split('/')[name.split('/').length - 1]}@${version}`;
}

public async addEip1271Witness(messageHash: Fr, witness: Fr[]) {
await this.db.addEip1271Witness(messageHash, witness);
return Promise.resolve();
}

/**
* Starts the Aztec RPC server by beginning the synchronisation process between the Aztec node and the database.
*
Expand Down
14 changes: 14 additions & 0 deletions yarn-project/aztec-rpc/src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import { NoteSpendingInfoDao } from './note_spending_info_dao.js';
* addresses, storage slots, and nullifiers.
*/
export interface Database extends ContractDatabase {
/**
* Add a eip1271 witness to the database.
* @param messageHash - The message hash.
* @param witness - An array of field elements representing the eip1271 witness.
*/
addEip1271Witness(messageHash: Fr, witness: Fr[]): Promise<void>;

/**
* Fetching the eip1271 witness for a given message hash.
* @param messageHash - The message hash.
* @returns A Promise that resolves to an array of field elements representing the eip1271 witness.
*/
getEip1271Witness(messageHash: Fr): Promise<Fr[]>;

/**
* Get auxiliary transaction data based on contract address and storage slot.
* It searches for matching NoteSpendingInfoDao objects in the MemoryDB's noteSpendingInfoTable
Expand Down
20 changes: 20 additions & 0 deletions yarn-project/aztec-rpc/src/database/memory_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,31 @@ export class MemoryDB extends MemoryContractDatabase implements Database {
private treeRoots: Record<MerkleTreeId, Fr> | undefined;
private globalVariablesHash: Fr | undefined;
private addresses: CompleteAddress[] = [];
private eip1271Witnesses: Record<string, Fr[]> = {};

constructor(logSuffix?: string) {
super(createDebugLogger(logSuffix ? 'aztec:memory_db_' + logSuffix : 'aztec:memory_db'));
}

/**
* Add a eip1271 witness to the database.
* @param messageHash - The message hash.
* @param witness - An array of field elements representing the eip1271 witness.
*/
public addEip1271Witness(messageHash: Fr, witness: Fr[]): Promise<void> {
this.eip1271Witnesses[messageHash.toString()] = witness;
return Promise.resolve();
}

/**
* Fetching the eip1271 witness for a given message hash.
* @param messageHash - The message hash.
* @returns A Promise that resolves to an array of field elements representing the eip1271 witness.
*/
public getEip1271Witness(messageHash: Fr): Promise<Fr[]> {
return Promise.resolve(this.eip1271Witnesses[messageHash.toString()]);
}

public addNoteSpendingInfo(noteSpendingInfoDao: NoteSpendingInfoDao) {
this.noteSpendingInfoTable.push(noteSpendingInfoDao);
return Promise.resolve();
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/aztec-rpc/src/simulator_oracle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export class SimulatorOracle implements DBOracle {
return completeAddress;
}

async getEip1271Witness(messageHash: Fr): Promise<Fr[]> {
const witness = await this.db.getEip1271Witness(messageHash);
if (!witness) throw new Error(`Unknown eip1271 witness for message hash ${messageHash.toString()}`);
return witness;
}

async getNotes(contractAddress: AztecAddress, storageSlot: Fr) {
const noteDaos = await this.db.getNoteSpendingInfo(contractAddress, storageSlot);
return noteDaos.map(({ contractAddress, storageSlot, nonce, notePreimage, siloedNullifier, index }) => ({
Expand Down
120 changes: 120 additions & 0 deletions yarn-project/aztec.js/src/abis/schnorr_eip_1271_account_contract.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Schnorr } from '@aztec/circuits.js/barretenberg';
import { ContractAbi } from '@aztec/foundation/abi';
import { CompleteAddress, NodeInfo, PrivateKey } from '@aztec/types';

import Eip1271AccountContractAbi from '../../abis/schnorr_eip_1271_account_contract.json' assert { type: 'json' };
import { Eip1271AccountEntrypoint } from '../entrypoint/eip_1271_account_entrypoint.js';
import { AccountContract } from './index.js';

/**
* Account contract that authenticates transactions using Schnorr signatures verified against
* the note encryption key, relying on a single private key for both encryption and authentication.
*/
export class Eip1271AccountContract implements AccountContract {
constructor(private encryptionPrivateKey: PrivateKey) {}

public getDeploymentArgs() {
return Promise.resolve([]);
}

public async getEntrypoint({ address, partialAddress }: CompleteAddress, { chainId, version }: NodeInfo) {
return new Eip1271AccountEntrypoint(
address,
partialAddress,
this.encryptionPrivateKey,
await Schnorr.new(),
chainId,
version,
);
}

public getContractAbi(): ContractAbi {
return Eip1271AccountContractAbi as unknown as ContractAbi;
}
}
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/account/contract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Entrypoint } from '../index.js';
export * from './ecdsa_account_contract.js';
export * from './schnorr_account_contract.js';
export * from './single_key_account_contract.js';
export * from './eip_1271_account_contract.js';

// docs:start:account-contract-interface
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { AztecAddress, Fr, FunctionData, PartialAddress, PrivateKey, TxContext } from '@aztec/circuits.js';
import { Signer } from '@aztec/circuits.js/barretenberg';
import { ContractAbi, encodeArguments } from '@aztec/foundation/abi';
import { FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/types';

import SchnorrSingleKeyAccountContractAbi from '../../abis/schnorr_single_key_account_contract.json' assert { type: 'json' };
import { generatePublicKey } from '../../index.js';
import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from '../../utils/defaults.js';
import { buildPayload, hashPayload } from './entrypoint_payload.js';
import { CreateTxRequestOpts, Entrypoint } from './index.js';

/**
* Account contract implementation that uses a single key for signing and encryption. This public key is not
* stored in the contract, but rather verified against the contract address. Note that this approach is not
* secure and should not be used in real use cases.
*/
export class Eip1271AccountEntrypoint implements Entrypoint {
constructor(
private address: AztecAddress,
private partialAddress: PartialAddress,
private privateKey: PrivateKey,
private signer: Signer,
private chainId: number = DEFAULT_CHAIN_ID,
private version: number = DEFAULT_VERSION,
) {}

public sign(message: Buffer) {
return this.signer.constructSignature(message, this.privateKey).toBuffer();
}

async generateEip1271Witness(message: Buffer) {
const signature = this.sign(message);
const publicKey = await generatePublicKey(this.privateKey);

const publicKeyBytes = publicKey.toBuffer();
const sigFr: Fr[] = [];
const pubKeyFr: Fr[] = [];
for (let i = 0; i < 64; i++) {
pubKeyFr.push(new Fr(publicKeyBytes[i]));
sigFr.push(new Fr(signature[i]));
}

return [...pubKeyFr, ...sigFr, this.partialAddress];
}

async createTxExecutionRequest(
executions: FunctionCall[],
opts: CreateTxRequestOpts = {},
): Promise<TxExecutionRequest> {
if (opts.origin && !opts.origin.equals(this.address)) {
throw new Error(`Sender ${opts.origin.toString()} does not match account address ${this.address.toString()}`);
}

const { payload, packedArguments: callsPackedArguments } = await buildPayload(executions);
const message = await hashPayload(payload);

const signature = this.sign(message);

const publicKey = await generatePublicKey(this.privateKey);
const args = [payload, publicKey.toBuffer(), signature, this.partialAddress];
const abi = this.getEntrypointAbi();
const packedArgs = await PackedArguments.fromArgs(encodeArguments(abi, args));
const txRequest = TxExecutionRequest.from({
argsHash: packedArgs.hash,
origin: this.address,
functionData: FunctionData.fromAbi(abi),
txContext: TxContext.empty(this.chainId, this.version),
packedArguments: [...callsPackedArguments, packedArgs],
});

return txRequest;
}

private getEntrypointAbi() {
// We use the SchnorrSingleKeyAccountContract because it implements the interface we need, but ideally
// we should have an interface that defines the entrypoint for SingleKeyAccountContracts and
// load the abi from it.
const abi = (SchnorrSingleKeyAccountContractAbi as any as ContractAbi).functions.find(f => f.name === 'entrypoint');
if (!abi) throw new Error(`Entrypoint abi for account contract not found`);
return abi;
}
}
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/account/entrypoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './entrypoint_payload.js';
export * from './entrypoint_utils.js';
export * from './single_key_account_entrypoint.js';
export * from './stored_key_account_entrypoint.js';
export * from './eip_1271_account_entrypoint.js';

/** Options for creating a tx request out of a set of function calls. */
export type CreateTxRequestOpts = {
Expand Down
31 changes: 30 additions & 1 deletion yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
TxReceipt,
} from '@aztec/types';

import { CreateTxRequestOpts, Entrypoint } from '../account/entrypoint/index.js';
import { CreateTxRequestOpts, Eip1271AccountEntrypoint, Entrypoint } from '../account/entrypoint/index.js';
import { CompleteAddress } from '../index.js';

/**
Expand Down Expand Up @@ -94,6 +94,9 @@ export abstract class BaseWallet implements Wallet {
getSyncStatus(): Promise<SyncStatus> {
return this.rpc.getSyncStatus();
}
addEip1271Witness(messageHash: Fr, witness: Fr[]) {
return this.rpc.addEip1271Witness(messageHash, witness);
}
}

/**
Expand All @@ -108,6 +111,32 @@ export class EntrypointWallet extends BaseWallet {
}
}

/**
* A wallet implementation supporting EIP1271.
*/
export class EipEntrypointWallet extends BaseWallet {
constructor(rpc: AztecRPC, protected accountImpl: Eip1271AccountEntrypoint) {
super(rpc);
}
createTxExecutionRequest(executions: FunctionCall[], opts: CreateTxRequestOpts = {}): Promise<TxExecutionRequest> {
// We could even use it in here :thinking:. We just need a nonce really. If we have the nonce we should be all good.
return this.accountImpl.createTxExecutionRequest(executions, opts);
}
sign(messageHash: Buffer): Promise<Buffer> {
return Promise.resolve(this.accountImpl.sign(messageHash));
}
/**
* Signs the `messageHash` and adds the witness to the RPC.
* @param messageHash - The message hash to sign
* @returns
*/
async signAndAddEip1271Witness(messageHash: Buffer): Promise<void> {
const witness = await this.accountImpl.generateEip1271Witness(messageHash);
await this.rpc.addEip1271Witness(Fr.fromBuffer(messageHash), witness);
return Promise.resolve();
}
}

/**
* A wallet implementation that forwards authentication requests to a provided account.
*/
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/end-to-end/src/e2e_account_contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Account,
AccountContract,
EcdsaAccountContract,
Eip1271AccountContract,
Fr,
SchnorrAccountContract,
SingleKeyAccountContract,
Expand Down Expand Up @@ -80,4 +81,7 @@ describe('e2e_account_contracts', () => {
describe('ecdsa stored-key account', () => {
itShouldBehaveLikeAnAccountContract(() => new EcdsaAccountContract(PrivateKey.random()));
});
describe('ecdsa stored-key account', () => {
itShouldBehaveLikeAnAccountContract(() => new Eip1271AccountContract(PrivateKey.random()));
});
});
Loading

0 comments on commit 7b20d0e

Please sign in to comment.