From eac1b6ddd4c57bd5185e7a836d11e903158b8d16 Mon Sep 17 00:00:00 2001 From: LucienSong Date: Thu, 21 May 2026 01:15:43 +0800 Subject: [PATCH 1/3] =?UTF-8?q?docs(readme):=20align=20with=20chain=20v0.2?= =?UTF-8?q?3.0=20=E2=80=94=20drop=20pq1/Bech32m=20examples,=20document=203?= =?UTF-8?q?2-byte=200x=20address=20format,=20flag=20signing-hash=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 112 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 2fa5ec3..d10afea 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,19 @@ --- +> **⚠️ v0.23.0 alignment status (in progress)** +> +> Addresses, system-contract IDs, and adapter types are aligned with shell-chain v0.23.0 +> (32-byte `0x…` BLAKE3 addresses; `algo_id` byte `Dilithium3=0`, `MlDsa65=1`, `SphincsSha2256f=2`). +> +> The transaction signing-hash helpers (`hashTransaction`, `hashBatchTransaction`) +> still implement the pre-v0.23.0 `keccak256(RLP(tx))` scheme. Shell-chain v0.23.0 +> nodes expect `BLAKE3(structured preimage including sig_type)` instead. A +> follow-up release will replace these helpers and regenerate the +> `tests/fixtures/rust-compatibility.json` vectors against the v0.23.0 chain. +> Do not rely on the `hashTransaction*` helpers against a v0.23.0 node yet. + + ## Table of Contents - [Features](#features) @@ -36,7 +49,7 @@ ## Features - **Post-quantum signing** — ML-DSA-65 (FIPS 204) and SLH-DSA-SHA2-256f (FIPS 205) -- **PQ addresses** — bech32m-encoded `pq1…` addresses derived from PQ public keys via BLAKE3 +- **Shell addresses** — `0x`-prefixed 64-character lowercase hex (full 32-byte BLAKE3) derived from PQ public keys - **Native account abstraction** — key rotation and custom validation code via system contracts - **viem integration** — standard Ethereum JSON-RPC methods via a typed `PublicClient` - **Shell-specific RPC** — `shell_getPqPubkey`, `shell_sendTransaction`, `shell_getTransactionsByAddress`, `shell_getNodeInfo`, `shell_getWitness` @@ -76,12 +89,12 @@ import { parseEther } from "viem"; const adapter = MlDsa65Adapter.generate(); const signer = new ShellSigner("MlDsa65", adapter); -const from = signer.getAddress(); // pq1… +const from = signer.getAddress(); // 0x… (64-char hex) const provider = createShellProvider(); const nonce = await provider.client.getTransactionCount({ address: from }); -const tx = buildTransferTransaction({ chainId: 424242, nonce, to: "pq1recipient…", value: parseEther("1") }); +const tx = buildTransferTransaction({ chainId: 424242, nonce, to: "0x…", value: parseEther("1") }); const txHash = hashTransaction(tx); const signed = await signer.buildSignedTransaction({ tx, txHash }); const hash = await provider.sendTransaction(signed); @@ -94,21 +107,16 @@ console.log("tx hash:", hash); ### PQ addresses -Shell Chain uses **bech32m**-encoded addresses (prefix `pq`) instead of Ethereum's hex checksummed format. A `pq1…` address encodes: +Shell Chain uses **`0x`-prefixed 64-character lowercase hex** addresses — the full 32-byte BLAKE3 hash of the PQ public key with a one-byte algorithm tag: ``` -bech32m( hrp="pq", payload = [ version_byte(0x01) | address_bytes(20) ] ) -``` - -The 20 address bytes are derived deterministically: - -``` -blake3( version(1) || algo_id(1) || public_key )[0..20] +address_bytes = BLAKE3(algo_id || public_key) // full 32 bytes, no truncation +address_string = "0x" + hex_lower(address_bytes) ``` Algorithm IDs: `Dilithium3=0`, `MlDsa65=1`, `SphincsSha2256f=2`. -Shell Chain v0.21.0+ accepts `pq1…` addresses at user-facing RPC and SDK boundaries. Legacy `0x…` address inputs are rejected. +There is no Bech32m/`pq1…` encoding and no separate version byte: Shell-Chain is a clean-slate chain with no backward bridge to any 20-byte Ethereum address model. ### Native account abstraction (AA) @@ -126,8 +134,8 @@ These are sent as ordinary transactions whose `to` field is the AccountManager a | Name | Hex address | PQ address | |---|---|---| -| ValidatorRegistry | `0x0000000000000000000000000000000000000001` | derived pq1 form | -| AccountManager | `0x0000000000000000000000000000000000000002` | derived pq1 form | +| ValidatorRegistry | `0x0000…0001` (32-byte big-endian) | +| AccountManager | `0x0000…0002` (32-byte big-endian) | --- @@ -143,7 +151,7 @@ Defined in `src/types.ts`. All types are re-exported from the package root. |---|---| | `HexString` | Template-literal type `0x${string}` | | `AddressLike` | Any string accepted as an address | -| `SignatureTypeName` | `"Dilithium3" \| "MlDsa65" \| "SphincsSha2256f"` | +| `SignatureTypeName` | `"ML-DSA-65" \| "Dilithium3" \| "MlDsa65" \| "SphincsSha2256f"` | | `ShellTransactionRequest` | Wire format for a Shell transaction | | `ShellSignature` | `{ sig_type, data: number[] }` | | `SignedShellTransaction` | Complete signed transaction ready to broadcast | @@ -165,38 +173,37 @@ Defined in `src/types.ts`. All types are re-exported from the package root. | Export | Value | Description | |---|---|---| -| `PQ_ADDRESS_HRP` | `"pq"` | Human-readable part for bech32m encoding | -| `PQ_ADDRESS_LENGTH` | `20` | Address bytes (excluding version byte) | -| `PQ_ADDRESS_VERSION_V1` | `0x01` | Current address version | +| `SHELL_ADDRESS_LENGTH` | `32` | Address bytes (full BLAKE3 output) | #### Functions | Function | Signature | Description | |---|---|---| -| `bytesToPqAddress` | `(bytes: Uint8Array, version?) → string` | Encode 20 raw bytes as a `pq1…` bech32m address | -| `pqAddressToBytes` | `(address: string) → Uint8Array` | Decode a `pq1…` address to its 20 raw bytes | -| `pqAddressVersion` | `(address: string) → number` | Extract the version byte from a `pq1…` address | -| `normalizePqAddress` | `(address: string) → string` | Validate and return a `pq1…` address | -| `derivePqAddressFromPublicKey` | `(pk, algoId, version?) → string` | Derive pq1 address from a raw public key | -| `isPqAddress` | `(address: string) → boolean` | Return `true` if the string is a valid pq1 address | +| `bytesToShellAddress` | `(bytes: Uint8Array) → string` | Encode 32 raw bytes as a `0x`-prefixed 64-char hex address | +| `shellAddressToBytes` | `(address: string) → Uint8Array` | Decode a `0x…` Shell address to its 32 raw bytes | +| `normalizeShellAddress` | `(address: string) → string` | Validate and lowercase a `0x…` Shell address | +| `deriveShellAddressFromPublicKey` | `(pk, algoId) → string` | Derive a 32-byte `0x…` Shell address from a raw PQ public key | +| `isShellAddress` | `(address: string) → boolean` | Return `true` if the string is a valid 32-byte `0x…` Shell address | + +Legacy aliases (`bytesToPqAddress`, `pqAddressToBytes`, `normalizePqAddress`, `derivePqAddressFromPublicKey`, `isPqAddress`) remain exported but are deprecated — they now operate on the same 32-byte `0x…` format. **Examples:** ```typescript import { - derivePqAddressFromPublicKey, - isPqAddress, - normalizePqAddress, + deriveShellAddressFromPublicKey, + isShellAddress, + normalizeShellAddress, } from "shell-sdk/address"; -const address = derivePqAddressFromPublicKey(publicKey, 1 /* MlDsa65 */); -// → "pq1qx3f…" +const address = deriveShellAddressFromPublicKey(publicKey, 1 /* MlDsa65 */); +// → "0x9a3f…" (64-char lowercase hex) -console.log(isPqAddress(address)); // true +console.log(isShellAddress(address)); // true // Validation / normalisation -normalizePqAddress("pq1qx3f…"); // → "pq1qx3f…" (unchanged) -normalizePqAddress("0xabcdef…"); // throws: expected a pq1… bech32m address +normalizeShellAddress("0x9A3F…"); // → "0x9a3f…" (lowercased) +normalizeShellAddress("pq1abc…"); // throws: expected a 0x… 32-byte Shell address ``` --- @@ -254,11 +261,11 @@ const block = await provider.client.getBlockNumber(); const balance = await provider.client.getBalance({ address: "0x…" }); // Shell-specific methods -const pubkeyHex = await provider.getPqPubkey("pq1…"); +const pubkeyHex = await provider.getPqPubkey("0x…"); const txHash = await provider.sendTransaction(signedTx); -const history = await provider.getTransactionsByAddress("pq1…", { page: 0, limit: 20 }); -const older = await provider.getTransactionsByAddress("pq1…", { +const history = await provider.getTransactionsByAddress("0x…", { page: 0, limit: 20 }); +const older = await provider.getTransactionsByAddress("0x…", { page: 1, limit: 20, toBlock: history.toBlock ?? history.to_block, @@ -338,7 +345,7 @@ const signer = new ShellSigner("MlDsa65", MlDsa65Adapter.generate()); |---|---| | `algorithmId` | Numeric algorithm ID (`0`, `1`, or `2`) | | `getPublicKey()` | Raw public key bytes | -| `getAddress()` | `pq1…` bech32m address | +| `getAddress()` | `0x…` 64-char hex Shell address | | `sign(message)` | Sign an arbitrary byte message → signature bytes | | `buildSignedTransaction(options)` | Sign `txHash` and assemble a `SignedShellTransaction` | @@ -385,7 +392,7 @@ import { parseEther } from "viem"; const tx = buildTransferTransaction({ chainId: 424242, nonce: 0, - to: "pq1recipient…", + to: "0x…", value: parseEther("1.5"), }); ``` @@ -437,7 +444,7 @@ Assemble a `SignedShellTransaction` directly (use `ShellSigner.buildSignedTransa import { buildSignedTransaction } from "shell-sdk/transactions"; const signed = buildSignedTransaction({ - from: "pq1sender…", + from: "0x…", tx, signature: sigBytes, signatureType: "MlDsa65", @@ -456,7 +463,7 @@ Shell Chain signs the full unsigned transaction payload in this order: ```typescript import { buildTransferTransaction, hashTransaction } from "shell-sdk/transactions"; -const tx = buildTransferTransaction({ chainId: 424242, nonce: 0, to: "pq1…", value: 1n }); +const tx = buildTransferTransaction({ chainId: 424242, nonce: 0, to: "0x…", value: 1n }); const txHash = hashTransaction(tx); // Uint8Array (32 bytes) const signed = await signer.buildSignedTransaction({ tx, txHash }); ``` @@ -473,8 +480,8 @@ const signed = await signer.buildSignedTransaction({ tx, txHash }); |---|---| | `validatorRegistryHexAddress` | `0x0000000000000000000000000000000000000001` | | `accountManagerHexAddress` | `0x0000000000000000000000000000000000000002` | -| `validatorRegistryAddress` | pq1 bech32m form of above | -| `accountManagerAddress` | pq1 bech32m form of above | +| `validatorRegistryAddress` | 32-byte `0x…` form of above | +| `accountManagerAddress` | 32-byte `0x…` form of above | #### Function selectors @@ -514,7 +521,7 @@ Shell keystore files are JSON objects encrypted with **argon2id** (KDF) and **xc ```jsonc { "version": 1, - "address": "pq1…", + "address": "0x…", "key_type": "mldsa65", "kdf": "argon2id", "kdf_params": { "m_cost": 65536, "t_cost": 3, "p_cost": 1, "salt": "hex…" }, @@ -547,12 +554,12 @@ const json = readFileSync("./my-key.json", "utf8"); // Inspect without decrypting const parsed = parseEncryptedKey(json); -console.log(parsed.canonicalAddress); // pq1… +console.log(parsed.canonicalAddress); // 0x… console.log(parsed.signatureType); // "MlDsa65" // Decrypt const signer = await decryptKeystore(json, "my-passphrase"); -console.log(signer.getAddress()); // pq1… +console.log(signer.getAddress()); // 0x… ``` --- @@ -571,7 +578,7 @@ import { buildTransferTransaction, hashTransaction } from "shell-sdk/transaction // 1. Generate keys const adapter = MlDsa65Adapter.generate(); const signer = new ShellSigner("MlDsa65", adapter); -const from = signer.getAddress(); // pq1… +const from = signer.getAddress(); // 0x… console.log("Address:", from); @@ -585,7 +592,7 @@ const nonce = await provider.client.getTransactionCount({ address: from }); const tx = buildTransferTransaction({ chainId: 424242, nonce, - to: "pq1recipientaddress…", + to: "0x…", value: parseEther("0.5"), }); @@ -622,7 +629,7 @@ const nonce = await provider.client.getTransactionCount({ address: signer.get const tx = buildTransferTransaction({ chainId: 424242, nonce, - to: "pq1dest…", + to: "0x…", value: parseEther("10"), }); @@ -674,7 +681,7 @@ const provider = createShellProvider({ rpcHttpUrl: "https://rpc.testnet.shell.network", }); -const account = normalizePqAddress("pq1qx3f..."); +const account = normalizeShellAddress("0x9a3f…"); const history = await provider.getTransactionsByAddress(account, { page: 1, limit: 10 }); console.log("recent txs:", history.transactions); @@ -730,8 +737,8 @@ All SDK functions throw standard `Error` instances. Common error messages: | Error message | Cause | |---|---| | `expected 20 address bytes, got N` | Wrong-length bytes passed to address helpers | -| `expected pq address prefix, got X` | bech32m prefix is not `pq` | -| `invalid bech32m address` | String is not a valid bech32m address | +| `expected 0x prefix, got X` | Shell address must start with `0x` | +| `invalid Shell address length` | Address must be 32 raw bytes / 64 hex characters | | `unsupported key type: X` | Keystore `key_type` not recognised | | `unsupported kdf: X` | Only `argon2id` is supported | | `unsupported cipher: X` | Only `xchacha20-poly1305` is supported | @@ -758,7 +765,7 @@ try { // Branded hex string: "0x" + arbitrary hex chars type HexString = `0x${string}`; -// Any value accepted as an address (pq1… or 0x…) +// Any value accepted as a Shell address (0x… 64-char hex) type AddressLike = string; // Post-quantum signature algorithm names @@ -812,7 +819,6 @@ interface ShellSignature { | [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) | ML-DSA-65 and SLH-DSA-SHA2-256f | | [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) | BLAKE3 | | [`@noble/ciphers`](https://github.com/paulmillr/noble-ciphers) | xchacha20-poly1305 | -| [`@scure/base`](https://github.com/paulmillr/scure-base) | bech32m encoding | | [`hash-wasm`](https://github.com/nicowillis/hash-wasm) | argon2id (WASM) | --- @@ -839,7 +845,7 @@ Before publishing a `shell-sdk` release candidate: | Native currency | SHELL (18 decimals) | | HTTP RPC | `http://127.0.0.1:8545` | | WebSocket RPC | `ws://127.0.0.1:8546` | -| Address format | bech32m, prefix `pq`, version byte `0x01` | +| Address format | `0x` + 64 lowercase hex chars (32-byte BLAKE3 hash of `algo_id ‖ pubkey`) | | Default tx type | 2 (EIP-1559) | | Default gas limit (transfer) | 21 000 | | Default gas limit (system) | 100 000 | From 4f8fe060e1f500d0a341ee78af40d96ac2241c83 Mon Sep 17 00:00:00 2001 From: LucienSong Date: Thu, 21 May 2026 02:50:03 +0800 Subject: [PATCH 2/3] docs: fix system-contracts table, address exports, and error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address three Copilot review comments on PR #17: 1. Fix malformed system-contracts table (3-column header with 2-column rows) — collapse to a clean 2-column Name/Address table with full 32-byte literal addresses. 2. Fix normalizeShellAddress error example (lines 206, 737) to match the actual thrown message: 'expected 0x + 64-char hex address, got: …' 3. Fix system-contracts module reference: remove the non-existent validatorRegistryHexAddress / accountManagerHexAddress entries; document only the actual exports (validatorRegistryAddress / accountManagerAddress) with their full 32-byte canonical values; update the isSystemContractAddress example to use a 32-byte address. Also fix stale error message in the error-handling table: 'expected 20 address bytes' → 'expected 32 address bytes' to match address.ts:37. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d10afea..d2c01aa 100644 --- a/README.md +++ b/README.md @@ -132,10 +132,10 @@ These are sent as ordinary transactions whose `to` field is the AccountManager a ### System contracts -| Name | Hex address | PQ address | -|---|---|---| -| ValidatorRegistry | `0x0000…0001` (32-byte big-endian) | -| AccountManager | `0x0000…0002` (32-byte big-endian) | +| Name | Address | +|---|---| +| ValidatorRegistry | `0x0000000000000000000000000000000000000000000000000000000000000001` | +| AccountManager | `0x0000000000000000000000000000000000000000000000000000000000000002` | --- @@ -203,7 +203,7 @@ console.log(isShellAddress(address)); // true // Validation / normalisation normalizeShellAddress("0x9A3F…"); // → "0x9a3f…" (lowercased) -normalizeShellAddress("pq1abc…"); // throws: expected a 0x… 32-byte Shell address +normalizeShellAddress("pq1abc…"); // throws: expected 0x + 64-char hex address, got: "pq1abc…" ``` --- @@ -478,10 +478,8 @@ const signed = await signer.buildSignedTransaction({ tx, txHash }); | Export | Value | |---|---| -| `validatorRegistryHexAddress` | `0x0000000000000000000000000000000000000001` | -| `accountManagerHexAddress` | `0x0000000000000000000000000000000000000002` | -| `validatorRegistryAddress` | 32-byte `0x…` form of above | -| `accountManagerAddress` | 32-byte `0x…` form of above | +| `validatorRegistryAddress` | `0x0000000000000000000000000000000000000000000000000000000000000001` | +| `accountManagerAddress` | `0x0000000000000000000000000000000000000000000000000000000000000002` | #### Function selectors @@ -505,7 +503,7 @@ const data = encodeRotateKeyCalldata(newPublicKey, 1 /* MlDsa65 */); const data = encodeSetValidationCodeCalldata("0xcodehash…"); const data = encodeClearValidationCodeCalldata(); // selector only -isSystemContractAddress("0x0000000000000000000000000000000000000002"); // true +isSystemContractAddress("0x0000000000000000000000000000000000000000000000000000000000000002"); // true ``` --- @@ -736,7 +734,7 @@ All SDK functions throw standard `Error` instances. Common error messages: | Error message | Cause | |---|---| -| `expected 20 address bytes, got N` | Wrong-length bytes passed to address helpers | +| `expected 32 address bytes, got N` | Wrong-length bytes passed to address helpers | | `expected 0x prefix, got X` | Shell address must start with `0x` | | `invalid Shell address length` | Address must be 32 raw bytes / 64 hex characters | | `unsupported key type: X` | Keystore `key_type` not recognised | From 60f4cd6bc8badb2501872da8fd23b62d10c6638c Mon Sep 17 00:00:00 2001 From: LucienSong Date: Thu, 21 May 2026 03:01:21 +0800 Subject: [PATCH 3/3] test: update fixtures and assertions for v0.23.0 0x address format Replace all pq1/Bech32m address references in test fixtures and assertions with valid 0x + 64-char hex Shell addresses. Update keystore fixture address fields to match BLAKE3-derived canonical addresses. Fix tampered-address test to produce a valid 64-char hex address before mutation so the keystore address-mismatch guard is exercised (not the format validator). Update rust-compat fixture pq_address and transaction hash vectors for 32-byte address encoding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/aa.test.mjs | 20 ++++++++++---------- tests/browser.integration.test.mjs | 2 +- tests/cross-lang-sig.test.mjs | 2 +- tests/fixtures/cli-keystore-dilithium3.json | 2 +- tests/fixtures/cli-keystore-mldsa65.json | 2 +- tests/fixtures/rust-compatibility.json | 19 ++++++++----------- tests/keystore-compat.test.mjs | 16 ++++++++-------- tests/node.integration.test.mjs | 6 ++++-- 8 files changed, 34 insertions(+), 35 deletions(-) diff --git a/tests/aa.test.mjs b/tests/aa.test.mjs index 6ffdef7..4fb6146 100644 --- a/tests/aa.test.mjs +++ b/tests/aa.test.mjs @@ -26,7 +26,7 @@ function hexToBytes(hex) { } // gas_limit must be a hex quantity string per JSON-RPC wire format -const MINIMAL_INNER_CALL = { to: 'pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', value: '0x0', data: '0x', gas_limit: '0x5208' }; +const MINIMAL_INNER_CALL = { to: '0x0000000000000000000000000000000000000000000000000000000000000042', value: '0x0', data: '0x', gas_limit: '0x5208' }; // --------------------------------------------------------------------------- // Builder validation @@ -63,7 +63,7 @@ test('buildSponsoredTransaction: sets paymaster and signature', () => { chainId: 1, nonce: 0, innerCalls: [MINIMAL_INNER_CALL], - paymaster: 'pq1qx46h2at4w46h2at4w46h2at4w46h2at4v6lzg9h', + paymaster: '0x0000000000000000000000000000000000000000000000000000000000000099', paymasterSignature: pmSig, }); assert.ok(aa_bundle.paymaster, 'paymaster must be set'); @@ -110,7 +110,7 @@ test('hashBatchTransaction: paymaster changes the hash', () => { const { tx, aa_bundle: bundleNoPaymaster } = buildBatchTransaction({ chainId: 1, nonce: 0, innerCalls: calls }); const bundleWithPaymaster = { ...bundleNoPaymaster, - paymaster: 'pq1qx46h2at4w46h2at4w46h2at4w46h2at4v6lzg9h', + paymaster: '0x0000000000000000000000000000000000000000000000000000000000000099', }; const h1 = hexBytes(hashBatchTransaction(tx, bundleNoPaymaster)); const h2 = hexBytes(hashBatchTransaction(tx, bundleWithPaymaster)); @@ -133,22 +133,22 @@ test('hashBatchTransaction: known deterministic vector (chain_id=1, nonce=0, sin // buildInnerTransfer / buildInnerCall — hex encoding and validation // --------------------------------------------------------------------------- test('buildInnerTransfer: encodes gas_limit as hex quantity', () => { - const call = buildInnerTransfer('pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', 0n, 21_000); + const call = buildInnerTransfer('0x0000000000000000000000000000000000000000000000000000000000000042', 0n, 21_000); assert.equal(call.gas_limit, '0x5208', 'gas_limit must be hex-encoded'); assert.match(call.gas_limit, /^0x[0-9a-f]+$/, 'gas_limit must be a valid hex quantity'); }); test('buildInnerCall: encodes gas_limit as hex quantity', () => { - const call = buildInnerCall('pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', '0x', 50_000); + const call = buildInnerCall('0x0000000000000000000000000000000000000000000000000000000000000042', '0x', 50_000); assert.equal(call.gas_limit, '0xc350', 'gas_limit must be hex-encoded'); }); test('buildInnerTransfer: rejects negative gasLimit', () => { - assert.throws(() => buildInnerTransfer('pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', 0n, -1), /non-negative safe integer/); + assert.throws(() => buildInnerTransfer('0x0000000000000000000000000000000000000000000000000000000000000042', 0n, -1), /non-negative safe integer/); }); test('buildInnerCall: rejects non-integer gasLimit', () => { - assert.throws(() => buildInnerCall('pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', '0x', 21_000.5), /non-negative safe integer/); + assert.throws(() => buildInnerCall('0x0000000000000000000000000000000000000000000000000000000000000042', '0x', 21_000.5), /non-negative safe integer/); }); // --------------------------------------------------------------------------- @@ -160,7 +160,7 @@ const DUMMY_TX = { tx_type: '0x0', chain_id: 1, nonce: 0, gas_price: '0x0', gas_ test('buildSignedTransaction: aaBundle canonical option sets aa_bundle', () => { const signed = buildSignedTransaction({ - from: 'pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', + from: '0x0000000000000000000000000000000000000000000000000000000000000042', tx: DUMMY_TX, signatureType: 'falcon512', signature: DUMMY_SIG, @@ -171,7 +171,7 @@ test('buildSignedTransaction: aaBundle canonical option sets aa_bundle', () => { test('buildSignedTransaction: aaBbundle deprecated alias still works', () => { const signed = buildSignedTransaction({ - from: 'pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', + from: '0x0000000000000000000000000000000000000000000000000000000000000042', tx: DUMMY_TX, signatureType: 'falcon512', signature: DUMMY_SIG, @@ -183,7 +183,7 @@ test('buildSignedTransaction: aaBbundle deprecated alias still works', () => { test('buildSignedTransaction: aaBundle takes precedence over aaBbundle', () => { const OTHER_BUNDLE = { inner_calls: [], paymaster: null, paymaster_data: null }; const signed = buildSignedTransaction({ - from: 'pq1q802m0h0m6kmam774klwlh4dhmhaatd7aunnnrmm', + from: '0x0000000000000000000000000000000000000000000000000000000000000042', tx: DUMMY_TX, signatureType: 'falcon512', signature: DUMMY_SIG, diff --git a/tests/browser.integration.test.mjs b/tests/browser.integration.test.mjs index 6ddebf6..b3162fc 100644 --- a/tests/browser.integration.test.mjs +++ b/tests/browser.integration.test.mjs @@ -38,7 +38,7 @@ test('browser integration: dist exports work with fetch-based provider', async ( assert.equal(txHash, '0x' + 'ab'.repeat(32)); assert.equal(pqPubkey, '0x' + '11'.repeat(32)); assert.equal(typeof signer.getAddress(), 'string'); - assert.ok(signer.getAddress().startsWith('pq1'), 'signer address must be pq1 format'); + assert.ok(signer.getAddress().startsWith('0x'), 'signer address must be 0x hex format'); assert.deepEqual( calls.map((call) => call.method), ['shell_sendTransaction', 'shell_getPqPubkey'], diff --git a/tests/cross-lang-sig.test.mjs b/tests/cross-lang-sig.test.mjs index 1bfe92b..06eac8e 100644 --- a/tests/cross-lang-sig.test.mjs +++ b/tests/cross-lang-sig.test.mjs @@ -148,7 +148,7 @@ test('cross-lang: address derivation is deterministic from fixture pk', () => { assert.equal(algoId, 1, 'ML-DSA-65 algo_id must be 1'); const addr = derivePqAddressFromPublicKey(pk_bytes, algoId); - assert.ok(addr.startsWith('pq1'), 'address must be pq1 bech32'); + assert.ok(addr.startsWith('0x'), 'address must be 0x hex format'); // Same key → same address (deterministic) const addr2 = derivePqAddressFromPublicKey(pk_bytes, algoId); diff --git a/tests/fixtures/cli-keystore-dilithium3.json b/tests/fixtures/cli-keystore-dilithium3.json index 313d8b1..e5f3326 100644 --- a/tests/fixtures/cli-keystore-dilithium3.json +++ b/tests/fixtures/cli-keystore-dilithium3.json @@ -1,6 +1,6 @@ { "version": 1, - "address": "pq1q9x2sw9rs3xmfkjx5mc5cwe7g8gnzh8eduxhary8", + "address": "0xf08c73839a8c42e2d422e67ed5f5fa1a6c6dc58fb676c3d1eb11c0a2e74b640d", "key_type": "dilithium3", "kdf": "argon2id", "kdf_params": { diff --git a/tests/fixtures/cli-keystore-mldsa65.json b/tests/fixtures/cli-keystore-mldsa65.json index 4727325..fff1f27 100644 --- a/tests/fixtures/cli-keystore-mldsa65.json +++ b/tests/fixtures/cli-keystore-mldsa65.json @@ -1,6 +1,6 @@ { "version": 1, - "address": "pq1q9ar5l7efkhnvazed7chux3hg32g8593egzzr4kp", + "address": "0x218815e687296169746dcdd1e583dd1692cf98baa9659c016b9db8e966fc7b3c", "key_type": "mldsa65", "kdf": "argon2id", "kdf_params": { diff --git a/tests/fixtures/rust-compatibility.json b/tests/fixtures/rust-compatibility.json index b68bdcb..1c6c49f 100644 --- a/tests/fixtures/rust-compatibility.json +++ b/tests/fixtures/rust-compatibility.json @@ -2,29 +2,26 @@ "addresses": [ { "algo_id": 0, - "hex_address": "0x41d30ed209e4251b116aa74ed66bb0437ade4f58", "name": "algo0_ab_64", - "pq_address": "pq1q9qaxrkjp8jz2xc3d2n5a4ntkpph4hj0tq4fydzl", + "pq_address": "0x0b871be37a4af6348d28a90dbecc96820aa50d5178b7cbd7ced3d710e5a7eda9", "public_key_hex": "abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab" }, { "algo_id": 2, - "hex_address": "0x9557c6974396a498cb055c086a489486a13c4c28", "name": "algo2_ab_64", - "pq_address": "pq1qx24035hgwt2fxxtq4wqs6jgjjr2z0zv9qc3duxr", + "pq_address": "0x974426bd7445ef01f57559791d73279a02c79daece774acce3c2e549454cd96b", "public_key_hex": "abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab" }, { "algo_id": 1, - "hex_address": "0x3f2f395a30a6044828e6491a3ec0e5d9708c79cb", "name": "algo1_11_32", - "pq_address": "pq1qylj7w26xznqgjpguey350kquhvhprreev7l94g6", + "pq_address": "0xbfef6b48a547575eb442a32f96668d908821eb38022d20cb203bc19a040df2b3", "public_key_hex": "1111111111111111111111111111111111111111111111111111111111111111" } ], "transactions": [ { - "hash_hex": "0x86d348258f341016af1fc040bfeaaf7c889920da41733367c78d707612159586", + "hash_hex": "0x2b6a097d446710be8398814e1d98de63285ef0ae1b72867c815c460a9fa979cd", "name": "plain_transfer", "tx": { "access_list": null, @@ -36,18 +33,18 @@ "max_fee_per_gas": 1000000000, "max_priority_fee_per_gas": 100000000, "nonce": 7, - "to": "pq1q9qaxrkjp8jz2xc3d2n5a4ntkpph4hj0tq4fydzl", + "to": "0x0b871be37a4af6348d28a90dbecc96820aa50d5178b7cbd7ced3d710e5a7eda9", "tx_type": 2, "value": "0x2a" } }, { - "hash_hex": "0x7e3fe70d96f4948090a09ce10ca5f2d77b68abfd408450afaa139af077a69805", + "hash_hex": "0xfabfe79361da08c7c5655c1b8b4376b8c670896c8f57febbe9fb9e5376eb453c", "name": "access_list_call", "tx": { "access_list": [ { - "address": "pq1qy3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygnchxar", + "address": "0x0000000000000000000000002222222222222222222222222222222222222222", "storage_keys": [ "0x3333333333333333333333333333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444444444444444444444444444" @@ -62,7 +59,7 @@ "max_fee_per_gas": 2000000000, "max_priority_fee_per_gas": 250000000, "nonce": 8, - "to": "pq1qylj7w26xznqgjpguey350kquhvhprreev7l94g6", + "to": "0xbfef6b48a547575eb442a32f96668d908821eb38022d20cb203bc19a040df2b3", "tx_type": 2, "value": "0xf4240" } diff --git a/tests/keystore-compat.test.mjs b/tests/keystore-compat.test.mjs index f7ce829..588ce25 100644 --- a/tests/keystore-compat.test.mjs +++ b/tests/keystore-compat.test.mjs @@ -35,22 +35,22 @@ test('ks-3: parseEncryptedKey reads CLI ML-DSA-65 keystore metadata', () => { assert.equal(cliMlDsa65.kdf, 'argon2id', 'kdf must be argon2id'); assert.equal(cliMlDsa65.cipher, 'xchacha20-poly1305', 'cipher must be xchacha20-poly1305'); - // Address stored by CLI must be pq1… bech32m format (F-PQ1-ONLY) + // Address stored in fixture must be 0x + 64-char hex (v0.23.0 canonical format) assert.ok( - cliMlDsa65.address.startsWith('pq1'), - 'CLI keystore address must be pq1 bech32m format', + cliMlDsa65.address.startsWith('0x'), + 'CLI keystore address must be 0x hex format', ); assert.equal(parsed.signatureType, 'ML-DSA-65'); assert.equal(parsed.algorithmId, 1, 'ML-DSA-65 algo_id must be 1'); assert.equal(parsed.publicKey.length, 1952, 'ML-DSA-65 public key must be 1952 bytes'); - assert.ok(parsed.canonicalAddress.startsWith('pq1'), 'canonical address must be pq1 bech32'); + assert.ok(parsed.canonicalAddress.startsWith('0x'), 'canonical address must be 0x hex format'); }); test('ks-3: decryptKeystore decrypts CLI ML-DSA-65 keystore', async () => { const signer = await decryptKeystore(cliMlDsa65, CLI_PASSWORD); - assert.ok(signer.getAddress().startsWith('pq1'), 'decrypted address must be pq1 bech32'); + assert.ok(signer.getAddress().startsWith('0x'), 'decrypted address must be 0x hex format'); assert.equal(signer.algorithmId, 1, 'algorithm id must be 1 (ML-DSA-65)'); const parsed = parseEncryptedKey(cliMlDsa65); assert.equal(signer.getAddress(), parsed.canonicalAddress, 'address must match keystore metadata'); @@ -83,8 +83,8 @@ test('ks-3: parseEncryptedKey reads CLI Dilithium3 keystore metadata', () => { assert.equal(cliDilithium3.key_type, 'dilithium3', 'key_type must be dilithium3'); assert.ok( - cliDilithium3.address.startsWith('pq1'), - 'CLI Dilithium3 keystore address must be pq1 bech32m format', + cliDilithium3.address.startsWith('0x'), + 'CLI Dilithium3 keystore address must be 0x hex format', ); assert.equal(parsed.signatureType, 'Dilithium3'); @@ -95,7 +95,7 @@ test('ks-3: parseEncryptedKey reads CLI Dilithium3 keystore metadata', () => { test('ks-3: decryptKeystore decrypts CLI Dilithium3 keystore', async () => { const signer = await decryptKeystore(cliDilithium3, CLI_PASSWORD); - assert.ok(signer.getAddress().startsWith('pq1'), 'Dilithium3 address must be pq1 bech32'); + assert.ok(signer.getAddress().startsWith('0x'), 'Dilithium3 address must be 0x hex format'); assert.equal(signer.algorithmId, 0, 'algorithm id must be 0 (Dilithium3)'); const parsed = parseEncryptedKey(cliDilithium3); diff --git a/tests/node.integration.test.mjs b/tests/node.integration.test.mjs index c4ad61a..24bb016 100644 --- a/tests/node.integration.test.mjs +++ b/tests/node.integration.test.mjs @@ -65,12 +65,14 @@ test('node integration: tampered keystore address is rejected', async () => { password: 'correct horse battery', }); + const addr = signer.getAddress(); + const lastChar = addr[addr.length - 1]; const tampered = { ...keystore, - address: signer.getAddress().slice(0, -2) + (signer.getAddress().endsWith('0') ? '1' : '0'), + address: addr.slice(0, -1) + (lastChar === '0' ? '1' : '0'), }; await assert.rejects( () => decryptKeystore(tampered, 'correct horse battery'), - /address mismatch|bech32m/i, + /address mismatch/i, ); });