理解以太坊交易和消息:从状态变更到链下消息 - 第二部分

本文深入探讨了以太坊交易的未来发展方向,包括信标链(Beacon Chain)、EIP-4844 Blob交易、EIP-7702 Set Code交易和EIP-712 Typed Structured Data Signing。

理解以太坊交易和消息:从状态改变到链下消息 — 第二部分

之前的文章中,我们探讨了如今驱动以太坊的核心交易类型:Legacy, EIP-2930, 和 EIP-1559。如何创建以及它们实际上在链上的使用方式,而不仅仅是规范文档。

但以太坊不会停滞不前。

在这篇文章中,我们将深入探讨交易接下来的发展方向,以及它们如何变得更强大、更高效和更用户友好。我们将探讨三个重要的创新:

1. 以太坊的信标链(Beacon Chain)

2. EIP-4844 Blob 交易

3. EIP-7702 设置代码交易

4. EIP-712 类型化结构数据签名

如果你正在构建钱包、rollup 或依赖链下审批或高效 calldata 使用的协议,那么这些知识将使你的技术栈面向未来。

与往常一样,我们将使用真实示例和实时测试网数据,而不仅仅是理论。

让我们更深入地了解。

以太坊的信标链(Beacon Chain)

在我们深入研究 EIP-4844 Blob 交易 之前,我们需要了解什么是 信标链(Beacon Chain),它为什么存在,它为以太坊网络增加了什么,以及以太坊(1.0)到以太坊(2.0)的转变。

什么是信标链(Beacon Chain)

信标链(Beacon Chain)是以太坊从 工作量证明(proof-of-work, PoW) 过渡到 权益证明(proof-of-stake, PoS) 的第一部分(PoS 和 PoW 将在接下来的博客文章中介绍)。它于 2020 年启动,其主要目的是在隔离状态下测试和验证以太坊的 PoS 共识机制,而不处理真实的交易或应用状态。近两年来,它与以太坊的 PoW 链并行运行,生成 空块 仅用于测试和协调。

这种情况在 The Merge(合并) 期间发生了改变。

在 The Merge(合并)时,信标链接管了区块生产和共识的角色,不再存在两条不同的链。相反,只有一个权益证明的以太坊,现在每个节点需要两个不同的客户端。它开始接收来自 执行客户端(execution clients) 的执行负载(即实际的交易数据),并且 共识客户端(consensus clients) 使用其基于 PoS 的验证器网络将它们最终确定到区块中。同时,旧的 PoW 链禁用了其挖矿、共识和网络逻辑,有效地将所有控制权移交给信标链。

从那时起,以太坊变成了一个 单一的权益证明链,具有两个紧密连接的组件:

  • 共识层(信标链):处理验证器职责、区块最终确定和共识规则。
  • 执行层(以前的 PoW 链):处理交易、EVM 执行并管理以太坊的状态。

这两层可以使用引擎 API(Engine API)相互通信。

信标链(Beacon Chain)做什么

信标链 处理交易执行或智能合约逻辑,这仍然是 执行层 的责任。相反,它专注于共识职责,例如:

  • 接收和组织提议的区块
  • 处理链的认证
  • 运行 分叉选择规则(fork choice rule) 以决定哪个链是规范的
  • 根据网络参与情况应用奖励和惩罚

简而言之,虽然执行层是以太坊应用程序存在的地方,但信标链确保每个人都同意区块的顺序和有效性。

EIP-4844 Blob 交易

在短期和中期,甚至可能在长期内。Rollups(汇总)仍然是以太坊实现扩容的唯一无需信任的途径。由于 L1 gas 费用持续居高不下,整个生态系统越来越迫切地支持向Based Rollup 的使用方式进行广泛迁移。EIP-4844提供了一个至关重要的临时解决方案。它实现了完整分片将使用的相同交易格式,但实际上并没有对数据进行分片。相反,这些新的 携带 blob 的交易(blob-carrying transactions) 只是将数据添加到 信标链(beacon chain),在这里它被共识客户端下载并在短暂的保留窗口后自动丢弃。EIP-1559一样,正如我们在 之前的文章 中看到的那样。

然后有两个新字段:

  • max_fee_per_blob_gas:这设置了你愿意为 blob 空间 支付的最高费用,类似于 max_fee_per_gas 如何用于常规 gas。
  • blob_versioned_hashes:这是指向实际 blob 数据的哈希列表。这些哈希是使用一种称为 kzg_to_versioned_hash 的密码学函数派生的,它们充当 blob 内容的承诺。(我们这里不会介绍)

设置约束

为了构造或验证 EIP-4844 blob 交易,以太坊使用一种称为 KZG(Kate-Zaverucha-Goldberg)承诺 的密码学方案。该方案需要一个称为 可信设置(trusted setup) 的特殊工件——一个一次性预先计算的椭圆曲线幂列表,保存在代码的相同文件夹中

发布后,你应该能够看到:

https://sepolia.etherscan.io/tx/0xfd044e8bccdba170a8afd3ec9248cb97fb4ebce49adbe392c47385c23ea82c3b

https://sepolia.etherscan.io/tx/0xfd044e8bccdba170a8afd3ec9248cb97fb4ebce49adbe392c47385c23ea82c3b#blobs

EIP-7702 设置代码交易

尽管围绕智能合约钱包进行了所有创新,但大多数用户仍然依赖 EOA(外部拥有帐户),即经典的以太坊密钥对。但 EOA 存在限制:它们无法批量处理操作,无法委派权限,也无法验证超出标准 ECDSA 曲线的签名。这限制了开发人员可以实现的 UX 改进类型。

EIP-7702 提出了一个强大但简单的修复方案:使 EOA 能够暂时像智能合约一样运行,使用一种新的交易类型。在这种交易中,用户可以仅在交易期间将自定义代码附加到他们的账户。没有部署,没有长期状态更改,也没有额外的 gas 成本。需要注意的是,这意味着 null 目的地无效。

它是如何工作的

在执行交易之前,EIP-7702 处理一个可选的授权元组列表,每个元组包含一个 chain_idaddressnonce 和 ECDSA 签名。每个元组都会被验证(包括签名恢复和 nonce 检查),如果有效,则授权帐户的代码将暂时替换为一个特殊的 委托指示器0xef0100 || address。这表明任何代码执行(例如,通过 CALLDELEGATECALL 或直接交易执行)都应重定向到指定 address 处的逻辑。值得注意的是,这种委托会影响执行,但不会影响代码检查:CODESIZECODECOPY 操作码反映了委托合约的代码,而委托帐户上的 EXTCODESIZE 仅返回委托标记的大小(23 字节)。即使交易回滚,委托仍然存在,并且来自同一帐户的多个元组将使用最后一个有效元组进行解析。

批量交易示例:

我们将部署两个合约 MultiDelegationInvokerInvoked(附加两个地址),并将两个不同的 EOA 指定为权限。每个权限将签署一个委托,授予 Invoked 的字节码在其身份下执行的权限。最后,一个 EIP-7702 交易将调用 MultiDelegationInvoker,该交易将验证这些签名,注入委托的代码存根,并将执行路由到 Invoked 合约,所有这些都在一个原子调用中完成。

注意:我们将使用 Polygon Amoy 测试网

智能合约角度:

在你的收件箱中获取 Andrey Obruchkov 的故事

免费加入 Medium,以获取该作者的更新。

Invoked 智能合约:

pragma solidity ^0.8.24;

contract Invoked {
    event Pinged(address sender);

    function ping() external {
        emit Pinged(msg.sender);
    }
}

用于验证和授权的目标智能合约:

pragma solidity ^0.8.24;

contract MultiDelegationInvoker {
    event PingSuccess(address from);
    event PingStart(address from);

    function triggerPings(address[] calldata froms) external {
        emit PingStart(msg.sender);

        for (uint i = 0; i < froms.length; i++) {
            address from = froms[i];
            bytes memory code = new bytes(23);

            // Load the first 23 bytes of the code at `from`
            // 加载 `from` 处代码的前 23 个字节
            assembly {
                extcodecopy(from, add(code, 0x20), 0, 23)
            }

            // Check if it starts with 0xef0100
            // 检查它是否以 0xef0100 开头
            if (
                code.length == 23 &&
                uint8(code[0]) == 0xef &&
                uint8(code[1]) == 0x01 &&
                uint8(code[2]) == 0x00
            ) {
                // After verifying code starts with 0xef0100
                // 验证代码以 0xef0100 开头后
                address someModule;
                assembly {
                    someModule := shr(96, mload(add(code, 0x23)))
                }

                (bool ok, ) = someModule.call(abi.encodeWithSignature("ping()"));
                require(ok, "Ping failed");

                emit PingSuccess(from);
            } else {
                revert("Not delegated or invalid delegation format");
            }
        }
    }
}

让我们了解一下发生了什么:

for (uint i = 0; i < froms.length; i++) {
    address from = froms[i];
    …
}

froms 保存了你预先授权的 EOA。你将检查每个 EOA 的 “code” 是否有 23 字节的标记。

bytes memory code = new bytes(23);
assembly {
    extcodecopy(from, add(code, 0x20), 0, 23)
}

new bytes(23) 为长度 ( 0x17) 分配一个 32 字节的字,为数据分配另一个 32 字节的字。extcodecopy(from, …, 0, 23)from 代码的前 23 个字节(你注入的存根!)拉入 code[0..22]。(请记住 (0xef0100 || address) 被写入授权地址代码)

if (
    code.length == 23 &&
    uint8(code[0]) == 0xef &&
    uint8(code[1]) == 0x01 &&
    uint8(code[2]) == 0x00
) {
    …
} else {
    revert("Not delegated or invalid delegation format");
}

检查代码的前缀是否为 0xef0100,如前所述。如果为真,则该地址已分配有代码。

address realModule;
assembly {
    realModule := shr(96, mload(add(code, 0x23)))
}

extcodecopy 之后的 内存布局

  • code[0..2] = 标记字节
  • code[3..22] = 你签发的 20 字节 delegatee 地址

add(code, 0x23) = 指向 code[3] 的指针。

mload(...) 加载 32 字节(你的 20 字节地址 + 12 字节零填充)。

shr(96, …) 右移出填充,只留下 160 位地址。

(bool ok, ) = realModule.call(
    abi.encodeWithSignature("ping()")
);
require(ok, "Ping failed");
emit PingSuccess(from);

realModule 上的 你的 模块合约上执行 ping()。由于 EIP-7702 的委托调用语义,在该 ping() 中,msg.sender 仍然 是你委托的 EOA(而不是赞助者)。

钱包角度:

package main

import (
 "context"
 "crypto/ecdsa"
 "fmt"
 "log"
 "math/big"
 "strings"
 "time"
 "transactiontypes/account"

 "github.com/ethereum/go-ethereum/accounts/abi"
 "github.com/ethereum/go-ethereum/common"
 "github.com/ethereum/go-ethereum/core/types"
 "github.com/ethereum/go-ethereum/crypto"
 "github.com/ethereum/go-ethereum/ethclient"
 "github.com/ethereum/go-ethereum/rlp"
 "github.com/holiman/uint256"
)

const (
 // Public RPC URL for Polygon Amoy Testnet
 // Polygon Amoy 测试网的公共 RPC URL
 NodeRPCURL  = "https://polygon-amoy.drpc.org"
 AmoyChainID = 80002 // Polygon Amoy Testnet Chain ID
 // Polygon Amoy 测试网链 ID
)

func main() {
 ctx := context.Background()
 client, err := ethclient.Dial(NodeRPCURL)
 if err != nil {
  log.Fatal("RPC connection failed:", err)
  // RPC 连接失败:
 }

 // Load account
 // 加载帐户
 acc2Addr, acc2Priv := account.GetAccount(2)
 acc1Addr, acc1Priv := account.GetAccount(1)

 to := common.HexToAddress("0x87581c71b3693062f4d3e34617c3919ec1abf39b")

 // Define contract and parameters
 // 定义合约和参数
 moduleAddr := common.HexToAddress("0x4f9c96915a9ce8cd5eb11a2c35ab587fc97d5126")

 froms := []common.Address{
  *acc1Addr,
  *acc2Addr,
 }

 // Build calldata
 // 构建 calldata
 contractAbiJson := `[{"anonymous": false,"inputs": [{"indexed": false,"internalType": "address","name": "from","type": "address"}],"name": "PingStart","type": "event"},{"anonymous": false,"inputs": [{"indexed": false,"internalType": "address","name": "from","type": "address"}],"name": "PingSuccess","type": "event"},{"inputs": [{"internalType": "address[]","name": "froms","type": "address[]"}],"name": "triggerPings","outputs": [],"stateMutability": "nonpayable","type": "function"}]`
 parsedAbi, _ := abi.JSON(strings.NewReader(contractAbiJson))
 data, err := parsedAbi.Pack("triggerPings", froms)
 if err != nil {
  log.Fatal("ABI pack error:", err)
  // ABI 封装错误:
 }

 // Nonce and gas
 // Nonce 和 gas
 baseNonce2, err := client.PendingNonceAt(ctx, *acc2Addr)
 if err != nil {
  log.Fatal("Nonce fetch failed:", err)
  // Nonce 获取失败:
 }
 nonce2 := baseNonce2 + 1

 nonce1, err := client.PendingNonceAt(ctx, *acc1Addr)
 if err != nil {
  log.Fatal("Nonce fetch failed:", err)
  // Nonce 获取失败:
 }

 gasTipCap, err := client.SuggestGasTipCap(ctx)
 if err != nil {
  log.Fatal("Failed to fetch gas tip cap:", err)
  // 无法获取 gas tip cap:
 }
 baseFee, err := client.SuggestGasPrice(ctx)
 if err != nil {
  log.Fatal("Failed to fetch base fee:", err)
  // 无法获取基础费用:
 }
 gasFeeCap := new(big.Int).Add(baseFee, gasTipCap)

 // Create EIP-712-style signature for delegation
 // 为委托创建 EIP-712  风格的签名
 sig2, err := signEIP7702Delegation(acc2Priv, AmoyChainID, moduleAddr, nonce2)
 if err != nil {
  log.Fatal("Signature failed:", err)
  // 签名失败:
 }

 sig1, err := signEIP7702Delegation(acc1Priv, AmoyChainID, moduleAddr, nonce1)
 if err != nil {
  log.Fatal("Signature failed:", err)
  // 签名失败:
 }

 r2 := new(big.Int).SetBytes(sig2[:32])
 s2 := new(big.Int).SetBytes(sig2[32:64])
 v2 := uint8(sig2[64])

 r1 := new(big.Int).SetBytes(sig1[:32])
 s1 := new(big.Int).SetBytes(sig1[32:64])
 v1 := uint8(sig1[64])

 // Build EIP-7702 TxWithDelegation
 // 构建带有委托的 EIP-7702 交易
 delegation := types.SetCodeTx{
  ChainID:   uint256.NewInt(AmoyChainID),
  Nonce:     baseNonce2,
  GasTipCap: uint256.MustFromBig(gasTipCap),
  GasFeeCap: uint256.MustFromBig(gasFeeCap),
  Gas:       120000,
  To:        to,
  Data:      data,
  AuthList: []types.SetCodeAuthorization{
   {
    ChainID: *uint256.NewInt(AmoyChainID),
    Address: moduleAddr,
    Nonce:   nonce1,
    R:       *uint256.MustFromBig(r1),
    S:       *uint256.MustFromBig(s1),
    V:       v1,
   },
   {
    ChainID: *uint256.NewInt(AmoyChainID),
    Address: moduleAddr,
    Nonce:   nonce2,
    R:       *uint256.MustFromBig(r2),
    S:       *uint256.MustFromBig(s2),
    V:       v2,
   },
  },
 }

 fullTx := types.NewTx(&delegation)
 signedTx, err := types.SignTx(fullTx, types.LatestSignerForChainID(big.NewInt(AmoyChainID)), acc2Priv)
 if err != nil {
  log.Fatal("Signing failed:", err)
  // 签名失败:
 }

 err = client.SendTransaction(ctx, signedTx)
 if err != nil {
  log.Fatal("Tx failed:", err)
  // 交易失败:
 }

 fmt.Println("EIP-7702 Tx sent:", signedTx.Hash().Hex())
 // EIP-7702 交易已发送:

 time.Sleep(10 * time.Second)
 receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
 if err != nil {
  fmt.Println("Waiting...")
  // 等待...
 } else {
  fmt.Println("Tx mined in block", receipt.BlockNumber)
  // 交易在区块中被挖掘
 }
}

// signEIP7702Delegation creates a hash of (chainID, from, nonce) and signs it
// signEIP7702Delegation 创建 (chainID, from, nonce) 的哈希并对其进行签名
func signEIP7702Delegation(priv *ecdsa.PrivateKey, chainID int64, from common.Address, nonce uint64) ([]byte, error) {
 // Encode [chain_id, address, nonce] in RLP
 // 在 RLP 中编码 [chain_id, address, nonce]
 msgPayload, err := rlp.EncodeToBytes([]interface{}{
  big.NewInt(chainID),
  from,
  big.NewInt(int64(nonce)),
 })
 if err != nil {
  return nil, err
 }

 // Prepend MAGIC 0x05
 // 前置 MAGIC 0x05
 prefixed := append([]byte{0x05}, msgPayload...)

 // Hash
 // 哈希
 msgHash := crypto.Keccak256Hash(prefixed)
 return crypto.Sign(msgHash.Bytes(), priv)
}

发生了什么:

连接并准备

  • 通过 RPC 拨入 Polygon Amoy 测试网。
  • 从你的本地钱包助手加载两个 EOA ( acc1, acc2)(它们的地址和私钥)。

设置目标合约

  • to 是你正在调用的智能合约地址。
  • moduleAddr 是你将委托到的实现合约。

构建 calldata

  • triggerPings(address[]) 定义最小的 ABI。
  • 将你的两个授权地址 ( froms = [acc1, acc2]) 打包到 data 中。

获取 nonce 和 gas 参数

  • 读取 acc2 的待处理 nonce ( baseNonce2) 用于交易,然后计算其后增量 nonce ( nonce2 = baseNonce2 + 1) 用于委托条目。
  • 读取 acc1 的待处理 nonce ( nonce1) 用于其委托。
  • 建议 EIP-1559 小费和基础费用以计算 gasFeeCap

生成 EIP-7702 签名

  • 每个授权机构通过 signEIP7702Delegation() 签署元组 (chainID, moduleAddr, nonce)
  • acc1 使用 nonce1
  • acc2(交易发送者)使用 nonce2

组装 SetCodeTx

&types.SetCodeTx{
  ChainID: AmoyChainID,
  Nonce:   baseNonce2,
  To:      to,
  Data:    data,
  Gas:     120_000, // some hardcoded fee to not write too much code
          // 一些硬编码的费用,避免编写太多代码
  AuthList: []SetCodeAuthorization{
    { Address: moduleAddr, Nonce: nonce1, R:…, S:…, V:… },
    { Address: moduleAddr, Nonce: nonce2, R:…, S:…, V:… },
  },
}

签名和广播

  • 使用 SignTx 通过 acc2Priv 签署完整交易。
  • 通过 client.SendTransaction 发送它。
  • 轮询收据以确认挖掘。

输出应如下所示:

https://amoy.polygonscan.com/tx/0x5d9b3a8ebbfc26ee94024f5e1f608e4fd1c933d9dcd78f8b4b127379015854d1

EIP-712 类型化结构数据签名

签署一个不透明的字节 blob 很容易,但在实际应用程序中,我们需要签署具有诸如 “amount”、“recipient” 或 “order ID” 等字段的丰富、结构化消息,而不仅仅是随机十六进制。如果你滚动自己的哈希,则很容易犯一个破坏安全性的错误。

EIP‑712 通过定义一种清晰、经过同行评审的方式来将你的数据结构转换为摘要,钱包可以以人类可读的形式显示该摘要,从而解决了这个问题。这意味着用户可以准确地看到他们正在签署的内容,合约可以在链上验证它,并且你可以通过在链下进行审批而不是在链上进行审批来节省 gas。

它是如何工作的:

扩展的签名宇宙

  • 除了交易和原始字节字符串之外,EIP-712 还添加了 结构化数据,因此钱包可以呈现人类可读的字段,而不是不透明的十六进制。

编码模式

  • 类型化数据"\x19\x01" ‖ domainSeparator ‖ hashStruct(message)

前导字节始终不同,因此每种情况都是明确。

类型化数据

  • Solidity 风格的结构体 定义,例如
struct Mail { address from; address to; string contents; }
  • 支持的类型
  • 原子uint8–256int8–256bytes1–32booladdress
  • 动态bytesstring
  • 引用:固定/动态数组 ( Type[n]/ Type[]), 嵌套结构体

hashStruct

  • typeHash = keccak256(encodeType("Mail(address from,address to,string contents)"))
  • encodeData = 将每个字段打包到 32 字节的槽中(首先对动态类型进行哈希处理)
  • hashStruct(msg) = keccak256(typeHash ‖ encodeData(msg))

域分隔符(Domain Separator)

  • 一个特殊的 EIP712Domain 结构体(例如 { name, version, chainId, verifyingContract }),进行一次哈希处理,如下所示
domainSeparator = hashStruct(eip712Domain);

签名和验证

  • 客户端 调用 eth_signTypedData_v4(name, types, domain, message) → 钱包显示清晰的字段并返回签名
  • 合约 再次计算 digest = keccak256("\x19\x01"‖domainSeparator‖hashStruct(message)) 并使用 ecrecover 验证签名者。

注意:有关更详细的规范,你可以在此处阅读

Permit

EIP-2612 “permit” 构建在 EIP-712 的类型化结构数据签名之上。它定义了一个 Permit(owner, spender, value, nonce, deadline) 结构体,钱包使用 EIP-712 域分隔符对其进行哈希处理,呈现给用户以供批准,并在链下进行签名。然后,Token的 permit(owner, spender, value, deadline, v, r, s) 函数通过 EIP-712 重新计算相同的摘要,并使用 ecrecover 验证持有者的签名和 nonce,并以原子方式设置 allowance[owner][spender] = value。通过在底层利用 EIP-712,permit 实现了一种清晰、安全、gas 高效的 UX,而无需单独的链上 approve(...) 调用。

注意:想要支持这种批准的Token必须实现 permit 方法

示例:

在本例中,我们将编写一个智能合约,该合约通过其规范来验证 EIP-712,该规范已在前面解释过。

智能合约角度:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract PermitVerifier is EIP712 {
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
    bytes32 public constant PERMIT_TYPEHASH =
        0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

    constructor() EIP712("MyDApp", "1") {}

    function verifyPermit(
        address owner,
        address spender,
        uint256 value,
        uint256 nonce,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external view returns (bool) {
        bytes32 structHash = keccak256(
            abi.encode(
                PERMIT_TYPEHASH,
                owner,
                spender,
                value,
                nonce,
                deadline
            )
        );
        bytes32 digest = _hashTypedDataV4(structHash);
        return ECDSA.recover(digest, v, r, s) == owner;
    }
}

钱包角度:

package main

const (
 // Public RPC URL for Polygon Amoy Testnet
 // Polygon Amoy 测试网的公共 RPC URL
 NodeRPCURL  = "https://polygon-amoy.drpc.org"
 AmoyChainID = 80002 // Polygon Amoy Testnet Chain ID
 // Polygon Amoy 测试网链 ID
)

func main() {
 // 1) Connect to Amoy
 // 1) 连接到 Amoy
 client, err := ethclient.Dial("https://polygon-amoy.drpc.org")
 if err != nil {
  log.Fatal(err)
 }
 ctx := context.Background()

 // 2) Prepare the same EIP‑712 TypedData that the user signed
 // 2) 准备用户签名的相同 EIP-712 类型化数据
 acc2Addr, acc2Priv := account.GetAccount(2)

 verifierAddr := common.HexToAddress("0xf80bb731f8ba49624dce8edb1a8188782287ff1e")

 domain := apitypes.TypedDataDomain{
  Name:              "MyDApp",
  Version:           "1",
  ChainId:           math.NewHexOrDecimal256(AmoyChainID),
  VerifyingContract: verifierAddr.Hex(),
 }
 types := apitypes.Types{
  "EIP712Domain": {
   {Name: "name", Type: "string"},
   {Name: "version", Type: "string"},
   {Name: "chainId", Type: "uint256"},
   {Name: "verifyingContract", Type: "address"},
  },
  "Permit": {
   {Name: "owner", Type: "address"},
   {Name: "spender", Type: "address"},
   {Name: "value", Type: "uint256"},
   {Name: "nonce", Type: "uint256"},
   {Name: "deadline", Type: "uint256"},
  },
 }
 deadline := big.NewInt(time.Now().Add(time.Hour).Unix())
 nonce := big.NewInt(0)
 message := apitypes.TypedDataMessage{
  "owner":    acc2Addr.Hex(),
  "spender":  acc2Addr.Hex(),
  "value":    "1000000000000000000",
  "nonce":    nonce.String(),
  "deadline": deadline.String(),
 }
 typedData := apitypes.TypedData{
  Types:       types,
  PrimaryType: "Permit",
  Domain:      domain,
  Message:     message,
 }

 // 3) Sign or supply your existing (v,r,s)
 // 3) 签名或提供你现有的 (v,r,s)
 domainSep, _ := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
 msgHash, _ := typedData.HashStruct("Permit", typedData.Message)
 digest := crypto.Keccak256(
  []byte("\x19\x01"),
  domainSep,
  msgHash,
 )
 sig, _ := crypto.Sign(digest, acc2Priv)
 r := common.BytesToHash(sig[:32])
 s := common.BytesToHash(sig[32:64])
 v := uint8(```
// 6) 解码 bool 结果
 out, err := parsed.Unpack("verifyPermit", res)
 if err != nil {
  log.Fatal(err)
 }
 fmt.Println("签名有效吗?", out[0].(bool))

 if !out[0].(bool) {
  log.Fatal("签名验证失败")
 }

 fmt.Printf("签名验证成功,所有者为: %s\n", acc2Addr.Hex())
}

你应该看到如下输出:

Signature verified successfully for owner: <你的地址>

在这个例子中,我们仅仅演示了一个 只读 的演示,直观地展示了 EIP-712 签名如何在链下进行验证。在真正的 permit 流程中,你不能仅停留在 verifyPermit,你必须在链上实际调用 token 的 permit(...) 方法来:

  • 重新计算并验证 EIP-712 摘要
  • 检查并增加所有者的 nonce 和 deadline
  • 设置 allowance(owner, spender)

只有在 permit(...) 调用 之后 ,随后的 transferFrom(...) 才会成功。我们的例子展示了核心的签名和恢复逻辑,但是生产集成必须调用 tokenPermit.permit(...)

总结

在阅读了这篇深入的文章之后,你将会理解以太坊是如何在简单的 ETH 转账之外演变的,为什么 rollups 和 blob-carrying txs 会让你以 L1 成本的一小部分扩展到每秒数百笔交易。Beacon Chain 如何为 PoS 共识提供动力,使其与 EVM 执行分离,以及像 EIP-7702 "SetCode" 这样的新交易类型如何为纯 EOAs 解锁原子批处理和赞助的 meta-txs,而 EIP-712 和 EIP-2612 "permit" 如何让你在链下签署丰富的批准,并在链上以单个、高 gas 效率的调用来使用它们。

资源:

  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
andrey_obruchkov
andrey_obruchkov
江湖只有他的大名,没有他的介绍。