-
Notifications
You must be signed in to change notification settings - Fork 711
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
Support for code loading for upgradable contracts and proxying via LDC
.
#451
Comments
Thank you for writing this up @otrho ! |
Crazy idea: |
OK, so traditional upgrading and proxying are essentially the same thing, the former uses the latter. In that case the dynamic loading is how we could proceed. Solidity's But I think your crazy idea could be maybe done with a special 'upgradeable' flag for ABI methods. Each flagged method would have builtin delegation, managed entirely by the compiler, which has a stub to dynamically link those methods via storage and This actually seems like a nice compromise. Simpler for the compiler (no stack crap nor One caveat with compiler supported upgradability would be the cost of loading functions which aren't called. Every upgraded function would have to be loaded whether its called or not. Even checking whether a function has been upgraded or not has non-zero cost, since it's a storage read. |
Nice work on writing this up @otrho! Upgradeable Contracts
I guess we'd also want to ensure that there was some way for users who interact with the contract to easily check what the current "implementation" contract ID is, as users interacting with upgradeable contracts probably want to be able to verify the current implementation contract ID. I.e. they need to be able to construct transactions that are invalidated if the contract is updated before their transaction is submitted. I imagine this is easy to do with the "dynamic loading" approach, as your proxy contract can just provide a non-delegate method that returns the delegate contract ID. For an "upgradeable" feature, we'd probably need to generate some method for this. I guess this implies upgradeable contracts might also need a special case for the
Edit: Come to think of it, that means This seems like it would very much bake-in the way of upgrading contracts into Forc and Sway. On the other hand, if we were to go the dynamic loading approach, maybe we could be more hands-off and let the Apps team and wider Fuel ecosystem work out the nicest way of upgrading contracts over time with custom forc plugins, without us taking on the responsibility of getting it right the first time ourselves? Dynamic Loading Attributes?If we're to go the dynamic loading route, do we want to require some kind of attribute decorator for functions that touch LDC code anyway? Especially in the case that the function is also LDC + Storage InteractionRelated side-note: is the idea that code loaded via LDC can interact with contract storage in an arbitrary way? Perhaps we should conservatively disallow dynamically loading code that interacts with storage in favour of only allowing to load "pure" functions, so that an author can limit what parts of storage their loaded code has access to? That said, I guess it might be hard to know what limitations you want to apply when the whole point of the feature is upgradeability 🤔 |
Just throwing in my two cents. I'm not sure we should have upgradable contracts at all. If some part of an application is fine with delegated trust then why even run it on chain? The alternative is to keep older versions running and bridge between the old and the new version. This allows each user to decide if they want to "upgrade" in the same way they decide whether or not they want to stay on any contract or chain. |
Yes, although upgrades are not the only use case for proxies. Along the lines of ERC-1167, they are also used for "minimal proxy" clones. In cases where a potentially large number of contracts will be deployed and they have some portion of their functionality in common (think smart-contract wallets, where each contract is nearly identical other than perhaps the owner), a single template/implementation contract is deployed, and then any number of tiny proxies are deployed, each with perhaps a different owner. The proxies delegate all computation to the implementation (which may or may not be upgradeable). In the case of smart-contract wallets the users may need to opt-in to upgrades (pretty sure argent wallet takes this approach).
Yeah, this is what delegateCall does, basically allowing remote code (from an external contract) to be executed in the context of the calling contract, which allows upgradeable contracts via proxy to work. It is on the developer to ensure their contract only loads code from a trusted/verified source!
100%. in so called "Transparent Proxies" this is usually achieved by storing the implementation address in a "public" storage slot, and there is a getter for this called |
Considering this, my intuition is that we're probably better off going the dynamic loading route as it unlocks all this flexibility under a single feature, and let's the library ecosystem experiment with nice APIs on top for proxies and upgradability. Later on we can think about how we can enable those patterns more easily in tooling as the Fuel-y way is discovered. It might be a shame to bake-in specific support for upgradability into the language and stock Forc commands, only to end up requiring dynamic loading in the future for other kinds of proxies anyways, and then potentially have some library come up with a nicer / more Fuel-esque way of upgrading contracts anyways that makes the in-language support redundant? |
@freesig , to your point
This is pretty common though - a governance module proposes a change which only becomes actionable when a vote threshold is reached. |
Yeh not to derail this convo. @Voxelot gave the example of weighted staked voting as a reason that you might want this on-chain. This does make sense to me. I guess my worry is that all these approaches are by definition are a weaker security model and if it's not clear that you are interacting with a contract that does have lower security then users might get into trouble and blame it on fuel. It would be nice if you knew when interacting with a contract that it or any of it's dependencies do some form or security weakening so that it's clear. Imagine FOO DEX that has an upgradable contract via some DOA vote. If that vote was corrupted or some how influenced so the contract was upgraded to an malicious contract and anyone using FOO DEX looses their tokens will they blame FOO DEX or Fuel? I'm honestly not sure but it's something to consider when making these patterns super easy for devs to implicitly include. |
@freesig I agree that generally speaking, adding the option to upgrade contracts takes away from certain guarantees unique to blockchain, i.e: the code deployed at this ContractId will always be the same. Just my 2 cents... :) |
OK, there are still a bunch of questions/unknowns here. It looks like the preferred use case in Sway will be dynamic loading, implying conditional / optional calls to The unit of code that we'll be able to load is a function. But what does that mean? Does it need to be an entry point in the external contract? Currently they're the only functions with a published ABI and also the only functions guaranteed not to be inlined, so I think this is a must. At the same time the loaded code can't make any function calls of its own as they won't be loaded. i.e., for them to be usable they must have all calls inlined. This actually isn't really feasible. I think the idea that we allow loading of arbitrary code from another contract doesn't work. We need to be able to prepare that code for loading. It needs to have its ABI published (via build artefacts) and to be a self contained unit with all its library calls and other calls inlined. And if this is the case then we could keep absolute control flow for some cases and use relative jumps most of the time (and all of the time for loadable functions). Another question which remains around dynamic loading is what this looks like in Sway. We don't have function pointers (yet) so we'll need some other magic. Or both. Naively we could have a magic type (like the current ABI/Contract stuff) where you say:
For this to type-check the A safer approach would be to just declare a function signature as The compiler could handle it all, as in managing how the metadata around the external function specs are loaded from and stored in storage. Something like an Wow, when spelled out like that it sounds really dangerous. Obviously updating the |
Nick said:
I think that this iteration, update, feedback loop could be done off chain though, with proper publishing tools, like a |
I feel like the gold standard for contracts is to be immutable. So even upgradeable contracts ought to be striving to disable the upgrade mechanism at some point, ideally asap. The nice thing about upgrades via the proxy pattern is that an upgradable contract can have a fixed ContractId, ,allowing other contracts to integrate with them without themselves needing to be upgradeable just to keep up with the changing ContractIds. |
There is a need to try and nail the core functionality down, particularly around the VM, prior to beta-4 and definitely before mainnet. At a minimum we need PIC, which implies relative jumps are added to the VM and the Sway compiler employs them everywhere. @adlerjohn has suggested offline that the MVP proxy could then be implemented in ASM directly. It would be fairly coarse in that it could only load entire external contracts, but all that is required in that case is to perform the actual load and then to manage the delegation (for all selectors), along with the mechanisms for updating the metadata around the external contract description (i.e., the address and size of the external contract in storage). After this MVP the refinements around how to implement the same safely in Sway can be designed and implemented progressively without the pressure of future milestone deadlines and breaking existing code. |
LDC
has not really been tested thoroughly and currently the VM+compiler have many shortcomings around supporting its use.Related issues:
cfe
instruction to the vm. #346Goals
Loading code via
LDC
is desired as a cheaper alternative to making cross contract calls to utilise library oriented functionality from already on-chain code.I'm still a bit hazy on the actual mechanics of the following, but it's also desired for upgrading contracts and implementing proxying.
All of the discussion below assumes the granularity of 'code' to be loaded is a Sway function. They would be loaded and called using Sway's calling convention.
Problems
There are a few problems with the current implementation, some simple, some not so simple.
LDC
reverts if$sp
!=$ssp
. This was made in an effort to avoid clobbering user data but it actually mostly hinders its use. The user (i.e., the Sway compiler usually) should be able to manage the stack itself, understanding thatLDC
will load to$ssp
.LDC
were to be used on some arbitrary code in a contract, placing it at an offset different to where it is in the origin, the control flow will be corrupt. This is due to the VM (and compiler) primarily using absolute addressing for jumps.LDC
takes an external contract address, an offset and a length and copies the code to$ssp
. To use it today would have to be done entirely in embedded ASM with hardcoded values calculated manually.Proposed solutions
Respectively:
$sp
. Easy.Tooling and compiler
One proposal is to make code loading essentially a compile time operation. That is, if a contract wants to load from an external contract it must be specified at compile time (of the loading contract) where to load from. This would be the simplest solution for a few reasons:
Forc.toml
, which formalises the process somewhat.The compiler then has two options for performing the actual loads. These methods aren't exclusive though the former is simpler than the latter:
Forc.toml
is still used but the actual loading is done conditionally at runtime. In this case the compiler would need to manage the stack, copying it to somewhere safe first. Intrinsic functions would be needed for both reading symbol table values and for making the load.The dynamic loading would require some extra support from the VM for stack manipulation. Currently we have only
CFEI
andCFSI
which grow and shrink the stack (i.e., increment and decrement$sp
) by immediates. I'm thinking the process of moving the stack contents would be something like:code_len
bytes on the the stack, wherecode_len
is the size of the code loaded byLDC
. I.e.,$sp
+=code_len
.MCP
the current stack content from where it was to up to the top of the stack, being careful of overlaps, etc.LDC
the code. The new$ssp
should be at the bottom of the newly copied stack.$sp
to the top. This is only required ifLDC
resets$sp
to the new$ssp
. If it were to leave$sp
alone we'd be fine.Step 1 and probably 4 both need to increment
$sp
by an arbitrary value, requiring something like aCFE
instruction. #346Outstanding questions
For each of the use cases does the above address them?
For simply loading a library function the static method seems to work well. Particularly for functions called a lot, it'd be cheaper than contract calls.
For upgrading contracts? I'm not sure how that would work -- is the idea that you can implement a new contract, copy a bunch of old code from an older version and add/replace some newer functionality? It could use the static method above. This would be cheaper than publishing a contract which redundantly contains already on-chain code, so it makes sense to me. But is that the idea?
And for proxies? I think the idea is the proxy contract has addresses in storage and loads code from them to call, with an admin API for updating those addresses and lengths. In that case the dynamic method from above would work, I think. The admin would load from the symbol table and send them to the proxy. The proxy would call the load instrinsic.
Anyway, this is an actual Request For Comments, especially from @adlerjohn @Voxelot @simonr0204 @mitchmindtree @mohammadfawaz and @nfurfaro but I'm sure it's interesting for others on the @FuelLabs/client @FuelLabs/tooling and @FuelLabs/sway-compiler teams. @sezna too, if you like. 😉
The text was updated successfully, but these errors were encountered: