In the EVM world, externally-owned account transactions are confirmed sequentially. Chainlink nodes that use a single externally-owned account (EOA) per chain face several important challenges:
- Transactions are broadcast through a single FIFO queue, so the throughput is limited to a single lane. Low throughput is terrible for the overall user experience because concurrent clients must wait to have their requests fulfilled. When more clients make requests, the longer it takes for requests to be fulfilled.
- Transactions are not executed by priority. For example, consider a situation where there are two transactions in a row. The first transaction is a fulfillment of an API request to get the winner of the FIFA world cup 2022, and the second transaction is an ETH/USD price feed update used by multiple DeFi protocols. Relying on a single EOA forces the second transaction to be confirmed only after the first transaction is fulfilled. The first transaction is not as time-sensitive, but it is still fulfilled first.
- A stuck transaction will cause all the subsequent transactions to remain pending. For example, if a transaction becomes stuck because the gas price is not set high enough, that transaction must be bumped or canceled before subsequent transactions can be fulfilled.
- As a workaround, some node operators deploy multiple Chainlink nodes per chain. While this allows them to handle different types of requests separately (one node for price feeds and another to fulfill API requests), this comes with an overhead in infrastructure and maintenance costs.
To solve these challenges, we introduced two major features that will allow node operators to set up different transaction-sending strategies more securely while lowering their infrastructure costs:
- Chainlink nodes support multiple EOAs.
- Forwarder contracts allow a node operator to manage multiple EOAs and make them look like a single address. If you use a web2 analogy, forwarder contracts act like a reverse proxy server where the user is served by the same address and does not see which server the traffic is coming from. To do so, nodes call the forward function on the forwarder contract.
Combining multiple EOAs and forwarder contracts allows greater flexibility and security in terms of design:
- Node operators can expand horizontally using multiple EOAs. They can deploy one or multiple forwarder contracts for these EOAs. The combination of EOAs and forwarders offers a lot of flexibility for setting up different pipelines for handling transactions.
- Node operators can support different job types (OCR, VRF, API request..Etc) on the same node, which reduces maintenance and infrastructure costs.
- Security-wise, forwarder contracts distinguish between owner accounts and authorized sender accounts. Authorized senders are hot wallets such as the EOAs of a Chainlink node. If a node is compromised, the owner is responsible for changing the authorized senders list.
- Node operators do not need to manually compile and deploy operator and forwarder contracts. They can deploy them directly from the operator factory by calling the deploynewoperatorandforwarder function. From a design perspective, the owner of a forwarder contract is an operator contract. The owner of the operator contract is usually a more secure address with keys stored in a hardware wallet or protected by a multisig. Node operators can manage a set of forwarder contracts through an operator contract.
function typeAndVersion() external pure virtual returns (string)
The type and version of this contract.
|string||Type and version string|
function forward(address to, bytes data) external
Forward a call to another contract.
Only callable by an authorized sender
function ownerForward(address to, bytes data) external
Forward a call to another contract.
Only callable by the owner
function transferOwnershipWithMessage(address to, bytes message) external
Transfer ownership with instructions for recipient.Emit OwnershipTransferRequestedWithMessage event.
|to||address||address proposed recipient of ownership|
|message||bytes||instructions for recipient upon accepting ownership|
event OwnershipTransferRequestedWithMessage(address from, address to, bytes message)