Skip to content
Merged
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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ State-machine-driven hardware wallet connector for RAILGUN — Ledger-first, bro
## What It Does

- Connects to Ledger devices via WebHID (browser) or Node HID (scripts/tests)
- Signs RAILGUN transactions (BabyJubjub EdDSA via custom Ledger app)
- Signs RAILGUN transactions (BabyJubjub EdDSA via custom Ledger app), with CLEAR_SIGN transact builders (experimental)
- Reads the viewing public key and the canonical `0zk1…` address for on-device display/verify
- Signs ETH transactions, messages, EIP-712 typed data, fixed shield ownership markers, and EIP-7702 authorizations
- Preloads and signs with the RAILGUN app 7702 EOA path `m/7702'/1984'/account'/chainId/ephemeralIndex`
- Preloads and signs with a caller-customizable, chain-scoped RAILGUN 7702 EOA path `m/7702'/1984'/account'/chainId'/ephemeralIndex'` (a distinct EOA per chain)
- Installs sideloaded apps via SCP02/SCP03 secure channel
- Exposes a `HardwareConnector` interface compatible with the RAILGUN engine
- Drives its behavior through a pure finite state machine — no framework required
Expand Down Expand Up @@ -126,13 +127,13 @@ specific app builds.

## RAILGUN 7702 Hardware Signing (under development)

The SDK exposes the RAILGUN-app hardware path for EIP-7702 signer preload, authorization signing, and RelayAdapt7702 EIP-712 digest signing. The firmware derives Ethereum EOAs from:
The SDK exposes the RAILGUN-app hardware path for EIP-7702 signer preload, authorization signing, and RelayAdapt7702 EIP-712 digest signing. The firmware derives Ethereum EOAs from a caller-chosen path:

```text
m/7702'/1984'/railgunAccountIndex'/chainId/ephemeralIndex
m/7702'/1984'/account'/chainId'/ephemeralIndex'
```

The host sends the trailing three path words to the RAILGUN app as `W0 || W1 || W2`. The app hardens `W0` internally, derives the EOA, and signs against that same suffix for all 7702 operations.
The host sends the trailing three path words to the RAILGUN app as `W0(account) || W1(chainId) || W2(ephemeralIndex)`; the firmware hardens all three, derives the EOA, and signs against that same suffix for all 7702 operations. All three words are caller-customizable, and because `chainId` is one of them each chain derives a **distinct** EOA (chain-scoped — a wallet runs on many chains at once without reusing a 7702 address). Each word is a hardened index and must fit in 31 bits, so chains with `chainId >= 2**31` are rejected.

| Operation | SDK API | RAILGUN APDU |
|-----------|---------|--------------|
Expand Down Expand Up @@ -196,6 +197,8 @@ All public types and functions are re-exported from `src/index.ts`. Key exports:
| `createLedgerController` | Headless, framework-agnostic controller |
| `transition`, `createInitialContext` | Pure FSM for custom integration |
| `RailgunSigner`, `EthSigner` | Direct signer access |
| `RailgunSigner.getViewingPublicKey` / `getRailgunAddress` | On-device display/verify of the viewing pubkey (INS 0x10) and `0zk1…` address (INS 0x14) |
| `buildClearSignInit` … `buildClearSignFinalize`, `validateClearSignShape`, `parseClearSignFinalize` | CLEAR_SIGN transact protocol builders (INS 0x11, experimental) |
| `RAILGUN_SHIELD_MESSAGE` | Fixed replayable ETH-app ownership marker used by the shield ownership flow |
| `createEngineLedgerConnector` | Session-aware engine adapter with shield and ETH tx signing hooks |
| `RailgunSigner.get7702Signer` | Direct engine-compatible 7702 signer from a RAILGUN app signer |
Expand Down
26 changes: 20 additions & 6 deletions docs/api/signers.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,33 @@ const signature = await signer.sign(poseidonHash); // { R8: [x, y], S } — conf
> viewing secret required by the current engine format. Never log it or route it through
> `onProgress`/telemetry.

### Display / verify accessors

Two commands read public identifiers behind an on-device confirmation (the device shows the
value so the user can compare it against an out-of-band source). Neither exposes a secret.

| Method | Returns | Notes |
|--------|---------|-------|
| `getViewingPublicKey()` | `Uint8Array` (32B) | Compressed Ed25519 viewing **public** key (`INS 0x10`). Display/verify only — does **not** export the viewing secret; wallet loading still uses `getWalletArtifacts()`. |
| `getRailgunAddress()` | `string` | The canonical 127-char `0zk1…` address (`INS 0x14`) — a device-confirmed cross-check of the host-derived address. |

### Ethereum / EIP-7702 methods (under development)

The RAILGUN app derives Ethereum EOAs from the firmware path
`m/7702'/1984'/account'/chainId/ephemeralIndex`. These methods are gated on the app's
The RAILGUN app derives Ethereum EOAs from a **caller-chosen** path
`m/7702'/1984'/account'/chainId'/ephemeralIndex'`. All three trailing words —
`account` (W0), `chainId` (W1), `ephemeralIndex` (W2) — are settable, and because `chainId`
is one of them, **each chain derives a distinct EOA** (chain-scoped: a wallet can run on many
chains at once without reusing a 7702 address). Each word is a hardened index and must fit in
31 bits, so chains with `chainId >= 2**31` are rejected. These methods are gated on the app's
advertised capabilities and throw `APP_VERSION_MISMATCH` if unsupported.

| Method | Purpose |
|--------|---------|
| `getEthereumAddress(display?)` | Derive the RAILGUN-app EOA address + public key. |
| `prepareEthereumSigner(request)` | Preload/derive the EOA for a `{ railgunAccountIndex, chainId, ephemeralIndex }` session (binds later signatures to the same path). |
| `signEip7702Authorization(request)` | Sign an EIP-7702 authorization (`INS 0x08`). |
| `signEthereumTxHash(hash, options?)` | Sign a 32-byte Ethereum digest (`INS 0x09`). `options.display` defaults to `true` (clear signing). |
| `get7702Signer(request, options?)` | Return an engine-compatible RelayAdapt7702 hooked signer. |
| `prepareEthereumSigner(request)` | Preload/derive the EOA for a `{ railgunAccountIndex, chainId, ephemeralIndex }` session (binds later signatures to the same chain-scoped path). |
| `signEip7702Authorization(request)` | Sign an EIP-7702 authorization (`INS 0x08`). Rejects an explicit `path` whose chainId word (W1) disagrees with the authorization `chainId`. |
| `signEthereumTxHash(hash, options?)` | Sign a 32-byte Ethereum digest (`INS 0x09`). `options.display` defaults to `true` (clear signing); firmware requires `P1 = 0x01`, so standalone `display: false` is rejected. |
| `get7702Signer(request, options?)` | Return an engine-compatible RelayAdapt7702 hooked signer. `request` = `{ chainId, ephemeralIndex, railgunAccountIndex? }` — all three path words are customizable (`railgunAccountIndex` defaults to the signer's account). |

**Guidance:** prefer `display: true` (clear signing) so the device shows context; treat
blind/hash-only signing (`display: false`) as an explicit, audited opt-in. For RAILGUN
Expand Down
20 changes: 11 additions & 9 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,23 @@ if (capabilities.eip7702Authorization && capabilities.ethereumTxHash) {

const outerTxSignature = await signer.signEthereumTxHash(txHashBytes, {
session,
display: false,
display: true,
});
}
```

`prepareEthereumSigner()` binds the app-native Ethereum signing context to the
firmware-supported 7702 EOA path
`m/7702'/1984'/railgunAccountIndex'/chainId/ephemeralIndex`. The RAILGUN app's
`INS 0x07` preload APDU sends those trailing three words to derive the same EOA
that `INS 0x08` and `INS 0x09` later sign with.
`prepareEthereumSigner()` binds the app-native Ethereum signing context to a
**caller-chosen** 7702 EOA path
`m/7702'/1984'/account'/chainId'/ephemeralIndex'`. All three trailing words are
customizable, and because `chainId` is word W1 each chain derives a **distinct** EOA
(chain-scoped — one wallet across many chains without reusing an address). The RAILGUN
app's `INS 0x07` preload APDU sends those three words to derive the same EOA that
`INS 0x08` and `INS 0x09` later sign with. To vary the account per signer, use
`get7702Signer({ chainId, ephemeralIndex, railgunAccountIndex })`.

`signEip7702Authorization()` uses the custom RAILGUN app `INS 0x08` APDU.
`signEthereumTxHash()` uses `INS 0x09`; `display: true` requires the profile's
`clear` signing capability, while `display: false` uses the gated/blind signing
capability for the second step of a 7702 authorization flow.
`signEthereumTxHash()` uses `INS 0x09` with `P1 = 0x01` (on-device review); firmware
1.6.1 rejects a standalone `P1 = 0x00`, so pass `display: true`.

For an EIP-712 routing experiment through the standard Ethereum app, use the
custom-path helpers and verify the returned ETH-app address matches the RAILGUN
Expand Down
Loading