支持 multicall 的JSON-RPC 方法 eth_simulateV1 在多网络可用

  • Killari
  • 更新于 2024-12-11 23:08
  • 阅读 318

支持 multicall 的JSON-RPC 方法 eth_simulateV1 在多网络可用

eth_simulateV1 方法在发布 Geth v1.14.9Nethermind v1.28.0 后已经在以太坊主网上线。它也支持 Base、Optimism 和 Gnosis 等网络。

eth_simulateV1 方法是 eth_call 的增强版,旨在支持multicall和更高级的功能。以前,multicall功能是通过以下工具实现的:

  1. Alchemy forking

  2. Hardhat forking

  3. Flashbots eth_callBundle

  4. Multicall contracts

虽然这些方法各有优点,但缺乏标准化。此外,multicall合约仅限于在同一合约内进行多次调用,无法模拟多个交易。

相比之下,eth_simulateV1 提供了通过 JSON-RPC 进行multicall的标准化方法执行规范 484)。它保留了 eth_call 的核心功能,同时引入了显著的改进。

eth_simulateV1 的主要特性

  1. 跨区块调用: 定义自定义区块变量以进行细粒度模拟。

  2. 状态覆盖(Overrides): 在模拟过程中修改账户余额或替换合约代码。

  3. 预编译重载(Overrides): 用自定义代码替代预编译合约。

  4. 增强的日志记录: 访问详细的事件和 ETH 转账日志,超出单次调用结果。

  5. 验证模式: 强制施加更严格的规则,以确保链上兼容性。

这些功能为开发者提供了更多的权力和灵活性,使得 eth_simulateV1 成为高级交易模拟的强大工具。

在这篇博客中,我们将深入探讨这些新功能,并展示它们在改善以太坊开发工作流程中的潜力。

1) 在区块内模拟调用

使用 eth_call 函数,你可以在用户自定义的区块内执行单个调用。相比之下,通过 eth_simulateV1, 提供跨不同区块进行调用的能力,并提供对每个调用的区块参数进行重载的灵活性。

以下是一个简单的 eth_simulateV1 调用示例,展示了如何定义两个区块 10010x3e9)和 20010x7d1)并在其内部放置调用:

    {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "eth_simulateV1",
      "params": [
        {
          "blockStateCalls": [
            {
              "blockOverrides": {
                "number": "0x3e9",
                "time": "0x3e9",
                "gasLimit": "0x3ec",
                "feeRecipient": "0xc200000000000000000000000000000000000000",
                "prevRandao": "0xc300000000000000000000000000000000000000000000000000000000000000",
                "baseFeePerGas": "0x3ef",
                "blobBaseFee": "0x15"
              },
              "calls": [ "eth_calls" ]
            },
            {
              "blockOverrides": {
                "number": "0x7d1",
                "time": "0x7d1",
                "gasLimit": "0x3ec",
                "feeRecipient": "0xc200000000000000000000000000000000000000",
                "prevRandao": "0xc300000000000000000000000000000000000000000000000000000000000000",
                "baseFeePerGas": "0x3ef",
                "blobBaseFee": "0x15"
              },
              "calls": [ "more eth_calls" ]
            }
          ]
        },
        "latest"
      ]
    }

在定义区块时,你可以重载(Overrides)七个参数:numbertimegasLimitfeeRecipientprevRandaobaseFeePerGasblobBaseFee。所有这些参数,包括 numbertime,都可以自定义。然而,number 必须始终递增,而 time 必须递增或保持不变。

这些变量的一个有趣特点是,能够跳过时间或区块,例如,你可以定义区块 10,然后直接跳到区块 20。这允许你在不指定每个中间区块的情况下模拟未来事件。在使用 eth_simulateV1 方法时,区块 1020 之间的所有区块都会被模拟并返回。

请注意,单次调用中可创建的区块数有最大限制。默认情况下,该限制为 256 个区块,尽管可以根据客户端进行调整。

此外,还对模拟的总 gas 消耗和返回有效载荷的大小设定了限制。这些限制有助于管理单个 eth_simulateV1 调用对节点造成的负载。gas 和有效载荷大小限制也可以根据客户端自定义。

2) 状态覆盖

状态覆盖允许你修改任何账户的 balancecodenoncestatestate变量可以替换一个账户的整个状态,而 stateDiff 允许你通过逐步应用更改来增强现有状态。

这些强大的账户状态操控工具提供以下能力:

  1. 设置账户余额: 调整任何账户的余额,从而有效地铸造 ETH 以用于 DeFi 应用。

  2. 代币铸造: 修改 ERC20 智能合约的状态,直接向你的账户铸造代币。

  3. 智能合约替换: 替换现有智能合约的代码,以绕过其典型的检查或限制。

  4. 即时合约部署: 模拟智能合约的部署并立即与之互动,无需实际在链上部署。

考虑一个简单的智能合约,用于检索账户余额:

    pragma solidity 0.8.18;

    contract BalanceGetter {
        function getBalance(address addr) view external returns (uint256) {
            return address(addr).balance;
        }
    }

以下是一个调用,它设置我们的余额,通过直接设置一些账户状态来部署 BalanceGetter 合约,并调用它以请求我们的余额:

    {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "eth_simulateV1",
      "params": [
        {
          "blockStateCalls": [
            {
              "stateOverrides": {
                "0xc000000000000000000000000000000000000000": {
                  "balance": "0x2710"
                },
                "0xc200000000000000000000000000000000000000": {
                  "code": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f8b2cb4f14610030575b600080fd5b61004a600480360381019061004591906100e4565b610060565b604051610057919061012a565b60405180910390f35b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100b182610086565b9050919050565b6100c1816100a6565b81146100cc57600080fd5b50565b6000813590506100de816100b8565b92915050565b6000602082840312156100fa576100f9610081565b5b6000610108848285016100cf565b91505092915050565b6000819050919050565b61012481610111565b82525050565b600060208201905061013f600083018461011b565b9291505056fea2646970667358221220172c443a163d8a43e018c339d1b749c312c94b6de22835953d960985daf228c764736f6c63430008120033"
                }
              },
              "calls": [
                {
                  "from": "0xc000000000000000000000000000000000000000",
                  "to": "0xc200000000000000000000000000000000000000",
                  "input": "0xf8b2cb4f000000000000000000000000c000000000000000000000000000000000000000"
                }
              ]
            }
          ]
        },
        "latest"
      ]
    }

然后返回以下 JSON 响应:

    {
        "jsonrpc": "2.0",
        "id": 1,
        "result": [
            {
                "baseFeePerGas": "0x0",
                "blobGasUsed": "0x0",
                "calls": [
                    {
                        "returnData": "0x0000000000000000000000000000000000000000000000000000000000002710",
                        "logs": [],
                        "gasUsed": "0x55b3",
                        "status": "0x1"
                    }
                ],
                "difficulty": "0x0",
                "excessBlobGas": "0x3220000",
                "extraData": "0x",
                "gasLimit": "0x1c9c380",
                "gasUsed": "0x55b3",
                "hash": "0x4342f0ab870175757176d5888e82f27523a6e1d52c0ac20a6ddf599d54ce0e04",
                "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
                "miner": "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5",
                "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
                "nonce": "0x0000000000000000",
                "number": "0x1455a09",
                "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
                "parentHash": "0xb30e288a5518544cc71dd24389a21061adab20f45f17c0907054dccf7bf00c01",
                "receiptsRoot": "0x4ce78e593fcb88a20d7bb31c27879f800551f4069371e576f7efad0d9615a960",
                "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
                "size": "0x29b",
                "stateRoot": "0xcfb988bd139a3f44aa79f9cbaba606429d76749fd18e1e8ca9a6612a4e0c8384",
                "timestamp": "0x674f0627",
                "totalDifficulty": "0xc70d815d562d3cfa955",
                "transactions": [
                    "0x96c6b7b5b4835fd518fd914feb586452cc797d332f1bd234f72c9e692c4c427a"
                ],
                "transactionsRoot": "0xebe30ac336ac6714af65c684e278799f70965b18ae12e9b368056640ac111650",
                "uncles": [],
                "withdrawals": [],
                "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
            }
        ]
    }

这个响应的详细程度显著高于标准的 eth_call。它包含一个结果数组(result: []),其中每个元素代表一个区块,包含了执行调用的结果。如前所述,eth_simulateV1 将调用组织成区块,提供有关区块及其相应调用结果的全面信息。本质上,eth_simulateV1 返回的是你从 eth_getBlockByNumber 获取的模拟区块数据。然而,由于这些区块仅在 eth_simulateV1 调用期间存在,因此此信息直接包含在响应中。

在区块结果中,还返回有关调用执行的信息:

"calls": [
    {
      "returnData": "0x0000000000000000000000000000000000000000000000000000000000002710",
      "logs": [],
      "gasUsed": "0x55b3",
      "status": "0x1"
    }
  ]

returnData 表示我们的余额,与合约的预期结果一致。此外,我们还包含了返回数据中的日志,尽管在此示例中没有日志。日志的包含标志着相较于先前 eth_call 方法的显著增强,后者仅返回了 returnData 字段。除此之外,我们提供了 gasUsed 字段,指示调用执行期间消耗的 gas 量。这些信息对 gas 估算计算非常有价值,并且对节点而言成本微乎其微,因为 gas 计算是过程的一部分。

3) 预编译覆盖(override)

由于预编译本质上是链上的账户,因此可以按照与其他账户重写相同的方式进行替换。然而,预编译具有一个独特的特点——一旦被覆盖,就无法直接在调用中访问原始预编译。虽然可以实现你自己的 Solidity 版本的 ecrecover,但这种方法往往会导致 gas 效率低下。为了解决这个问题,我们引入了一个名为 movePrecompileToAddresseth_simulateV1 设置,使你能够覆盖预编译并随后将其移动到一个新账户。

一个实用的应用场景是覆盖 ecrecover 以执行 Uniswap Permit2 兑换。要执行这样的兑换,用户必须签名一条链外消息,然后将其包含在链上以验证用户的批准。

现在,让我们探讨如何模拟 Vitalik 账户(0xd8da6bf26964af9d7eed9e03e53415d37aa96045)的 USDC 到 WBTC 的兑换,并覆盖 ecrecover,而不需要访问 Vitalik 的私钥。

下面是我们将用来替换 ecrecover 的合约。合约分为几个部分:

  1. overrideToAddress 映射将包含我们希望覆盖以返回所选地址的 ecrecover 参数哈希。

  2. 一个 fallback 方法在调用 ecrecover 时被调用,查看 overrideToAddress 映射中是否有任何覆盖。如果存在覆盖,方法会检索相关的账户。如果没有覆盖,方法将默认调用地址为 0x123456 的合约,实际的 ecrecover 实现会被迁移到该地址。

    pragma solidity 0.8.18;

    contract ecRecoverOverride {
      mapping(bytes32 => address) overrideToAddress;
      fallback (bytes calldata input) external returns (bytes memory) {
        (bytes32 hash, uint8 v, bytes32 r, bytes32 s) = abi.decode(input, (bytes32, uint8, bytes32, bytes32));
        address overridedAddress = overrideToAddress[keccak256(abi.encode(hash, v, r, s))];
        if (overridedAddress == address(0x0)) {
          (bool success, bytes memory data) = address(0x0000000000000000000000000000000000123456).call{gas: 10000}(input);
          require(success, 'failed to call moved ecrecover at address 0x0000000000000000000000000000000000123456');
          return data;
        } else {
          return abi.encode(overridedAddress);
        }
      }
    }

一旦合约编写完成,我们可以创建 eth_simulateV1 查询。这涉及两个调用:第一个是 授权 Uniswap 的 Permit2 合约,第二个是执行实际的兑换。为了使兑换成功,我们需要覆盖位于地址 0x1ecrecover。这需要替换该地址的代码,将预编译移动到地址 0x123456,并用适当的覆盖更新 overrideToAddress 映射。

    {
        "jsonrpc": "2.0",
        "id": 162,
        "method": "eth_simulateV1",
        "params": [
            {
                "blockStateCalls": [
                    {
                        "calls": [
                            { // approve
                                "type": "0x2",
                                "from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
                                "nonce": "0x481",
                                "maxFeePerGas": "0x10e2249a2c",
                                "maxPriorityFeePerGas": "0x5f5e100",
                                "gas": "0x12631",
                                "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
                                "value": "0x0",
                                "input": "0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
                                "chainId": "0x1",
                                "accessList": []
                            },
                            { // swap
                                "type": "0x2",
                                "from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
                                "nonce": "0x482",
                                "maxFeePerGas": "0x11491519cc",
                                "maxPriorityFeePerGas": "0x5f5e100",
                                "gas": "0x424ee",
                                "to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
                                "value": "0x0",
                                "input": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000655f00d400000000000000000000000000000000000000000000000000000000000000030a080c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000658686d800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad00000000000000000000000000000000000000000000000000000000655f00e000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041a6b086e6ffec7e22a7cac3d71494f1c7ec44a85c66156aff9fe881bf1fb99bc053dc332293ea7dce14be4cb689d9b75e920b37deab9ed761325999e0b48a66bf1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000042c1d800000000000000000000000000000000000000000000000000072b3980a9ab9fe00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000072b3980a9ab9fe",
                                "chainId": "0x1",
                                "accessList": []
                            }
                        ],
                        "stateOverrides": {
                            "0x0000000000000000000000000000000000000001": { 
                                "state": { // override the mapping
                                    "0x010d8fdb5b1199f6ac26d39281e100201200fbc7de5bcb9710c3dfeb475c65f6": "0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"
                                },
                                // replace the ecrecover code with our own code
                                "code": "0x608060405234801561001057600080fd5b506000366060600080600080868681019061002b9190610238565b935093509350935060008060008686868660405160200161004f94939291906102bd565b60405160208183030381529060405280519060200120815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1603610191576000806212345673ffffffffffffffffffffffffffffffffffffffff166127108b8b6040516100fa929190610341565b60006040518083038160008787f1925050503d8060008114610138576040519150601f19603f3d011682016040523d82523d6000602084013e61013d565b606091505b509150915081610182576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161017990610403565b60405180910390fd5b809750505050505050506101b9565b806040516020016101a29190610464565b604051602081830303815290604052955050505050505b915050805190602001f35b600080fd5b6000819050919050565b6101dc816101c9565b81146101e757600080fd5b50565b6000813590506101f9816101d3565b92915050565b600060ff82169050919050565b610215816101ff565b811461022057600080fd5b50565b6000813590506102328161020c565b92915050565b60008060008060808587031215610252576102516101c4565b5b6000610260878288016101ea565b945050602061027187828801610223565b9350506040610282878288016101ea565b9250506060610293878288016101ea565b91505092959194509250565b6102a8816101c9565b82525050565b6102b7816101ff565b82525050565b60006080820190506102d2600083018761029f565b6102df60208301866102ae565b6102ec604083018561029f565b6102f9606083018461029f565b95945050505050565b600081905092915050565b82818337600083830152505050565b60006103288385610302565b935061033583858461030d565b82840190509392505050565b600061034e82848661031c565b91508190509392505050565b600082825260208201905092915050565b7f6661696c656420746f2063616c6c206d6f7665642065637265636f766572206160008201527f742061646472657373203078303030303030303030303030303030303030303060208201527f3030303030303030303030303030313233343536000000000000000000000000604082015250565b60006103ed60548361035a565b91506103f88261036b565b606082019050919050565b6000602082019050818103600083015261041c816103e0565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061044e82610423565b9050919050565b61045e81610443565b82525050565b60006020820190506104796000830184610455565b9291505056fea26469706673582212207ddee236692b0fb014c4a668a714cba393524150b3782202194780d8b923261464736f6c63430008120033",
                                "movePrecompileToAddress": "0x0000000000000000000000000000000000123456"
                            }
                        },
                        "blockOverride": {
                            "number": "0x11c507e",
                            "prevRandao": "0x1",
                            "time": "0x655ef9fb",
                            "gasLimit": "0x1c9c380",
                            "feeRecipient": "0x88c6c46ebf353a52bdbab708c23d0c81daa8134a",
                            "baseFee": "0x68b59f4cb"
                        }
                    }
                ],
                "traceTransfers": true,
                "validation": false
            },
            "0x11c507d"
        ]
    }

4) 事件日志与 ETH 转账日志

eth_call RPC 方法仅返回与第一个调用的返回值对应的输出。然而,在许多情况下,合约执行各个阶段发出的事件提供了更深入的洞察,以了解发生了什么。为了解决这个问题,eth_simulateV1 在其输出中包含日志,提供了更全面的执行视图。

除了这些日志,eth_simulateV1 还支持类似 ERC20 的以太坊转账日志。ETH 转账的日志可以通过 traceTransfers 标志进行开关控制。

例如,通过覆盖 Dope Wars 的治理时间锁合约,我们可以模拟特定治理投票的结果。这种模拟已经可以使用 The Interceptor 工具进行。

    {
      "jsonrpc": "2.0",
      "id": 204,
      "method": "eth_simulateV1",
      "params": [
        {
          "blockStateCalls": [
            {
              "calls": [
                {
                  "type": "0x2",
                  "from": "0xdbd38f7e739709fe5bfae6cc8ef67c3820830e0c",
                  "nonce": "0x0",
                  "maxFeePerGas": "0x0",
                  "maxPriorityFeePerGas": "0x0",
                  "to": "0xb57ab8767cae33be61ff15167134861865f7d22c",
                  "value": "0x0",
                  "input": "execute timelock",
                  "chainId": "0x1",
                  "accessList": []
                }
              ],
              "stateOverrides": {
                "0xb57ab8767cae33be61ff15167134861865f7d22c": {
                  "stateDiff": {},
                  "code": "Timelock contract replacement bytecode"
                }
              },
            }
          ],
          "traceTransfers": true,
          "validation": false
        },
        "0x11b1f64"
      ]
    }

以下是 eth_simulateV1 的调用结果:

    {
      "returnData": "0x",
      "logs": [
        {
          "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
          "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
            "0x000000000000000000000000b57ab8767cae33be61ff15167134861865f7d22c",
            "0x000000000000000000000000ced10840f87a2320fdca1dbe17d4f8e4211840a8"
          ],
          "data": "0x0000000000000000000000000000000000000000000000000f43fc2c04ee0000",
          "blockNumber": "0x11b1f65",
          "transactionHash": "0xdc7f600bef3a06b0864572f85634a4ffa00b8c4318949168727d89b4560b24b0",
          "transactionIndex": "0x0",
          "blockHash": "0x673fb12c793b9b118d6effdd74e9491a04e1666551f19bdb49fa95b9e134acaf",
          "logIndex": "0x0",
          "removed": false
        },
        {
          "address": "0xb57ab8767cae33be61ff15167134861865f7d22c",
          "topics": [
            "0xa560e3198060a2f10670c1ec5b403077ea6ae93ca8de1c32b451dc1a943cd6e7",
            "0x3e6eeeeced3a3b85bb1f37bb260f823dca5e1013558c4d93984762be0154ff21",
            "0x000000000000000000000000ced10840f87a2320fdca1dbe17d4f8e4211840a8"
          ],
          "data": "0x0000000000000000000000000000000000000000000000000f43fc2c04ee0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
          "blockNumber": "0x11b1f65",
          "transactionHash": "0xdc7f600bef3a06b0864572f85634a4ffa00b8c4318949168727d89b4560b24b0",
          "transactionIndex": "0x0",
          "blockHash": "0x673fb12c793b9b118d6effdd74e9491a04e1666551f19bdb49fa95b9e134acaf",
          "logIndex": "0x1",
          "removed": false
        }
      ],
      "gasUsed": "0xbe97",
      "status": "0x1"
    }

执行生成了两个不同的日志。第一个日志捕获了一个以太坊 (ETH) 转账事件,可以通过其来源地址 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 辨别,符合 ERC20 标准,其中第一个主题表示转账签名,第二个与 from 字段相关,第三个与 to 字段相关。data 字段对应于转账的 ETH 数量,在此情况下为 1.1 ETH。第二个日志记录了我们与时间锁合约的交互。

5) 验证模式

eth_simulateV1 的最后一个重要特性是验证标志。默认情况下,验证是关闭的,eth_simulateV1 的行为类似于 eth_call,例如,不检查 nonce,gas 是免费的等。当启用该标志时,客户端将把交易视为实际包含在链上。然而,总是会跳过两个检查:

  1. 你可以从合约发送 交易(sender is not EOA 检查被禁用)

  2. 你不需要为交易提供正确的签名字段。这使你能够在没有访问其私钥的情况下伪造任何账户

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

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

0 条评论

请先 登录 后评论
Killari
Killari
https://www.dark.florist/