From 47620f340353162ac37158f0526e66043dd14e6f Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Thu, 6 Aug 2026 13:18:24 +0530 Subject: [PATCH] refactor: drop Waku from the key registry naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry stores a plain secp256k1 public key and says nothing about how the encrypted payload reaches the recipient. Naming it after Waku was accurate only while Waku was the transport; now it just misleads. registerWakuPublicKey -> registerPublicKey getWakuPublicKey -> getPublicKey wakuPublicKeys -> messagingPublicKeys WakuKeyRegistered -> PublicKeyRegistered InvalidWakuKey -> InvalidPublicKey BREAKING — requires redeployment. Renaming external functions changes their selectors, so no already-deployed Chainvoice answers to this ABI. The registry is contract storage, so a redeploy also starts it empty: every user has to sign and register again regardless of what this commit does. That re-registration is what makes it safe to also fix the derivation message, which still read "ChainVoice Waku Key Derivation v1". Since everyone must re-register anyway, changing it costs nothing extra, and leaving it would have kept a Waku reference in the one constant that can never be changed casually. Bumped to v2 to make the break explicit. .env.example is blanked rather than left pointing at 0x7bC4…, which no longer matches this ABI and would fail confusingly at the first call. Contract suite: 28 passed. --- Deployments.md | 6 ++ README.md | 16 +++-- contracts/src/Chainvoice.sol | 29 ++++---- contracts/test/Chainvoice.t.sol | 50 +++++++------- frontend/.env.example | 7 +- frontend/README.md | 9 ++- frontend/src/contractsABI/ChainvoiceABI.js | 68 +++++++++---------- .../src/services/relay/relayKeyManager.js | 21 +++--- .../tests/services/relayKeyManager.test.js | 12 ++-- 9 files changed, 120 insertions(+), 98 deletions(-) diff --git a/Deployments.md b/Deployments.md index b1424785..7bed8a0f 100644 --- a/Deployments.md +++ b/Deployments.md @@ -10,6 +10,12 @@ as well as the constructor parameters that have been used. | Polygon | 137 | [v1](https://github.com/StabilityNexus/Chainvoice/releases/tag/v1) | `0xD044A85a5daC307217B9bF313A90E8a60AF7DdCe` | None — constructor takes no arguments (`owner = msg.sender`, `fee` hardcoded to `0.0005 ether`) | Mainnet | | Ethereum Sepolia | 11155111 | [v1](https://github.com/StabilityNexus/Chainvoice/releases/tag/v1) | `0x54a542dCDC306eE281b5De4613EcEfe6e6ABc562` | None — constructor takes no arguments (`owner = msg.sender`, `fee` hardcoded to `0.0005 ether`) | Testnet | +> ⚠️ **No deployment currently matches the contract in this repo.** The key +> registry functions were renamed (`registerPublicKey` / `getPublicKey`), which +> changed their selectors, so every address below is v1 only and incompatible +> with the current ABI. A fresh deployment is required, and its registry starts +> empty — every user must register their public key again. + --- **Note to Developers:** After making a new deployment, please: 1. create a git tag for the deployed version; diff --git a/README.md b/README.md index 490dbde9..b8e29fcc 100644 --- a/README.md +++ b/README.md @@ -150,8 +150,8 @@ npm run dev ### Frontend Configuration (`frontend/.env`) ```.env -#Ethereum Sepolia (11155111) -VITE_CONTRACT_ADDRESS_11155111=0x7bC4C5abb5b1B8355Aa65307C1cFDbe6254505d2 +#Ethereum Sepolia (11155111) — blank until redeployed, see note below +VITE_CONTRACT_ADDRESS_11155111= #Ethereum Classic (61) — blank until redeployed, see note below VITE_CONTRACT_ADDRESS_61= #Polygon Mainnet (137) — blank until redeployed, see note below @@ -160,11 +160,18 @@ VITE_CONTRACT_ADDRESS_137= VITE_WALLETCONNECT_PROJECT_ID=Your Project ID can be obtained from https://dashboard.reown.com/ ``` +> ⚠️ **Redeployment required.** Renaming the key registry functions changed +> their selectors, so no previously deployed Chainvoice matches the current ABI. +> Deploy `contracts/src/Chainvoice.sol` and fill in the addresses above, then +> record it in [Deployments.md](./Deployments.md). Registered keys do not carry +> over — every user must register again. + > ⚠️ Ethereum Classic and Polygon are left blank on purpose. Both still run the > v1 contract, which stores invoice payloads on-chain as strings and has no > public key registry, so it does not match the current ABI. The app treats any > non-empty address as supported, so filling these in would send calls those > contracts cannot decode. Populate them only after redeploying. + > ⚠️ **Security Note:** Never commit `.env` files to version control. Keep your private keys secure. ### Relay configuration @@ -186,9 +193,8 @@ VITE_RELAY_TIMEOUT_MS= ### Current (hash-based invoice storage) Stores only `keccak256` of the invoice data on-chain and exposes the public key -registry. This is the deployment the frontend is configured against. -- Ethereum Sepolia (11155111) -```0x7bC4C5abb5b1B8355Aa65307C1cFDbe6254505d2``` +registry. Awaiting redeployment after the key registry rename — see the note +under Environment Variables. ### v1 (Mainnet Deployment — Jan 1) Stores the invoice payload on-chain as strings. Superseded, kept for reference. diff --git a/contracts/src/Chainvoice.sol b/contracts/src/Chainvoice.sol index 2c8a0967..d2cd5219 100644 --- a/contracts/src/Chainvoice.sol +++ b/contracts/src/Chainvoice.sol @@ -38,7 +38,7 @@ contract Chainvoice { error TreasuryNotSet(); error NoFeesAvailable(); error WithdrawFailed(); - error InvalidWakuKey(); + error InvalidPublicKey(); error InvalidInvoiceHash(); // ========== Storage ========== @@ -61,8 +61,8 @@ contract Chainvoice { mapping(address => uint256[]) public sentInvoices; mapping(address => uint256[]) public receivedInvoices; - // ========== Waku Public Key Registry ========== - mapping(address => bytes) private wakuPublicKeys; + // ========== Messaging Public Key Registry ========== + mapping(address => bytes) private messagingPublicKeys; address public owner; address public treasuryAddress; @@ -76,7 +76,7 @@ contract Chainvoice { event InvoiceCancelled(uint256 indexed id, address indexed from, address indexed to, address tokenAddress); event InvoiceBatchCreated(address indexed creator, address indexed token, uint256 count, uint256[] ids); event InvoiceBatchPaid(address indexed payer, address indexed token, uint256 count, uint256 totalAmount, uint256[] ids); - event WakuKeyRegistered(address indexed user, bytes publicKey); + event PublicKeyRegistered(address indexed user, bytes publicKey); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event OwnershipTransferInitiated(address indexed currentOwner, address indexed pendingOwner); @@ -118,20 +118,23 @@ contract Chainvoice { return success; } - // ========== Waku Key Management ========== - /// @notice Register or update the caller's Waku ECIES public key. + // ========== Messaging Key Management ========== + /// @notice Register or update the caller's ECIES public key. + /// @dev Used by clients to encrypt invoice payloads for this address. The + /// registry is transport-agnostic: it says nothing about how the + /// encrypted payload is delivered. /// @param publicKey The uncompressed secp256k1 public key (65 bytes). - function registerWakuPublicKey(bytes calldata publicKey) external { - if (publicKey.length != 65 || publicKey[0] != 0x04) revert InvalidWakuKey(); - wakuPublicKeys[msg.sender] = publicKey; - emit WakuKeyRegistered(msg.sender, publicKey); + function registerPublicKey(bytes calldata publicKey) external { + if (publicKey.length != 65 || publicKey[0] != 0x04) revert InvalidPublicKey(); + messagingPublicKeys[msg.sender] = publicKey; + emit PublicKeyRegistered(msg.sender, publicKey); } - /// @notice Get a user's registered Waku public key. + /// @notice Get a user's registered ECIES public key. /// @param user The address to look up. /// @return The public key bytes (empty if not registered). - function getWakuPublicKey(address user) external view returns (bytes memory) { - return wakuPublicKeys[user]; + function getPublicKey(address user) external view returns (bytes memory) { + return messagingPublicKeys[user]; } // ========== Single-invoice create ========== diff --git a/contracts/test/Chainvoice.t.sol b/contracts/test/Chainvoice.t.sol index bf757ffb..0d45976c 100644 --- a/contracts/test/Chainvoice.t.sol +++ b/contracts/test/Chainvoice.t.sol @@ -12,7 +12,7 @@ contract ChainvoiceTest is Test { address bob = address(0xB0B); address charlie = address(0xC4A7); - event WakuKeyRegistered(address indexed user, bytes publicKey); + event PublicKeyRegistered(address indexed user, bytes publicKey); function setUp() public { chainvoice = new Chainvoice(); @@ -311,10 +311,10 @@ contract ChainvoiceTest is Test { } /* ------------------------------------------------------------ */ - /* WAKU KEY REGISTRY */ + /* MESSAGING KEY REGISTRY */ /* ------------------------------------------------------------ */ - function testRegisterWakuPublicKey() public { + function testRegisterPublicKey() public { // 65-byte uncompressed secp256k1 public key (0x04 prefix + 64 bytes) bytes memory pubKey = new bytes(65); pubKey[0] = 0x04; @@ -323,15 +323,15 @@ contract ChainvoiceTest is Test { } vm.prank(alice); - chainvoice.registerWakuPublicKey(pubKey); + chainvoice.registerPublicKey(pubKey); - bytes memory stored = chainvoice.getWakuPublicKey(alice); + bytes memory stored = chainvoice.getPublicKey(alice); assertEq(stored.length, 65); assertEq(stored[0], pubKey[0]); assertEq(stored[64], pubKey[64]); } - function testRegisterWakuPublicKey_EmitsEvent() public { + function testRegisterPublicKey_EmitsEvent() public { bytes memory pubKey = new bytes(65); pubKey[0] = 0x04; for (uint256 i = 1; i < 65; i++) { @@ -339,13 +339,13 @@ contract ChainvoiceTest is Test { } vm.expectEmit(true, false, false, true); - emit WakuKeyRegistered(alice, pubKey); + emit PublicKeyRegistered(alice, pubKey); vm.prank(alice); - chainvoice.registerWakuPublicKey(pubKey); + chainvoice.registerPublicKey(pubKey); } - function testUpdateWakuPublicKey() public { + function testUpdatePublicKey() public { bytes memory key1 = new bytes(65); key1[0] = 0x04; for (uint256 i = 1; i < 65; i++) key1[i] = bytes1(uint8(i)); @@ -355,21 +355,21 @@ contract ChainvoiceTest is Test { for (uint256 i = 1; i < 65; i++) key2[i] = bytes1(uint8(i + 50)); vm.startPrank(alice); - chainvoice.registerWakuPublicKey(key1); + chainvoice.registerPublicKey(key1); - bytes memory stored1 = chainvoice.getWakuPublicKey(alice); + bytes memory stored1 = chainvoice.getPublicKey(alice); assertEq(keccak256(stored1), keccak256(key1)); // Update to a new key - chainvoice.registerWakuPublicKey(key2); + chainvoice.registerPublicKey(key2); vm.stopPrank(); - bytes memory stored2 = chainvoice.getWakuPublicKey(alice); + bytes memory stored2 = chainvoice.getPublicKey(alice); assertEq(keccak256(stored2), keccak256(key2)); } - function testGetWakuPublicKey_Unregistered() public { - bytes memory stored = chainvoice.getWakuPublicKey(address(0xDEAD)); + function testGetPublicKey_Unregistered() public { + bytes memory stored = chainvoice.getPublicKey(address(0xDEAD)); assertEq(stored.length, 0); } @@ -383,21 +383,21 @@ contract ChainvoiceTest is Test { for (uint256 i = 1; i < 65; i++) bobKey[i] = bytes1(uint8(i + 50)); vm.prank(alice); - chainvoice.registerWakuPublicKey(aliceKey); + chainvoice.registerPublicKey(aliceKey); vm.prank(bob); - chainvoice.registerWakuPublicKey(bobKey); + chainvoice.registerPublicKey(bobKey); - assertEq(keccak256(chainvoice.getWakuPublicKey(alice)), keccak256(aliceKey)); - assertEq(keccak256(chainvoice.getWakuPublicKey(bob)), keccak256(bobKey)); + assertEq(keccak256(chainvoice.getPublicKey(alice)), keccak256(aliceKey)); + assertEq(keccak256(chainvoice.getPublicKey(bob)), keccak256(bobKey)); } - function testRegisterWakuPublicKey_RevertIfInvalidLength() public { + function testRegisterPublicKey_RevertIfInvalidLength() public { bytes memory shortKey = hex"04aabbccdd"; vm.prank(alice); - vm.expectRevert(Chainvoice.InvalidWakuKey.selector); - chainvoice.registerWakuPublicKey(shortKey); + vm.expectRevert(Chainvoice.InvalidPublicKey.selector); + chainvoice.registerPublicKey(shortKey); } function testCreateInvoice_RevertIfZeroHash() public { @@ -406,14 +406,14 @@ contract ChainvoiceTest is Test { chainvoice.createInvoice(bob, 1 ether, address(0), bytes32(0)); } - function testRegisterWakuPublicKey_RevertIfInvalidPrefix() public { + function testRegisterPublicKey_RevertIfInvalidPrefix() public { bytes memory badPrefixKey = new bytes(65); badPrefixKey[0] = 0x03; // wrong prefix, should be 0x04 for (uint256 i = 1; i < 65; i++) badPrefixKey[i] = bytes1(uint8(i)); vm.prank(alice); - vm.expectRevert(Chainvoice.InvalidWakuKey.selector); - chainvoice.registerWakuPublicKey(badPrefixKey); + vm.expectRevert(Chainvoice.InvalidPublicKey.selector); + chainvoice.registerPublicKey(badPrefixKey); } function testCreateInvoiceWithDataHash() public { diff --git a/frontend/.env.example b/frontend/.env.example index f53745fb..3ea1d0fd 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,7 +1,12 @@ # env-copy +# REDEPLOYMENT REQUIRED. Renaming the registry functions changed their +# selectors, so no currently deployed Chainvoice matches this ABI. Deploy the +# current contracts/src/Chainvoice.sol and put the new addresses here. +# Registered keys do not carry over — every user must register again. + #Ethereum Sepolia (11155111) -VITE_CONTRACT_ADDRESS_11155111=0x7bC4C5abb5b1B8355Aa65307C1cFDbe6254505d2 +VITE_CONTRACT_ADDRESS_11155111= # Left blank deliberately. Both chains still run the v1 contract, which stores # invoice payloads on-chain as strings and has no public key registry, so it diff --git a/frontend/README.md b/frontend/README.md index 35a7f8cc..8e0783ad 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -52,15 +52,20 @@ http://localhost:5173 The frontend reads Vite environment variables from `.env`. -The provided `.env.example` includes deployed contract addresses for supported networks and the WalletConnect project ID placeholder: +The provided `.env.example` lists the contract addresses per network alongside the WalletConnect project ID placeholder: ```env -VITE_CONTRACT_ADDRESS_11155111=0x7bC4C5abb5b1B8355Aa65307C1cFDbe6254505d2 +VITE_CONTRACT_ADDRESS_11155111= VITE_CONTRACT_ADDRESS_61= VITE_CONTRACT_ADDRESS_137= VITE_WALLETCONNECT_PROJECT_ID= ``` +> ⚠️ **Redeployment required.** Renaming the key registry functions changed +> their selectors, so no previously deployed Chainvoice matches the current ABI. +> Deploy `contracts/src/Chainvoice.sol` and fill in the addresses above. +> Registered keys do not carry over — every user must register again. + > ⚠️ Ethereum Classic and Polygon are left blank on purpose. Both still run the > v1 contract, which does not match the current ABI. The app treats any > non-empty address as supported, so filling these in would send calls those diff --git a/frontend/src/contractsABI/ChainvoiceABI.js b/frontend/src/contractsABI/ChainvoiceABI.js index 33cb6176..44a922fc 100644 --- a/frontend/src/contractsABI/ChainvoiceABI.js +++ b/frontend/src/contractsABI/ChainvoiceABI.js @@ -221,6 +221,25 @@ export const ChainvoiceABI = [ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getPublicKey", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getReceivedInvoices", @@ -343,25 +362,6 @@ export const ChainvoiceABI = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "getWakuPublicKey", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "initiateOwnershipTransfer", @@ -507,7 +507,7 @@ export const ChainvoiceABI = [ }, { "type": "function", - "name": "registerWakuPublicKey", + "name": "registerPublicKey", "inputs": [ { "name": "publicKey", @@ -833,38 +833,38 @@ export const ChainvoiceABI = [ }, { "type": "event", - "name": "TreasuryAddressUpdated", + "name": "PublicKeyRegistered", "inputs": [ { - "name": "previousTreasury", + "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { - "name": "newTreasury", - "type": "address", - "indexed": true, - "internalType": "address" + "name": "publicKey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" } ], "anonymous": false }, { "type": "event", - "name": "WakuKeyRegistered", + "name": "TreasuryAddressUpdated", "inputs": [ { - "name": "user", + "name": "previousTreasury", "type": "address", "indexed": true, "internalType": "address" }, { - "name": "publicKey", - "type": "bytes", - "indexed": false, - "internalType": "bytes" + "name": "newTreasury", + "type": "address", + "indexed": true, + "internalType": "address" } ], "anonymous": false @@ -926,12 +926,12 @@ export const ChainvoiceABI = [ }, { "type": "error", - "name": "InvalidToken", + "name": "InvalidPublicKey", "inputs": [] }, { "type": "error", - "name": "InvalidWakuKey", + "name": "InvalidToken", "inputs": [] }, { diff --git a/frontend/src/services/relay/relayKeyManager.js b/frontend/src/services/relay/relayKeyManager.js index cf290415..4d5cfd78 100644 --- a/frontend/src/services/relay/relayKeyManager.js +++ b/frontend/src/services/relay/relayKeyManager.js @@ -3,13 +3,14 @@ import { ethers } from 'ethers'; /** * Message the user signs to derive their messaging keypair. * - * Do not change this string. The derived public key is what users have - * already registered in the on-chain registry; a different message derives - * a different key, silently breaking decryption for everyone who registered - * before the change. It is named after Waku for historical reasons only — - * the derivation itself is transport-independent. + * Treat this as a wire-compatibility constant: the derived public key is + * what users register on-chain, and a different message derives a different + * key, silently breaking decryption for anyone who registered under the old + * one. It may only change alongside a contract redeployment, which clears + * the registry and forces everyone to re-register anyway. Bump the version + * suffix if that ever happens again. */ -const DERIVATION_MESSAGE = 'ChainVoice Waku Key Derivation v1'; +const DERIVATION_MESSAGE = 'ChainVoice Messaging Key Derivation v2'; const KEY_STORAGE_PREFIX = 'chainvoice_relay_keys_'; /** secp256k1 key sizes, as the on-chain registry validates them. */ @@ -269,16 +270,12 @@ export function hasCachedKeys(address) { /** * Register the user's messaging public key on-chain. * - * The contract method is still named `registerWakuPublicKey`; it is a - * transport-agnostic secp256k1 key registry and is already deployed, so - * the name is kept as-is. - * * @param {import('ethers').Contract} contract - Chainvoice contract instance * @param {Uint8Array} publicKey - the user's public key * @returns {Promise} */ export async function registerPublicKeyOnChain(contract, publicKey) { - const tx = await contract.registerWakuPublicKey(bytesToHex(publicKey)); + const tx = await contract.registerPublicKey(bytesToHex(publicKey)); return await tx.wait(); } @@ -290,7 +287,7 @@ export async function registerPublicKeyOnChain(contract, publicKey) { * @returns {Promise} - the public key bytes, or null if not registered */ export async function fetchPublicKeyFromChain(contract, userAddress) { - const keyHex = await contract.getWakuPublicKey(userAddress); + const keyHex = await contract.getPublicKey(userAddress); if (!keyHex || keyHex === '0x' || keyHex === '0x0' || keyHex.length <= 2) { return null; } diff --git a/frontend/tests/services/relayKeyManager.test.js b/frontend/tests/services/relayKeyManager.test.js index 0d9d6043..f17c3a8a 100644 --- a/frontend/tests/services/relayKeyManager.test.js +++ b/frontend/tests/services/relayKeyManager.test.js @@ -149,7 +149,7 @@ describe("fetchPublicKeyFromChain", () => { // This decides whether the sender encrypts a payload or falls back to the // on-chain summary, so every "unregistered" shape has to be recognised. const contractReturning = (value) => ({ - getWakuPublicKey: jest.fn().mockResolvedValue(value), + getPublicKey: jest.fn().mockResolvedValue(value), }); it.each(["0x", "0x0", ""])( @@ -190,7 +190,7 @@ describe("fetchPublicKeyFromChain", () => { it("looks up the address it was given", async () => { const contract = contractReturning("0x"); await fetchPublicKeyFromChain(contract, ADDRESS); - expect(contract.getWakuPublicKey).toHaveBeenCalledWith(ADDRESS); + expect(contract.getPublicKey).toHaveBeenCalledWith(ADDRESS); }); }); @@ -199,12 +199,12 @@ describe("registerPublicKeyOnChain", () => { const { publicKey } = await deriveRelayKeyPair(makeSigner(), ADDRESS); const wait = jest.fn().mockResolvedValue({ status: 1 }); const contract = { - registerWakuPublicKey: jest.fn().mockResolvedValue({ wait }), + registerPublicKey: jest.fn().mockResolvedValue({ wait }), }; const receipt = await registerPublicKeyOnChain(contract, publicKey); - expect(contract.registerWakuPublicKey).toHaveBeenCalledWith(bytesToHex(publicKey)); + expect(contract.registerPublicKey).toHaveBeenCalledWith(bytesToHex(publicKey)); expect(wait).toHaveBeenCalled(); expect(receipt).toEqual({ status: 1 }); }); @@ -212,14 +212,14 @@ describe("registerPublicKeyOnChain", () => { it("submits a 65-byte 0x04-prefixed key, as the contract requires", async () => { const { publicKey } = await deriveRelayKeyPair(makeSigner(), ADDRESS); const contract = { - registerWakuPublicKey: jest + registerPublicKey: jest .fn() .mockResolvedValue({ wait: jest.fn().mockResolvedValue({}) }), }; await registerPublicKeyOnChain(contract, publicKey); - const submitted = contract.registerWakuPublicKey.mock.calls[0][0]; + const submitted = contract.registerPublicKey.mock.calls[0][0]; expect(submitted).toMatch(/^0x04[0-9a-f]{128}$/); }); });