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}$/); }); });