Skip to content

Commit 22e0d96

Browse files
sirtimidclaude
andcommitted
refactor(evm-wallet-experiment): drop SES-lockdown workarounds (#938)
Closes #938. Now that vats can request `crypto`, `SubtleCrypto`, and `Math` via their `globals` allowlist (via #937), drop the workarounds that existed because `crypto.getRandomValues` and `Math.random` were unreachable inside vat compartments: - Drop `entropy?: Hex` from the throwaway `KeyringInitOptions` and its plumbing through both coordinators, setup scripts, docs, and the docker e2e helper. The keyring vat now generates the throwaway key itself via `crypto.getRandomValues`. - Collapse `makeSaltGenerator` in `lib/delegation.ts` to a crypto-only implementation; drop the counter fallback and its `entropy` param. - Endow `crypto` + `SubtleCrypto` in the keyring and delegator vat globals (delegator imports `delegation.ts`, which evaluates `generateSalt = makeSaltGenerator()` at load). - Drop stale "Math.random is blocked under SES lockdown" JSDoc from `bundler-client.ts` and `provider.ts`; the raw-fetch implementations are left in place per the issue's speculative/lower-priority note. - Simplify the `initializeKeyring` option unwrapping in both coordinators now that the throwaway branch carries no payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 175b7c0 commit 22e0d96

20 files changed

Lines changed: 71 additions & 229 deletions

packages/evm-wallet-experiment/README.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ For a deeper explanation of the components and data flow, see [How It Works](./d
99
- **Peer signing has no interactive approval for message/typed-data requests.** Transaction signing over peer requests is now disabled and peer-connected wallets must use delegation redemption for sends, but message and typed-data peer signing still execute immediately without an approval prompt.
1010
- **`revokeDelegation()` and hybrid redemption require a bundler or peer relay.** Hybrid accounts submit on-chain `disableDelegation` / redemption via ERC-4337 UserOps; configure a bundler (and optional paymaster). **Stateless 7702** accounts use a direct EIP-1559 transaction instead; only the JSON-RPC provider must be configured. **Away wallets without a bundler** relay delegation redemptions to the home wallet via CapTP (requires the home wallet to be online). If the on-chain transaction fails, the local delegation status is not changed.
1111
- **Mnemonic encryption is optional.** The keyring vat can encrypt the mnemonic at rest using AES-256-GCM with a PBKDF2-derived key. Pass a `password` and `salt` to `initializeKeyring()` to enable encryption. Without a password, the mnemonic is stored in plaintext. When encrypted, the keyring starts in a locked state on daemon restart and must be unlocked with `unlockKeyring(password)` before signing operations work.
12-
- **Throwaway keyring needs secure entropy.** `initializeKeyring({ type: 'throwaway' })` requires either `crypto.getRandomValues` in the runtime or caller-provided entropy via `{ type: 'throwaway', entropy: '0x...' }`. Under SES lockdown (where `crypto` is unavailable inside vat compartments), the caller must generate 32 bytes of entropy externally and pass it in.
1312

1413
## Architecture
1514

@@ -126,9 +125,7 @@ import { makeWalletClusterConfig } from '@ocap/evm-wallet-experiment';
126125
// 1. Launch the wallet subcluster with a throwaway keyring
127126
const config = makeWalletClusterConfig({ bundleBaseUrl: '/bundles' });
128127
const { rootKref } = await kernel.launchSubcluster(config);
129-
// Under SES lockdown, pass entropy generated outside the vat:
130-
const entropy = `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex')}`;
131-
await coordinator.initializeKeyring({ type: 'throwaway', entropy });
128+
await coordinator.initializeKeyring({ type: 'throwaway' });
132129

133130
// 2. Connect to the home kernel via the OCAP URL
134131
// This automatically:
@@ -315,15 +312,15 @@ const userOpHash = await coordinator.redeemDelegation({
315312

316313
### Coordinator -- Lifecycle
317314

318-
| Method | Description |
319-
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
320-
| `bootstrap(vats, services)` | Called by the kernel during subcluster launch. Wires up vat references. |
321-
| `initializeKeyring(options)` | Initialize the keyring vat. Options: `{ type: 'srp', mnemonic, password?, salt? }` or `{ type: 'throwaway', entropy? }`. Under SES lockdown, pass `entropy` (32-byte hex) for throwaway keys. When `password` is provided for SRP, the mnemonic is encrypted at rest (requires a random `salt` hex string). |
322-
| `unlockKeyring(password)` | Unlock an encrypted keyring after daemon restart. Required before any signing operations when the mnemonic was encrypted with a password. |
323-
| `isKeyringLocked()` | Returns `true` if the keyring is encrypted and has not been unlocked yet. |
324-
| `configureProvider(chainConfig)` | Configure the provider vat with an RPC URL and chain ID. |
325-
| `connectExternalSigner(signer)` | Connect an external signing backend (e.g., MetaMask). |
326-
| `configureBundler(config)` | Configure the ERC-4337 bundler. Accepts `{ bundlerUrl, chainId, entryPoint?, usePaymaster?, sponsorshipPolicyId? }`. |
315+
| Method | Description |
316+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
317+
| `bootstrap(vats, services)` | Called by the kernel during subcluster launch. Wires up vat references. |
318+
| `initializeKeyring(options)` | Initialize the keyring vat. Options: `{ type: 'srp', mnemonic, password?, salt? }` or `{ type: 'throwaway' }`. When `password` is provided for SRP, the mnemonic is encrypted at rest (requires a random `salt` hex string). |
319+
| `unlockKeyring(password)` | Unlock an encrypted keyring after daemon restart. Required before any signing operations when the mnemonic was encrypted with a password. |
320+
| `isKeyringLocked()` | Returns `true` if the keyring is encrypted and has not been unlocked yet. |
321+
| `configureProvider(chainConfig)` | Configure the provider vat with an RPC URL and chain ID. |
322+
| `connectExternalSigner(signer)` | Connect an external signing backend (e.g., MetaMask). |
323+
| `configureBundler(config)` | Configure the ERC-4337 bundler. Accepts `{ bundlerUrl, chainId, entryPoint?, usePaymaster?, sponsorshipPolicyId? }`. |
327324

328325
### Coordinator -- Signing
329326

packages/evm-wallet-experiment/docs/setup-guide.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -561,11 +561,10 @@ yarn ocap daemon exec launchSubcluster '{"config": { ... }}'
561561

562562
### 3e. Initialize with a throwaway key
563563

564-
The away wallet gets a throwaway key (for signing UserOps within delegations). Under SES lockdown, `crypto.getRandomValues` is unavailable in vat compartments, so you must generate entropy externally:
564+
The away wallet gets a throwaway key (for signing UserOps within delegations):
565565

566566
```bash
567-
ENTROPY="0x$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")"
568-
yarn ocap daemon queueMessage ko4 initializeKeyring "[{\"type\": \"throwaway\", \"entropy\": \"$ENTROPY\"}]"
567+
yarn ocap daemon queueMessage ko4 initializeKeyring '[{"type":"throwaway"}]'
569568
```
570569

571570
### 3f. Connect to the home wallet

packages/evm-wallet-experiment/scripts/setup-away.sh

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ CONFIG=$(BUNDLE_DIR="$BUNDLE_DIR" DM="$DELEGATION_MANAGER" RPC_HOST="$AWAY_RPC_H
373373
},
374374
keyring: {
375375
bundleSpec: bd + '/keyring-vat.bundle',
376-
globals: ['TextEncoder', 'TextDecoder']
376+
globals: ['TextEncoder', 'TextDecoder', 'crypto', 'SubtleCrypto']
377377
},
378378
provider: {
379379
bundleSpec: bd + '/provider-vat.bundle',
@@ -407,14 +407,7 @@ ok "Subcluster launched — coordinator: $ROOT_KREF"
407407
# ---------------------------------------------------------------------------
408408

409409
info "Initializing throwaway keyring..."
410-
# Generate 32 bytes of entropy outside the SES compartment (crypto.getRandomValues
411-
# is unavailable inside vats). The entropy is passed to the keyring vat which uses
412-
# it as the private key for the throwaway account.
413-
ENTROPY="0x$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")"
414-
INIT_ARGS=$(ENTROPY="$ENTROPY" node -e "
415-
process.stdout.write(JSON.stringify([{ type: 'throwaway', entropy: process.env.ENTROPY }]));
416-
")
417-
daemon_qm --quiet "$ROOT_KREF" initializeKeyring "$INIT_ARGS" >/dev/null
410+
daemon_qm --quiet "$ROOT_KREF" initializeKeyring '[{"type":"throwaway"}]' >/dev/null
418411
ok "Throwaway keyring initialized"
419412

420413
info "Verifying accounts..."

packages/evm-wallet-experiment/scripts/setup-home.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ CONFIG=$(BUNDLE_DIR="$BUNDLE_DIR" DM="$DELEGATION_MANAGER" RPC_HOST="$RPC_HOST"
338338
},
339339
keyring: {
340340
bundleSpec: bd + '/keyring-vat.bundle',
341-
globals: ['TextEncoder', 'TextDecoder']
341+
globals: ['TextEncoder', 'TextDecoder', 'crypto', 'SubtleCrypto']
342342
},
343343
provider: {
344344
bundleSpec: bd + '/provider-vat.bundle',

packages/evm-wallet-experiment/src/cluster-config.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,20 @@ describe('cluster-config', () => {
101101
bundleBaseUrl: BUNDLE_BASE_URL,
102102
});
103103

104-
const baseGlobals = ['TextEncoder', 'TextDecoder'];
105-
for (const vatName of ['keyring', 'provider', 'delegator']) {
104+
const providerConfig = config.vats.provider as { globals?: string[] };
105+
expect(providerConfig.globals).toStrictEqual([
106+
'TextEncoder',
107+
'TextDecoder',
108+
]);
109+
110+
for (const vatName of ['keyring', 'delegator']) {
106111
const vatConfig = config.vats[vatName] as { globals?: string[] };
107-
expect(vatConfig.globals).toStrictEqual(baseGlobals);
112+
expect(vatConfig.globals).toStrictEqual([
113+
'TextEncoder',
114+
'TextDecoder',
115+
'crypto',
116+
'SubtleCrypto',
117+
]);
108118
}
109119

110120
const coordConfig = config.vats.coordinator as { globals?: string[] };

packages/evm-wallet-experiment/src/cluster-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function makeWalletClusterConfig(
3737
? {
3838
delegator: {
3939
bundleSpec: `${bundleBaseUrl}/delegator-vat.bundle`,
40-
globals: ['TextEncoder', 'TextDecoder'],
40+
globals: ['TextEncoder', 'TextDecoder', 'crypto', 'SubtleCrypto'],
4141
},
4242
}
4343
: {
@@ -58,7 +58,7 @@ export function makeWalletClusterConfig(
5858
},
5959
keyring: {
6060
bundleSpec: `${bundleBaseUrl}/keyring-vat.bundle`,
61-
globals: ['TextEncoder', 'TextDecoder'],
61+
globals: ['TextEncoder', 'TextDecoder', 'crypto', 'SubtleCrypto'],
6262
},
6363
provider: {
6464
bundleSpec: `${bundleBaseUrl}/provider-vat.bundle`,

packages/evm-wallet-experiment/src/lib/bundler-client.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
/**
22
* Bundler client using raw fetch for ERC-4337 interactions.
33
*
4-
* Avoids viem's createClient/http which use Math.random() (blocked under
5-
* SES lockdown). All methods are simple JSON-RPC calls over fetch.
6-
*
74
* @module lib/bundler-client
85
*/
96

@@ -188,9 +185,6 @@ async function bundlerRpcOnce(
188185
/**
189186
* Create a bundler client for ERC-4337 operations.
190187
*
191-
* Uses raw fetch instead of viem's createClient to avoid Math.random()
192-
* usage that is blocked under SES lockdown.
193-
*
194188
* @param config - Bundler configuration.
195189
* @returns A bundler client with ERC-4337 actions.
196190
*/

packages/evm-wallet-experiment/src/lib/delegation.test.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,10 @@ describe('lib/delegation', () => {
5252
expect(generate()).not.toBe(generate());
5353
});
5454

55-
it('two generators produce independent sequences', () => {
55+
it('produces distinct salts from independent generators', () => {
5656
const gen1 = makeSaltGenerator();
5757
const gen2 = makeSaltGenerator();
58-
// Each generator's counter is independent — advance gen1 several times
59-
// without touching gen2 and verify gen2 still produces valid salts.
60-
gen1();
61-
gen1();
62-
gen1();
63-
expect(gen2()).toMatch(/^0x[\da-f]{64}$/iu);
64-
});
65-
66-
it('accepts entropy without throwing', () => {
67-
const entropy = '0xdeadbeef' as `0x${string}`;
68-
const generate = makeSaltGenerator(entropy);
69-
expect(generate()).toMatch(/^0x[\da-f]{64}$/iu);
58+
expect(gen1()).not.toBe(gen2());
7059
});
7160
});
7261

packages/evm-wallet-experiment/src/lib/delegation.ts

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -56,52 +56,20 @@ export type SaltGenerator = () => Hex;
5656
/**
5757
* Create a salt generator for delegation uniqueness.
5858
*
59-
* Prefers crypto.getRandomValues when available. In SES compartments
60-
* where crypto is not endowed, falls back to a closure-local counter
61-
* hashed with optional caller-supplied entropy. Each call to
62-
* makeSaltGenerator produces an independent counter, so two vat instances
63-
* each get their own sequence rather than sharing module-level state.
64-
*
65-
* @param entropy - Optional caller-supplied entropy hex string. When provided
66-
* and crypto is unavailable, mixed into the counter hash so that separate
67-
* vat instances produce distinct salts even though both start at counter 1.
68-
* @returns A salt generator function.
59+
* @returns A salt generator function backed by `crypto.getRandomValues`.
6960
*/
70-
export function makeSaltGenerator(entropy?: Hex): SaltGenerator {
71-
// eslint-disable-next-line n/no-unsupported-features/node-builtins
72-
if (globalThis.crypto?.getRandomValues) {
73-
return () => {
74-
const bytes = new Uint8Array(32);
75-
// eslint-disable-next-line n/no-unsupported-features/node-builtins
76-
globalThis.crypto.getRandomValues(bytes);
77-
return toHex(bytes);
78-
};
79-
}
80-
81-
// SES fallback: unique per generator lifetime but not cryptographically random.
82-
// The salt only needs uniqueness, not unpredictability.
83-
let counter = 0;
84-
if (entropy !== undefined) {
85-
return () => {
86-
counter += 1;
87-
return keccak256(
88-
encodePacked(['bytes', 'uint256'], [entropy, BigInt(counter)]),
89-
);
90-
};
91-
}
61+
export function makeSaltGenerator(): SaltGenerator {
9262
return () => {
93-
counter += 1;
94-
return keccak256(encodePacked(['uint256'], [BigInt(counter)]));
63+
const bytes = new Uint8Array(32);
64+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
65+
globalThis.crypto.getRandomValues(bytes);
66+
return toHex(bytes);
9567
};
9668
}
9769

9870
/**
9971
* Generate a random salt for delegation uniqueness.
10072
*
101-
* Uses a module-level counter as the SES fallback. Prefer
102-
* {@link makeSaltGenerator} when creating delegations in a vat, since it
103-
* gives each vat instance an independent counter.
104-
*
10573
* @returns A hex-encoded random salt.
10674
*/
10775
export const generateSalt: SaltGenerator = makeSaltGenerator();

packages/evm-wallet-experiment/src/lib/keyring.test.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mnemonicToAccount } from 'viem/accounts';
2-
import { describe, it, expect, vi } from 'vitest';
2+
import { describe, it, expect } from 'vitest';
33

44
import { makeKeyring, generateMnemonicPhrase } from './keyring.ts';
55

@@ -110,46 +110,6 @@ describe('lib/keyring', () => {
110110
const keyring = makeKeyring({ type: 'throwaway' });
111111
expect(keyring.getMnemonic()).toBeUndefined();
112112
});
113-
114-
it('requires secure randomness when creating throwaway keys without entropy', () => {
115-
vi.stubGlobal('crypto', undefined);
116-
try {
117-
expect(() => makeKeyring({ type: 'throwaway' })).toThrow(
118-
'Throwaway keyring requires crypto.getRandomValues or caller-provided entropy',
119-
);
120-
} finally {
121-
vi.unstubAllGlobals();
122-
}
123-
});
124-
125-
it('accepts caller-provided entropy for throwaway keys', () => {
126-
vi.stubGlobal('crypto', undefined);
127-
try {
128-
const entropy =
129-
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
130-
const keyring = makeKeyring({
131-
type: 'throwaway',
132-
entropy: entropy as `0x${string}`,
133-
});
134-
const accounts = keyring.getAccounts();
135-
expect(accounts).toHaveLength(1);
136-
expect(accounts[0]).toMatch(/^0x[\da-f]{40}$/u);
137-
} finally {
138-
vi.unstubAllGlobals();
139-
}
140-
});
141-
142-
it.each(['0xshort', '0x', 'not-hex', `0x${'ff'.repeat(33)}`])(
143-
'rejects invalid entropy: %s',
144-
(badEntropy) => {
145-
expect(() =>
146-
makeKeyring({
147-
type: 'throwaway',
148-
entropy: badEntropy as `0x${string}`,
149-
}),
150-
).toThrow('Invalid entropy');
151-
},
152-
);
153113
});
154114
});
155115

0 commit comments

Comments
 (0)