From 353833e1785b6fba99ad0454fa499834ccf6b97f Mon Sep 17 00:00:00 2001 From: zkbenny <benny@zklink.org> Date: Sun, 1 Sep 2024 19:37:34 +0800 Subject: [PATCH] feat: add PasskeyBinder --- .../scripts/upgrade-consistency-checker.ts | 8 +- system-contracts/SystemContractsHashes.json | 11 +- system-contracts/contracts/DefaultAccount.sol | 5 +- system-contracts/contracts/PasskeyBinder.sol | 127 ++++++++++++++++++ .../contracts/interfaces/IPasskeyBinder.sol | 11 ++ system-contracts/scripts/constants.ts | 5 + 6 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 system-contracts/contracts/PasskeyBinder.sol create mode 100644 system-contracts/contracts/interfaces/IPasskeyBinder.sol diff --git a/l1-contracts/scripts/upgrade-consistency-checker.ts b/l1-contracts/scripts/upgrade-consistency-checker.ts index 3ccb9db64..932c68ea0 100644 --- a/l1-contracts/scripts/upgrade-consistency-checker.ts +++ b/l1-contracts/scripts/upgrade-consistency-checker.ts @@ -58,16 +58,16 @@ const maxNumberOfHyperchains = 100; const expectedStoredBatchHashZero = "0x1574fa776dec8da2071e5f20d71840bfcbd82c2bca9ad68680edfedde1710bc4"; const expectedL2BridgeAddress = "0x11f943b2c77b743AB90f4A0Ae7d5A4e7FCA3E102"; const expectedL1LegacyBridge = "0x57891966931Eb4Bb6FB81430E6cE0A03AAbDe063"; -const expectedGenesisBatchCommitment = "0x2d00e5f8d77afcebf58a6b82ae56ba967566fe7dfbcb6760319fb0d215d18ffd"; -const expectedIndexRepeatedStorageChanges = BigNumber.from(54); +const expectedGenesisBatchCommitment = "0x667177606c5d72ce5988172b151b0a97e6cd67de002f86ec66c3899cd9ce7d4c"; +const expectedIndexRepeatedStorageChanges = BigNumber.from(56); const expectedProtocolVersion = BigNumber.from(2).pow(32).mul(24); -const expectedGenesisRoot = "0xabdb766b18a479a5c783a4b80e12686bc8ea3cc2d8a3050491b701d72370ebb5"; +const expectedGenesisRoot = "0x2e86468e2aa39e313daed4f4ea1865ef11876cc700fea35a1695de22af99915b"; const expectedRecursionNodeLevelVkHash = "0xf520cd5b37e74e19fdb369c8d676a04dce8a19457497ac6686d2bb95d94109c8"; const expectedRecursionLeafLevelVkHash = "0xf9664f4324c1400fa5c3822d667f30e873f53f1b8033180cd15fe41c1e2355c6"; const expectedRecursionCircuitsSetVksHash = "0x0000000000000000000000000000000000000000000000000000000000000000"; const expectedBootloaderHash = "0x010008e742608b21bf7eb23c1a9d0602047e3618b464c9b59c0fba3b3d7ab66e"; -const expectedDefaultAccountHash = "0x01000563374c277a2c1e34659a2a1e87371bb6d852ce142022d497bfb50b9e32"; +const expectedDefaultAccountHash = "0x01000567eb1d0eac3e32d1a5b5a0ececcbaf7a0b38b3fd7ce1eb8ff8296ef544"; const validatorOne = process.env.ETH_SENDER_SENDER_OPERATOR_COMMIT_ETH_ADDR!; diff --git a/system-contracts/SystemContractsHashes.json b/system-contracts/SystemContractsHashes.json index 7cb1c5b9c..d70a272a8 100644 --- a/system-contracts/SystemContractsHashes.json +++ b/system-contracts/SystemContractsHashes.json @@ -45,8 +45,8 @@ "contractName": "DefaultAccount", "bytecodePath": "artifacts-zk/contracts-preprocessed/DefaultAccount.sol/DefaultAccount.json", "sourceCodePath": "contracts-preprocessed/DefaultAccount.sol", - "bytecodeHash": "0x01000563374c277a2c1e34659a2a1e87371bb6d852ce142022d497bfb50b9e32", - "sourceCodeHash": "0xa42423712ddaa8f357d26e46825fda80a9a870d0ac7ff52c98884355f1173ec7" + "bytecodeHash": "0x01000567eb1d0eac3e32d1a5b5a0ececcbaf7a0b38b3fd7ce1eb8ff8296ef544", + "sourceCodeHash": "0x1a601a1c617c81daf95a03933b436987a03d6984ed6a49e71310dc706449e2fc" }, { "contractName": "EmptyContract", @@ -97,6 +97,13 @@ "bytecodeHash": "0x010000e563d4ad7b4822cc19d8f74f2c41ee3d3153379be4b02b27d4498d52b6", "sourceCodeHash": "0x91847512344ac5026e9fd396189c23ad9e253f22cb6e2fe65805c20c915797d4" }, + { + "contractName": "PasskeyBinder", + "bytecodePath": "artifacts-zk/contracts-preprocessed/PasskeyBinder.sol/PasskeyBinder.json", + "sourceCodePath": "contracts-preprocessed/PasskeyBinder.sol", + "bytecodeHash": "0x010001ddd2d9e5935a6fff10d41e305a9ff33340cdcf15536546332efa3e3c68", + "sourceCodeHash": "0xa331e26de173330a95f7f6eec7a2ad66379d783c996d12eea3d5d6bb9efce6a4" + }, { "contractName": "PubdataChunkPublisher", "bytecodePath": "artifacts-zk/contracts-preprocessed/PubdataChunkPublisher.sol/PubdataChunkPublisher.json", diff --git a/system-contracts/contracts/DefaultAccount.sol b/system-contracts/contracts/DefaultAccount.sol index 4c7356dd8..6f78ad3ef 100644 --- a/system-contracts/contracts/DefaultAccount.sol +++ b/system-contracts/contracts/DefaultAccount.sol @@ -3,11 +3,12 @@ pragma solidity 0.8.20; import {IAccount, ACCOUNT_VALIDATION_SUCCESS_MAGIC} from "./interfaces/IAccount.sol"; +import {IPasskeyBinder} from "./interfaces/IPasskeyBinder.sol"; import {TransactionHelper, Transaction} from "./libraries/TransactionHelper.sol"; import {SystemContractsCaller} from "./libraries/SystemContractsCaller.sol"; import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; import {EfficientCall} from "./libraries/EfficientCall.sol"; -import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, INonceHolder} from "./Constants.sol"; +import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, INonceHolder, SYSTEM_CONTRACTS_OFFSET} from "./Constants.sol"; import {Utils} from "./libraries/Utils.sol"; /** @@ -21,6 +22,8 @@ import {Utils} from "./libraries/Utils.sol"; contract DefaultAccount is IAccount { using TransactionHelper for *; + IPasskeyBinder public constant PASSKEY_BINDER = IPasskeyBinder(address(SYSTEM_CONTRACTS_OFFSET + 0xff)); + /** * @dev Simulate the behavior of the EOA if the caller is not the bootloader. * Essentially, for all non-bootloader callers halt the execution with empty return data. diff --git a/system-contracts/contracts/PasskeyBinder.sol b/system-contracts/contracts/PasskeyBinder.sol new file mode 100644 index 000000000..301453cde --- /dev/null +++ b/system-contracts/contracts/PasskeyBinder.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +contract PasskeyBinder { + //curve prime field modulus + uint256 private constant p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF; + //short weierstrass second coefficient + uint256 private constant b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B; + //short weierstrass first coefficient + uint256 private constant a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC; + + struct P256PublicKey { + uint256 x; + uint256 y; + } + + mapping(bytes32 keyIdHash => P256PublicKey) private authorizedKeys; + mapping(bytes32 keyIdHash => address account) private keyIdHashToAccount; + mapping(address account => string[] keyIds) private accountToKeyIdList; + + /// @dev Event emitted when a P256 key is added + event AddedP256Key(bytes32 indexed keyIdHash, string keyId, uint256 x, uint256 y); + + /// @dev Event emitted when a P256 key is removed + event RemovedP256Key(bytes32 indexed keyIdHash, uint256 x, uint256 y); + + /// @dev Error emitted when a P256 key is not on the curve + error KeyNotOnCurve(uint256 x, uint256 y); + /// @dev Error emitted when an empty key is attempted to be added + error InvalidEmptyKey(); + /// @dev Error emitted when a P256 key is already stored and attempted to be added + error KeyAlreadyExists(string keyId); + /// @dev Error emitted when a P256 key is not stored and attempted to be removed + error KeyDoesNotExist(string keyId); + /// @dev Error emitted when a P256 key is not owned by the caller + error DoesNotOwner(string keyId); + + function addKey(string calldata _keyId, uint256 _x, uint256 _y) external { + // slither-disable-next-line tx-origin + require(msg.sender == tx.origin, "Not authorized"); + _addKey(_keyId, _x, _y); + } + + function _addKey(string calldata _keyId, uint256 _x, uint256 _y) internal { + if (!isValidPublicKey(_x, _y)) revert KeyNotOnCurve(_x, _y); + bytes32 keyIdHash_ = keccak256(abi.encodePacked(_keyId)); + + if (bytes(_keyId).length == 0) revert InvalidEmptyKey(); + + P256PublicKey storage publicKey_ = authorizedKeys[keyIdHash_]; + + // update key + if (publicKey_.x != 0 || publicKey_.y != 0) { + revert KeyAlreadyExists(_keyId); + } + + authorizedKeys[keyIdHash_] = P256PublicKey(_x, _y); + keyIdHashToAccount[keyIdHash_] = msg.sender; + accountToKeyIdList[msg.sender].push(_keyId); + + emit AddedP256Key(keyIdHash_, _keyId, _x, _y); + } + + function removeKey(string calldata _keyId) external { + bytes32 keyIdHash_ = keccak256(abi.encodePacked(_keyId)); + if (keyIdHashToAccount[keyIdHash_] != msg.sender) revert DoesNotOwner(_keyId); + P256PublicKey memory publicKey_ = authorizedKeys[keyIdHash_]; + uint256 x_ = publicKey_.x; + uint256 y_ = publicKey_.y; + + if (x_ == 0 && y_ == 0) revert KeyDoesNotExist(_keyId); + + delete authorizedKeys[keyIdHash_]; + delete keyIdHashToAccount[keyIdHash_]; + uint256 length = accountToKeyIdList[msg.sender].length; + for (uint256 i = 0; i < length; i++) { + if (keccak256(abi.encodePacked(accountToKeyIdList[msg.sender][i])) == keyIdHash_) { + accountToKeyIdList[msg.sender][i] = accountToKeyIdList[msg.sender][length - 1]; + accountToKeyIdList[msg.sender].pop(); + break; + } + } + + emit RemovedP256Key(keyIdHash_, x_, y_); + } + /** + * @notice Returns the P256 public key coordinates of a given key ID if it is a signer + * @param keyIdHash The ID Hash of the key to get + * @return x_ The X value of the public key + * @return y_ The Y value of the public key + */ + function getKey(bytes32 keyIdHash) external view returns (uint256 x_, uint256 y_) { + P256PublicKey memory publicKey_ = authorizedKeys[keyIdHash]; + x_ = publicKey_.x; + y_ = publicKey_.y; + } + + function getKeyIdLength(address _account) external view returns (uint256) { + return accountToKeyIdList[_account].length; + } + + function getKeyIdByIndex(address _account, uint256 _index) external view returns (string memory) { + return accountToKeyIdList[_account][_index]; + } + + function getAccountByKeyIdHash(bytes32 keyIdHash) external view returns (address) { + return keyIdHashToAccount[keyIdHash]; + } + + function getP256PublicKey(bytes32 keyIdHash) external view returns (P256PublicKey memory) { + return authorizedKeys[keyIdHash]; + } + + function isValidPublicKey(uint256 x, uint256 y) internal pure returns (bool) { + if (x >= p || y >= p || ((x == 0) && (y == 0))) { + return false; + } + unchecked { + uint256 LHS = mulmod(y, y, p); // y^2 + uint256 RHS = addmod(mulmod(mulmod(x, x, p), x, p), mulmod(x, a, p), p); // x^3+ax + RHS = addmod(RHS, b, p); // x^3 + a*x + b + + return LHS == RHS; + } + } +} diff --git a/system-contracts/contracts/interfaces/IPasskeyBinder.sol b/system-contracts/contracts/interfaces/IPasskeyBinder.sol new file mode 100644 index 000000000..accf6adaa --- /dev/null +++ b/system-contracts/contracts/interfaces/IPasskeyBinder.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +interface IPasskeyBinder { + /// @dev Returns the public key associated with the given keyIdHash. + /// @param keyIdHash The hash of the keyId. + /// @return x_ The x-coordinate of the public key. + /// @return y_ The y-coordinate of the public key. + function getKey(bytes32 keyIdHash) external view returns (uint256 x_, uint256 y_); +} diff --git a/system-contracts/scripts/constants.ts b/system-contracts/scripts/constants.ts index 406b4cb6e..84a7e0d7c 100644 --- a/system-contracts/scripts/constants.ts +++ b/system-contracts/scripts/constants.ts @@ -166,6 +166,11 @@ export const SYSTEM_CONTRACTS: ISystemContracts = { codeName: "PubdataChunkPublisher", lang: Language.Solidity, }, + passkeyBinder: { + address: "0x00000000000000000000000000000000000080ff", + codeName: "PasskeyBinder", + lang: Language.Solidity, + }, create2Factory: { // This is explicitly a non-system-contract address. // We do not use the same address as create2 factories on EVM, since