Relayers - OpenZeppelin 文档

本文档介绍了OpenZeppelin Defender的Relayers模块,它允许用户通过API请求或Defender的其他模块发送链上交易,自动处理gas费用、私钥安全存储、交易签名、nonce管理和gas价格估算等问题。文档还详细介绍了Relayer的使用场景、API密钥、地址管理、策略配置、交易发送、签名、信息查询以及安全考虑等。

中继器

中继器允许你通过常规 API 请求或通过其他 Defender 模块(如 Actions、Workflows 和 Deploy)发送链上交易。中继器还自动处理Gas费用的支付,并负责私钥的安全存储、交易签名、nonce 管理、煤气定价估算和重新提交。使用中继器,你无需担心在后端服务器上存储私钥或监控煤气价格和交易以确保它们被确认。

用例

  • 自动执行智能合约上的交易以触发状态转变。

  • 使用外部数据更新链上预言机。

  • 发送元交易以构建无煤气体验。

  • 通过空投代币给你的新用户来响应你应用中的注册。

  • 从协议合约中提取资金到安全钱包,

  • 构建具有完全自定义逻辑和灵活性的机器人。

什么是中继器?

中继器是一个基于以太坊的外部拥有账户(EOA),仅分配给你的团队。每当你创建一个新的中继器时,Defender 会在安全的金库中创建一个新的私钥。每当你请求 Defender 通过该中继器发送交易时,将使用相应的私钥进行签名。

你可以把每个中继器视为一个发送交易的队列,通过同一个中继器发送的所有交易将按顺序发送,并且来自同一个账户,由你的团队独占控制。了解更多关于技术实现的信息,请访问 这里.

管理中继器

要创建中继器,只需单击页面右上角的 创建中继器 按钮,指定一个名称并选择网络。

管理中继器详情

请记住,你需要为每个中继器单独提供 ETH(或本链代币)以确保它们有足够的资金支付所发送交易的Gas费。如果中继器的资金降至 0.1 ETH 以下,Defender 会向你发送电子邮件通知。
通过 Deploy 向导创建的测试网中继器将在可能的情况下自动提供资金。阅读更多信息 这里.

API 密钥

每个中继器可以有一个或多个 API 密钥 相关联。要通过中继器发送交易,你需要用一个 API 密钥/秘密对来验证请求。你可以根据需要创建或删除 API 密钥,这不会改变发送地址或中继器余额。

要为中继器创建 API 密钥,请单击中继器,然后单击 更多 按钮以展开下拉菜单并选择 创建 API 密钥

管理中继器创建 API 密钥

一旦 API 密钥创建,确保记下密钥。API 秘密在创建时仅可见一次——如果你没有记下它,它将永远丢失。

管理中继器 API 密钥

中继器的 API 密钥 其私钥无关。私钥始终保存在安全的密钥金库中,从不暴露(有关更多信息,请参见 安全注意事项 部分)。这种解耦允许你自由旋转 API 密钥,同时保持中继器的相同地址。

地址

每次创建中继器时,都会创建一个新的 EOA 作为其后盾。出于安全考虑,无法将现有的私钥导入中继器,也无法导出 Defender 创建的中继器的私钥。如果你在系统中授予中继器地址特权角色以避免锁定,请考虑在需要时有一个管理方法将其切换到其他地址。

策略

你可以通过指定策略来限制中继器的行为。

要配置中继器的策略,请转到 中继器页面,选择中继器,然后转到 策略 选项卡。你将看到一个表单,你可以选择启用策略并调整其参数。

管理中继器策略

煤气价格上限

为每个通过中继器发送的交易指定一个最大煤气价格。当启用此策略时,Defender 将覆盖任何超出指定上限的交易的 gasPricemaxFeePerGas。请考虑交易的煤气价格是根据中继器实际向区块链发送交易时的煤气价格预言机所指定的,因此此策略可以用作保护煤气价格飙升的措施。

除了你在此处可以指定的最大煤气价格策略外,Defender 还为具有最低煤气要求的网络实现了最低煤气价格策略。查看你使用的各个网络的要求。
接收者白名单

为每个使用中继器发送的交易指定授权合约的列表。Defender 将拒绝并丢弃任何其目标地址不在列表中的交易。

白名单仅适用于交易的 to 字段。它不会过滤 ERC20 或其他资产接收者。
EIP1559 定价

指定中继器发送的交易是否应默认采用 EIP1559。这适用于中继器发送具有动态煤气定价或未指定的 gasPricemaxFeePerGas/ maxPriorityFeePerGas 的交易时。请注意,此策略选项仅在 EIP1559 兼容网络上显示。

新中继器默认启用 EIP1559 定价策略。如果你有一个创建时没有默认选项的中继器,你始终可以启用此标志。
私有交易

指定交易是否应通过私有内存池发送。这意味着在交易被包含在区块之前,将不会公开看到交易。

参数可以在以下状态之间切换:true 以启用私有内存池交易,false 以选择公共可见性。或者,用户可以通过将值设置为 flashbots-normalflashbots-fast 来指定交易速度。默认情况下,当策略设置为 true 时,速度默认为 flashbots-normal,允许以无缝方式包含,同时保持交易隐私。此配置使用户能够有效地根据其特定的隐私和速度要求调整交易策略。你可以在 这里 阅读有关使用 Flashbots 进行更快交易的信息。

通过使用 Flashbots Protect RPC ,私有交易仅在 主网 上启用。因此,在通过 Defender 发送私有交易时,可能会适用相同的 关键考虑因素

中继器组

中继器组是多个单独中继器的集合,这些中继器协同工作以提交交易。通过将中继器分组,你可以提高整体交易吞吐量和冗余性,从而增强交易提交过程的可靠性。中继器组旨在在多个中继器之间分配工作负载,确保没有单个中继器成为瓶颈。

中继器组的好处

  • 增加吞吐量: 中继器组可以处理更高数量的交易,因为工作负载被分摊到多个中继器。

  • 冗余性: 如果组中的一个中继器失败或变慢,其他中继器可以接管,从而降低延迟风险。

  • 效率: 通过协调多个中继器,你可以优化交易提交并确保交易尽快处理。

  • 集中管理: 在单个 API 密钥下管理多个中继器的能力简化了管理,使在复杂系统中保持控制变得更加容易。

中继器组的缺点

  • 统一配置: 策略和配置在组中的所有中继器上统一应用,使得管理单个中继器设置变得困难。

  • 功能限制: 某些功能,例如消息签名,不能在属于组的中继器上使用。

  • 组限制操作: 组内的中继器不能独立使用;它们必须作为组的一部分共同运作。

  • 潜在的交易顺序问题: 由于交易根据它们的条件在不同中继器之间分配,因此它们可能不会按接收顺序处理,导致某些交易被挖矿时失序。

健康监测

中继器组依赖定期健康检查,以确保交易在组中的中继器之间高效分配。这些健康检查评估每个中继器的性能和可用性,帮助系统决定哪些中继器最适合处理新交易。

系统定期评估组中的每个中继器。它根据几个关键因素为每个中继器计算一个“权重”。然后使用这些权重来决定如何在组内分配交易,优先考虑最可靠和响应最迅速的中继器。

  • 第一个交易处理的速度(最高优先级): 最重要的因素是一个中继器开始处理交易的速度。系统会查看第一笔待处理交易发送和处理所需的时间。更快的中继器被认为更健康,并享有更高的优先级。

  • 待处理交易数量: 系统检查每个中继器队列中有多少交易在等待。如果一个中继器有很多待处理交易,这可能表明它超负荷,可能会在快速处理新交易方面遭遇困难。

  • 剩余余额: 中继器的可用余额也会被考虑。一个中继器需要有足够的余额来支付交易费用。如果中继器的余额不足,它可能会在处理交易时遇到困难,从而影响其健康评分。

用户可以手动调整个别中继器的权重。例如,将权重设置为 0 将防止使用某个中继器,从而提供对激活中继器的精确控制。

发送交易

通过中继器发送交易的最简单方法是使用 Defender SDK 包。客户端使用 API 密钥/秘密进行初始化,并公开一个简单的 API,用于通过相应中继器发送交易。

const { Defender } = require('@openzeppelin/defender-sdk');
const client = new Defender({
  relayerApiKey: 'YOUR_API_KEY',
  relayerApiSecret: 'YOUR_API_SECRET'
});

const tx = await client.relayerSigner.sendTransaction({
  to, value, data, gasLimit, speed: 'fast'
});

const mined = await tx.wait();
为了提高中继器的可靠性,我们建议在单个中继器上发送不超过 50 笔交易/分钟 ,特别是在像 Polygon、Optimism、Arbitrum 等快速移动的链上。例如,如果你希望实现 250 笔交易/分钟的吞吐量,则需要在 5 个中继器之间进行负载平衡。这 5 个中继器可以属于同一个账户。
在初始化中继器客户端时,你无需输入私钥,因为私钥保存在 Defender 金库中。
目前,zkSync 除了使用 eth_estimateGas 端点外,没有精确计算 gasLimit 的方法。因此,Defender 不能进行任何 gasLimit 的设置,并用 RPC 估算覆盖用户输入。

使用 ethers.js

中继器客户端通过自定义 签名者 集成了 ethers.js。这使你可以在代码库中以最小的更改切换到中继器并发送交易。

const { Defender } = require('@openzeppelin/defender-sdk');
const { ethers } = require('ethers');

const credentials = { relayerApiKey: YOUR_RELAYER_API_KEY, relayerApiSecret: YOUR_RELAYER_API_SECRET };
const client = new Defender(credentials);

const provider = client.relaySigner.getProvider();
const signer = client.relaySigner.getSigner(provider, { speed: 'fast', validUntil });

const erc20 = new ethers.Contract(ERC20_ADDRESS, ERC20_ABI, signer);
const tx = await erc20.transfer(beneficiary, 1e18.toString());
const mined = await tx.wait();

在上述示例中,我们还使用了 DefenderRelayProvider 来进行网络调用。签名者可以与任何提供者一起使用,例如 ethers.getDefaultProvider(),但你也可以依赖 Defender 作为网络提供者。

你可以在 这里 阅读有关 ethers 集成的更多信息。

使用 web3.js

中继器客户端也与 web3.js 集成,并通过自定义 提供者 进行支持。这使你可以使用熟悉的 web3 接口使用中继器发送交易并查询网络。

const { Defender } = require('@openzeppelin/defender-sdk');
const Web3 = require('web3');

const credentials = { relayerApiKey: YOUR_RELAYER_API_KEY, relayerApiSecret: YOUR_RELAYER_API_SECRET };
const client = new Defender(credentials);

const provider = client.relaySigner.getProvider();

const web3 = new Web3(provider);

const [from] = await web3.eth.getAccounts();
const erc20 = new web3.eth.Contract(ERC20_ABI, ERC20_ADDRESS, { from });
const tx = await erc20.methods.transfer(beneficiary, (1e18).toString()).send();

在上述示例中,transfer 交易由中继器签名并广播,所有其他额外的 JSON RPC 调用通过 Defender 私有端点路由。

你可以在 这里 阅读有关 web3 集成的更多信息。

使用意图支持的中继签名者

当使用 Defender SDK 的中继签名者从 ethers/web3 合约的上下文发送交易时,需要注意的是,在 意图模式 中的交易不受支持。如果 API 返回一个 意图响应,将抛出错误,因为当前实现期望某些缺失的字段。

为避免此问题,建议在交易提交时回退到使用默认的 SDK sendTransaction 方法。这确保交易可以无错误处理。

此外,当前实现不跟踪重新提交的交易,这可能导致由于重试而生成多个哈希。在背景中交易哈希变化时,实施能够有效处理这些场景的逻辑变得至关重要,确保过程顺利完成。

EIP1559 支持

由于并非所有支持的网络都与 EIP1559 兼容,因此 EIP1559 交易支持仅在 被识别为兼容 和由团队启用的网络上激活。

中继器可以通过以下方式发送 EIP1559 交易:

  • 通过 UI 发送交易,启用 EIP1559Pricing 策略

  • 通过 API 发送交易,同时指定 maxFeePerGasmaxPriorityFeePerGas

  • 通过 API 发送交易,设置 speed 并启用 EIP1559Pricing 策略

一旦发送任何交易,在其生命周期的每个阶段(如替换和重新定价),都会具有相同的类型,因此目前无法在已提交的情况下更改类型。

任何尝试将 maxFeePerGasmaxPriorityFeePerGas 发送到不兼容 EIP1559 的网络将被中继器拒绝和丢弃。

你可以通过查看中继器 策略 来判断网络是否支持 EIP1559。如果EIP1559Pricing 策略没有显示,则表明我们尚未为该网络添加 EIP1559 支持。

如果你发现我们已经支持的 EIP1559 兼容网络,但没有启用 EIP,请随时通过 https://www.openzeppelin.com/defender2-feedback 联系我们。

私有交易

私有交易允许中继器发送在公共内存池中不可见的交易,而是通过使用特殊的 eth_sendRawTransaction 提供者通过私有内存池转发,其取决于网络和当前支持(如 Flashbots 网络覆盖)。

中继器可以通过以下任一方式发送私有交易:

  • 通过 API 发送交易,启用 privateTransactions 策略或设置为 flashbots-normalflashbots-fast

  • 通过 API 发送交易,isPrivate 参数设置为 true

  • 通过 UI 发送交易并勾选内存池可见性复选框

中继器发送交易视图中的内存池可见性复选框

向不支持私有交易的网络发送带有 isPrivate 标志设置为 true 的交易将被中继器拒绝和丢弃。

目前,仅支持以下网络:

速度

中继器还可以接受一个速度参数,而不是通常的 gasPricemaxFeePerGas/ maxPriorityFeePerGas,该参数可以是 safeLowaveragefastfastest。这些值在交易发送或重新提交时会映射到实际的煤气价格,并根据网络的状态而变化。

如果提供速度参数,则将根据 EIP1559Pricing 中继器策略对交易进行定价。

主网的煤气价格和优先费用是根据 EthGasStationEtherChainGasNowBlockativeEtherscan 报告的值计算的。在 Polygon 及其测试网中,使用 gas station。在其他网络中,煤气价格从对网络的 eth_gasPriceeth_feeHistory 的调用中获得。

固定煤气定价

另外,你可以通过设置 gasPrice 参数或 maxFeePerGasmaxPriorityFeePerGas 参数,指定 固定的 gasPrice固定的 maxFeePerGas 和 maxPriorityFeePerGas 的组合 进行交易。以固定定价进行的交易要么是以指定定价计算挖掘,要么在未能在 validUntil 时间之前挖掘时替换为 NOOP 交易。

请记住,你需要提供 speedgasPricemaxFeePerGas/ maxPriorityFeePerGas 中的任意一个,但不能混合它们的发送交易请求。

任何发送交易请求而未包含任何定价参数时,它将以 fast 默认速度进行定价。
如果你同时提供固定的 maxFeePerGasmaxPriorityFeePerGas,请确保 maxFeePerGas 大于或等于 maxPriorityFeePerGas。否则,它将被拒绝。

有效截止时间

每个通过中继器的交易在 validUntil 时间之前有效,以提交到网络。在 validUntil 时间之后,交易将被替换为 NOOP 交易,以防止中继器在交易的 nonce 上被卡住。NOOP 交易不会做任何事情,只是推进中继器的 nonce。

validUntil 默认设置为在交易创建后的 8 小时。请注意,你可以将 validUntil 与 固定定价 结合使用,以获得极快的挖掘时间并在 gasPricemaxFeePerGas 上优于其他交易。

如果你使用 ethers.js,可以将 validForSeconds 选项设置为 validUntil。在下面的示例中,我们配置一个 DefenderRelaySigner 来发出在其创建后 120 秒内有效的交易。

const { DefenderRelayProvider, DefenderRelaySigner } = require('@openzeppelin/defender-sdk-relay-signer-client/ethers');
const { ethers } = require('ethers');

const credentials = { apiKey: API_KEY, apiSecret: API_SECRET };
const provider = new DefenderRelayProvider(credentials);
const signer = new DefenderRelaySigner(credentials, provider, { speed: 'fast', validForSeconds: 120 });
validUntil 是 UTC 时间戳。确保使用 UTC 时区,而不是本地时区。

交易ID

由于中继器可以在预期的时间框架内未获得确认的情况下,根据更新的煤气定价重新提交交易,因此给定交易的 hash 随着时间的推移可能会变化。要跟踪给定交易的状态,中继器 API 返回一个你可以用来 查询transactionId 标识符。

import { Relayer } from '@openzeppelin/defender-sdk-relay-signer-client';
const relayer = new Relayer({ apiKey: API_KEY, apiSecret: API_SECRET });
const latestTx = await relayer.getTransaction(tx.transactionId);

返回的交易对象 latestTx 将具有以下形状:

interface RelayerTransactionBase {
  transactionId: string; // Defender 交易标识符
  hash: string; // 以太坊交易哈希
  to: string;
  from: string;
  value?: string;
  data?: string;
  speed: 'safeLow' | 'average' | 'fast' | 'fastest';
  gasLimit: number;
  nonce: number;
  status: 'pending' | 'sent' | 'submitted' | 'inmempool' | 'mined' | 'confirmed' | 'failed';
  chainId: number;
  validUntil: string;
}
getTransaction 函数将返回来自 Defender 服务的交易最新视图,该视图每分钟更新一次。

替换交易

虽然中继器会在交易未被确认的情况下自动重新提交交易,并在其有效截止时间后自动取消交易,但如果交易尚未被挖掘,你仍然可以手动替换或取消你的交易。这使你能够取消不再有效的交易,调整其 TTL,或增加其速度或煤气定价。

为此,使用 @openzeppelin/defender-sdk-relay-clientreplaceByNoncereplaceById 方法:

// 取消交易有效负载(交易到随机地址,值和数据为零)
replacement = {
  to: '0x6b175474e89094c44da98b954eedeac495271d0f',
  value: '0x00',
  data: '0x',
  speed: 'fastest',
  gasLimit: 21000
};

// 通过 nonce 替换交易
tx = await relayer.replaceTransactionByNonce(42, replacement);

// 或者通过 transactionId 替换
tx = await relayer.replaceTransactionById('5fcb8a6d-8d3e-403a-b33d-ade27ce0f85a', replacement);

你还可以通过在使用 ethersweb3.js 适配器发送交易时设置 nonce 来替换待处理交易:

// 使用 ethers
erc20 = new ethers.Contract(ERC20_ADDRESS, ERC20_ABI, signer);
replaced = await erc20.functions.transfer(beneficiary, 1e18.toString(), {
  nonce: 42
});

// 使用 web3.js
erc20 = new web3.eth.Contract(ERC20_ABI, ERC20_ADDRESS, { from });
replaced = await erc20.methods.transfer(beneficiary, (1e18).toString()).send({
  nonce: 42
});
只能 替换相同类型的交易。例如,如果你尝试替换 EIP1559 交易,则 无法用 传统交易替换。同时,如果提供了 speed,交易将根据原始类型要求进行重新定价。

Webhooks 通知

通过 webhook,可以高效地监听交易状态更改。该方法允许你的应用程序在交易状态更新时实时接收通知。

配置 Webhook 通知的步骤

  1. 访问中继器页面

  2. 选择交易状态: 在中继器页面,你将找到选择希望接收通知的特定交易状态的选项。可用状态:

  • 待处理:Defender 收到交易。

  • 已发送:交易已准备发送(定价和签名)。

  • 已提交:交易已提交至网络。

  • 在内存池中:在内存池中找到的交易。

  • 已挖矿:交易已挖矿。

  • 已确认:交易已确认(至少 12 次确认)。

  1. 选择通知通道: 在选择所需状态之后,你需要选择 webhook 通知通道。这就是 webhook 通知将被发送的地方。

  2. 保存你的配置: 选择状态并配置通知通道后,请保存设置。这将注册你的 webhook,并开始接收所选事件的通知。

Webhook 通知示例:

{
  "event": "transaction_status_change",
  "timestamp": "2024-06-13T12:29:41.254Z",
  "transaction": {
    "signature": {
      "r": "0xee81d58c53c1d3432c95847c71a525417bad6e8fa711007137b2e11155ba8f94",
      "s": "0x362ce1f75d660504e22590f678c243bc24954ccfbf17864f4aab05fd8b1d6ca3",
      "v": "0x1b"
    },
    "maxPriorityFeePerGas": 7556907973,
    "maxFeePerGas": 39004490874,
    "chainId": 11155111,
    "hash": "0xea04c34422295ef60b57fea50790b4f9396d852274fa46ee5cf8d0407d7cc32b",
    "transactionId": "1eece8bb-05d6-493f-903a-12c750700b81",
    "value": "0x5af3107a4000",
    "gasLimit": 25200,
    "to": "0x5e87fD270D40C47266B7E3c822f4a9d21043012D",
    "from": "0xf87921a0999d522383afa2b41db2538231a647f0",
    "data": "0x",
    "nonce": 19,
    "status": "mined",
    "speed": "fast",
    "validUntil": "2024-06-13T20:29:01.051Z",
    "createdAt": "2024-06-13T12:29:01.546Z",
    "sentAt": "2024-06-13T12:29:01.546Z",
    "pricedAt": "2024-06-13T12:29:01.546Z",
    "isPrivate": false
  }
}

列出交易

你还可以列出通过中继器发送的最新交易,可以选择按状态(待处理、已挖矿或失败)进行过滤。这在防止你的 Actions 脚本重新发送已在路中的交易时特别有用:在发送交易之前,你可以使用按 pending 状态过滤的列表方法查看队列中是否有与你即将发送的交易具有相同目标和 calldata 的交易。

const txs = await relayer.list({
  since: new Date(Date.now() - 60 * 1000),
  status: 'pending', // 可以是 'pending'、'mined' 或 'failed'
  limit: 5, // 将首先返回最新的交易
  usePagination: true,
  next: '' // 可选的下一游标用于分页
  sort: 'desc'
})

删除待处理交易

在中继器被堵塞且无法处理交易的情况下,系统提供了一种删除待处理交易的功能。此操作旨在作为最后手段,以解决未被挖掘至少 30 分钟的交易问题。这种功能可以在中继器抽屉中的待处理交易选项卡中从中继器中激活,当中继器具有待处理交易时。

关键点:

  • 预期用途:此功能专门针对解决因未挖掘交易而卡住的中继器问题。建议仅在确认已超过 30 分钟没有交易被挖掘后使用。

  • 操作概述:在启动删除操作后,中继器将进入暂停状态。在此暂停期间,系统将发送 NOOP(无操作指令)以清除某些交易或完全从数据库中删除它们。这一决定根据每个待处理交易的特定特征来进行。

  • 持续时间:删除待处理交易并恢复正常操作的整个过程可能需要长达 30 分钟。包括评估每个交易、实施必要的操作以及确保中继器准备好恢复其职能所需的时间。

  • 恢复操作:删除操作完成后,中继器将自动恢复其标准活动。用户无需采取进一步措施来重新激活中继器。

  • 通知:一旦过程完成并且中继器恢复其操作,用户将收到电子邮件通知。

意图机制

“意图”是一个引入的概念,旨在提高交易提交的效率和可靠性。意图不是立即提交的交易,而是指示交易准备就绪但暂时保留的占位符。这一机制有助于管理交易队列过大或网络或中继器处理交易缓慢的情况。

意图用于维护交易的顺序并防止系统过载。在以下两种主要场景中使用意图:

  • 交易量大: 当待处理交易数量超过允许的最大在飞交易时,新交易作为意图存储。这防止系统被淹没,确保交易以正确的顺序提交。

  • 处理缓慢: 如果网络或中继器处理交易缓慢(例如,过去 30 分钟内没有交易被挖掘),新交易作为意图存储以避免造成拥堵。

意图响应

意图响应类似于正常交易的响应,但有以下不同:

  • hashnull

  • isIntenttrue,指示交易作为意图发送,此字段在正常交易中不会设置。此字段在意图处理时不会被移除或更改。

  • pendingIntentSince 是意图创建的时间戳,指示意图仍处于待定状态。此字段在意图处理后将被移除。

  • relayerId 是创建时的中继器组相同 ID。一旦分配,该 ID 将更改为处理意图的中继器 ID。

  • 不设置与煤气相关的字段,交易将在处理时定价。

{
  "hash": null,
  "transactionId": "transaction123",
  "value": "0x1",
  "gasLimit": 21000,
  "to": "0x123...",
  "data": "0x",
  "status": "pending",
  "speed": "fast",
  "validUntil": "2000-01-01T20:00:00.000Z"
  "isIntent": true,
  "relayerId": "relayer123",
  "tenantRelayerGroupId": "tenant123|relayerGroup123",
  "createdAt": "2000-01-01T12:00:00.000Z"
}
取消意图

中继器交易意图可以被取消并永久删除。这一功能特别有用,当你需要防止特定意图被提交或希望在队列中腾出空间供其他更紧急的交易使用时。

为此,可以使用 @openzeppelin/defender-sdk-relay-clientcancelTransactionById 方法:

tx = await relayer.cancelTransactionById('5fcb8a6d-8d3e-403a-b33d-ade27ce0f85a');

签名

除了发送交易,中继器还可以根据 EIP-191 标准 使用其私钥签署任意消息(以 \x19Ethereum Signed Message:\n 为前缀)。你可以通过客户端的 sign 方法或等效的 ethers.js 方法访问此功能。

const signResponse = await relayer.sign({ message });
与大多数库不同,中继器使用非确定性 ECDSA 签名。这意味着如果你请求中继器多次签名相同的消息,你将得到多个不同的签名,这可能与使用 ethers.js 或 web3.js 签名时的结果不同。所有这些不同的签名都是有效的。有关更多信息,请参见 RFC6979
对于属于中继器组的中继器,该方法不可用。

签名类型数据

除了签名的 API 方法外,中继器还实现了 signTypedData,你可以使用它根据 EIP712 标准 签署消息以进行类型数据签名。 你可以提供 domainSeparatorhashStruct(message),或使用等效的 ethers.js 方法。

const signTypedDataResponse = await relayer.signTypedData({
  domainSeparator,
  hashStructMessage
});
对于属于中继器组的中继器,该方法不可用。

中继器信息

可以使用 DefenderRelaySigner 类的 getAddress 方法检索中继器的地址。

const address = await signer.getAddress();

如果你需要有关中继器的更多信息,可以查看客户端的 getRelayer 方法。它返回以下数据:

const info = await relayer.getRelayer();
console.log('中继器信息', info);

export interface RelayerModel {
  relayerId: string;
  name: string;
  address: string;
  network: string;
  paused: boolean;
  createdAt: string;
  pendingTxCost: string;
}
对于属于中继器组的中继器,该方法将返回中继器组信息。

中继器状态

要获得中继器的当前状态的更好洞察,可以使用 DefenderRelaySigner 类的 getRelayerStatus 方法。该方法提供有关中继器的实时信息,例如其 nonce、交易配额和待处理交易数量。

const address = await signer.getRelayerStatus();

如果你需要有关中继器的信息,可以查看客户端的 getRelayer 方法。它返回以下数据:

export interface RelayerStatus {
  relayerId: string;
  name: string;
  nonce: number;
  address: string;
  numberOfPendingTransactions: number;
  paused: boolean;
  pendingTxCost?: string;
  txsQuotaUsage: number;
  rpcQuotaUsage: number;
  lastConfirmedTransaction?: {
    hash: string;
    status: string;
    minedAt: string;
    sentAt: string;
    nonce: number;
  };
}
对于属于中继器组的中继器,该方法将返回组的状态响应数组。

网络调用

Defender 还提供了一种简单的方法来对网络进行任意 JSON RPC 调用。你可以使用低级 relayer.call 方法发送任何 JSON RPC HTTP 请求:

const balance = await relayer.call('eth_getBalance', ['0x6b175474e89094c44da98b954eedeac495271d0f', 'latest']);

如果你使用 ethers.js,可以通过自定义 DefenderRelayProvider 提供者 对象来支持:

const provider = new DefenderRelayProvider(credentials);
const balance = await provider.getBalance('0x6b175474e89094c44da98b954eedeac495271d0f');

提取资金

你可以在 中继器页面 提取中继器的资金,选择中继器,然后单击 提取

中继器提取按钮

提取 界面,你可以选择以 ETH 发送资金或从内置的 ERC20 代币列表中选择。

中继器提取资金屏幕

深入了解

每个中继器都与一个私钥关联。当收到发送交易的请求时,中继器会验证请求,原子性分配一个 nonce,为支付Gas费保留余额,根据其 EIP1559 定价策略解析其速度为 gasPricemaxFeePerGas/ maxPriorityFeePerGas,使用其私钥进行签名,并将其排队提交到区块链。仅在此过程完成后,响应才会发送回客户端。然后,交易通过多个节点提供者广播以获得冗余,并在 API 出现故障时尝试重新发送三次。

每分钟,系统会检查所有在飞交易。如果它们尚未被挖掘且经过时间超过(具体取决于交易速度),则将以其各自的交易类型定价增加 10% 重新提交(或更新其速度的最新定价,如果更高),这可能高达 报告的煤气定价的 150%。此过程会导致交易哈希发生变化,但其 ID 会得到保留。另一方面,如果交易已被挖掘,则仍需监控几个区块,直到我们认为其已经确认。

资金不足

Defender 小心追踪发送到中继器的交易成本。在中继器无法立即处理交易的情况下,Defender 会累积所有待处理交易的成本。在允许任何新交易提交之前,Defender 将确保中继器的余额足以覆盖所有待处理交易的成本,包括新交易。因此,在这种情况下,你可能会遇到特定交易的“资金不足”错误。

  • 一笔交易的成本计算为:txCost = gasLimit * maxFeePerGas + value

  • 中继器的余额计算为:predictedBalance = balance - pendingTxCosts

“资金不足”错误将在以下情况下抛出:txCost > predictedBalance

交易吞吐量和负载平衡

我们建议使用中继器组,以提高吞吐量和冗余性。但是,如果这不是一个选项,你可以使用下面的方法来优化你的设置。

中继器会原子性地分配 nonce,这使它们能够处理许多并发交易。但是,为了优化基础设施,所有数值(以下所有数字是所有中继器在一个账户中的累积)都是有限制的。

默认情况下,当你为特定中继器创建 API 密钥时,它会自动分配以下速率限制。

  • 100 请求/秒,突发 300 请求。

这些速率限制适用于读取(例如,获取交易状态)和写入(例如,发送交易)。

如果你需要额外的吞吐量以满足你的用例,请通过 defender-support@openzeppelin.com 联系我们。你需要处于 企业等级 才能提高吞吐量。

为了提高中继器的可靠性,我们建议在单个中继器上发送不超过 50 笔交易/分钟,特别是在快速移动的链上,如 Polygon、Optimism、Arbitrum 等。如果你希望实现 250 笔交易/分钟的吞吐量,则需要在 5 个中继器之间进行负载平衡。这 5 个中继器可以属于同一个账户。

你可以使用 Defender SDK 包在多个中继器之间进行负载平衡。以下是如何做到这一点的简单示例:

require('dotenv').config();

const { Defender } = require('@openzeppelin/defender-sdk');

async function loadbalance() {
  const LOAD_BALANCE_THRESHOLD = 50;
  const relayerCredsForMainNet = [\
    {\
      relayerApiKey: process.env.RELAYER_API_KEY_1,\
      relayerApiSecret: process.env.RELAYER_API_SECRET_1,\
    },\
    {\
      relayerApiKey: process.env.RELAYER_API_KEY_2,\
      relayerApiSecret: process.env.RELAYER_API_SECRET_2,\
    },\
  ];
  const relayerClientsForMainNet = relayerCredsForMainNet.map((creds) => new Defender(creds));

  const getNextAvailableRelayer = async () => {
    for (const client of relayerClientsForMainNet) {
      const relayerStatus = await client.relaySigner.getRelayerStatus();
      if (relayerStatus.numberOfPendingTransactions < LOAD_BALANCE_THRESHOLD) {
        return client;
      }
      console.log(
        `${relayerStatus.relayerId} is busy. Pending transactions: ${relayerStatus.numberOfPendingTransactions}/${LOAD_BALANCE_THRESHOLD}`,
      );
    }
    return undefined;
  };

  const executeTransaction = async () => {
    const client = await getNextAvailableRelayer();
    if (!client) throw new Error('Unable to load balance. All relayers are operating above the suggested threshold.');

    const txResponse = await client.relaySigner.sendTransaction({
      to: '0x179810822f56b0e79469189741a3fa5f2f9a7631',
      value: 1,
      speed: 'fast',
      gasLimit: '21000',
    });
    console.log('txResponse', JSON.stringify(txResponse, null, 2));
  };

  await executeTransaction();
}

async function main() {
  try {
    return await loadbalance();
  } catch (e) {
    console.log(`Unexpected error:`, e);
    process.exit(1);
  }
}

if (require.main === module) {
  main().catch(console.error);
}

安全考虑

所有私钥都存储在 AWS 密钥管理服务中。密钥在 KMS 中生成并且从未离开 KMS,即所有签名操作都在 KMS 内执行。此外,我们依赖动态生成的 AWS 身份和访问管理策略,将租户之间访问私钥进行隔离。

至于 API 秘密,这些密钥仅在创建期间保存在内存中,当它们被发送到客户端时。之后,它们会被哈希并安全地存储在 AWS Cognito 中,后者在后台用于对中继请求进行身份验证。这使得 API 密钥易于旋转,同时在 KMS 上保持相同的私钥。

Rollups

在向 Rollup 链(如 Arbitrum 或 Optimism)发送交易时,中继器目前依赖于链的排序器/聚合器。这意味着,如果排序器出现故障或对交易进行审查,中继器将无法绕过它,并直接提交到Layer1。

不活跃

如果测试网中继器在 60 天内没有发送任何交易,则被视为不活跃。当测试网中继器不活跃时,我们提供一个 14 天的宽限期来标记中继器为活跃。如果用户未采取任何行动,则中继器将在此期限结束后自动删除。

← 部署

监控 →

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

0 条评论

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