version_manager.gno
9.79 Kb ยท 257 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 "chain/runtime"
16 "errors"
17 "strings"
18
19 "gno.land/p/gnoswap/store"
20 "gno.land/p/nt/avl"
21 "gno.land/p/nt/avl/rotree"
22 "gno.land/p/nt/ufmt"
23)
24
25// versionManager is the concrete implementation of VersionManager interface.
26// It manages multiple versioned implementations of a domain (e.g., protocol_fee/v1, protocol_fee/v2).
27//
28// Storage Access Model:
29// Implementation realms do NOT receive direct storage permissions. Instead, when calls flow
30// from the domain proxy to the implementation, runtime.CurrentRealm() remains the domain
31// (proxy) realm, which already has write permission to the KVStore. This design prevents
32// external callers from directly invoking implementation realms to modify storage.
33type versionManager struct {
34 // initializers stores registered initializer functions keyed by package path
35 // Each initializer bootstraps a specific version's implementation
36 initializers *avl.Tree
37
38 // domainKVStore is the shared storage layer accessible by all versions
39 // The domain (proxy) realm is the owner and has write permission
40 domainKVStore store.KVStore
41
42 // initializeDomainStoreFn wraps the KVStore into domain-specific storage interface
43 // This abstraction decouples the version manager from domain-specific storage implementations
44 initializeDomainStoreFn func(kvStore store.KVStore) any
45
46 // domainPath defines the base path for this domain (e.g., "gno.land/r/gnoswap/protocol_fee")
47 // Used for security validation to ensure only authorized packages can register
48 domainPath string
49
50 // currentPackagePath holds the package path of the active implementation
51 // (e.g., "gno.land/r/gnoswap/protocol_fee/v2")
52 currentPackagePath string
53
54 // currentImplementation is the active version's instance
55 currentImplementation any
56}
57
58// RegisterInitializer registers a new version implementation for the domain.
59// This method must be called by each version package (e.g., v1, v2) during initialization.
60//
61// The registration process:
62// 1. Validates the caller is within the authorized domain path
63// 2. Stores the initializer function for later version switching
64//
65// Parameters:
66// - initializer: A function that receives a storage interface and returns an implementation instance
67//
68// Returns:
69// - error: If caller is unauthorized, already registered, or permission setup fails
70//
71// Security: Only packages under the domainPath prefix can register (enforced by isContainDomainPath)
72func (vm *versionManager) RegisterInitializer(initializer func(store any) any) error {
73 // Validate initializer is not nil to prevent panic during initialization
74 if initializer == nil {
75 return errors.New("version_manager: initializer cannot be nil")
76 }
77
78 // Ensure the caller is within the domain path (e.g., protocol_fee/v1, protocol_fee/v2)
79 previousRealm := runtime.PreviousRealm()
80 if previousRealm.IsUser() {
81 return errors.New("version_manager: caller cannot be user")
82 }
83
84 targetPackagePath := previousRealm.PkgPath()
85 if !vm.isContainDomainPath(targetPackagePath) {
86 return errors.New("version_manager: caller is not in the domain path")
87 }
88
89 // Check if this package path has already been registered
90 if vm.initializers.Has(targetPackagePath) {
91 return errors.New("version_manager: initializer already registered")
92 }
93
94 // Register the initializer function for this package path
95 vm.initializers.Set(targetPackagePath, initializer)
96
97 chain.Emit(
98 "RegisterInitializer",
99 "domainPath", vm.domainPath,
100 "registeredPackagePath", targetPackagePath,
101 )
102
103 // Initialize the current implementation if it hasn't been done yet
104 if vm.currentPackagePath == "" || vm.currentImplementation == nil {
105 vm.currentPackagePath = targetPackagePath
106 vm.currentImplementation = initializer(vm.initializeDomainStoreFn(vm.domainKVStore))
107
108 chain.Emit(
109 "InitializeImplementation",
110 "domainPath", vm.domainPath,
111 "newPackagePath", targetPackagePath,
112 )
113 }
114
115 return nil
116}
117
118// ChangeImplementation performs a hot-swap to a different version implementation.
119// This enables zero-downtime upgrades by switching the active implementation at runtime.
120//
121// The switching process:
122// 1. Validates the target version has been registered via RegisterInitializer
123// 2. Retrieves and executes the target version's initializer
124//
125// Parameters:
126// - packagePath: The full package path of the target version (e.g., "gno.land/r/gnoswap/protocol_fee/v2")
127//
128// Returns:
129// - error: If target version is not registered or initializer is invalid
130func (vm *versionManager) ChangeImplementation(packagePath string) error {
131 // Verify the target implementation has been registered
132 if !vm.initializers.Has(packagePath) {
133 return errors.New("version_manager: initializer not found for package path:" + packagePath)
134 }
135
136 // Retrieve the registered initializer function
137 result, ok := vm.initializers.Get(packagePath)
138 if !ok {
139 return errors.New("version_manager: initializer not found for package path:" + packagePath)
140 }
141
142 // Type-assert to ensure it's the expected function signature
143 initializer, ok := result.(func(store any) any)
144 if !ok {
145 return errors.New("version_manager: initializer is not a function")
146 }
147
148 prevPackagePath := vm.currentPackagePath
149 vm.currentPackagePath = packagePath
150 vm.currentImplementation = initializer(vm.initializeDomainStoreFn(vm.domainKVStore))
151
152 chain.Emit(
153 "ChangeImplementation",
154 "domainPath", vm.domainPath,
155 "previousPackagePath", prevPackagePath,
156 "newPackagePath", packagePath,
157 )
158
159 return nil
160}
161
162// GetDomainPath returns the base domain path for this version manager.
163// Example: "gno.land/r/gnoswap/protocol_fee"
164func (vm *versionManager) GetDomainPath() string {
165 return vm.domainPath
166}
167
168// GetInitializers returns the AVL tree containing all registered initializer functions.
169// Keys are package paths, values are initializer functions.
170// Useful for inspecting which versions are available.
171func (vm *versionManager) GetInitializers() *rotree.ReadOnlyTree {
172 return rotree.Wrap(vm.initializers, makeInitializerSafe)
173}
174
175// GetCurrentPackagePath returns the package path of the currently active implementation.
176func (vm *versionManager) GetCurrentPackagePath() string {
177 return vm.currentPackagePath
178}
179
180// GetCurrentImplementation returns the instance of the currently active version.
181// The returned value should be type-asserted to the domain-specific interface.
182func (vm *versionManager) GetCurrentImplementation() any {
183 return vm.currentImplementation
184}
185
186// isContainDomainPath checks if the calling contract is within the authorized domain path.
187// This is a critical security check that prevents unauthorized external contracts from
188// registering implementations.
189//
190// Validation rules:
191// - Package path must start with domainPath + "/"
192//
193// Example:
194// - domainPath: "gno.land/r/gnoswap/protocol_fee"
195// - Valid callers: "gno.land/r/gnoswap/protocol_fee/v1", "gno.land/r/gnoswap/protocol_fee/v2"
196// - Invalid callers: "gno.land/r/gnoswap/other", "gno.land/r/attacker/malicious"
197func (vm *versionManager) isContainDomainPath(targetPackagePath string) bool {
198 // `domainPath` is set via `runtime.CurrentRealm().PkgPath()` in each contract.
199 // Therefore, there is no need for a separate trailing slash check,
200 // and the prefix is determined by directly appending `/` for version detection.
201 prefix := vm.domainPath + "/"
202
203 return strings.HasPrefix(targetPackagePath, prefix)
204}
205
206// NewVersionManager creates a new version manager instance for a specific domain.
207// This should be called once per domain during system initialization.
208//
209// Parameters:
210//
211// - domainPath: The base package path for the domain (e.g., "gno.land/r/gnoswap/protocol_fee")
212// Used for access control to ensure only authorized packages can register
213//
214// - kvStore: The shared key-value store that all versions will access
215// The domain realm (proxy) is the owner and has write permission to this store
216//
217// - initializeDomainStoreFn: A factory function that wraps the KVStore into a domain-specific storage interface
218// This abstraction allows each version to work with a familiar storage API
219// Example: func(kvStore store.KVStore) any { return NewProtocolFeeStore(kvStore) }
220//
221// Returns:
222// - VersionManager: An initialized version manager ready to accept implementation registrations
223//
224// Usage Pattern:
225// 1. Create version manager in parent domain package
226// 2. Each version (v1, v2, v3) calls RegisterInitializer during their init()
227// 3. Use ChangeImplementation to switch between versions at runtime
228func NewVersionManager(
229 domainPath string,
230 kvStore store.KVStore,
231 initializeDomainStoreFn func(kvStore store.KVStore) any,
232) VersionManager {
233 return &versionManager{
234 domainPath: domainPath,
235 domainKVStore: kvStore,
236 initializeDomainStoreFn: initializeDomainStoreFn,
237 initializers: avl.NewTree(),
238 currentPackagePath: "",
239 currentImplementation: nil,
240 }
241}
242
243// makeInitializerSafe creates a safe copy of an initializer function for read-only tree access.
244// This is used by GetInitializers to wrap the internal AVL tree in a read-only view,
245// preventing external modification of registered initializers.
246func makeInitializerSafe(data any) any {
247 fn, ok := data.(func(store any) any)
248 if !ok {
249 panic(ufmt.Sprintf("expected func(store any) any, got %T", data))
250 }
251
252 cpy := func(store any) any {
253 return fn(store)
254 }
255
256 return cpy
257}