Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/adr/001-dynamic-import-aztec-bb-js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ADR-001: Dynamic import for @aztec/bb.js

## Status

Accepted

## Context

`@aztec/bb.js` is a heavy WASM-based cryptographic library used for ZK proof generation (UltraHonk/UltraPlonk backends). It is only needed when users perform specific actions: signing transactions, approving votes, or authenticating with ZK proofs.

If imported statically (`import { UltraHonkBackend } from '@aztec/bb.js'`), Next.js bundles the entire library into the initial JS chunk. This significantly increases the first load JS size for every page, even pages that never generate proofs.

## Decision

Use dynamic `import()` for `@aztec/bb.js` (and `@noir-lang/noir_js`) at the point of use inside proof generation hooks:

```typescript
// Instead of top-level static import
const { UltraHonkBackend } = await import("@aztec/bb.js");
const { Noir } = await import("@noir-lang/noir_js");
```

This applies to:
- `packages/nextjs/hooks/app/useGenerateProof.ts`
- `packages/nextjs/hooks/app/useAuthProof.ts`

## Consequences

- First load JS stays small — cryptographic libraries are only fetched when a user initiates a proof generation action
- Slight delay when generating the first proof in a session (library download + WASM initialization), but this is acceptable since proof generation itself already takes seconds
- Must use `await import()` pattern consistently — never add static imports for these packages in frontend code
57 changes: 57 additions & 0 deletions docs/adr/002-ultrahonk-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ADR-002: UltraPlonk to UltraHonk Migration

## Status

Accepted

## Context

UltraPlonk is deprecated from bbup v0.87.0+. zkVerify supports UltraHonk as replacement. Migration needed across the full stack: frontend proof generation, backend proof submission (Kurier API), and smart contract verification.

## Decision

New accounts (contractVersion >= 2) use UltraHonk. Old accounts (contractVersion 1) continue using UltraPlonk. The `contractVersion` field on the Account model drives all branching.

## Key Findings (not obvious from docs)

### 1. `{ keccak: true }` is mandatory

zkVerify only supports Keccak256 hash for UltraHonk. bb.js defaults to non-keccak. Must pass `{ keccak: true }` to both `generateProof` and `getVerificationKey`:

```typescript
const backend = new UltraHonkBackend(bytecode);
const { proof, publicInputs } = await backend.generateProof(witness, { keccak: true });
const vk = await backend.getVerificationKey({ keccak: true });
```

Without keccak, the VK size is 1825 bytes instead of expected 1760 bytes, and proofs won't verify.

### 2. Variant must be `'Plain'`, not `'ZK'`

bb.js `acirProveUltraKeccakHonk` generates **Plain** (non-ZK) proofs. There is no `--zk` option in the bb.js API. Using `variant: 'ZK'` causes proof verification to fail silently (`optimisticVerify: "failed"`).

### 3. Kurier API format differs completely from UltraPlonk

| Aspect | UltraPlonk | UltraHonk |
|--------|-----------|----------|
| VK encoding | base64 | hex `0x`-prefixed |
| Proof encoding | base64 (public inputs concatenated) | hex `0x`-prefixed |
| Public inputs | Concatenated into proof bytes | Separate `publicSignals` array |
| proofOptions (register-vk) | `{ numberOfPublicInputs }` | `{ variant: 'Plain' }` |
| proofOptions (submit-proof) | `{ numberOfPublicInputs }` | `{ variant: 'Plain' }` |

### 4. VK file naming includes proof type

VK files use the pattern `vkey-{circuitType}-{proofType}.json` to support both systems simultaneously:
- `vkey-transaction-ultraplonk.json` (old accounts)
- `vkey-transaction-ultrahonk.json` (new accounts)

### 5. Smart contract vkHash must match

The `vkHash` in `contracts-config.ts` is used when deploying new multisig contracts. It must match the vkHash from the UltraHonk VK registration on zkVerify. Old deployed contracts are unaffected (vkHash is immutable).

## Consequences

- Both proving systems coexist — no breaking changes for existing accounts
- `contractVersion` must be included in ALL API responses that return account data (including `user.service.ts getAccounts`)
- Future bb.js versions may add ZK support — at that point, switch variant from `'Plain'` to `'ZK'` for better privacy
4 changes: 2 additions & 2 deletions docs/zero-knowledge-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ In PolyPay:

## The Four Proofs

When you sign a transaction in PolyPay, the ZK circuit proves four things simultaneously. The circuit is written in [Noir](https://noir-lang.org), a domain-specific language for zero-knowledge proofs, and compiled to [UltraPlonk](https://rknhr-uec.github.io/aztec-protocol-spec/protocol-specs/cryptography/proving-system/overview) for efficient verification.
When you sign a transaction in PolyPay, the ZK circuit proves four things simultaneously. The circuit is written in [Noir](https://noir-lang.org), a domain-specific language for zero-knowledge proofs. New accounts use [UltraHonk](https://docs.zkverify.io/architecture/verification_pallets/ultrahonk) as the proving backend, while legacy accounts (contractVersion 1) continue using UltraPlonk.

### Proof 1: "I know the transaction"

Expand Down Expand Up @@ -108,5 +108,5 @@ This two-step verification ensures only authorized signers can sign transactions
## Learn More

- [Noir Language Documentation](https://noir-lang.org/docs)
- [UltraPlonk Proving System](https://rknhr-uec.github.io/aztec-protocol-spec/protocol-specs/cryptography/proving-system/overview)
- [UltraHonk Proving System](https://docs.zkverify.io/architecture/verification_pallets/ultrahonk)
- [zkVerify Documentation](https://docs.zkverify.io)
4 changes: 2 additions & 2 deletions docs/zkverify-horizen-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

PolyPay uses multiple blockchain layers for privacy-preserving multisig operations:

- **[zkVerify](https://docs.zkverify.io/)**: Verifies zero-knowledge proofs (ultraplonk) off-chain, providing proof verification and aggregation as a service
- **[zkVerify](https://docs.zkverify.io/)**: Verifies zero-knowledge proofs off-chain (UltraHonk for new accounts, UltraPlonk for legacy accounts), providing proof verification and aggregation as a service
- **[Horizen](https://www.horizen.io/)**: EVM-compatible L3 blockchain where multisig accounts (`MetaMultiSigWallet` contracts) are deployed and transactions are executed
- **[Base](https://base.org/)**: EVM-compatible L2 blockchain, also supported as a destination chain for account deployment and transaction execution

Expand Down Expand Up @@ -33,7 +33,7 @@ User proves ownership of their commitment without revealing the secret.

![Authentication Flow](.gitbook/assets/zkverify-horizen/authentication-flow.png)

- **zkVerify**: Verify ultraplonk proof, return `jobId` and `zkVerifyTxHash`
- **zkVerify**: Verify proof (UltraHonk or UltraPlonk based on account version), return `jobId` and `zkVerifyTxHash`
- **Destination chain**: No interaction

### 2. Account Creation (CREATE_ACCOUNT)
Expand Down
1 change: 1 addition & 0 deletions packages/backend/assets/vkey-auth-ultrahonk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"vkHash":"0x7c3f7708bd192d01a1740da057d05e1ed48bd8733e54bda264a151449a66996d"}
1 change: 1 addition & 0 deletions packages/backend/assets/vkey-transaction-ultrahonk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"vkHash":"0xb3c5381523a496996868370791ec7ae490be7e2c996296fb67708daed8a6ea38"}
15 changes: 11 additions & 4 deletions packages/backend/src/account/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CreateAccountDto,
CreateAccountBatchDto,
UpdateAccountDto,
ULTRAHONK_CONTRACT_VERSION,
} from '@polypay/shared';
import { RelayerService } from '@/relayer-wallet/relayer-wallet.service';
import { EventsService } from '@/events/events.service';
Expand Down Expand Up @@ -86,6 +87,7 @@ export class AccountService {
name: dto.name,
threshold: dto.threshold,
chainId: dto.chainId,
contractVersion: ULTRAHONK_CONTRACT_VERSION,
},
});

Expand Down Expand Up @@ -235,6 +237,7 @@ export class AccountService {
name: dto.name,
threshold: dto.threshold,
chainId: deployment.chainId,
contractVersion: ULTRAHONK_CONTRACT_VERSION,
},
});

Expand Down Expand Up @@ -299,7 +302,7 @@ export class AccountService {
}

return createdAccounts.map((account) =>
this.formatAccountResponse(account),
AccountService.formatAccountResponse(account),
);
}

Expand All @@ -322,7 +325,7 @@ export class AccountService {
throw new NotFoundException('Account not found');
}

return this.formatAccountResponse(account);
return AccountService.formatAccountResponse(account);
}

/**
Expand All @@ -340,15 +343,18 @@ export class AccountService {
orderBy: { createdAt: 'desc' },
});

return accounts.map((account) => this.formatAccountResponse(account));
return accounts.map((account) =>
AccountService.formatAccountResponse(account),
);
}

private formatAccountResponse(account: {
static formatAccountResponse(account: {
id: string;
address: string;
name: string | null;
threshold: number;
chainId: number;
contractVersion: number;
createdAt: Date;
signers: Array<{
isCreator: boolean;
Expand All @@ -362,6 +368,7 @@ export class AccountService {
name: account.name,
threshold: account.threshold,
chainId: account.chainId,
contractVersion: account.contractVersion,
createdAt: account.createdAt,
signers: account.signers.map((as) => ({
commitment: as.user.commitment,
Expand Down
8 changes: 7 additions & 1 deletion packages/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Injectable, UnauthorizedException, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ZkVerifyService } from '@/zkverify/zkverify.service';
import { LoginDto, RefreshDto } from '@polypay/shared';
import {
LoginDto,
RefreshDto,
ULTRAHONK_CONTRACT_VERSION,
} from '@polypay/shared';
import { PrismaService } from '@/database/prisma.service';
import { ConfigService } from '@nestjs/config';
import { CONFIG_KEYS } from '@/config/config.keys';
Expand Down Expand Up @@ -37,6 +41,8 @@ export class AuthService {
vk: dto.vk,
},
'auth',
undefined,
ULTRAHONK_CONTRACT_VERSION,
);
} catch (error) {
this.logger.error(`Proof verification failed: ${error.message}`);
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/transaction/transaction.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export class TransactionService {
},
'transaction',
account.chainId,
account.contractVersion,
);

// 5. Create transaction + first vote + delete reservation
Expand Down Expand Up @@ -268,6 +269,7 @@ export class TransactionService {
},
'transaction',
transaction.account.chainId,
transaction.account.contractVersion,
);

const voterName = await this.getSignerDisplayName(
Expand Down
17 changes: 4 additions & 13 deletions packages/backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@nestjs/common';
import { PrismaService } from '@/database/prisma.service';
import { CreateUserDto } from '@polypay/shared';
import { AccountService } from '@/account/account.service';

@Injectable()
export class UserService {
Expand Down Expand Up @@ -75,19 +76,9 @@ export class UserService {
},
});

return accounts.map((account) => ({
id: account.id,
address: account.address,
name: account.name,
threshold: account.threshold,
chainId: account.chainId,
createdAt: account.createdAt,
signers: account.signers.map((signer) => ({
commitment: signer.user.commitment,
name: signer.displayName,
isCreator: signer.isCreator,
})),
}));
return accounts.map((account) =>
AccountService.formatAccountResponse(account),
);
}

/**
Expand Down
Loading
Loading