如何通过 ERC-1271 在 DApp 中实现验证智能合约钱包签名
学习如何通过实现 ERC-1271 来验证 Dapp 中智能合约钱包的签名。
在本教程中,我们将讨论智能合约钱包,它们的工作原理以及如何允许它们登录到你的 Dapp 中。我们将涵盖 EIP-1271,该标准定义了一种在账户为智能合约(钱包)时验证签名的方式。我们还将讨论 EIP-4337 作为智能合约钱包的额外上下文。
智能合约钱包(SCW)是一种建立在区块链网络上的智能合约上的加密货币钱包类型,例如以太坊。与传统的由私钥控制的外部拥有账户(EOA)不同,智能合约钱包提供了额外的功能和安全特性。它们允许进行更复杂的操作、可定制的规则、恢复选项和多签功能。
智能合约钱包的关键特性包括:
ERC-4337,也称为以太坊改进提案(EIP)4337,是以太坊中的账户抽象标准。它旨在通过引入新的交易类型“UserOperation”简化用户体验,该类型将交易验证与执行分离。该标准允许用户使用智能合约钱包与以太坊网络进行交互,而无需直接管理 gas 费用和 nonce 管理。
ERC-4337 的关键组件包括:
CREATE2
确保钱包地址是确定性的,并且独立于钱包创建的顺序。这允许用户在本地生成钱包地址,而无需依赖现有用户或执行自定义操作。总之,ERC-4337 旨在通过简化与以太坊网络的交互并通过智能合约钱包引入高级功能,改善用户体验。它将交易验证与执行分离,允许更灵活的 gas 支付选项,并支持无缝账户创建。
当用户将他们的钱包连接到应用时,可能会被要求签署一条消息以证明他们的身份。对于外部拥有账户(EOA),用户使用他们的私钥对消息进行签名。验证方可以使用恢复算法 ,如 ecrecover
,来确定谁签署了消息。
智能合约钱包可以生成签名,但它们没有像 EOA 那样的私钥。但是,智能合约本身可提供验证签名的机制。EIP-1271 通过一个标准接口解决了这个问题。
EIP-1271 是一个用于验证由智能合约钱包(SCW)生成的签名的合约的标准接口。它于 2018 年作为以太坊改进提案(EIP)提出,并已被广泛采用,用于需要签名验证的 DApp。
EIP-1271 接口定义了一个名为 isValidSignature
的函数。该函数接受两个参数:
bytes32 _messageHash
:被签名的消息哈希bytes _signature
:智能合约钱包生成的签名该函数返回一个 bytes4
值,指示签名是否有效。可能的返回值包括:
0x1626ba7e
:签名有效0xffffffff
:签名无效以下是常见的 web3 认证流程的工作方式:
ecrecover
( 恢复算法 )验证签名,如果消息被正确签名,应返回用户的地址。但是,这种方法不能直接用于智能合约钱包,因为你不能对 SCW 签名使用 ecrecover
,因为 SCW 可能具有自定义签名验证逻辑。
要使智能合约的钱包验证工作,我们需要修改验证过程。我们应该调用智能合约钱包的 isValidSignature
函数来验证签名是否被它批准,正如 EIP-1271 中所定义的那样。
ethers.js
的示例脚本以下是一个用于在 ethers.js
中验证智能合约钱包和 EOA 签名有效性的示例脚本:
verifySig.js
// importing the required modules from ethers.js
const { providers, utils, Contract } = require("ethers");
// importing ABI for interface of ERC1271 so we can call the `isValidSignature` function
const IERC1271Abi = [{"inputs":[{"internalType":"address[]","name":"addrs","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"returnData","type":"bytes"}],"name":"LogErr","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"bytes32","name":"priv","type":"bytes32"}],"name":"LogPrivilegeChanged","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"executeBySelf","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"executeBySender","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"isValidSignature","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"privileges","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"bytes32","name":"priv","type":"bytes32"}],"name":"setAddrPrivilege","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"tipMiner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"tryCatch","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
// This is a constant magic value defined in EIP-1271 that's returned when the signature is valid
const MAGICVALUE = 0x1626ba7e;
// function to check if a signature is valid
const isValidSignature = async (signingAddress, message, signature) => {
const hash = utils.hashMessage(message); // hash the message
const apiKey = "demo" // replace with your Alchemy API key for the network you are verifying the signature for, in this case Polygon Mainnet
const provider = new providers.JsonRpcProvider(
`https://polygon-mainnet.g.alchemy.com/v2/${apiKey}`
); // get your provider
const bytecode = await provider.getCode(signingAddress); // get the bytecode
const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== "0x"; // check if it is a smart contract wallet
if (isSmartContract) {
// verify the message for a decentralized account (contract wallet)
const contractWallet = new Contract(signingAddress, IERC1271Abi, provider); // make an instance for the contact wallet
const verification = await contractWallet.isValidSignature(hash, signature); // verify if the signature is valid using the `isValidSignature` function
console.log("Message is verified?", verification === MAGICVALUE); // log if the signature is valid
return verification === MAGICVALUE; // return true or false based on if the signature is valid or not
} else {
// verify the message for an externally owned account (EOA) using the recovery algorithm
const sig = ethers.utils.splitSignature(signature);
const recovered = await contract.verifyHash(hash, sig.v, sig.r, sig.s);
console.log("Message is verified?", recovered === signingAddress);
return recovered === signingAddress;
}
};
async function main() {
let isValid = await isValidSignature(
"0x4836a472ab1dd406ecb8d0f933a985541ee3921f",
"0x787177",
"0xc0f8db6019888d87a0afc1299e81ef45d3abce64f63072c8d7a6ef00f5f82c1522958ff110afa98b8c0d23b558376db1d2fbab4944e708f8bf6dc7b977ee07201b00"
);
console.log(isValid);
}
main();
在这里,我们检查一个账户是智能合约钱包还是 EOA,如果是 SCW,我们调用其 isValidSignature
函数来验证签名的有效性,否则,我们使用 EOA 的恢复算法来验证签名的有效性。
目前,在 Ethers 的 Github 存储库中有一个 PR,用于直接向 Ethers 添加此功能。一旦该 PR 被合并,使用 Ethers 验证 SCW 的签名将变得更加容易。
还有一些库可以使开发人员在智能合约钱包的情况下轻松进行签名验证。这些库的示例包括 eip1271-verification-util
和 signature-validator
。
下面是一个示例脚本,描述了如何使用 eip1271-verification-util 库验证前端 dapp 中智能合约钱包的签名:
📘 注意
下面给出的代码不能在 nodejs 项目中使用,只能在前端应用程序中使用,因为 eip1271-verification-util 库是专门为 dapps 设计的,用于验证智能合约钱包的签名。
// Setup: npm i @etherspot/eip1271-verification-util
// importing ethers
import ethers from "ethers";
// importing the `isValidEip1271Signature` function from `eip1271-verification-util`
import { isValidEip1271Signature } from "@etherspot/eip1271-verification-util";
const checkSig = async () => {
// the random message (nonce) that was signed
const data = "0x787177";
// defining signer and the rpc url
const signerAddress = '0x4836a472ab1dd406ecb8d0f933a985541ee3921f';
// the rpc url to make requests
const rpcUrl = 'https://polygon-mainnet.g.alchemy.com/v2/demo'
// The signature to verify as a hex string
const signature = '0xc0f8db6019888d87a0afc1299e81ef45d3abce64f63072c8d7a6ef00f5f82c1522958ff110afa98b8c0d23b558376db1d2fbab4944e708f8bf6dc7b977ee07201b00'
// Hashed data used for the signature to verify. The dApp will need to pre-compute this as no hashing will occur in the function, and this will be directly used in isValidEip1271Signature
const hash = ethers.utils.hashMessage(ethers.utils.arrayify(data));
// calling the imported function to verify the signature and passing the required params
const isValidSig = await isValidEip1271Signature(
rpcUrl,
signerAddress,
hash,
signature
);
// logging if the signature is valid or not
console.log("is signature valid:", isValidSig);
};
如你所见,使用 eip1271-verification-util
库的isValidEip1271Signature
函数,你可以验证智能合约钱包的签名。
总之,EIP-1271 是一个重要的提案,它为智能合约验证签名定义了一个标准。它允许人们在去中心化应用中使用智能合约钱包。实施 EIP-1271 对于任何希望保持领先地位的 dApp 都至关重要。
本翻译由 DeCert.me 协助支持, 在 DeCert 构建可信履历,为自己码一个未来。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!