-
Notifications
You must be signed in to change notification settings - Fork 372
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
[RFC] Contract versioning (upgrade/proxy) #694
Comments
I'm wondering what to do with persisted values from previous versions. If each new version creates new persisted values, will we need to do some sort of migration? |
Indeed, it is an option, and it may lead to some intriguing migration workflows, such as: // contract foo
var users []User
var paused bool
func ListUsers() {...}
func AddUser() { if paused {panic()}}
// contract bar
var users []User
func init() {users = foo.ListUsers()} To enhance migration capabilities, the An additional option, even with independent states between versions, is to implement patterns through data-oriented contracts and dynamic whitelisted intermediary contracts. My suggestion would be to weigh the potential drawbacks of sharing state between versions and impose appropriate restrictions. However, if we are unable to identify a viable solution, then it may be prudent to maintain independent states and concentrate our efforts on crafting great migration patterns and powerful structures. |
Personally, I don't think it's easy to plan ahead for the next version and add It would be nice to have some sort of switch in vmkeeper to disallow changes to that realm, e.g. func init() {users = foo.ListUsers()} If this could be used as a kind of migration, it would be very useful. :-) |
Related with #709. |
I think that the way that we handle contract versioning should first of all differ between importing packages and realms. I think that if a package or a realm imports another package, the import should be pretty much identical to what happens in Go: the import path is "gno.lang/p/demo/foo", the package is saved as an import in gno.mod, which gnoland is aware of at the time of publishing, and the specific version in gno.mod is always used. For realms, due to their nature of state preservation and of potentially being end-user programs, I don't think version locking like for packages is a good idea, with the larger issue being data upgrades and security fixes.
My proposal for realm upgrades is the following:
var stateVersion int64
func init() {
stateMigrations := []func(){
0: func() {...},
// ....
}
if stateVersion < len(stateMigrations)-1 {
for _, mig := range stateMigrations[stateVersion:] {
if mig != nil {
mig()
}
}
}
} This way we can control code to run at every upgrade, or only on a certain upgrade, etc.
Corollary: a realm owner, if they update dependencies and the data type of an imported package is destructively changed, will also have to write an upgrade function for that variable if present in the state. This is just something I'd been thinking upon for the past few weeks, but I think it is a sound proposal to allow upgrades effectively in a manner that is useful for all parties involved in contract creation and usage. Let me know what y'all think! |
Related discussion on Reddit w/ @progger: https://www.reddit.com/r/Gnoland/comments/13f43zf/comment/jl815x7/ |
@thehowl Interesting points! Don't you think it will be difficult and potentially error prone to handle automatic upgrade / detect destructive/non-destructive changes ? Personally I would force the user to provide an upgrade if there's any changes in the variables. func upgrade(posts []Post) [8]Post { return [8]Post(posts[:8]) }
func upgrade(users []string) (usernames []string) { return users } I like this kind of API but I fear it can be too restrictive. Say your new data format is a concatenation of both I prefer the second version in the So from a dev perspective, he only needs to provide that variable to handle upgrades: var upgrades = []func(){
// probably nothing at index 0 because there's no upgrade for version 0
1: func() {
// upgrade for version 1
},
2: func() {
// upgrade for version 2
},
} But then I wonder how do you access the old format of the realm data ? In such function, you only have access to the new format right ? |
Good points @tbruyelle. I'll get the "easier" one out of the way;
Well, my idea with the
Yeah, which is why I was proposing using the init-stateVersion paradigm only for non-variable upgrades. I think another proposal could be: only have init-stateVersion upgrades, but then have reflection on a realm's state which can also access a previous version's state. It's possibly a good idea: I can imagine some code like the below which to me looks potentially powerful and a good solution; but it does feel more cumbersome to write "simple" upgrades import "reflect"
var stateVersion int
var var1 int
func init() {
// ...
lv := reflect.RealmHistory().Last()
var1 = lv.Value("oldVar1").Interface().(int)
// ...
} As for the original
I think that from a usability perspective, some variable upgrades I shouldn't need to specify how they happen. Ie. if I upgrade
Yes, I think that eventually old upgrade functions would have to be "cleaned up" in the source code; or if anything relegated to files which also have a mechanism similar to |
I would propose a simpler solution, and it is keeping the migration to be done by the realm itself. We can do it using semantic versioning on the Realm path as Go does:
// this is inside v2 package
package myrealmname
import "gno.land/r/myorg/myrealmname/v2/compat"
import old "gno.land/r/myorg/myrealmname"
func MyRealmCall(id string, filters string) string {
result := old.MyRealmCall(id) // Note that this call is old and it does not have filtering options
if result != "" { // This user had the data stored on the previous realm version, getting it
fr := compat.Filter(result, filters) // Compatibility layer
// TODO add more code here to migrate old data to the new realm
}
// New code getting information from this realm
} WDYT? I think that using this approach is diverging less from the standard Go World and will be easier to understand for newcomers. |
Update on Technical Distinctions:
Conceptual Comparisons:
This subtle distinction implies variances in how we approach versioning and package upgrades. For instance, barring security concerns that may warrant marking a package as deprecated, it's generally acceptable to retain an outdated version of a Package Deployment & Versioning:
For For |
ProxyI found @ajnavarro 's Proxy idea simple to absorb as a dev. "The latest uploaded version is the source of the truth".
LimitationsAbout the upgrade limitations mentioned by @thehowl here I think it's totally acceptable due to the complexity of its nature. See that OpenZeppelin implemented a similar restriction approach to apply some limits over the state but allow function signature changes.
Versioning
Indeed. I suggest a simple mechanism to attach a timestamp to the file name when creating or updating it, preserving its myrealmname only to be used as "Proxy". Pattern:
|
Was discussing with @kristovatlas; here is an update on how it could work. Someone writes From a chain perspective, we can assist packages by implementing a changelog, a concept of "latest" version, and a shortcut like a symbolic link ( If a contract imports A DAO can elect packages to become more official and create an alias from Regarding We are considering the introduction of different contract "classes." One class could be "flexible," allowing authors to easily change their contract. Another class could be "fused," where privileges are permanently dropped. A third class could be managed by a DAO, involving slow governance decisions. In addition to upgrade and versioning features, we will focus on making these aspects transparent, providing automated disclaimers for recent changes, contracts, and outdated versions. |
Here's what I think: VersioningI propose using SemVer(without pre-releases and metadata) for realm and packages.
ImportingYou can just import any package/realm simply by using import path, version will be resolved and locked by Note:
Example: // foo.gno
package foo
import "gno.land/p/{handle}/mypkg"
[...] // gno.mod
module gno.land/r/{handle}/foo
require (
gno.land/p/{handle}/mypkg v1.0.0
) Upgrading
Few more options to consider:
|
@moul If I understood correctly, I don't think this is a good idea. Even when a new major version is released, we should allow bug-fix releases for previous major versions, for security reasons. We can retract versions using gno.mod if needed, see below. Adding to @harry-hov comment, Go modules spec is providing the needed tools to deprecate and/or detract previous versions:
@harry-hov the problem I see with this approach is, who is gonna pay the cost of the migration? |
@ajnavarro I think that the immutability of smart contracts post-deployment is a sensible design choice, as it ensures transparency and plays a crucial role in establishing trust among users. When it comes to security vulnerabilities, deploying new versions of contracts offers a viable solution for addressing these issues. Making contracts upgradable for security reasons introduces the potential for malicious actors to exploit this flexibility, effectively trading one set of security concerns for another. |
@wwqiu, my point aligns with what I've previously mentioned about generating new versions and addressing problems in older ones. From what I've gathered based on @moul's input, once a new major version (e.g., v2.x.x) is introduced, the addition of earlier major versions (e.g., v1.x.x) becomes prohibited. However, my argument is in favor of continuing to allow the inclusion of any version that developers need. It goes without saying that, as with all package managers, version numbers are fixed and cannot be altered. When we say that a contract is upgradable, we talk about creating a new version for that contract, not modifying previous versions. |
|
We won't be implementing specific versioning features for Gno.land's mainnet. Getting versioning right is complex and hard to do while we still don't have clear examples of packages that have had to go through a migration strategy. For now, many upgrade patterns are possible manually, which can fit different use cases: https://github.com/gnolang/gno/tree/master/examples/gno.land/r/x/manfred_upgrade_patterns One thing we can likely look to do before we launch are private packages: #2191. These are simple to implement and can possibly guarantee some degree of beta testing on-chain. Although, it should be noted, these aren't meant for any kind of quick development or early iterations: those who intend to work on realms still in development should deploy them on machines using I'll be closing this issue as it contains some discussions which can be useful for context; but ultimately of relative importance when we'll finally have a proposal for versioning which considers the real-life use cases and scenarios faced by users on the live gno.land chain. |
Context
When a smart contract goes live, it may need to be updated or changed for various reasons.
To enable smart contract upgradability, it is important to address challenges such as data loss, immutability, security, interoperability, data migration, and governance.
Gno smart contracts use human-readable package URLs, and packages have a hash address derived from their URL. The expected URL structure is
$zone.land/{p,r}/$user/$dir[/$subdir]
ongno.land
and potentially other instances.Current proposal and open questions
The solution will be a blend of core features and recommended patterns, as shown in #125.
For version management, a Git-like approach can be used, where each version has a content-deterministic hash and a named version. The hash is immutable, while the version can be changed, much like a Git tag or Unix symlink.
Publishing a package to
gno.land/r/manfred/pkg
creates an immutable package atgno.land/r/manfred/pkg@hash
, with a symlink for access without a version suffix. Previous versions are only accessible viagno.land/r/manfred/pkg@previous-hash
. Alternatively, a counter per path can be used, allowing access via/v1
,/v2
, and so on.While the webUI allows access to various URLs, there is a question of how to update existing contracts that depend on prior versions of a dependency. Possible solutions include updating the import paths via
gno.mod
or the source, using aBumpDep
VM call, or patching.gno
files.Regarding data state, the question is whether to share it between contract versions based on variable name and type, or keep it independent and require developers to write patterns.
Additional challenges exist around UX (#688), transparency, and governance. A potential solution is to implement a security layering system similar to kernel security rings.
In our efforts to support upgradeability, we could consider incorporating a built-in feature to pause or deprecate previous versions. Additionally, we could investigate the feasibility of automatically generating and sharing release notes while browsing packages.
One last consideration is the possibility of adding an
UpdatePkg
VM call to allow the addition of new files to existing packages.Potentially related with package metadata (#498).
Related with upgrading policy for stdlibs (#239).
Related with security UX (#688).
Related with upgrade pattern examples (#125).
The text was updated successfully, but these errors were encountered: