Skip to content

Commit dcac36a

Browse files
authored
feat: ERC-4337-compatible passkey (P-256) account system (#2443)
* feat: add passkey signing * feat: fix the evm e2e test to have passkey signing * fix: make taht run on e2e * fix: fix lint * fix: address comments by gemini
1 parent bcab722 commit dcac36a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+9294
-25
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,8 @@ playwright/chrome-extensions/keplr/
357357
playwright/yarn.lock
358358

359359
debug_container.dot
360+
361+
.gocache
362+
AGENTS.md
363+
precompile.test
364+
tx_log.json

CLAUDE.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Nibiru Chain is a breakthrough L1 blockchain and smart contract ecosystem providing superior throughput, reduced latency, and improved security. It operates two virtual machines in parallel:
8+
- **NibiruEVM**: Ethereum Virtual Machine for Ethereum compatibility
9+
- **Wasm**: For CosmWasm smart contracts
10+
11+
## Repository Structure
12+
13+
### Core Directories
14+
- `/x/` - Custom blockchain modules (oracle, evm, devgas, epochs, inflation, tokenfactory, sudo)
15+
- `/app/` - Core blockchain application logic and module integration
16+
- `/eth/` - Ethereum-specific implementations (accounts, crypto, RPC)
17+
- `/proto/` - Protocol Buffer definitions for blockchain messages
18+
- `/cmd/nibid/` - CLI binary for interacting with the blockchain
19+
- `/evm-core-ts/` - TypeScript library for EVM interactions
20+
- `/gosdk/` - Go SDK for programmatic blockchain interactions
21+
22+
### Key Modules
23+
- **evm**: Ethereum Virtual Machine implementation
24+
- **oracle**: Decentralized price oracle system
25+
- **devgas**: Revenue sharing for smart contract developers
26+
- **epochs**: Time-based hook system for periodic tasks
27+
- **inflation**: Tokenomics implementation
28+
- **tokenfactory**: Token creation and management
29+
30+
## Essential Commands
31+
32+
### Build & Install
33+
```bash
34+
# Install the nibid binary
35+
make install
36+
37+
# Build the project
38+
make build
39+
40+
# Alternative with just
41+
just build
42+
just install
43+
```
44+
45+
### Local Development
46+
```bash
47+
# Run a local blockchain node
48+
make localnet
49+
50+
# Alternative single-node testnet
51+
just init-local-testnet [moniker]
52+
just start
53+
```
54+
55+
### Testing
56+
```bash
57+
# Run all unit tests
58+
make test-unit
59+
60+
# Run specific package tests
61+
go test ./x/evm/...
62+
63+
# Run with coverage
64+
make test-coverage
65+
66+
# Run integration tests
67+
make test-coverage-integration
68+
69+
# Run EVM E2E tests
70+
cd evm-e2e && npm test
71+
```
72+
73+
### Code Quality
74+
```bash
75+
# Format code
76+
make format
77+
78+
# Run linter
79+
make lint
80+
81+
# Fix linting issues
82+
make lint-fix
83+
84+
# Generate protobuf code
85+
make proto-gen
86+
```
87+
88+
### Development Utilities
89+
```bash
90+
# Install development tools
91+
make tools-clean
92+
93+
# Run simulation tests
94+
make sim-full
95+
96+
# Generate documentation
97+
make docs-generate
98+
99+
# Check for breaking changes
100+
make proto-break-check
101+
```
102+
103+
## System Requirements
104+
105+
- Go version 1.22.2 or higher
106+
- Node.js 20+ and npm 10+ (for TypeScript/EVM tests)
107+
- Make and Git
108+
- For validators: 8-core x64 CPU, 64GB RAM, 1TB+ SSD
109+
110+
## Development Workflow
111+
112+
1. **Setup Environment**
113+
```bash
114+
git clone https://github.com/NibiruChain/nibiru
115+
cd nibiru
116+
make install
117+
```
118+
119+
2. **Run Local Network**
120+
```bash
121+
make localnet
122+
# or
123+
just init-local-testnet test-moniker
124+
just start
125+
```
126+
127+
3. **Make Changes**
128+
- Core logic: `/x/` modules
129+
- Application: `/app/`
130+
- Tests: `*_test.go` files
131+
132+
4. **Test Changes**
133+
```bash
134+
# Test specific module
135+
go test ./x/oracle/...
136+
137+
# Run integration tests
138+
make test-coverage-integration
139+
```
140+
141+
5. **Lint & Format**
142+
```bash
143+
make lint-fix
144+
make format
145+
```
146+
147+
## Technical Details
148+
149+
### Build System
150+
- Primary: Makefile with includes from `/contrib/make/`
151+
- Alternative: justfile for common tasks
152+
- Protocol Buffers: buf for proto compilation
153+
154+
### Dependencies
155+
- Cosmos SDK v0.47.11
156+
- CometBFT/Tendermint consensus
157+
- go-ethereum fork for EVM compatibility
158+
- CosmWasm for WASM smart contracts
159+
160+
### Testing Framework
161+
- Go: Built-in testing with testify assertions
162+
- TypeScript: Jest for EVM E2E tests
163+
- Simulation: Custom simulation framework
164+
165+
## Troubleshooting
166+
167+
### Common Issues
168+
1. **Import errors**: Run `go mod tidy` and `go mod download`
169+
2. **Proto errors**: Run `make proto-gen`
170+
3. **Build failures**: Ensure Go 1.22.2+ is installed
171+
4. **Test failures**: Check if local node is running for integration tests
172+
173+
### Local Network Ports
174+
- RPC: http://localhost:26657
175+
- gRPC: localhost:9090
176+
- EVM RPC: http://localhost:8545
177+
- API: http://localhost:1317
178+
179+
## Documentation Resources
180+
181+
- Main docs: https://docs.nibiru.fi
182+
- Contracts: https://github.com/NibiruChain/contracts
183+
- TypeScript SDK: https://www.npmjs.com/package/@nibiruchain/ts-sdk
184+
- Oracle docs: See `/x/oracle/README.md`
185+
186+
## Community
187+
188+
- Discord: https://discord.gg/nibiru
189+
- Twitter: https://twitter.com/NibiruChain
190+
- Telegram: https://t.me/nibiruchainofficial

evm-e2e/.entrypoint.addr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0x31cB3eE2aC481dbF1fc85a3Ab1Aad925Af4b3b5a

evm-e2e/.passkeyfactory.addr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0xF4434B66C39dA329a7230Faf919Ea65d3aCeB0AA

evm-e2e/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Localnet has JSON RPC enabled by default.
2121

2222
```bash
2323
npm install
24+
(cd passkey-sdk && npm install)
2425
```
2526

2627
### Configure environment in `.env` file
@@ -58,3 +59,40 @@ test/contract_send_nibi.test.ts:
5859
Ran 6 tests across 4 files. [38.08s]
5960

6061
```
62+
63+
### Deploy PasskeyAccountFactory on Nibiru localnet
64+
65+
Assumes:
66+
- Nibiru localnet running with the P-256 precompile at `0x0000000000000000000000000000000000000100`.
67+
- An ERC-4337 EntryPoint already deployed; pass its address via `ENTRY_POINT`.
68+
69+
```bash
70+
# Optional: provide QX/QY (0x-prefixed 32-byte coords) to create the first account.
71+
ENTRY_POINT=0xYourEntryPoint \
72+
QX=0x... \
73+
QY=0x... \
74+
npx hardhat run scripts/deploy-passkey.js --network localhost
75+
```
76+
77+
### Run an ERC-4337 bundler against Nibiru RPC
78+
79+
Uses a Stackup-style Docker image with a temp config. Required env: `RPC_URL`, `ENTRY_POINT`, `CHAIN_ID`. Optional:
80+
`PRIVATE_KEY` (bundler signer), `BUNDLER_PORT` (default 4337), `BUNDLER_IMAGE` (default
81+
`ghcr.io/stackup-wallet/stackup-bundler:latest`).
82+
83+
```bash
84+
RPC_URL=http://127.0.0.1:8545 \
85+
ENTRY_POINT=0x... \
86+
CHAIN_ID=12345 \
87+
npm run bundler
88+
```
89+
90+
### Passkey ERC-4337 test coverage
91+
92+
The `bun test` suite now exercises the passkey ERC-4337 flow end-to-end. During the run it:
93+
94+
- Builds the `passkey-sdk`, deploys a fresh `EntryPointV06` + `PasskeyAccountFactory`, and funds the dev bundler key.
95+
- Starts the local bundler from `passkey-sdk/dist/local-bundler.js` on port `14437`.
96+
- Executes the CLI passkey script against that bundler to prove a full user operation.
97+
98+
Ensure `node`, `npm`, and `tsup` dependencies are installed (`npm install` in both `evm-e2e/` and `evm-e2e/passkey-sdk/`) and that port `14437` is free before running `bun test` or `just test-e2e`.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.24;
3+
4+
import "@account-abstraction/contracts/core/EntryPoint.sol";
5+
6+
/// @notice Thin wrapper that tags EntryPoint v0.6 with a version string for bundlers that require it.
7+
contract EntryPointV06 is EntryPoint {
8+
function entryPointVersion() external pure returns (string memory) {
9+
return "0.6.0";
10+
}
11+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
5+
import {UserOperation, SIG_VALIDATION_FAILED} from "./UserOperation.sol";
6+
import {IEntryPoint} from "./interfaces/IEntryPoint.sol";
7+
8+
/// @notice Minimal ERC-4337-style account secured by a P-256 pubkey (raw r,s signatures).
9+
/// @dev Uses Nibiru RIP-7212 precompile at 0x...0100. Signature format: abi.encode(r,s).
10+
contract PasskeyAccount {
11+
IEntryPoint public entryPoint;
12+
bytes32 public qx;
13+
bytes32 public qy;
14+
uint256 public nonce;
15+
bool private initialized;
16+
17+
event Executed(address indexed target, uint256 value, bytes data);
18+
19+
modifier onlyEntryPoint() {
20+
require(msg.sender == address(entryPoint), "not entrypoint");
21+
_;
22+
}
23+
24+
function initialize(address _entryPoint, bytes32 _qx, bytes32 _qy) external {
25+
require(!initialized, "initialized");
26+
require(_entryPoint != address(0), "entrypoint=0");
27+
initialized = true;
28+
entryPoint = IEntryPoint(_entryPoint);
29+
qx = _qx;
30+
qy = _qy;
31+
}
32+
33+
receive() external payable {}
34+
35+
function validateUserOp(
36+
UserOperation calldata userOp,
37+
bytes32 userOpHash,
38+
uint256 missingAccountFunds
39+
) external onlyEntryPoint returns (uint256 validationData) {
40+
if (userOp.nonce != nonce) return SIG_VALIDATION_FAILED;
41+
uint256 verified = _verify(userOpHash, userOp.signature);
42+
if (verified != 1) return SIG_VALIDATION_FAILED;
43+
nonce++;
44+
if (missingAccountFunds > 0) {
45+
entryPoint.depositTo{value: missingAccountFunds}(address(this));
46+
}
47+
return 0;
48+
}
49+
50+
function execute(address to, uint256 value, bytes calldata data) external onlyEntryPoint {
51+
(bool ok, ) = to.call{value: value}(data);
52+
require(ok, "exec failed");
53+
emit Executed(to, value, data);
54+
}
55+
56+
function _verify(bytes32 userOpHash, bytes calldata signature) internal view returns (uint256) {
57+
bytes32 digest;
58+
bytes32 r;
59+
bytes32 s;
60+
61+
// For Node/EVM tests we accept compact r,s signatures over userOpHash.
62+
if (signature.length == 64) {
63+
(r, s) = abi.decode(signature, (bytes32, bytes32));
64+
digest = sha256(abi.encodePacked(userOpHash));
65+
} else {
66+
// WebAuthn signatures are over sha256(authenticatorData || sha256(clientDataJSON)).
67+
// Signature payload layout: abi.encode(authenticatorData, clientDataJSON, r, s)
68+
(bytes memory authData, bytes memory clientDataJSON, bytes32 rFull, bytes32 sFull) =
69+
abi.decode(signature, (bytes, bytes, bytes32, bytes32));
70+
r = rFull;
71+
s = sFull;
72+
digest = sha256(abi.encodePacked(authData, sha256(clientDataJSON)));
73+
}
74+
75+
bytes memory input = abi.encodePacked(digest, r, s, qx, qy);
76+
(bool ok, bytes memory out) = address(0x100).staticcall(input);
77+
if (!ok || out.length != 32) {
78+
return 0;
79+
}
80+
return uint256(bytes32(out));
81+
}
82+
}
83+
84+
/// @notice Factory deploying cheap PasskeyAccount minimal-proxy clones.
85+
contract PasskeyAccountFactory {
86+
address public immutable IMPLEMENTATION;
87+
address public immutable ENTRY_POINT;
88+
89+
event AccountCreated(address indexed account, bytes32 qx, bytes32 qy, bytes32 salt);
90+
91+
constructor(address _entryPoint) {
92+
ENTRY_POINT = _entryPoint;
93+
IMPLEMENTATION = address(new PasskeyAccount());
94+
}
95+
96+
function createAccount(bytes32 _qx, bytes32 _qy) external returns (address account) {
97+
bytes32 salt = _salt(_qx, _qy);
98+
account = Clones.cloneDeterministic(IMPLEMENTATION, salt);
99+
PasskeyAccount(payable(account)).initialize(ENTRY_POINT, _qx, _qy);
100+
emit AccountCreated(account, _qx, _qy, salt);
101+
}
102+
103+
function accountAddress(bytes32 _qx, bytes32 _qy) external view returns (address predicted) {
104+
bytes32 salt = _salt(_qx, _qy);
105+
predicted = Clones.predictDeterministicAddress(IMPLEMENTATION, salt, address(this));
106+
}
107+
108+
function _salt(bytes32 _qx, bytes32 _qy) internal pure returns (bytes32) {
109+
return keccak256(abi.encodePacked(_qx, _qy));
110+
}
111+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
struct UserOperation {
5+
address sender;
6+
uint256 nonce;
7+
bytes initCode;
8+
bytes callData;
9+
uint256 callGasLimit;
10+
uint256 verificationGasLimit;
11+
uint256 preVerificationGas;
12+
uint256 maxFeePerGas;
13+
uint256 maxPriorityFeePerGas;
14+
bytes paymasterAndData;
15+
bytes signature;
16+
}
17+
18+
uint256 constant SIG_VALIDATION_FAILED = 1;

0 commit comments

Comments
 (0)