version_manager.gno
11.11 Kb · 266 lines
1// Package version_manager implements a runtime version management system using the Strategy Pattern.
2// It enables dynamic switching between different implementation versions of the same domain (e.g., v1, v2, v3)
3// while maintaining a unified storage layer. This approach allows for seamless upgrades without migration overhead.
4//
5// Key Features:
6// - Dynamic implementation registration and switching
7// - Domain-scoped security (only authorized packages can register)
8// - Zero-downtime upgrades through hot-swapping
9//
10// Architecture Pattern: Strategy + Plugin Architecture
11package version_manager
12
13import (
14 "chain"
15 "errors"
16 "strings"
17
18 "gno.land/p/gnoswap/store/v1"
19)
20
21// ErrSpoofedRealm is returned when the supplied realm token does not match the
22// live crossing frame (rlm.IsCurrent() == false). It signals a stale or
23// spoofed token captured in an earlier frame.
24const ErrSpoofedRealm = "rlm does not match the current crossing frame"
25
26// versionManager is the concrete implementation of VersionManager interface.
27// It manages multiple versioned implementations of a domain (e.g., protocol_fee/v1, protocol_fee/v2).
28//
29// Storage Access Model:
30// Implementation realms do NOT receive direct storage permissions. Instead, when calls flow
31// from the domain proxy to the implementation, the proxy realm (which has write permission
32// to the KVStore) is the one that drives the storage. This design prevents external callers
33// from directly invoking implementation realms to modify storage.
34type versionManager struct {
35 // initializers stores registered initializer functions keyed by package path
36 // Each initializer bootstraps a specific version's implementation
37 initializers map[string]func(_ int, rlm realm, store any) any
38
39 // domainKVStore is the shared storage layer accessible by all versions
40 // The domain (proxy) realm is the owner and has write permission
41 domainKVStore store.KVStore
42
43 // initializeDomainStoreFn wraps the KVStore into domain-specific storage interface
44 // This abstraction decouples the version manager from domain-specific storage implementations
45 initializeDomainStoreFn func(_ int, rlm realm, kvStore store.KVStore) any
46
47 // domainPath defines the base path for this domain (e.g., "gno.land/r/gnoswap/protocol_fee")
48 // Used for security validation to ensure only authorized packages can register
49 domainPath string
50
51 // currentPackagePath holds the package path of the active implementation
52 // (e.g., "gno.land/r/gnoswap/protocol_fee/v2")
53 currentPackagePath string
54
55 // currentImplementation is the active version's instance
56 currentImplementation any
57}
58
59// RegisterInitializer registers a new version implementation for the domain.
60// This method must be called by each version package (e.g., v1, v2) during initialization.
61//
62// The registration process:
63// 1. Validates the realm token is the live crossing frame (rejects spoofed tokens)
64// 2. Validates the caller is within the authorized domain path
65// 3. Stores the initializer function for later version switching
66//
67// Parameters:
68// - _: Interrealm-call discriminator; callers pass 0.
69// - rlm: Propagated current realm context from the domain wrapper; this implementation validates rlm.IsCurrent() and inspects rlm.Previous() to identify and authorize the registering version package.
70// - initializer: Callback receiving the discriminator, current realm context, and domain-specific storage wrapper, and returning the version implementation instance.
71//
72// Returns:
73// - error: nil when the initializer is registered (and the first one is activated); otherwise an error for a spoofed realm, nil initializer, user caller, caller outside the domain path, or duplicate package path.
74//
75// Security: Only packages under the domainPath prefix can register (enforced by isContainDomainPath).
76func (vm *versionManager) RegisterInitializer(_ int, rlm realm, initializer func(_ int, rlm realm, store any) any) error {
77 if !rlm.IsCurrent() {
78 return errors.New(ErrSpoofedRealm)
79 }
80
81 // Validate initializer is not nil to prevent panic during initialization
82 if initializer == nil {
83 return errors.New("version_manager: initializer cannot be nil")
84 }
85
86 // Ensure the caller is within the domain path (e.g., protocol_fee/v1, protocol_fee/v2).
87 // rlm.Previous() corresponds to v1's runtime.PreviousRealm().
88 previousRealm := rlm.Previous()
89 if previousRealm.IsUser() {
90 return errors.New("version_manager: caller cannot be user")
91 }
92
93 targetPackagePath := previousRealm.PkgPath()
94 if !vm.isContainDomainPath(targetPackagePath) {
95 return errors.New("version_manager: caller is not in the domain path")
96 }
97
98 // Check if this package path has already been registered
99 if _, ok := vm.initializers[targetPackagePath]; ok {
100 return errors.New("version_manager: initializer already registered")
101 }
102
103 // Register the initializer function for this package path
104 vm.initializers[targetPackagePath] = initializer
105
106 chain.Emit(
107 "RegisterInitializer",
108 "domainPath", vm.domainPath,
109 "registeredPackagePath", targetPackagePath,
110 )
111
112 // Initialize the current implementation if it hasn't been done yet
113 if vm.currentPackagePath == "" || vm.currentImplementation == nil {
114 vm.currentPackagePath = targetPackagePath
115 vm.currentImplementation = initializer(0, rlm, vm.initializeDomainStoreFn(0, rlm, vm.domainKVStore))
116
117 chain.Emit(
118 "InitializeImplementation",
119 "domainPath", vm.domainPath,
120 "newPackagePath", targetPackagePath,
121 )
122 }
123
124 return nil
125}
126
127// ChangeImplementation performs a hot-swap to a different version implementation.
128// This enables zero-downtime upgrades by switching the active implementation at runtime.
129//
130// The switching process:
131// 1. Validates the realm token is the live crossing frame (rejects spoofed tokens)
132// 2. Validates the target version has been registered via RegisterInitializer
133// 3. Retrieves and executes the target version's initializer
134//
135// Authorization is the caller realm's responsibility — version_manager only rejects
136// spoofed realm tokens. Upgrade ACLs (admin / governance) live in the wrapping /r/ realm
137// (see each module's upgrade.gno).
138//
139// Parameters:
140// - _: Interrealm-call discriminator; callers pass 0.
141// - rlm: Propagated current realm context from the domain wrapper; this implementation validates rlm.IsCurrent() before switching.
142// - packagePath: Full package path of the target version; it must be a key in the registered initializer map.
143//
144// Returns:
145// - error: nil when packagePath becomes active; otherwise ErrSpoofedRealm, an unknown-package error, or an invalid-initializer error.
146func (vm *versionManager) ChangeImplementation(_ int, rlm realm, packagePath string) error {
147 if !rlm.IsCurrent() {
148 return errors.New(ErrSpoofedRealm)
149 }
150
151 // Retrieve the registered initializer function
152 initializer, ok := vm.initializers[packagePath]
153 if !ok {
154 return errors.New("version_manager: initializer not found for package path:" + packagePath)
155 }
156
157 if initializer == nil {
158 return errors.New("version_manager: initializer is not a function")
159 }
160
161 prevPackagePath := vm.currentPackagePath
162 vm.currentPackagePath = packagePath
163 vm.currentImplementation = initializer(0, rlm, vm.initializeDomainStoreFn(0, rlm, vm.domainKVStore))
164
165 chain.Emit(
166 "ChangeImplementation",
167 "domainPath", vm.domainPath,
168 "previousPackagePath", prevPackagePath,
169 "newPackagePath", packagePath,
170 )
171
172 return nil
173}
174
175// GetDomainPath returns the base domain path for this version manager.
176// Example: "gno.land/r/gnoswap/protocol_fee"
177//
178// Returns:
179// - string: Base package path used to scope registered version implementations.
180func (vm *versionManager) GetDomainPath() string {
181 return vm.domainPath
182}
183
184// GetInitializers returns the map containing all registered initializer functions.
185// Keys are package paths, values are initializer functions.
186// Useful for inspecting which versions are available.
187//
188// Returns:
189// - map[string]func(_ int, rlm realm, store any) any: Current registry mapping version package paths to initializer callbacks.
190func (vm *versionManager) GetInitializers() map[string]func(_ int, rlm realm, store any) any {
191 return vm.initializers
192}
193
194// GetCurrentPackagePath returns the package path of the currently active implementation.
195//
196// Returns:
197// - string: Active implementation's package path, or the empty string before registration.
198func (vm *versionManager) GetCurrentPackagePath() string {
199 return vm.currentPackagePath
200}
201
202// GetCurrentImplementation returns the instance of the currently active version.
203// The returned value should be type-asserted to the domain-specific interface.
204//
205// Returns:
206// - any: Active version implementation instance, or nil before the first initializer is registered.
207func (vm *versionManager) GetCurrentImplementation() any {
208 return vm.currentImplementation
209}
210
211// isContainDomainPath checks if the calling contract is within the authorized domain path.
212// This is a critical security check that prevents unauthorized external contracts from
213// registering implementations.
214//
215// Validation rules:
216// - Package path must start with domainPath + "/"
217//
218// Example:
219// - domainPath: "gno.land/r/gnoswap/protocol_fee"
220// - Valid callers: "gno.land/r/gnoswap/protocol_fee/v1", "gno.land/r/gnoswap/protocol_fee/v2"
221// - Invalid callers: "gno.land/r/gnoswap/other", "gno.land/r/attacker/malicious"
222func (vm *versionManager) isContainDomainPath(targetPackagePath string) bool {
223 // `domainPath` is set via the current realm's PkgPath in each contract.
224 // Therefore, there is no need for a separate trailing slash check,
225 // and the prefix is determined by directly appending `/` for version detection.
226 prefix := vm.domainPath + "/"
227
228 return strings.HasPrefix(targetPackagePath, prefix)
229}
230
231// NewVersionManager creates a new version manager instance for a specific domain.
232// This should be called once per domain during system initialization.
233//
234// Parameters:
235//
236// - domainPath: The base package path for the domain (e.g., "gno.land/r/gnoswap/protocol_fee")
237// Used for access control to ensure only authorized packages can register
238//
239// - kvStore: The shared key-value store that all versions will access
240// The domain realm (proxy) is the owner and has write permission to this store
241//
242// - initializeDomainStoreFn: A factory function that wraps the KVStore into a domain-specific storage interface
243// This abstraction allows each version to work with a familiar storage API
244// Example: func(_ int, rlm realm, kvStore store.KVStore) any { return NewProtocolFeeStore(kvStore) }
245//
246// Returns:
247// - VersionManager: An initialized version manager ready to accept implementation registrations
248//
249// Usage Pattern:
250// 1. Create version manager in parent domain package
251// 2. Each version (v1, v2, v3) calls RegisterInitializer during their init()
252// 3. Use ChangeImplementation to switch between versions at runtime
253func NewVersionManager(
254 domainPath string,
255 kvStore store.KVStore,
256 initializeDomainStoreFn func(_ int, rlm realm, kvStore store.KVStore) any,
257) VersionManager {
258 return &versionManager{
259 domainPath: domainPath,
260 domainKVStore: kvStore,
261 initializeDomainStoreFn: initializeDomainStoreFn,
262 initializers: make(map[string]func(_ int, rlm realm, store any) any),
263 currentPackagePath: "",
264 currentImplementation: nil,
265 }
266}