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

Document 'approval' and 'preRelayedCall' mechanics; switch to CamelCase #2163

Merged
merged 1 commit into from
Jul 1, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 33 additions & 31 deletions EIPS/eip-1613.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,22 @@ 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 `accept_relayed_call(address relay, address from, bytes encoded_function, uint gas_price, uint transaction_fee )` view function that returns **zero** if and only if it is willing to accept a transaction and pay for it.
`accept_relayed_call` 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 `accept_relayed_call` returns **zero**. Some examples of `accept_relayed_call()` implementations:
* Use `getSender()` and `getMessageData()` instead of `msg.sender` and `msg.data`, everywhere. `RelayRecipient` provides these functions and gets the information from `RelayHub`.
* Implement a `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:
* 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.
* A dapp can provide off-chain a signed message called `approval` to a transaction sender and validate it.
* 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.
* Implement `post_relayed_call(address relay, address from, bytes encoded_function, bool success, uint used_gas, uint transaction_fee )`
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.

This method is called after a transaction is relayed. By default, it does nothing.
It can be used as a method to charge the user in dapp-specific manner.
* 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:

Expand All @@ -87,20 +89,20 @@ Glossary of terms used in the processes below:
* `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](https://bit.ly/2EWWVN8)
![Sequence Diagram](http://bit.ly/2WZqM23)

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's owner sends the required stake to `RelayHub` by calling `RelayHub.stake(address relay, uint unstake_delay)`.
* Relay's owner sends the required stake to `RelayHub` by calling `RelayHub.stake(address relay, uint unstakeDelay)`.
* `RelayHub` puts the `owner` and `unstake delay` in the relays map, indexed by `relay` address.
* Relay calls `RelayHub.register_relay(uint transaction_fee, string memory url)` with the relay's `transaction fee` (as a multiplier on transaction gas cost), and a URL for incoming transactions.
* Relay calls `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, transaction_fee, relay_stake, unstake_delay, url)`.
* `RelayHub` emits an event, `RelayAdded(Relay, owner, transactionFee, relayStake, unstakeDelay, url)`.
* Relay starts a timer to perform a `keepalive` transaction every 6000 blocks.
* `Relay` goes to sleep and waits for signing requests.

Expand All @@ -119,8 +121,8 @@ The process of sending a relayed transaction:
* `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 `accept_relayed_call`, also a view function, stating whether it's willing to accept the charges).
* The transaction's recipient contract will accept this transaction when submitted, by calling `RelayHub.canRelay()`, a view function,
which checks the recipient's `acceptRelayedCall`, also a view function, stating whether it's willing to accept the charges).
* The transaction nonce matches `RelayHub.nonces[sender]`.
* The relay address in the transaction matches Relay's address.
* The transaction's recipient has enough ETH deposited in `RelayHub` to pay the transaction fee.
Expand All @@ -142,34 +144,34 @@ The process of sending a relayed transaction:
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.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.
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:
* Records `gasLeft()` as initial_gas for later payment.
* Records `gasleft()` as `initialGas` 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 relay address written in the transaction matches msg.sender.
* Verifies that the transaction's `nonce` matches the stated origin's nonce in `RelayHub.nonces`.
* Calls recipient's `accept_relayed_call` 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.
* Calls recipient's `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.
* Calls recipient's `preRelayedCall` function. If this call reverts the `TransactionRelayed` will be emitted with status `PreRelayedFailed`.
* Sends the transaction to the recipient. If this call reverts the `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.get_sender()` will be able to extract the real sender, and trust it because the transaction came from the known `RelayHub` address.
`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.
* Recipient contract handles the transaction.
* `RelayHub` calls recipient's `post_relayed_call`
* `RelayHub` checks call's return value of call, and emits `TransactionRelayed(transaction_hash, bool result)`.
* `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 can_relay returns non-zero, since the relay was responsible to verify this view function prior to submitting.
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 `accept_relayed_call()` function was inconsistent, `Relay` refuses service to that recipient for a while (or blacklists it indefinitely, if it happens often).
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:

* Relay's owner (the address that initially funded it) calls `RelayHub.remove_relay_by_owner(Relay)`.
* Relay's owner (the address that initially funded it) calls `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.
Expand Down Expand Up @@ -216,8 +218,8 @@ 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 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.
Expand All @@ -230,10 +232,10 @@ It is suggested to be higher by 2-3 from current nonce, to allow the relay proce

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.

##### Attack: Dapp attempts to burn relays funds by implementing an inconsistent accept_relayed_call() 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 accept_relayed_call (e.g. return zero for even blocks, nonzero for odd blocks), and uses it to exhaust relay resources through unpaid transactions.
##### Attack: Dapp attempts to burn relays funds by implementing an inconsistent acceptRelayedCall() 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 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 accept_relayed_call has acted inconsistently, because the relay has verified its view function before sending the transaction.
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.

Expand All @@ -249,12 +251,12 @@ This protection is probably an overkill, since the attack doesn't scale regardle

##### 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 accept_relayed_call function return nonzero for that sender.
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 accept_relayed_call() 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.
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,
Expand Down