Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}