在 Hedera 测试网部署多个智能合约时,如何解决间歇性的“nonce 已被使用”错误?

bgu*_*uiz 5 javascript hedera-hashgraph ethers.js hardhat hedera

当我同时将 2 个智能合约部署到 Hedera Testnet 时,出现以下错误:

nonce has already been used [ See: https://links.ethers.org/v5-errors-NONCE_EXPIRED ]
Run Code Online (Sandbox Code Playgroud)

这种情况间歇性地发生,大约有 20% 的时间发生。它们的发生似乎是不确定的,尽管我无法证实这一点。

我的部署代码是:

        const deploymentSigner: Signer = (await hre.ethers.getSigners())[0];
        // console.log('Deployment signer address', (await deploymentSigner.getAddress()));
        const scDeploymentPromises: Promise<Contract>[] = [];
        for (let idx = 0; idx < scNamesToDeploy.length; ++idx) {
            const scName = scNamesToDeploy[idx];
            const scFactory: ContractFactory =
                await hre.ethers.getContractFactory(scName);
            console.log(`Deploying ${scName} on ${networkName} ...`);
            const sc: Contract = await scFactory.deploy();
            // NOTE deployment with constructor params not needed for these particular SCs
            const scDeploymentPromise: Promise<Contract> = sc.deployed();
            scDeploymentPromises.push(scDeploymentPromise);
        }

        // NOTE collect deployment promises without `await`-ing them,
        // so as to be able to run them in parallel.
        const deployedScs: Contract[] = await Promise.all(scDeploymentPromises);
        deployedScs.forEach((sc, idx) => {
            const scName = scNamesToDeploy[idx];
            console.log(`Deployed ${scName} on ${networkName} at ${sc.address}`);
        });
Run Code Online (Sandbox Code Playgroud)

其中scNamesToDeploy是在其他地方初始化的字符串数组,其中包含我要部署的智能合约名称。

值得注意的是,如果我更改第一个循环中的代码以执行以下操作

nonce has already been used [ See: https://links.ethers.org/v5-errors-NONCE_EXPIRED ]
Run Code Online (Sandbox Code Playgroud)

...因此跳过第二个循环,此错误将停止发生。因此,我认为这与第一个部署事务之后第二个部署事务发生得太快有关。但是,从技术上讲,这应该是可能的,我什至应该能够在同一个“块”内提交两个交易。(“区块”用引号引起来,因为 Hedera 没有区块,AFAICT。)


细节

版本

我正在使用 Hardhat ( hardhat@2.13.0) + ethers.js ( ethers@5.7.2via @nomicfoundation/hardhat-toolbox@2.0.2)。

错误详情

这是完整的错误消息:

nonce has already been used [ See: https://links.ethers.org/v5-errors-NONCE_EXPIRED ] (error={"name":"ProviderError","_stack":"ProviderError: [Request ID: 356c0cee-949f-4f52-b936-25bbba9ef603] Nonce too low\n    at HttpProvider.request (/Users/user/code/hedera/hedera-scratch/chain/node_modules/hardhat/src/internal/core/providers/http.ts:88:21)\n    at processTicksAndRejections (node:internal/process/task_queues:95:5)\n    at async EthersProviderWrapper.send (/Users/user/code/hedera/hedera-scratch/chain/node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:20)","code":32001,"_isProviderError":true}, method="sendTransaction", transaction=undefined, code=NONCE_EXPIRED, version=providers/5.7.2)\
Run Code Online (Sandbox Code Playgroud)

网络配置

这是来自networks以下部分hardhat.config.js

        const deploymentSigner: Signer = (await hre.ethers.getSigners())[0];
        // console.log('Deployment signer address', (await deploymentSigner.getAddress()));
        const scDeploymentPromises: Promise<Contract>[] = [];
        for (let idx = 0; idx < scNamesToDeploy.length; ++idx) {
            const scName = scNamesToDeploy[idx];
            const scFactory: ContractFactory =
                await hre.ethers.getContractFactory(scName);
            console.log(`Deploying ${scName} on ${networkName} ...`);
            const sc: Contract = await scFactory.deploy();
            // NOTE deployment with constructor params not needed for these particular SCs
            const scDeploymentPromise: Promise<Contract> = sc.deployed();
            scDeploymentPromises.push(scDeploymentPromise);
        }

        // NOTE collect deployment promises without `await`-ing them,
        // so as to be able to run them in parallel.
        const deployedScs: Contract[] = await Promise.all(scDeploymentPromises);
        deployedScs.forEach((sc, idx) => {
            const scName = scNamesToDeploy[idx];
            console.log(`Deployed ${scName} on ${networkName} at ${sc.address}`);
        });
Run Code Online (Sandbox Code Playgroud)

请注意,我使用的hederatestnetrelay是连接到 Hedera 测试网的本地实例hedera-json-rpc-relay,因为这似乎比直接连接到公共 RPC 端点更稳定,在直接连接到公共 RPC 端点时,我会遇到不同的间歇性错误(“调用 RPC 时出现未知错误”)

Rya*_*ndt 5

nonce has already been used当提交的交易的随机数值等于或小于它之前提交的另一个交易(相同的 EOA)时,就会发生错误。

EOA 的正确行为是为每个后续交易将随机数值增加 1(或更多)。但是,您看到此错误表明这种情况没有发生,并且您自己的评估是

因此,我认为这与第一个部署事务之后第二个部署事务发生得太快有关。

方向是正确的。看起来 ethers.js 在 2 个交易中重复使用了相同的随机数值,仅仅是因为它们或多或少同时发生。这可能是因为在幕后,ethers.js 需要使用eth_getTransactionCountRPC 端点,并且该端点通过网络传输,因此它可能会检索到“过时”值,尤其是在发出 2 个彼此间隔毫秒的请求时。

手动尝试一下:

curl -X POST --data '{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["0x...your deployment EOA addres...","latest"],"id":1}'

解决此问题的一种方法是覆盖部署事务的事务参数以手动指定随机数:

    const deploymentSigner: Signer = (await hre.ethers.getSigners())[0];
    // console.log('Deployment signer address', (await deploymentSigner.getAddress()));
    const deploymentSignerInitialNonce = await deploymentSigner.getTransactionCount();
    const scDeploymentPromises: Promise<Contract>[] = [];
    for (let idx = 0; idx < scNamesToDeploy.length; ++idx) {
        // NOTE manually override the nonce value to solve for intermittent failures
        // that otherwise occur with concurrent deployment transactions.
        // This is intended to **only** work if the deployment account is **not** submitting
        // any other transactions to the network at the same time.
        const deploymentTxOverrides = {
            nonce: (deploymentSignerInitialNonce + idx),
        };
        // console.log('Deployment Tx Overrides', deploymentTxOverrides);
        const scName = scNamesToDeploy[idx];
        const scFactory: ContractFactory =
            await hre.ethers.getContractFactory(scName);
        console.log(`Deploying ${scName} on ${networkName} ...`);
        const sc: Contract = await scFactory.deploy(deploymentTxOverrides);
        // TODO deployment with constructor params (but not needed in this case)
        const scDeploymentPromise: Promise<Contract> = sc.deployed();
        scDeploymentPromises.push(scDeploymentPromise);
    }

    // NOTE collect deployment promises without `await`-ing them,
    // so as to be able to run them in parallel.
    const deployedScs: Contract[] = await Promise.all(scDeploymentPromises);
    deployedScs.forEach((sc, idx) => {
        const scName = scNamesToDeploy[idx];
        console.log(`Deployed ${scName} on ${networkName} at ${sc.address}`);
        deploymentCacheNetwork[scName] = sc.address;
    });
Run Code Online (Sandbox Code Playgroud)

具体来说:

(1)await deploymentSigner.getTransactionCount();手动检索eth_getTransactionCount部署SC的EOA

(2){ nonce: (deploymentSignerInitialNonce + idx) };在第一个 for 循环中每次运行将随机数增加 1

(3)await scFactory.deploy(deploymentTxOverrides);使用手动指定的nonce值执行部署交易