Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passkey support #16

Merged
merged 19 commits into from
Sep 6, 2024
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
6 changes: 3 additions & 3 deletions l1-contracts/scripts/upgrade-consistency-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@ const maxNumberOfHyperchains = 100;
const expectedStoredBatchHashZero = "0x1574fa776dec8da2071e5f20d71840bfcbd82c2bca9ad68680edfedde1710bc4";
const expectedL2BridgeAddress = "0x11f943b2c77b743AB90f4A0Ae7d5A4e7FCA3E102";
const expectedL1LegacyBridge = "0x57891966931Eb4Bb6FB81430E6cE0A03AAbDe063";
const expectedGenesisBatchCommitment = "0x667177606c5d72ce5988172b151b0a97e6cd67de002f86ec66c3899cd9ce7d4c";
const expectedGenesisBatchCommitment = "0xbac9e5a16fb537337fdd23693eef715c18349a695505580ace203c0ca1bd342f";
const expectedIndexRepeatedStorageChanges = BigNumber.from(56);
const expectedProtocolVersion = BigNumber.from(2).pow(32).mul(24);

const expectedGenesisRoot = "0x2e86468e2aa39e313daed4f4ea1865ef11876cc700fea35a1695de22af99915b";
const expectedGenesisRoot = "0x7692f38725c1969ab55613dab4e74e12be95e66493528531144107870a6921fa";
const expectedRecursionNodeLevelVkHash = "0xf520cd5b37e74e19fdb369c8d676a04dce8a19457497ac6686d2bb95d94109c8";
const expectedRecursionLeafLevelVkHash = "0xf9664f4324c1400fa5c3822d667f30e873f53f1b8033180cd15fe41c1e2355c6";
const expectedRecursionCircuitsSetVksHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
const expectedBootloaderHash = "0x010008e742608b21bf7eb23c1a9d0602047e3618b464c9b59c0fba3b3d7ab66e";
const expectedDefaultAccountHash = "0x01000567eb1d0eac3e32d1a5b5a0ececcbaf7a0b38b3fd7ce1eb8ff8296ef544";
const expectedDefaultAccountHash = "0x010008af0fe0bacedf23ca0e881ca8554e4d879a81249c55fed6006740eae70c";

const validatorOne = process.env.ETH_SENDER_SENDER_OPERATOR_COMMIT_ETH_ADDR!;

Expand Down
8 changes: 4 additions & 4 deletions system-contracts/SystemContractsHashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
"contractName": "DefaultAccount",
"bytecodePath": "artifacts-zk/contracts-preprocessed/DefaultAccount.sol/DefaultAccount.json",
"sourceCodePath": "contracts-preprocessed/DefaultAccount.sol",
"bytecodeHash": "0x01000567eb1d0eac3e32d1a5b5a0ececcbaf7a0b38b3fd7ce1eb8ff8296ef544",
"sourceCodeHash": "0x1a601a1c617c81daf95a03933b436987a03d6984ed6a49e71310dc706449e2fc"
"bytecodeHash": "0x010008af0fe0bacedf23ca0e881ca8554e4d879a81249c55fed6006740eae70c",
"sourceCodeHash": "0x87b5bd0fbcf98e9d7dc4b2ece48e410ef9adbfaf3d7dc226f6a21709ac070a7f"
},
{
"contractName": "EmptyContract",
Expand Down Expand Up @@ -101,8 +101,8 @@
"contractName": "PasskeyBinder",
"bytecodePath": "artifacts-zk/contracts-preprocessed/PasskeyBinder.sol/PasskeyBinder.json",
"sourceCodePath": "contracts-preprocessed/PasskeyBinder.sol",
"bytecodeHash": "0x010001ddd2d9e5935a6fff10d41e305a9ff33340cdcf15536546332efa3e3c68",
"sourceCodeHash": "0xa331e26de173330a95f7f6eec7a2ad66379d783c996d12eea3d5d6bb9efce6a4"
"bytecodeHash": "0x010001159907d8c069d2b90b03a32980e5a063c1f1006585c5c1113b587d399e",
"sourceCodeHash": "0xd7343fc706c5ef7490419b655f4ebb27074dc6560a9f2d5faba9c8d9d433a8b8"
},
{
"contractName": "PubdataChunkPublisher",
Expand Down
111 changes: 109 additions & 2 deletions system-contracts/contracts/DefaultAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ 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 {PasskeyHelper, WebAuthnSignatureStruct, SINGLE_TX_R1_TYPE, MULTI_TX_R1_TYPE, MULTI_TX_K1_TYPE} from "./libraries/PasskeyHelper.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, SYSTEM_CONTRACTS_OFFSET} from "./Constants.sol";
import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, SYSTEM_CONTRACTS_OFFSET, INonceHolder} from "./Constants.sol";
import {Utils} from "./libraries/Utils.sol";

/**
Expand All @@ -22,8 +23,23 @@ import {Utils} from "./libraries/Utils.sol";
contract DefaultAccount is IAccount {
using TransactionHelper for *;

/// @notice Structure used to represent a zkSync's EIP-712 type transaction hash.
struct TransactionHashStruct {
// The hash of zkSync's EIP-712-signed transaction.
bytes32 txHash;
}

IPasskeyBinder public constant PASSKEY_BINDER = IPasskeyBinder(address(SYSTEM_CONTRACTS_OFFSET + 0xff));

/// @dev The EIP-712 typehash for the TransactionHash.
bytes32 constant TRANSACTION_HASH_TYPEHASH = keccak256("TransactionHash(bytes32 txHash)");

/// @dev The EIP-712 typehash for the multi transaction.
bytes32 constant MULTI_TRANSACTION_TYPEHASH =
keccak256(
"MultiTransaction(TransactionHash[] transactionHashes,UserOperationHash[] userOpHashes,ChainDomain[] userOpDomains)ChainDomain(string name,string version,uint256 chainId,address verifyingContract)TransactionHash(bytes32 txHash)UserOperationHash(bytes32 txHash)"
);

/**
* @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.
Expand Down Expand Up @@ -104,11 +120,98 @@ contract DefaultAccount is IAccount {
uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
require(totalRequiredBalance <= address(this).balance, "Not enough balance for fee + value");

if (_isValidSignature(txHash, _transaction.signature)) {
if (_validateSignature(txHash, _transaction.signature)) {
magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC;
}
}

/// @notice Verify signatures for different types of transactions.
/// @param _hash The hash of the current transaction to be signed.
/// @param _signature The signature for different types of transactions.
function _validateSignature(bytes32 _hash, bytes memory _signature) internal returns (bool) {
if (_signature.length == 65) {
return _isValidSignature(_hash, _signature);
} else if (_signature.length > 65) {
(bytes2 magicNum, bytes memory encodedSignature) = abi.decode(_signature, (bytes2, bytes));

if (magicNum == SINGLE_TX_R1_TYPE) {
(bytes32 credentialIdHash, WebAuthnSignatureStruct memory decodedSignature) = PasskeyHelper
.decodeWebAuthnP256Signature(encodedSignature);
(uint256 x, uint256 y) = _getPasskeyPublicKey(credentialIdHash);
return PasskeyHelper.verifyByP256Contract(_hash, decodedSignature, x, y);
} else if (magicNum == MULTI_TX_R1_TYPE) {
(bytes32 rootHash, bytes memory passkeySignature) = _decodeMultiTxRootHashAndSignature(
_hash,
encodedSignature
);

(bytes32 credentialIdHash, WebAuthnSignatureStruct memory decodedSignature) = PasskeyHelper
.decodeWebAuthnP256Signature(passkeySignature);
(uint256 x, uint256 y) = _getPasskeyPublicKey(credentialIdHash);

return PasskeyHelper.verifyByP256Contract(rootHash, decodedSignature, x, y);
} else if (magicNum == MULTI_TX_K1_TYPE) {
(bytes32 rootHash, bytes memory signature) = _decodeMultiTxRootHashAndSignature(
_hash,
encodedSignature
);
return _isValidSignature(rootHash, signature);
}
}

return false;
}

function _getPasskeyPublicKey(bytes32 _credentialIdHash) internal returns (uint256, uint256) {
bytes memory returnData = SystemContractsCaller.systemCallWithPropagatedRevert(
uint32(gasleft()),
address(PASSKEY_BINDER),
0,
abi.encodeCall(IPasskeyBinder.getAuthorizedKey, (_credentialIdHash))
);
(address passkeyOwner, uint256 x, uint256 y) = abi.decode(returnData, (address, uint256, uint256));
require(passkeyOwner == address(this), "Passkey is not owned by the account");
require(x != 0 && y != 0, "Passkey is not set");
return (x, y);
}

/// @notice Decode the root hash and signature for the multi transaction.
/// @param _hash The hash of the current transaction to be signed.
/// @param _encodedSignature The encoded signature.
function _decodeMultiTxRootHashAndSignature(
bytes32 _hash,
bytes memory _encodedSignature
) internal view returns (bytes32, bytes memory) {
(
bytes32 userOpsRootHash,
bytes32 userOpDomainsRootHash,
bytes memory txHashPrefix,
bytes memory txHashSuffix,
bytes memory signature
) = abi.decode(_encodedSignature, (bytes32, bytes32, bytes, bytes, bytes));

bytes32 txRootHash = keccak256(
abi.encodePacked(txHashPrefix, hash(TransactionHashStruct({txHash: _hash})), txHashSuffix)
);

bytes32 rootHashWithNonPrefix = keccak256(
abi.encode(MULTI_TRANSACTION_TYPEHASH, txRootHash, userOpsRootHash, userOpDomainsRootHash)
);

bytes32 domainSeparator = keccak256(
abi.encode(
TransactionHelper.EIP712_DOMAIN_TYPEHASH,
keccak256("ZKLink Nova Multi Transaction Validator"),
keccak256("0.1.0"),
block.chainid
)
);

bytes32 rootHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, rootHashWithNonPrefix));

return (rootHash, signature);
}

/// @notice Method called by the bootloader to execute the transaction.
/// @param _transaction The transaction to execute.
/// @dev It also accepts unused _txHash and _suggestedSignedHash parameters:
Expand Down Expand Up @@ -199,6 +302,10 @@ contract DefaultAccount is IAccount {
return recoveredAddress == address(this) && recoveredAddress != address(0);
}

function hash(TransactionHashStruct memory txHashStruct) internal pure returns (bytes32) {
return keccak256(abi.encode(TRANSACTION_HASH_TYPEHASH, txHashStruct.txHash));
}

/// @notice Method for paying the bootloader for the transaction.
/// @param _transaction The transaction for which the fee is paid.
/// @dev It also accepts unused _txHash and _suggestedSignedHash parameters:
Expand Down
156 changes: 87 additions & 69 deletions system-contracts/contracts/PasskeyBinder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

pragma solidity 0.8.20;

contract PasskeyBinder {
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IPasskeyBinder} from "./interfaces/IPasskeyBinder.sol";

contract PasskeyBinder is IPasskeyBinder {
using EnumerableSet for EnumerableSet.Bytes32Set;

//curve prime field modulus
uint256 private constant p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
//short weierstrass second coefficient
Expand All @@ -15,110 +20,123 @@ contract PasskeyBinder {
uint256 y;
}

mapping(bytes32 keyIdHash => P256PublicKey) private authorizedKeys;
mapping(bytes32 keyIdHash => address account) private keyIdHashToAccount;
mapping(address account => string[] keyIds) private accountToKeyIdList;
struct AuthorizedKey {
address owner;
P256PublicKey publicKey;
}

mapping(address account => EnumerableSet.Bytes32Set credentialIdHashSet) private accountToCredentialIdHashSet;
mapping(bytes32 credentialIdHash => AuthorizedKey) private authorizedKeys;

/// @dev Event emitted when a P256 key is added
event AddedP256Key(bytes32 indexed keyIdHash, string keyId, uint256 x, uint256 y);
event AddedP256PublicKey(bytes32 indexed credentialIdHash, address indexed owner, uint256 x, uint256 y);

/// @dev Event emitted when a P256 key is removed
event RemovedP256Key(bytes32 indexed keyIdHash, uint256 x, uint256 y);
event RemovedP256PublicKey(bytes32 indexed credentialIdHash, address indexed owner, 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 an empty credential id hash is attempted to be added
error InvalidCredentialIdHash();
/// @dev Error emitted when a P256 key is already stored and attempted to be added
error KeyAlreadyExists(string keyId);
error KeyAlreadyExists(bytes32 credentialIdHash);
/// @dev Error emitted when a P256 key is not stored and attempted to be removed
error KeyDoesNotExist(string keyId);
error KeyDoesNotExist(bytes32 credentialIdHash);
/// @dev Error emitted when a P256 key is not owned by the caller
error DoesNotOwner(string keyId);
error DoesNotOwner(bytes32 credentialIdHash);
/// @dev Error emitted when a P256 key is attempted to be add by not EOA
error DoesNotEOA();

function addKey(string calldata _keyId, uint256 _x, uint256 _y) external {
/**
* @notice Adds a P256 public key to the contract
* @param _credentialIdHash The ID Hash of the credential to add
* @param _x The X value of the public key
* @param _y The Y value of the public key
*/
function addP256PublicKey(bytes32 _credentialIdHash, uint256 _x, uint256 _y) external {
address sender = msg.sender;
// slither-disable-next-line tx-origin
require(msg.sender == tx.origin, "Not authorized");
_addKey(_keyId, _x, _y);
if (sender != tx.origin) revert DoesNotEOA();
_addP256PublicKey(_credentialIdHash, sender, _x, _y);
}

function _addKey(string calldata _keyId, uint256 _x, uint256 _y) internal {
function _addP256PublicKey(bytes32 _credentialIdHash, address sender, 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();
if (_credentialIdHash == bytes32(0)) revert InvalidCredentialIdHash();

P256PublicKey storage publicKey_ = authorizedKeys[keyIdHash_];
AuthorizedKey storage authorizedKey = authorizedKeys[_credentialIdHash];

// update key
if (publicKey_.x != 0 || publicKey_.y != 0) {
revert KeyAlreadyExists(_keyId);
}
if (authorizedKey.owner != address(0)) revert KeyAlreadyExists(_credentialIdHash);

authorizedKeys[keyIdHash_] = P256PublicKey(_x, _y);
keyIdHashToAccount[keyIdHash_] = msg.sender;
accountToKeyIdList[msg.sender].push(_keyId);
authorizedKeys[_credentialIdHash] = AuthorizedKey({owner: sender, publicKey: P256PublicKey({x: _x, y: _y})});
accountToCredentialIdHashSet[sender].add(_credentialIdHash);

emit AddedP256Key(keyIdHash_, _keyId, _x, _y);
emit AddedP256PublicKey(_credentialIdHash, sender, _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
* @notice Removes a P256 public key from the contract
* @param _credentialIdHash The ID Hash of the credential to remove
*/
function getKey(bytes32 keyIdHash) external view returns (uint256 x_, uint256 y_) {
P256PublicKey memory publicKey_ = authorizedKeys[keyIdHash];
x_ = publicKey_.x;
y_ = publicKey_.y;
}
function removeP256PublicKey(bytes32 _credentialIdHash) external {
address sender = msg.sender;
AuthorizedKey memory authorizedKey = authorizedKeys[_credentialIdHash];
address publicKeyOwner = authorizedKey.owner;

function getKeyIdLength(address _account) external view returns (uint256) {
return accountToKeyIdList[_account].length;
if (publicKeyOwner == address(0)) revert KeyDoesNotExist(_credentialIdHash);
if (publicKeyOwner != sender) revert DoesNotOwner(_credentialIdHash);

uint256 x = authorizedKey.publicKey.x;
uint256 y = authorizedKey.publicKey.y;

delete authorizedKeys[_credentialIdHash];
accountToCredentialIdHashSet[sender].remove(_credentialIdHash);

emit RemovedP256PublicKey(_credentialIdHash, sender, x, y);
}

function getKeyIdByIndex(address _account, uint256 _index) external view returns (string memory) {
return accountToKeyIdList[_account][_index];
/**
* @notice Returns authorized key infos by credential id hash
* @param _credentialIdHash The ID Hash of the credential to get
* @return owner The owner of the public key
* @return x The X value of the public key
* @return y The Y value of the public key
*/
function getAuthorizedKey(bytes32 _credentialIdHash) external view returns (address owner, uint256 x, uint256 y) {
AuthorizedKey memory authorizedKey = authorizedKeys[_credentialIdHash];

owner = authorizedKey.owner;
x = authorizedKey.publicKey.x;
y = authorizedKey.publicKey.y;
}

function getAccountByKeyIdHash(bytes32 keyIdHash) external view returns (address) {
return keyIdHashToAccount[keyIdHash];
/**
* @notice Returns the number of credential id hash set length
* @param _account The account to get the credential id hash set length
* @return The number of credential id hash set length
*/
function getCredentialIdHashSetLength(address _account) external view returns (uint256) {
return accountToCredentialIdHashSet[_account].length();
}

function getP256PublicKey(bytes32 keyIdHash) external view returns (P256PublicKey memory) {
return authorizedKeys[keyIdHash];
/**
* @notice Returns the credential id hash by index
* @param _account The account to get the credential id hash
* @param _index The index to get the credential id hash
* @return The credential id hash by index
*/
function getCredentialIdHashByIndex(address _account, uint256 _index) external view returns (bytes32) {
return accountToCredentialIdHashSet[_account].at(_index);
}

function isValidPublicKey(uint256 x, uint256 y) internal pure returns (bool) {
if (x >= p || y >= p || ((x == 0) && (y == 0))) {
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
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;
Expand Down
Loading
Loading