Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft ERC: Gas stations network #1

Open
yoav-tabookey opened this issue Nov 19, 2018 · 2 comments
Open

Draft ERC: Gas stations network #1

yoav-tabookey opened this issue Nov 19, 2018 · 2 comments

Comments

@yoav-tabookey
Copy link
Owner

yoav-tabookey commented Nov 19, 2018

Simple Summary

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.

Abstract

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.

Motivation

  • Increase user adoption of smart contracts by:
    • Removing the user hassle of acquiring ETH. Transactions are still paid by ETH but costs can be borne by the dapp or paid by the user through other means.
    • Removing the need to interact directly with the blockchain, while maintaining decentralization and censorship-resistance.
      Contracts can "listen" on multiple public channels, and users can interact with the contracts through common protocols that are generally permitted even in restrictive environments.
  • Ethereum nodes get a revenue source without requiring mining equipment. The entire network benefits from having more nodes.
  • No protocol changes required. The gas station network is self-organized via a smart contract, and dapps interact with the network by implementing an interface.

Specification

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:

  • Maintain a list of active relays. Senders select a Relay from this list for each transaction. The selection process is discussed below.
  • Mediate all communication between relays and contracts.
  • Provide contracts with trusted versions of the real msg.sender and msg.data.
  • Hold ETH stakes placed by relays. A minimum stake size is enforced. Stake can be withdrawn after a relay unregisters and waits for a cooldown period.
  • Hold ETH prepayments made by contracts and use them to compensate relays.
  • Penalize provably-offensive relays by giving their stakes to an address providing the proof, thus keeping relays honest.
  • Provide a free way for relays to know whether they'll be compensated for a future transaction.

Roles of a Relay node:

  • Maintain a hot wallet with a small amount of ETH, to pay for gas.
  • Provide a public interface for user apps to send gasless transactions via channels such as https or whisper.
  • Publish it's public interfaces and its price (as a multiplier of the actual transaction gas cost) in RelayHub.
  • Help RelayHub maintain the list of relays in a trustless way. Relays are incentivized to remove other verifiably-stale relays.
  • Optionally monitor reverted transactions of other relays through RelayHub, catching offending relays and claiming their stakes. This can be done by anyone, not just a relay.

Implementing a RelayRecipient contract:

  • Know the address of RelayHub and trust it to provide information about the transaction.
  • Maintain a small balance of ETH gas prepayment deposit in 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.
  • Use get_sender() and get_data() instead of msg.sender and msg.data, everywhere. RelayRecipient provides these functions and gets the information from RelayHub.
  • Implement a may_relay(address relay, address from, uint32 gasPrice, bytes transaction) view function that returns true if and only if it is willing to accept a transaction and pay for it.
    may_relay is called by RelayHub as a view function when a Relay inquires it, and also during the actual transaction. Transactions are reverted if false, and Relay only gets compensated for transactions (whether successful or reverted) if may_relay returns true. Some examples of may_relay() implementations:
    • Whitelist of trusted dapp members.
    • Balance sheet of registered users, maintained by the dapp owner. Users pay the dapp with a credit card or other non-ETH means, and are credited in the RelayRecipient balance sheet.
      Users can never cost the dapp more than they were credited for.
    • Whitelist of known transactions used for onboarding new users. This allows certain anonymous calls and is subject to Sybil attacks.
      Therefore it should be combined with a restricted gasPrice, and a whitelist of trusted relays, to reduce the incentive for relays to create bogus transactions and rob the dapp's prepaid gas deposit.
      Dapps allowing anonymous onboarding transactions might benefit from registering their own 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.

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 keypair 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.

Sequence Diagram

The process of registering/refreshing a Relay:

  • Relay starts listening as a web app (or on some other communication channel).
  • If starting for the first time (no key yet), generate a key pair for Relay's address.
  • If Relay's address doesn't hold sufficient funds for gas (e.g. because it was just generated), Relay stays inactive until its owner funds it.
  • Relay's owner funds it.
  • Relay sends the required stake to RelayHub.
  • Relay calls RelayHub.register_relay(address owner, uint transaction_fee, string[] url, address optional_stale_relay_for_removal), with its owner (the address that funded it),
    the relay's transaction fee (as a multiplier on transaction gas cost), and one or more URL for incoming transactions.
    The optional_stale_relay_for_removal arg is the address of a stale relay found in the list.
  • RelayHub checks Relay.balance and emits NeedsFunding(Relay) to alert the owner if it runs low.
  • RelayHub ensures that Relay has a sufficient stake.
  • RelayHub puts the owner, current timestamp, transaction fee, and urls, in the relays map, indexed by relay address.
  • RelayHub emits an event, RelayAdded(Relay, transaction_fee, relay_stake, urls).
  • If optional_stale_relay_for_removal is in the relays map and is stale (hasn't communicated in a few days), RelayHub removes it.
    Relay benefits by receiving a gas refund for the freed storage, so it's incentivized to always include a stale relay if there is one.
  • Relay starts a timer to perform a keepalive transaction after a certain amount of time if no real transactions are relayed through it.
    Relay is considered stale if it hasn't sent anything to RelayHub in a while, e.g. 4 days.
  • Relay goes to sleep and waits 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:
    • Relay published transaction fees.
    • Relay stake size and lock-up time.
    • Recent relay transactions (visible through TransactionRelayed events from RelayHub).
    • Optionally, reputation/blacklist/whitelist held by the sender app itself, or its backend, on per-app basis (not part of the gas stations network).
  • Sender prepares the transaction with its address, the recipient address, its current nonce from RelayHub.nonces, Relay's transaction fee, and gas price, and signs it.
  • Sender verifies that RelayHub.balances[recipient] holds enough ETH to pay Relay's fee.
  • Sender sends the signed transaction to Relay's web interface.
  • 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:
    • The transaction's recipient contract will accept this transaction when submitted, by calling RelayHub.can_relay(), a view function,
      which checks the recipient's may_relay, also a view function, stating whether it's willing to accept the charges).
    • The transaction nonce matches RelayHub.nonces[sender].
    • The transaction's recipient has enough ETH deposited in RelayHub to pay the transaction fee.
    • Relay has enough ETH to pay for the gas required by the transaction.
  • If any of Relay's checks fail, it returns an error to sender, and doesn't proceed.
  • Relay submits the signed wrapped transaction to the blockchain.
  • Relay immediately returns the signed wrapped transaction to the sender. This step is discussed below, in attacks/mitigations.
  • Sender receives the wrapped transaction and verifies that:
    • It's a valid relay call to RelayHub. from Relay's address.
    • The transaction's ethereum nonce matches Relay's current nonce.
    • Relay is sufficiently funded to pay for it.
    • The wrapped transaction is valid and signed by sender.
    • Recipient contract has sufficient funds in RelayHub.balances to pay for Relay's fee as stated in the transaction.
  • If any of sender's checks fails, it goes back to selecting a new Relay. Sender may also file a report on the unresponsive relay to its backend or save it locally, to down-sort this relay in future transactions.
  • 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 RC.penalize_repeated_nonce through another relay, to collect 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:
    • Records gasLeft() as initial_gas for later payment.
    • Verifies the transaction is sent from a registered relay.
    • Verifies that the signature of the internal transaction matches its stated origin (sender's key).
    • Verifies that the transaction's nonce matches the stated origin's nonce in RelayHub.nonces.
    • Checks Relay.balance and emits NeedsFunding(Relay) to alert the owner if it runs low.
    • Calls recipient's may_relay function, asking whether it's going to accept the transaction. If not, RelayHub reverts.
      In this case, Relay doesn't get paid, as it was its responsibility to check RelayHub.can_relay before releasing the transaction.
    • Sends the transaction to the recipient. The call is made using call(), so reverts won't kill the transaction, just return false.
      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.get_sender() will be able to extract the real sender, and trust it because the transaction came from the known RelayHub address.
  • Recipient contract handles the transaction.
  • RelayHub checks call's return value of call, and emits TransactionRelayed(transaction_hash, bool result).
  • RelayHub increases RC.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 can_relay returns false, 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 may_relay() 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:

  • Relay's owner (the address that initially funded it) calls RelayHub.remove_relay_by_owner(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.
  • Once the owner's unstake delay is over, owner calls RelayHub.unstake(), and withdraws the stake.

Removal of stale/invalid relays:

  • During registration/refresh, Relay helps purging stale relays.
  • Relay scans the relays in RelayHub, e.g. by going through old RelayAdded events.
  • Relay looks for stale relays (where the latest recorded timestamp is a few days ago).
  • If Relay finds such relay, it passes the stale relay as optional_relay_removal during registration.
  • RelayHub verifies that the reported stale relay is indeed stale or invalid, removes it from the relays map and emits RelayRemoved(r).
    The storage refund offsets Relay's registration cost, so Relay is incentivized to remove a stale relay whenever if can find one.

Rationale

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:

  • No network changes.
  • Minimal changes to contracts, apps and frameworks.

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:

  • Denial-of-service attacks against individual senders, i.e. transactions censorship.
  • Denial-of-service and financial attacks against individual relays.
  • Denial-of-service and financial attacks against individual contracts.
  • Denial-of-service attacks against the entire network, either by attacking existing entities, or by introducing any number of malicious entities.

Attacks and mitigations

Attack: Relay attempts to censor a transaction by not signing it, or otherwise ignoring a user request.

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.

Attack: Relay attempts to censor a transaction by signing it, returning it to the sender, but never putting it on the blockchain.

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.

Attack: Relay attempts to censor a transaction by signing it, but publishing a different transaction with the same nonce.

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.penalize_repeated_nonce(bytes transaction1, bytes transaction2), which verifies the attack, confiscates Relay's stake,
and splits it between the sender and the other relay who delivered the penalize_repeated_nonce call.
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.

Attack: Dapp attempts to burn relays funds by implementing an inconsistent may_relay() and using multiple sender addresses to generate expensive transactions, thus performing a DoS attack on relays and reducing their profitability.

In this attack, a contract sets an inconsistent may_relay (e.g. return true for even blocks, false 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 may_relay 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.

Attack: User attempts to rob dapps by registering its own relay and sending expensive transactions to dapps.

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 may_relay function return false 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 may_relay() only returns true 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.

Attack: Attacker attempts to reduce network availability by registering many unreliable relays.

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.

Attack: Relay attempts to unregister other relays.

Removal of stale relays is trustless. RelayHub verifies whether the removed relay has performed any action recently, and would revert any transaction that tries to remove an active relay.

Attack: Attacker attempts to replay a relayed transaction.

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.

Backwards Compatibility

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.

Implementation

A working implementation of the gas stations network is being developed by TabooKey and will be released for public benefit soon. It consists of RelayHub, RelayRecipient, web3 hooks, an implementation of a gas station inside geth, and sample dapps using the gas stations network.

Copyright

Copyright and related rights waived via CC0.

@christoph2806
Copy link

from reddit, comment of @vbuterin https://www.reddit.com/r/ethereum/comments/cmrqsl/gsn_gas_station_network_the_ultimate_ethereum/ew5rold/
I'm very worried about this approach becoming popular. The reason is that if a dapp (who is that? The company writing the software?) is paying tx fees for users, then malicious users could burn the company's money through a fake-user DoS attack (this is profitable if you're a mining pool, and 100% deniable if you take care to anonymize!). To prevent this, it seems like you would need strong, non-cryptoeconomic anti-DoS. And in today's world, that basically means hooking into centralized de-facto identity providers (Google, Facebook, phone numbers...).

IMO we do just need to bite the bullet and accept that using many kinds of dapps is a some-setup-required proposition.

@yoav-tabookey
Copy link
Owner Author

yoav-tabookey commented Aug 11, 2019

See my reply to @vbuterin https://www.reddit.com/r/ethereum/comments/cmrqsl/gsn_gas_station_network_the_ultimate_ethereum/ewmvc7z?utm_source=share&utm_medium=web2x

We designed GSN to be used in different ways, not necessarily paying for CAC.

Consider, for example, the case where users own and use an ERC-20 token, but don't have ETH. Maybe they paid Fiat for a service, and it minted an ERC-20 token. Or maybe they were airdropped the token as a limited form of CAC.

In this use-case, a malicious user can only burn his own funds, not the company's (or the DAO's). The user acquires tokens. The contract then accepts GSN calls and compensates relays, only for users that have sufficient token balance. At the end of the transaction, the contract gets the actual cost of the transaction and charges the user for it, in tokens.

At no time, could the user burn company money. The user can generate transactions as long as the token balance is sufficient. Once the balance is too low, the contract's view function, acceptRelayedCall(), starts rejecting transactions. Relays will know they're not getting paid, so they won't relay the transactions.

Another use-case that came up at ethereum-magicians, is mixers. Consider a mixer, where a user wishes to withdraw his mixed ETH from a new address. The mixer accepts and pays for GSN transactions, only of they include a proof of ownership of enough funds to pay for the transaction. The mixer then charges the user for whatever the GSN transaction cost was, then sends the rest of the funds to the new address. The mixer doesn't take any risk here. It always gets reimbursed during withdrawal.

@vbuterin IMO we do just need to bite the bullet and accept that using many kinds of dapps is a some-setup-required proposition.

That would keep Ethereum confined to a relatively small group of users, determined enough to jump through the hoops. GSN is about making Ethereum as large as the Web, by making onboarding as easy as the Web.

@vbuterin , I think we briefly talked about it, back in Cape Town, regarding financial inclusion. Consider the dapp built at the ETHCapeTown hackathon, that implemented a land registrar for South African townships. They need such dapp since the state's registrar not really functional for them, but most township residents don't have a bank account, cannot pass KYC of any exchange, so they have no practical way to acquire ETH and use Ethereum. GSN would be their gateway to Ethereum. We can't expect them to go through a setup process that involves the international banking system.

In this land registrar use-case, the contract is a DAO and there's no company. First-time users would have to get "gas tokens" from someone in their community who already has them and probably exchanges them with Rand cash, creating an allowance for the new user to register or transfer a house. There's no risk for the DAO, as the user pre-paid someone for the gas (or at least someone in the community has vouched for them). The initial "gas tokens" would be minted by investors who do have a bank account and a way to acquire ETH, so they deposit it for the contract's GSN allowance in exchange for gas tokens, which they can sell to local users for cash, at some profit. Investors have no risk, since gas is always paid upfront, cash.

I can describe more models we've encountered when discussing GSN with the community, but you probably get the picture. GSN is about much more than just CAC. It's a way to bring everyone on board.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants