使用 ERC-1271,让 Dapp 兼容智能合约钱包

如何通过 ERC-1271 在 DApp 中实现验证智能合约钱包签名

学习如何通过实现 ERC-1271 来验证 Dapp 中智能合约钱包的签名。

在本教程中,我们将讨论智能合约钱包,它们的工作原理以及如何允许它们登录到你的 Dapp 中。我们将涵盖 EIP-1271,该标准定义了一种在账户为智能合约(钱包)时验证签名的方式。我们还将讨论 EIP-4337 作为智能合约钱包的额外上下文。

智能合约钱包和 ERC-4337

智能合约钱包

智能合约钱包(SCW)是一种建立在区块链网络上的智能合约上的加密货币钱包类型,例如以太坊。与传统的由私钥控制的外部拥有账户(EOA)不同,智能合约钱包提供了额外的功能和安全特性。它们允许进行更复杂的操作、可定制的规则、恢复选项和多签功能。

智能合约钱包的关键特性包括:

  • 可定制性:用户可以根据自己的需求添加特定功能或修改现有功能。
  • 安全性:这些钱包使用先进的安全功能,如多签访问和恢复机制。
  • 可升级性:智能合约钱包的功能可以更新,而不会丢失存储的资产。
  • 可互操作性:它们可以与区块链网络上的其他智能合约和去中心化应用程序(dApp)进行交互。

ERC-4337

ERC-4337,也称为以太坊改进提案(EIP)4337,是以太坊中的账户抽象标准。它旨在通过引入新的交易类型“UserOperation”简化用户体验,该类型将交易验证与执行分离。该标准允许用户使用智能合约钱包与以太坊网络进行交互,而无需直接管理 gas 费用和 nonce 管理。

ERC-4337 的关键组件包括:

  • UserOperation:一种封装用户期望操作的新交易类型,包括发送者、有效载荷和与 gas 相关的信息。“UserOperation”将验证和执行步骤分离,从而实现更高效和安全的交易处理。
  • EntryPoint:作为“UserOperations”的网关的智能合约。它负责验证和执行“UserOperations”,并可以与其他合约(如Paymasters和Factories)进行交互,以促进交易。
  • Paymasters:这些是可以代表用户赞助交易的智能合约。它们支持各种支付机制,如 ERC-20 代币,用于 gas 费用,为用户提供更大的灵活性。
  • Factories:负责创建新智能合约钱包的智能合约。它们使用 CREATE2 确保钱包地址是确定性的,并且独立于钱包创建的顺序。这允许用户在本地生成钱包地址,而无需依赖现有用户或执行自定义操作。
  • 声誉评分和节流:为防止滥用和拒绝服务(DoS)攻击,ERC-4337 引入了全局实体的声誉评分和节流机制,如Paymaster和Factories。实体需要抵押一定数量的 ETH,以确保它们的行为不会导致其他“UserOperations”失效。

总之,ERC-4337 旨在通过简化与以太坊网络的交互并通过智能合约钱包引入高级功能,改善用户体验。它将交易验证与执行分离,允许更灵活的 gas 支付选项,并支持无缝账户创建。

合约如何签署消息?

当用户将他们的钱包连接到应用时,可能会被要求签署一条消息以证明他们的身份。对于外部拥有账户(EOA),用户使用他们的私钥对消息进行签名。验证方可以使用恢复算法 ,如 ecrecover,来确定谁签署了消息。

智能合约钱包可以生成签名,但它们没有像 EOA 那样的私钥。但是,智能合约本身可提供验证签名的机制。EIP-1271 通过一个标准接口解决了这个问题。

EIP-1271

EIP-1271 是一个用于验证由智能合约钱包(SCW)生成的签名的合约的标准接口。它于 2018 年作为以太坊改进提案(EIP)提出,并已被广泛采用,用于需要签名验证的 DApp

EIP-1271 接口定义了一个名为 isValidSignature 的函数。该函数接受两个参数:

  • bytes32 _messageHash:被签名的消息哈希
  • bytes _signature:智能合约钱包生成的签名

该函数返回一个 bytes4 值,指示签名是否有效。可能的返回值包括:

  • 0x1626ba7e:签名有效
  • 0xffffffff:签名无效

钱包验证通常如何工作

以下是常见的 web3 认证流程的工作方式:

  1. 服务器生成用户必须签署的消息。该消息包含一个随机 nonce,以防止重放攻击。
  2. 用户使用他们的私钥对消息的哈希进行签名。
  3. 服务器使用 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 的签名将变得更加容易。

用于 SCW 签名验证的库

还有一些库可以使开发人员在智能合约钱包的情况下轻松进行签名验证。这些库的示例包括 eip1271-verification-utilsignature-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 构建可信履历,为自己码一个未来。

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO