-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
Easy parallelizability #648
Comments
I don't entirely understand the calculation for |
interesting. Can you give an example of when this would be best taken advantage of? |
Wondering why this is at transaction level, not contract level. If a contract could specify what other contracts it can communicate with, then all transactions sent to it would be computed in parallel, and as a bonus the blockchain itself could eventually be. |
Given addresses are randomly distributed throughout the address space, I'm not sure how useful a prefix-based scheme is likely to be - how often will contracts you wish to interact with be conveniently clustered under a common prefix? |
@MaiaVictor What if the contract can communicate with other contracts specified at runtime? |
Once we have #98 clients can also implement optimistic concurrent processing of txs. The assumption here would be, that most txs in a block don't share any state, except for |
@Arachnid From my understanding, you could maybe have 10 contracts for a given Token (e.g. SNT) where each SNT contract only has access to 1/10 of the address space, with the ability to do cross-token-contract swaps as well. If this is possible, then you could make SNT transactions parrelizable to some extent. That's the best example I could come up with, but I am sure there are more sensible ones. |
I would rather that DELEGATECALL (and for the sake of symmetry, CALLCODE) worked out-of-range. Blindly deploying a contract with libraries is unlikely to group them in the same range. Technically, one could brute-force them into doing so by grinding away at addresses and nonces, but this would be incredibly user-unfriendly. It is also backwards-incompatible. I do realize that a DELEGATECALL could fail if the targeted contract selfdestructed. One possible solution would be that a DELEGATECALL uses the code of a contract at the beginning of a block, and ignore selfdestructs for this one purpose. That adds a wart, but it also improves the life of the average user. All that said, I agree with @Arachnid. In a way, this penalizes non-address-grinded multi-contract dapps, For that matter, if address grinding ever became a significant factor in dapp design, users will inevitably want to cluster themselves around popular ranges, leading to pseudo-shards. Elegant and emergent in its own bizarre way, but I don't think it would be desirable compared to an actual first-class sharding system. @heikoheiko: I'm curious why sharing |
Agree. However, there is currently no gas incentive to make nicely parallelizable txs, and furthermore the worst case still has zero parallelization, and client devs would still have to design for the worst case. This EIP increases the gas limit for "normal usage" by up to 8x but does NOT worsen the worst case at all, and it also introduces an up to 8x subsidy for parallelizable txs.
Most transactions only need to interact with one contract; with EIP86 maybe two. To give one trivial example, any transactions that use two distinct ERC20 tokens will usually have independent read/write ranges.
Consider things this way:
Ideally, we want to retain support for 1b and 2b, but recognize and subsidize 1a and 2a. If we recognize 1a in protocol, then we don't recognize all of 2a, because there are cases where a contract could theoretically interact with anything (quick example: decentralized exchange), but the transaction can be more static (one particular order in the DEX with a known counterparty). However, any instance of 1a is also an instance of 2a, as if a given contract C only affects D1, D2 and D3 then any transaction sending to C can only affect C, D1, 2 and D3. Hence, 1a is a subset of 2a, and so 2a is the more general thing to recognize and subsidize in protocol.
Some quick examples:
I would even go so far as to say that nearly all activity on the current ETH chain would be parallelizable.
This is what specification v2 is for. Libraries are generally read-only, and v2 allows for intersecting read ranges, so it should cover all of these use cases. |
I think you might have misunderstood. My scheme does not require you to choose a single prefix. You can set the read or write range for a transaction as the union of multiple prefixes. So address grinding should not be necessary or particularly helpful. |
Not sure I understand your proposal. The intuition behind my proposal is that the finishing_gas_height of a transaction can be thought of as an upper bound on the computational "depth" of the transcation - that is, the number of clock cycles of time needed to execute that transaction and all of its dependencies that can't be parallelized. I'll give an example. Suppose you have four transactions, and suppose that the full address range is the letters ABCDEFGHIJ for simplicity. The first tx has 25000 gas and read/write ranges ABCD. The second tx has 50000 gas and read ranges EFGHIJ and write ranges EFG. The third tx has 65000 gas and read ranges EFGHIJ and write ranges HIJ. All three of those txs can be executed in parallel. Now, suppose the fourth tx has 20000 gas and has read/write ranges CDEF. Executing this tx requires having already executed the first and the second, though not the third (as the third does not write anything that the fourth reads or writes); hence its finishing_gas_height is max(25000, 50000) + 20000 = 70000. A computer with 8 threads would be able to finish processing all four transactions in 70000 cycles (assuming 1 cycle = 1 gas); hence these 4 transactions could fit into a block with a 70000 gas limit. Hope that makes some sense. |
I like this proposal a lot. Given that developers usually opt towards "pull" rather than "push" transactions, most transactions should be able to take advantage of this. If the subsidy is removed, could this be implemented as a soft fork? Client devs may struggle with multi-threaded implementations and I don't see a pressing need for non-miners to implement this. |
@vbuterin I have a question on In your example 4 transactions you calculate: The number But then you mention that:
Unless I am missing something, the block's max gas limit required shouldn't change due to the way you process the transactions .Shouldn't it be |
Some additional comments on the actual spec: I really like the idea and believe it will really help relieve the network pressure in Status-like ICO situations. Even though I suppose it would be a bit more work, I think that |
This EIP will change the role of the block gas limit. So a 4.7m gas limit would be not just 4.7m gas worth of computation, but rather 4.7m gas worth of time on an 8-core machine. Perhaps renaming it to
Agree. I definitely believe v2 is superior. |
One thing that should be noted here is that the CPU-intensive workload should scale linearly with the number of cores, but what does not scale that well is the size of memory and database access. It could be that this change will effectively make state reads 8 times cheaper in gas, depending on the database. On the other hand, if the transaction gives an explicit list of addresses (i.e. not only prefixes) it will read from, we might already load these accounts into the memory of the core processing this transaction before we start processing it. |
Extremely in favor of this proposal. In addition to the immediate scaling gains, I have a strong hunch that defining and incentivizing parallelism will open up very useful design space for us in the future. If we do this, let's do v2 if at all possible. A lot of current design patterns heavily use |
Good point. Question: are SSD reads parallelizable? If not, then state reading opcodes would definitely need to be increased in price.
One problem with this is that accounts can have arbitrarily large storage trees, and we can't load that into memory. |
I see. That makes sense.
Yes I think this sounds like a bit more accurate description for this field. So a block would have a |
Makes sense. I didn't put a lot of thought on this, so that was just speculation, but let me explain what I had in mind: if a contract pre-determined a set of contracts it can interact to, then, in situations of stress, there could (guessing) be an automated way to fork the contract from the rest of network, letting it run on its own cluster for a day or two. It would be a separate, merge-mined network for that time. Eventually, it could be re-merged. Assuming splitting and merging was a costly operation, that would only be possible if we had a guarantee the contract won't receive a transaction that interacted with contracts on the other network. That is why the restriction couldn't be on transaction level, as any transaction could trigger a re-merge. |
Then it wouldn't be parallelizable, I guess. Except if we had |
Sure. But this is not at all compatible with the goal of "easy parallelizability" :) Networks splitting and merging is the sort of thing we're considering for later stages of the sharding roadmap, whereas this proposal is intended to be more here-and-now. |
@chriseth Given that the state trie is keyed by Only lookups of fully pre-determined addresses would benefit, although we'd have to charge for "pre-loading" gas ahead of time in case the transaction never actually inspects those accounts. |
This assumes that disk/memory operations are parallelizable, when in fact we usually find these to be some of the largest bottlenecks while optimizing. |
Can we cement this with a simple example? Suppose I call a contract with 150,000 gas in thread A, which moves tokens in thread B (utilizing 50,000 gas). Given that there are 8 threads, how much gas does this transaction consume? Edit: I think I'm confused about the gas consumption ramifications. Is this proposing individual transactions spend only some fraction of gas commensurate with the subset of the state space they update? Or is it proposing a way to pack more gas spending into a block with a fixed gas limit? |
Question: What are the tools that need to be made available for handling interdependent state changes of contracts within the read and writing range? |
Or is this proposal simply proposing to handle only the independent ones? |
The latter. |
Something can be a bottleneck and be parallelizable at the same time. Currently, the problem is that each individual database op may be dependent on the result of the previous, whereas with this scheme you could be fetching several storage keys at the same time. I'd definitely be very interested in getting more data on parallelization and SSD reads. |
TL;DR: Is it a known fact that the bottleneck is transaction processing, and not network latency (or something else altogether)? As much as I like the proposal in general, got to highlight: it does not immediately follow that introducing parallelisation like this will result in a Imagine a newly-received block being validated by a node. Say for example that:
A miner pool's node is likely to be capable of many more concurrent threads than the largest batch here, yet even they have to process the latter two categories one tx at a time. Slower miners that can't provide such concurrency are likely to vote the gas limit down, if their uncle probability increases. Many more miners may come to be seen as "slow" if the bottleneck turns out to be not validation time, but block propagation time (i.e. time in transit). This parameter is likely to increase, since for the same new Sure, slow miners would eventually be forced off the market by their own inefficiency. But that may take a while - at least 'till unprofitability of such behaviour becomes apparent to their operators. The time period, I'd make a guess, is months, if not years. IMO it's still worth diverting mindshare towards this goal from other development (probably because it's not my own mindshare being diverted q:D). Just something to keep in mind - this proposal is about activating processing power that's currently underused, to put market pressure on those that are grossly under-performing. It may back-fire, though, by using an unexpected criteria to determine who it is that's "under-performing". If Also, if implemented, this will likely introduce "fee levels" for various categories of transactions, where a "batch of 1" (or "old-style unbatched") would need to have gas price at least Not calling this part "bad", only that such an outcome would be a rational market response. |
This part is not necessarily true; contract creation is in many cases deterministic. Though in some portion of cases (eg. where each operation in some dapp causes the same contract to create a new contract, with an incrementing nonce determining the address) it's certainly difficult, and in those cases, yes, you would have to just pay the
Network bandwidth can be fairly easily scaled up; just get a better internet connection. IO can also be scaled up; if all else fails, require nodes to have 512 GB of RAM. Transaction processing, however, is single-threaded, which makes it especially hard to scale at this point. This proposal fixes that side of things; I agree that optimizations on the other two are also necessary. |
@vbuterin SSD reads are, in principle, able to be satisfied in parallel. However, there are a bunch of gotchas:
TL;DR implementation quality and hardware/firmware quality make an enormous difference. Source: filesystems work used to be my full-time job. Edit: there's a lot of implementation improvement that could be made on the geth and parity storage implementations, given the specific constraints of what the EVM can actually store (fixed-size keys and values, and a huge advantage for having a log-structured COW backing, since you need a kind of snapshotting mechanism to do rollbacks of aborted transactions.) Edit 2: |
Note we can't rely on averages or amortisation - as long as an attacker can force a worst-case, they will. |
I have a nice title for this eip: Vertical Sharding :) |
My usual preferred lingo is that parallelization is something that can happen within nodes, whereas sharding specifically is between nodes (and particularly, between different administrative domains). Though I'll admit this is not settled terminology :) |
I agree with that terminology. |
Sharding is internode/interdomain, and I guess its name derives from shards of the Ethereum crystal, and is a subcomponent or channel of the network. |
Referenced here: "A solution is to require a transaction to include a static list of the set of accounts that it can access; like EIP 6488 but much stricter in that it requires a precise enumeration rather than a range." "The requirement for transactions to specify account lists incidentally adds a high degree of parallelizability; this is in many ways a supercharged version of EIP 648." |
Will this create an incentive for miners to include transactions that have a small address coverage range before ones that have a larger range, since they could stack more of the first kind? Asking because this could influence how projects build their multi-contracts infrastructure. |
@PhABC I've pondered this in an earlier comment. The gist - it'd still primarily be determined by That is, there IMO would be no economic incentive to include |
To be clear, the only changes needed by transaction originators/wallets would be to add Reference implementations of RLP generation would also be great so we can design client-side JS libraries. |
Considering Cryptokitties...this ought to be moved up the priority queue. |
The only direct economic incentive seems for miners: with this proposal they can fit What about an alternative approach where there are no new transaction types, but transactions in a block are grouped in batches of independent transactions? The client would optimistically execute all the transactions in each batch and reject the block if there is a non-empyty intersection; each batch would have an effective This solution does not require additional range checks while the EVM is running on the client node (these checks can be done after each batch has been executed) and can immediately give an effective discount to old-style transactions, if the miner is smartly batching them. |
As discussed at the sharding workshop last week, above definitions of starting and finishing gas heights can lead to suboptimal results if transactions are not ordered properly (or I have misunderstood the proposal). Example:
If the transactions are ordered
However, if the order is
So in this order the global gas limit would be exceeded, although a sensible miner strategy can execute the transactions in the same amount of time ("schedule the next transaction if there's a free thread and it doesn't overlap with any non-executed earlier transaction"). I think this could be fixed by keeping track of I wanted to point this out, but in my opinion it is not worth implementing this more complicated algorithm. Both methods appear be equivalent if the miner (or, more generally, the proposer) chooses an optimal transaction order in the first place. This needs to be done anyway in order to maximize gas usage if transactions do have overlapping access lists. |
Whatever happened to this? Long ago it was slated for metropolis |
@joeykrug, cross-posting my reply from reddit: It was discussed on an AllCoreDevs call around the time (it was never "slated for Metropolis", as in it was never officially
Once it became widely realized that disk I/O is the bottleneck, research began on optimizing the state trie's data structure, and how it might be possible to parallelize SSD reads: Roadmap for Turbo-Geth. After EIP 648, the next research thrust was on stateless clients: "this is in many ways a supercharged version of EIP 648." Stateless clients would enable a sort of 'parallelized state i/o' because all accessed state is pre-specified in the witness list (i.e. all accessed state is pre-loaded from disk into memory before the transaction is processed; in theory disk I/O of the validating node is no longer the bottleneck). In subsequent months, discussions around sharding and stateless clients mainly revolve around the question of "stateless clients vs storage rent". And that brings us to the present day, ime. |
Would this not allow the DAO-style soft forks, and with it, censorship? http://hackingdistributed.com/2016/07/05/eth-is-more-resilient-to-censorship/ If the ranges are tight, then miners can cheaply figure out what are the effects of the transaction, and censor them. If the range are broad, then there is less effect of the parallelisation. They way I see it - it is trading off censorship resistance for more parallel execution. Not sure this tradeoff is worth doing |
There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review. |
This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment. |
Why not use a similar approach to BlockSTM? I think Polygon has already implemented it for EVM or at least an MVP. |
This belongs to pessimistic concurrency control, but BlockSTM is based on optimistic concurrency control. They are not very similar. |
Parameters
MAX_THREADS
: 8Specification
Introduce a new type of transaction (following same general format as #232):
Where
ranges
is an RLP list, where each value in the list is an address prefix, representing the range of all addresses with that prefix. That is, for example:\x01
refers to the set of addresses from 0x0100000000000000000000000000000000000000 to 0x01ffffffffffffffffffffffffffffffffffffff\x02\x48\xab
refers to the set of addresses from 0x0248ab0000000000000000000000000000000000 to 0x0248abffffffffffffffffffffffffffffffffff'l#\xfa\xce\x01O \xb3\xeb\xb6Z\xe9m\r\x7f\xf3*\xb9L\x17'
refers to the one-element set containing only the address 0x6c23face014f20b3ebb65ae96d0d7ff32ab94c17Prefixes longer than 20 bytes are illegal (ie. transactions containing such prefixes are invalid). We call the "range of a transaction" the union of all ranges specified in that transaction if the transaction is of this new type, and the set of all addresses otherwise. For example, if a transaction's
ranges
field is of the form[\x01, \x02\x03, \xff\x8c\x45]
, its range is the set of all addresses that start with either 0x01, or 0x0203, or 0xff8c45.We keep track of a
starting_gas_height
andfinishing_gas_height
for each transaction. We define thestarting_gas_height
of a transaction T to be the maximum of allfinishing_gas_height
s of all transactions T' before T such that either (i) T' is more than MAX_THREADS behind T (ie.txindex_of(T) - txindex_of(T') > MAX_THREADS
) or (ii) the ranges of T and T' intersect. We define thefinishing_gas_height
of T to be thestarting_gas_height
of T plus the amount of gas consumed while executing T.The current rule that a transaction is invalid if its
start_gas
plus the currenttotal_gas_used
exceeds the blockgas_limit
is removed, and replaced with a rule that a transaction T is invalid if:T.starting_gas_height + T.start_gas > gas_limit
. Notice that in the case where all transactions use the entire address space as their range, this is exactly equivalent to the current status quo, but in the case where transactions use disjoint ranges, this increases capacity.Any CALL, CREATE, CALLCODE, DELEGATECALL or STATICCALL that attempts to access an account outside a transaction's range fails; any EXTCODESIZE, EXTCODECOPY, BALANCE or SELFDESTRUCT that attempts to access an account outside a transaction's range immediately throws an exception. Addresses 0...127 have one exception: any account can STATICCALL them. We add a binding norm on protocol development that no address may be introduced into that range which has logic such that it may return a differing value depending on transactions that take place within a block (though changes between blocks are acceptable).
Rationale
This allows transactions in the EVM to be processed in parallel much more easily, by specifying statically what addresses they can access; it also appropriately incentivizes making transactions that are easy to parallelize. It does this in a maximally backwards-compatible and non-intrusive way where old-style transactions can continue to work.
The exception for addresses 0...127 is introduced to allow access to precompiles and to system contracts like BLOCKHASH.
Note that this EIP does change the role of the block gas limit from being a limit on total gas consumed to being a limit on gas height. This means that the theoretical max capacity of the blockchain would increase by a factor of NUM_THREADS, though if we want to avoid increasing uncle rates it does require the assumption that miners are running on hardware that has NUM_THREADS CPU cores. For clarity, it may be a good idea to rename the block
gas_limit
togas_height_limit
.Client implementation
A client can follow the following algorithm:
TXINDEX = 0
txgrab
as an empty list.TXINDEX
does not intersect with any transaction intxgrab
, (ii)len(txgrab) < NUM_THREADS
and (iii)TXINDEX < len(block.transactions)
, add the transaction in the block at indexTXINDEX
totxgrab
and setTXINDEX += 1
.txgrab
in parallel.TXINDEX == len(block.transactions)
, exit. Otherwise, go back to step 2.Specification, v2
Modify the new transaction type to be as follows:
The read range of a transaction is the union of all
read_ranges
, and the write range of a transaction is the union of allwrite_ranges
. Replace "the ranges of T and T' intersect" above with "the read or write range of T intersects with the write range of T' " (where T is the current transaction and T' is a previous transaction). Read-only operations are allowed to access the read or write ranges, write-only operations are allowed to access the write range only.This adds a further optimization where if two transactions' read ranges intersect at data that is not written to, then they can still be processed in parallel.
The exception for addresses 0...127 can be removed, as any contracts that wish to use the precompiles can simply include the addresses in their read range, and even if all transactions include those addresses in their read range parallelizability will not be significantly impacted as almost no one wants to write to those addresses.
Proposed amendment 1 (thanks @Arachnid for suggestion)
Instead of each range being an RLP list, it is simply a byte array, where each byte represents a byte prefix (eg. the byte array
\x03\x35\xfe
means "the set of all addresses starting with 0x03, 0x35 or 0xfe"). An empty byte array represents the set of all addresses. This simplifies implementation at some cost to granularity, though the value of that level of granularity is arguably low.The text was updated successfully, but these errors were encountered: