This repository was archived by the owner on Feb 26, 2024. It is now read-only.
Move from promise-based decoder that makes web3 calls to generator-based decoder that makes requests to caller #1900
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR (the idea for which is due to @gnidan) restructures the decoder so that, instead of being based on
asyncfunctions and promises, it is based on generators and iterators; and instead of the decoding functions havingweb3andcontractAddresspassed in as arguments, and the decoder makingweb3calls, it makes requests to the caller that the caller must respond to.Don't worry -- if you're using the contract decoder, you shouldn't notice any changes! (Aside from two minor enhancements which I'll get back to later; skip to the list of minor changes at the end if you want to read about those.) If you're using the
forEvmStateinterface, however, this is a breaking change. Fortunately, I don't think anyone is using that interface except for the debugger, and the upsides of the change are definitely worth it.So how does this new interface work? As mentioned,
decode(for whichforEvmStateremains just a wrapper) is now a generator, meaning it returns an iterator (call itdecoder). To use it, calldecoder.next()to get a result; if this is the final result, i.e. ifresult.doneistrue, thenresult.valueholds the decoded value. If it's not the final result, i.e. ifresult.doneisfalse, thenresult.valueholds a request object; one must formulate a response to this request and calldecoder.next(response), repeating until one gets a final result.Currently, the only requests the decoder makes are for storage. These requests have the form
{type: "storage", slot}, whereslotis a BN. (Note there is noaddressfield; this wouldn't fit into the current structure -- for now supplying an address is left to the caller; however, I might add anaddressfield in the future once such a thing makes sense. Similarly there's noblockfield, and I have no plans to change that; that wouldn't make sense for the debugger's use case, anyway.)The contract decoder responds to storage requests by looking up the storage slot using
web3. (The address is the address of the contract being decoded; as for what block to use, see the part at the end about contract decoder enhancements regarding that.) The debugger, by contrast, responds to all storage requests with zeroes. This is because the debugger passes in all the nonzero storage it knows upfront; any storage it doesn't know, it presumes to be zero.(I briefly considered moving to a structure where only
definitionandpointerwould be passed in as arguments, with the decoder making requests for all other information it needed, rather than havinginfopassed in upfront; but this would be a bunch of extra work for no upside that I could see other than a dubious sort of consistency. So instead I'm going the opposite route -- I'm going to continue have as much as possible passed in upfront and will only use requests where necessary.)So, as mentioned, right now we're only using requests for storage (and only for those slots whose value wasn't passed in upfront). But the motivating reason for this change, the thing that necessitates it, is that soon we're going to need to make requests for code, too, and these will require looking up that code on the blockchain even when called from the debugger, and (for reasons not worth getting into here) it's not possible for the debugger to pass in a
web3object like the contract decoder previously did.But this change has a number of other upsides, besides just allowing code requests! For instance, it saves us from having to pass in some sort of web3 options object, which we don't currently do but I was fearing would be necessary. Now, instead of passing in an options object, to dictate what web3 calls the decoder should make, the caller just decides what calls it wants to make to respond to requests. Also, this new structure should allow the decoder to be used to implement a major part of reconstruct mode, once we start on that.
So, that's how to use the new interface to the decoder. But what's changed internally? Surprisingly little! Parts that used
Promise.allfor parallelism have been changed to be serial instead, and of course the storage read functions have been changed to make requests when necessary, but a large portion of the change was effected with a simple:%s/await/yield*/. It turns out that the way thatyield*works basically fits exactly with what we want, so functions can simply call each other withyield*.You see, in a generator, the expression
yield* itdelegates to the iteratorit, causing the generator to yield in sequence the values coming fromit... except for the final, finishing value fromit. That one, instead of being yielded by the generator, becomes the value of the expressionyield* it. (If you wanted to yield it, you could writeyield yield* it, but we don't want to do that.) This means that we can have our generator functions simply usereturnwhen we want them to return a value to their caller like usual, and useyieldwhen we want them to issue a request, pause the decoder's execution, and wait for a response. It's really that simple!Note, by the way, that many of the read functions were actually not
asyncbefore, but were simple pure functions; only the ones that needed to beasyncwereasync. I've kept that in this PR -- the read functions that were previously pure functions, are still pure functions; only theasyncones have been replaced by generators.There is one ugly thing about this restructuring, though, and that's the TypeScript typings. You see, TypeScript has no way to know about the structure mentioned above, where real values are
returned and requests areyielded. This means that the decoding functions have to includeUint8Arrayas a possible return/yield type, even though they will never return or yield this, because TypeScript cannot discern this; it can't tell that the read functions only returnUint8Arrays and neveryieldthem. Until this proposed change makes it into TypeScript, we're just stuck with this ugliness. Note that due to our general abuse ofany, this mostly isn't visible at the moment, but once we have the new decoder output format and things get more structured, it'll become noticeable.So that's what's different in the decoder! What about the debugger? Well, the selectors
data.views.decoderanddata.current.identifiers.decodedare gone; with the new structure, it soon won't be possible to do decoding in a selector. (As of this PR, it's still possible because, as mentioned above, the only responses being passed in are zeroes, not information fromweb3; but that won't be the case for long.) In their place is a new data saga,decode.The
decodesaga takes a definition and pointer as arguments and decodes them based on the current state; the main data saga now just calls this with ayield*rather than using ayield callto decode. Note that the part of the state that keeps track of whether the decoder is ready, and the actions that manage it, have been removed, as they're no longer necessary.What about the interface to the decoder? Well,
session.variablenow just runs thedecodesaga on the specified variable, andsession.variablesnow just runs it on all variables (as determined bydata.current.identifiers.refs). As forsession.decodeReady, it's been retained for compatibility, but it now simply always returns (a promise that resolves to)true. (Although, users should probably not have been usingdecodeReadydirectly, but, just in case they were, it's been retained.)But wait!
Sessionmethods can't run sagas, you say! Ah, but they can! It turns out that the redux-saga API includesmiddleware.run, which allows for running any specified saga. However, in order to do this, we need a copy of the saga middleware object. So, over instore, theconfigureStorefunction has been modified so that, instead of just returning the store, it returns an object containing both the store and the saga middleware; and theSessionconstructor now saves the latter inthis._sagaMiddlewarein addition to saving the store. I then added anasyncmethodsession._runSaga, which uses the redux-saga API to run a specified saga (with specified arguments) and return a promise that resolves when the saga is done running. (The name starts with an underscore to indicate that this is not for external use.) With this, it's now possible forSessionmethods to run sagas, assession.variableandsession.variablesnow do!Finally, a few other minor changes:
stateandvariablefunctions actually respect theblockargument if it's passed in, instead of ignoring it. This determines the block that will be used for allweb3requests.state, since it already included both the variables and the balance, I decided to toss in the nonce too, because why not. I stopped short of also including the code, though.decodefunction in their helpers, that did some logic to convert the decoder output to more easily-usable JS objects; I've replaced this logic with a call toDebugUtils.nativize, since for some reason this logic wasn't working properly in some cases. (The first use oftruffle-debug-utilsintruffle-debugger, I believe, even if it's only in the tests.)lodash.rangefromtruffle-decodersince (as of this PR) it no longer uses it.And that's it! With this, the preparatory work is out of the way, and we're ready to decode external function pointers.