Agree. My original proposal was to make the transaction invalid (and thereby any block that contains it invalid) if the tx PANICs.
The miner’s problem with verifying transactions (in an abstraction system) is essentially the same as problem as the soft fork DoS vector, right? That is, a miner needs to avoid wasting time processing transactions that don’t pay gas.
A key aspect of the DoS problem is that the miner needs to verify not just one transaction, but a whole block of transactions. The DoS attack exploits this by flooding the pending tx pool with spam tx’s that are initially valid, but later become invalid (after some block includes an unseen tx designed to make the pending spam tx’s invalid). If ejecting invalid tx’s from the pending pool takes longer than the block time, then miners are forced to produce empty blocks. So its not enough to verify individual tx’s in O(1) time, if it still takes O(N) time to process a pending tx pool (where N is the size of the pending pool).
To solve the miners’ problem, they need to be able efficiently (i.e. in sub-linear time) eject invalid tx’s from the pending pool. One way to do this is to keep track of each tx’s read/write range (the list of accounts and storage locations that a tx touches). Then when a new block arrives, the only pending tx’s that need to be revalidated are those with overlapping ranges (i.e. if some tx in the block has a write range that overlaps with the read range of a pending tx). The pending tx’s with non-overlapping ranges remain valid and don’t need to be reprocessed.
Does this make sense? Just want to make sure because I already said all this in an EIP issue, and don’t want to continue repeating myself if it doesn’t.
One possible issue is, what if the attacker sends N transactions with the same read/write set?
I suppose that you could simply add a restriction that the mempool can only have 64 transactions that share the same read/write set.
You could solve it even more easily by disabling PANIC as soon as the first external call happens - that way, you would only need to restrict to 64 transactions with the same target address, ie. basically the status quo, but that would make the scheme less general.
Also, do we necessarily need to re-process every transaction in the mempool every time we get a new block? The alternative is that we just keep them in the mempool, and when creating a block go through them and accept the ones that are still valid and delete the ones that are invalid.
One thing that does become harder with account abstraction is the ability to predict transaction validity going more than one account into the future. That said, what we can do is run an algorithm where we accept even invalid transactions into the mempool, and then every time a block changes some accounts, go through the transactions that contain those accounts in their read/write list and see if any of them become valid. One could optimize further by only going through transactions whose sender is one of the senders of the transactions in a given block. Basically, the goal would be something like “keep at most N transactions in the mempool, and use various heuristics that could be updated over time to try to make sure those are the N that are valid now or most likely to be valid in the future”.
Sorry guys - I do not really understand how a contract that “everyone owns” would work.
If there is a contract that everyone owns, any attacker can submit zillions of transactions against it, so whatever gas this contract would own would be drained.
Please explain usefulness of such contracts, or give some examples. My feeling is that any meaningful contract that allows to spend gas without authentication, would suffer from this "money drain vulnerability? if this is not true, please explain, may be I am missing something …
For a miner to decide whether to process a transaction, they may be able to run the byte code of each contract through semantic execution (like KEVM).
If it is possible to prove that whatever the input string is, the execution either/or:
- fails within certain bound of gas spent
- pays gas to the miner
then this transaction is safe to start processing.
If the non-mining nodes perform the same analysis on the contract, and it is possible to prove the above, then the non-mining nodes can distinguish between cases (1) and (2) and therefore help shield miners from invalid transactions
they may be able to run the byte code of each contract through semantic execution (like KEVM).
This would add more computation cost for validating a tx, so is there an incentive for nonmining nodes to do this?
The scheme I propose (with PANIC) has a 200000 gas limit for this, which is still a fairly low bound. Currently, the pre-gas-payment verification process is an ECDSA signature plus reading two balances, which is equivalent to ~3800 gas, so 200k would be higher, but it’s not insurmountable. We could even restrict to a lower amount initially.
Added one more item above:
Combine PANIC and PAYGAS
- How it works: remove PANIC. Instead, have all exceptions have behavior equivalent to PANIC if PAYGAS has not yet been called.
- Pros: allows accounts to set their own base verification gas limit. A miner can run the algorithm of running the code for up to N gas, where N is chosen by the miner, until PAYGAS has been called. Also, removes the need for gasprice to be part of the transaction body.
- Cons: makes the output state of a message slightly more complicated, as it also needs to carry the information of whether or not a PAYGAS opcode was triggered and if so with what gasprice.
An interesting thing to consider is to give callers an option to provide a unique PoW for each transaction.
In this case PANICLIMIT would be linearly proportional to the PoW complexity submitted by the caller.
Each miner would publish “PoW PANICLIMIT price” in the same way they now advertise gas price.
This could work as an option - there would be a standard small PANICLIMIT, and then to extend PANICLIMIT you would pay linearly with PoW. In this way, one could make the standard PANICLIMIT way smaller (say 10,000), and then if a caller would want a larger limit, she would need to do PoW - this would work as a way to discourage callers from putting too much validation load on miners. A miner would first check the PoW and then do the rest of the validation according to the specific PANICLIMIT value corresponding to the PoW submitted. If the miner decides that it is overloaded, it could raise the “PoW price”.
Technically this could all be done extra-protocol; miners would specify a PoW algo, and a threshold (that could go up linearly with PANICLIMIT), and transaction senders could figure it out.
The “nonce abstraction” was one of the most problematic aspects of the original EIP-86:
Here non-unique tx hashes are listed as a Con, though it was argued before to be a feature.
One interesting use case that could be enabled by a new nonce scheme and tx format is the ability to re-sign a tx with a different gas price, but get the same tx hash. This use case brings to light that there are actually two ways the “tx hash” is currently used: one as a “tx id” for nodes to broadcast and share their pending tx inventory, and the other as the “tx hash” to uniquely identify whether or not a tx has been included in the chain. For this use case, two tx’s with different gas prices would have different pending tx id’s, but the same tx hash.
If we want to have unique tx hashes, then we can always not abstract the nonce checking and incrementing part. Any of the abstraction proposals listed here are friendly to that; all you’d need to do is add that stuff back to the apply_transaction function.
As mentioned, with the various PAYGAS proposals it is impossible to pay with anything but Ether. Another limitation is that the first contract has to pay for the gas, so chained validation contracts are more difficult to build (although I’m not sure how relevant this is). I thought a bit about this and realized that we can get both features by the backdoor via a pay-but-refund mechanism, without any additional protocol changes. The general process could work as follows:
- “entry contract” (i.e. the first called contract) pays for gas
- entry contract measures gas usage (via two GAS calls)
- entry contract makes sure that gas is paid in another way (by another contract or with another token)
- entry contract asks for a refund of the paid gas
- refunding contract checks eligibility and refunds
As an example, consider a validation contract A that authenticates a user and another contract B that pays for some of A’s transactions (e.g. to a specific third contract, but rate limited to 5 transactions per day for a certain set of users). B expects to be called by A and trusts A to behave faithfully as it has checked the contract code against some template. After the main call, A calls B’s method
refund_gas(gas_used) which pays
gas_used * GASPRICE to A. So if everything works fine (and the calculation of
gas_used is exact), A will have the same balance before and after the transaction and effectively B has paid for the gas. If the transaction fails though, A will not get refunded. This shouldn’t be too much of an issue though as the user should be able to ensure that the transaction won’t fail. To be even more safe A could assert some conditions before the PAYGAS call.
As a second example, consider a miner who wants to be paid in GTK (gas tokens) instead of Ether. He sets up a refund contract that defines a token type (GTK) and an exchange rate (GTK per unit of gas). He accepts only transactions with Ether gas prices of, say, 10 times his minimum gas price in GTK. When the user sends a transaction to an entry contract that wants to pay in GTK, it first asserts that the coinbase is a refund contract and that the GTK-gasprice is acceptable. Next, it PAYsGAS and makes the main call, measuring actual gas usage. Finally, it pays a second time for the gas, but now extra-protocol and in GTK, at the gas price defined in the refund contract. Then, it asks the refund contract to pay
gas_used * GASPRICE. The refund contract checks that it trusts the contract (by comparing the contract code against a template) and that the correct amount of GTK has been paid earlier and refunds the Ether.
In the second example, the miner is in fact affected by a failing transaction: He’s paid in Ether instead of GTK. But this is mitigated by a much larger gas price, so that he’s happy in both cases. Again, the sender doesn’t like this, but he can ensure that his transaction won’t fail.
The main problem I’d be concerned about with these kinds of designs is that there are real complexity tradeoffs with making gas payment be function calls, and requiring multiple pieces of contract code to be involved, and in general I don’t think the costs of making it relatively easy to pay gas with ETH only are that high (after all, ETH has been marketed as “the currency for transaction fee payment”). So I personally am inclined not to go this far, at least for v1.
I agree that it is quite complex and that there’s not really a need justifying it (at least today, things might change if (more-)stable coins become reliable). So I’m not advocating to actually implement it. The nice thing about this is though that it doesn’t require protocol changes, it’s merely an illustration of what’s possible with account abstraction.
Ownership model is another alternative. In this model there is gas, but you do not buy it. Instead of paying gas for each transaction, you say that long term ETH ownership entitles a user to a certain average gas per month. Computational limits become soft instead of hard. If you are doing too many transactions, the priority of your transactions goes down.
You then do not need to have any instructions like PAYGAS and you do not need to go buy ETH all the time - you buy it once and forever.
I really need to make a detailed post about why things like this are a bad idea. I know that the DPOS crowd seems to be hyping this approach a lot recently because it makes transactions look free™ (never mind that you’re still paying, you’re just paying capital lockup cost instead of directly, and you’d have to pay in advance for expected future usage so most users would overpay), but the problem is that this screws around with incentives. In any environment where blocks are full, miners or whatever other block proposers have to choose between what transactions they accept, and it’s the game-theoretic equilibrium for them to accept bribes in exchange for doing this. If an “officially sanctioned” mechanism to make this bribe (ie. the txfee) does not exist, then it would happen through side channels (eg. think services like http://confirmtx.com/, plus corporations having private agreements with each other).
Also, it fails at its intended goal, because you still need an opcode that determines when a transaction has passed basic verification; otherwise, attackers will be able to send transactions that burn any user’s gas reserve.
I’d like to add a vote for allowing nonceless (or at least parallelizable nonce) schemes. Requiring strictly-incrementing nonces has been a real-world stumbling block for me in dapp development. (Specifically in the context of signing offline transactions without an easy way to check the current nonce or guarantee all transactions get executed.)
If we need unique TX IDs, then requiring some sort of unique salt is fine, I’d just prefer it weren’t strictly incrementing nonces.
A unique salt requirement is not possible because that would impose O(N) state requirements per account. But otherwise thanks for the feedback