Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}