Make smart contracts (e.g. dapps) accessible to non-ether users by allowing contracts to accept "collect-calls", paying for incoming calls. Let contracts "listen" on publicly accessible channels (e.g. web URL or a whisper address). Incentivize nodes to run "gas stations" to facilitate this. Require no network changes, and minimal contract changes.
Communicating with dapps currently requires paying ETH for gas, which limits dapp adoption to ether users. Therefore, contract owners may wish to pay for the gas to increase user acquisition, or let their users pay for gas with fiat money. Alternatively, a 3rd party may wish to subsidize the gas costs of certain contracts. Solutions such as described in EIP-1077 could allow transactions from addresses that hold no ETH.
The gas stations network is an EIP-1077 compliant effort to solve the problem by creating an incentive for nodes to run gas stations, where gasless transactions can be "fueled up". It abstracts the implementation details from both the dapp maintainer and the user, making it easy to convert existing dapps to accept "collect-calls".
The network consists of a single public contract trusted by all participating dapp contracts, and a decentralized network of relay nodes (gas stations) incentivized to listen on non-ether interfaces such as web or whisper, pay for transactions and get compensated by that contract. The trusted contract can be verified by anyone, and the system is otherwise trustless. Gas stations cannot censor transactions as long as there's at least one honest gas station. Attempts to undermine the system can be proven on-chain and offenders can be penalized.
The system consists of a RelayHub
singleton contract, participating contracts inheriting the RelayRecipient
contract, a decentralized network of Relay
nodes, a.k.a. Gas Stations,
and user applications (e.g. mobile or web) interacting with contracts via relays.
Roles of the RelayHub
:
Relay
from this list for each transaction. The selection process is discussed below.Roles of a Relay
node:
RelayHub
.Implementing a RelayRecipient
contract:
RelayHub
and trust it to provide information about the transaction.RelayHub
. Can be paid directly by the RelayRecipient
contract, or by the dapp's owner on behalf of the RelayRecipient
address.
The dapp owner is responsible for ensuring sufficient balance for the next transactions, and can stop depositing if something goes wrong, thus limiting the potential for abuse of system bugs. In DAO usecases it will be up to the DAO logic to maintain a sufficient deposit.getSender()
and getMessageData()
instead of msg.sender
and msg.data
, everywhere. RelayRecipient
provides these functions and gets the information from RelayHub
.acceptRelayedCall(address relay, address from, bytes memory encodedFunction, uint gasPrice, uint transactionFee, bytes memory approval)
view function that returns zero if and only if it is willing to accept a transaction and pay for it.
acceptRelayedCall
is called by RelayHub
as a view function when a Relay
inquires it, and also during the actual transaction. Transactions are reverted if non-zero, and Relay
only gets compensated for transactions (whether successful or reverted) if acceptRelayedCall
returns zero. Some examples of acceptRelayedCall()
implementations:RelayRecipient
balance sheet.
Users can never cost the dapp more than they were credited for.approval
to a transaction sender and validate it.Relay
and accepting anonymous transactions only from that Relay
, whereas other transactions can be accepted from any relay.
Alternatively, dapps may use the balance sheet method for onboarding as well, by applying the methods suggested in the attacks/mitigations section below. Implement preRelayedCall(address relay, address from, bytes memory encodedFunction, uint transactionFee) returns (bytes32)
. This method is called before a transaction is relayed. By default, it does nothing.
Implement postRelayedCall(ddress relay, address from, bytes memory encodedFunction, bool success, uint usedGas, uint transactionFee, bytes32 preRetVal)
. This method is called after a transaction is relayed. By default, it does nothing.
These two methods can be used to charge the user in dapp-specific manner.
Glossary of terms used in the processes below:
RelayHub
- the RelayHub singleton contract, used by everyone.Recipient
- a contract implementing RelayRecipient
, accepting relayed transactions from the RelayHub contract and paying for the incoming transactions.Sender
- an external address with a valid key pair but no ETH to pay for gas.Relay
- a node holding ETH in an external address, listed in RelayHub and relaying transactions from Senders to RelayHub for a fee.The process of registering/refreshing a Relay
:
RelayHub
by calling RelayHub.stake(address relay, uint unstakeDelay)
.RelayHub
puts the owner
and unstake delay
in the relays map, indexed by relay
address.RelayHub.registerRelay(uint transactionFee, string memory url)
with the relay's transaction fee
(as a multiplier on transaction gas cost), and a URL for incoming transactions. RelayHub
ensures that Relay has a sufficient stake.RelayHub
puts the transaction fee
in the relays map.RelayHub
emits an event, RelayAdded(Relay, owner, transactionFee, relayStake, unstakeDelay, url)
.keepalive
transaction every 6000 blocks.Relay
goes to sleep and waits for signing requests.The process of sending a relayed transaction:
Sender
selects a live Relay
from RelayHub's list by looking at RelayAdded
events from RelayHub
, and sorting based on its own criteria. Selection may be based on a mix of:TransactionRelayed
events from RelayHub
).RelayHub.nonces
, RelayHub's address, and Relay's address, and then signs it.RelayHub.balances[recipient]
holds enough ETH to pay Relay's fee.Relay.balance
has enough eth to send the transactionnonce
value and decides on the max_nonce
parameter.Relay
wraps the transaction with a transaction to RelayHub
, with zero ETH value.Relay
signs the wrapper transaction with its key in order to pay for gas.Relay
verifies that:RelayHub.canRelay()
, a view function,
which checks the recipient's acceptRelayedCall
, also a view function, stating whether it's willing to accept the charges).RelayHub.nonces[sender]
.RelayHub
to pay the transaction fee.max_nonce
is higher than current Relay's nonce
Sender
receives the wrapped transaction and verifies that:RelayHub
. from Relay's address.max_nonce
.Relay
is sufficiently funded to pay for it.sender
.RelayHub.balances
to pay for Relay's fee as stated in the transaction.Sender
may also submit the raw wrapped transaction to the blockchain without paying for gas, through any Ethereum node.
This submission is likely ignored because an identical transaction is already in the network's pending transactions, but no harm in putting it twice, to ensure that it happens.
This step is not strictly necessary, for reasons discussed below in attacks/mitigations, but may speed things up.Sender
monitors the blockchain, waiting for the transaction to be mined.
The transaction was verified, with Relay's current nonce, so mining must be successful unless Relay submitted another (different) transaction with the same nonce.
If mining fails due to such attack, sender may call RelayHub.penalizeRepeatedNonce
through another relay, to collect his reward and burn the remainder of the offending relay's stake, and then go back to selecting a new Relay for the transaction.
See discussion in the attacks/mitigations section below.RelayHub
receives the transaction:gasleft()
as initialGas
for later payment.nonce
matches the stated origin's nonce in RelayHub.nonces
.acceptRelayedCall
function, asking whether it's going to accept the transaction. If not, the TransactionRelayed
will be emitted with status CanRelayFailed
, and chargeOrCanRelayStatus
will contain the return value of acceptRelayedCall
. In this case, Relay doesn't get paid, as it was its responsibility to check RelayHub.canRelay
before releasing the transaction.preRelayedCall
function. If this call reverts the TransactionRelayed
will be emitted with status PreRelayedFailed
.TransactionRelayed
will be emitted with status RelayedCallFailed
.
When passing gas to call()
, enough gas is preserved by RelayHub
, for post-call handling. Recipient may run out of gas, but RelayHub
never does.
RelayHub
also sends sender's address at the end of msg.data
, so RelayRecipient.getSender()
will be able to extract the real sender, and trust it because the transaction came from the known RelayHub
address.RelayHub
calls recipient's postRelayedCall
.RelayHub
checks call's return value of call, and emits TransactionRelayed(address relay, address from, address to, bytes4 selector, uint256 status, uint256 chargeOrCanRelayStatus)
.RelayHub
increases RelayHub.nonces[sender]
.RelayHub
transfers ETH balance from recipient to Relay.owner
, to pay the transaction fee, based on the measured transaction cost.
Note on relay payment: The relay gets paid for actual gas used, regardless of whether the recipient reverted.
The only case where the relay sustains a loss, is if canRelay
returns non-zero, since the relay was responsible to verify this view function prior to submitting.
Any other revert is caught and paid for. See attacks/mitigations below.Relay
keeps track of transactions it sent, and waits for TransactionRelayed
events to see the charge.
If a transaction reverts and goes unpaid, which means the recipient's acceptRelayedCall()
function was inconsistent, Relay
refuses service to that recipient for a while (or blacklists it indefinitely, if it happens often).
See attacks/mitigations below.The process of winding a Relay
down:
RelayHub.removeRelayByOwner(Relay)
.RelayHub
ensures that the sender is indeed Relay's owner, then removes Relay
, and emits RelayRemoved(Relay)
.RelayHub
starts the countdown towards releasing the owner's stake.Relay
receives its RelayRemoved
event.Relay
sends all its remaining ETH to its owner.Relay
shuts down.RelayHub.unstake()
, and withdraws the stake.The rationale for the gas stations network design is a combination of two sets of requirements: Easy adoption, and robustness.
For easy adoption, the design goals are:
The robustness requirement translates to decentralization and attack resistance. The gas stations network is decentralized, and we have to assume that any entity may attack other entities in the system.
Specifically we've considered the following types of attacks:
Relay is expected to return the signed transaction to the sender, immediately. Sender doesn't need to wait for the transaction to be mined, and knows immediately whether it's request has been served. If a relay doesn't return a signed transaction within a couple of seconds, sender cancels the operation, drops the connection, and switches to another relay. It also marks Relay as unresponsive in its private storage to avoid using it in the near future.
Therefore, the maximal damage a relay can cause with such attack, is a one-time delay of a couple of seconds. After a while, senders will avoid it altogether.
This attack will backfire and not censor the transaction. The sender can submit the transaction signed by Relay to the blockchain as a raw transaction through any node, so the transaction does happen, but Relay may be unaware and therefore be stuck with a bad nonce which will break its next transaction.
Reusing the nonce is the only DoS performed by a Relay, that cannot be detected within a couple of seconds during the http request.
It will only be detected when the malicious transaction with the same nonce gets mined and triggers the RelayHub.TransactionRelayed
event.
However, the attack will backfire and cost Relay its entire stake.
Sender has a signed transaction from Relay with nonce N, and also gets a mined transaction from the blockchain with nonce N, also signed by Relay.
This proves that Relay performed a DoS attack against the sender.
The sender calls RelayHub.penalizeRepeatedNonce(bytes transaction1, bytes transaction2)
, which verifies the attack, confiscates Relay's stake,
and sends half of it to the sender who delivered the penalizeRepeatedNonce
call. The other half of the stake is burned by sending it to address(0)
. Burning is done to prevent cheating relays from effectively penalizing themselves and getting away without any loss.
The sender then proceeds to select a new relay and send the original transaction.
The result of such attack is a delay of a few blocks in sending the transaction (until the attack is detected) but the relay gets removed and loses its entire stake. Scaling such attack would be prohibitively expensive, and actually quite profitable for senders and honest relays.
In this attack, the Relay did create and return a perfectly valid transaction, but it will not be mined until this Relay fills the gap in the nonce with 'missing' transactions.
This may delay the relaying of some transactions indefinitely. In order to mitigate that, the sender includes a max_nonce
parameter with it's signing request.
It is suggested to be higher by 2-3 from current nonce, to allow the relay process several transactions.
When the sender receives a transaction signed by a Relay he validates that the nonce used is valid, and if it is not, the client will ignore the given relay and use other relays to relay given transaction. Therefore, there will be no actual delay introduced by such attack.
In this attack, a contract sets an inconsistent acceptRelayedCall (e.g. return zero for even blocks, nonzero for odd blocks), and uses it to exhaust relay resources through unpaid transactions. Relays can easily detect it after the fact. If a transaction goes unpaid, the relay knows that the recipient contract's acceptRelayedCall has acted inconsistently, because the relay has verified its view function before sending the transaction. It might be the result of a rare race condition where the contract's state has changed between the view call and the transaction, but if it happens too frequently, relays will blacklist this contract and refuse to serve transactions to it. Each offending contract can only cause a small damage (e.g. the cost of 2-3 transactions) to a relay, before getting blacklisted.
Relays may also look at recipients' history on the blockchain, looking for past unpaid transactions (reverted by RelayHub without pay), and denying service to contracts with a high failure rate. If a contract caused this minor loss to a few relays, all relays will stop serving it, so it can't cause further damage.
This attack doesn't scale because the cost of creating a malicious contract is in the same order of magnitude as the damage it can cause to the network. Causing enough damage to exhaust the resources of all relays, would be prohibitively expensive.
The attack can be made even more impractical by setting RelayHub to require a stake from dapps before they can be served, and enforcing an unstaking delay, so that attackers will have to raise a vast amount of ETH in order to simultaneously create enough malicious contracts and attack relays. This protection is probably an overkill, since the attack doesn't scale regardless.
If a malicious sender repeatedly abuses a recipient by sending meaningless/reverted transactions and causing the recipient to pay a relay for nothing, it is the recipient's responsibility to blacklist that sender and have its acceptRelayedCall function return nonzero for that sender. Collect calls are generally not meant for anonymous senders unknown to the recipient. Dapps that utilize the gas station networks should have a way to blacklist malicious users in their system and prevent Sybil attacks.
A simple method that mitigates such Sybil attack, is that the dapp lets users buy credit with a credit card, and credit their account in the dapp contract, so acceptRelayedCall() only returns zero for users that have enough credit, and deduct the amount paid to the relay from the user's balance, whenever a transaction is relayed for the user. With this method, the attacker can only burn its own resources, not the dapp's.
A variation of this method, for free dapps (that don't charge the user, and prefer to pay for their users transactions) is to require a captcha during user creation in their web interface, or to login with a Google/Facebook account, which limits the rate of the attack to the attacker's ability to open many Google/Facebook accounts. Only a user that passed that process is given credit in RelayRecipient. The rate of such Sybil attack would be too low to cause any real damage.
Registering a relay requires placing a stake in RelayHub, and the stake can only be withdrawn after the relay is unregistered and a long cooldown period has passed, e.g. a month.
Each unreliable relay can only cause a couple of seconds delay to senders, once, and then it gets blacklisted by them, as described in the first attack above. After it caused this minor delay and got blacklisted, the attacker must wait a month before reusing the funds to launch another unreliable relay. Simultaneously bringing up a number of unreliable relays, large enough to cause a noticeable network delay, would be prohibitively expensive due to the required stake, and even then, all those relays will get blacklisted within a short time.
Transactions include a nonce. RelayHub maintains a nonce (counter) for each sender. Transactions with bad nonces get reverted by RelayHub. Each transaction can only be relayed once.
The user doesn't really have to execute the raw transaction. It's enough that the user can. The relationship between relay and sender is mutual distrust. The process described above incentivizes the relay to execute the transaction, so the user doesn't need to wait for actual mining to know that the transaction has been executed.
Once relay returns the signed transaction, which should happen immediately, the relay is incentivized to also execute it on chain, so that it can advance its nonce and serve the next transaction. The user can (but doesn't have to) also execute the transaction. To understand why the attack isn't viable, consider the four possible scenarios after the signed transaction was returned to the sender:
As this matrix shows, the relay is always incentivized to execute the transaction, once it returned it to the user, in order to end up in #1 or #3, and avoid the risk of #4. It's just a way to commit the relay to do its work, without requiring the user to wait for on-chain confirmation.
The gas stations network is implemented as smart contracts and external entities, and does not require any network changes.
Dapps adding gas station network support remain backwards compatible with their existing apps/users. The added methods apply on top of the existing ones, so no changes are required for existing apps.
A working implementation of the gas stations network is being developed by TabooKey. It consists of RelayHub
, RelayRecipient
, web3 hooks
, an implementation of a gas station inside geth
, and sample dapps using the gas stations network.
Copyright and related rights waived via CC0.