Search Apps Documentation Source Content File Folder Download Copy Actions Download

version_manager.gno

10.43 Kb · 255 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"
 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.
 24var ErrSpoofedRealm = errors.New("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//   - rlm: the live realm token threaded in by the caller; rlm.Previous() identifies the
 69//     version package that is registering. The leading `_ int` is a v2 sentinel that surfaces
 70//     as a `0` at every call site so the realm-threading is visible to the reader.
 71//   - initializer: A function that receives a storage interface and returns an implementation instance
 72//
 73// Returns:
 74//   - error: If realm token is spoofed, caller is unauthorized, already registered, or initializer is nil
 75//
 76// Security: Only packages under the domainPath prefix can register (enforced by isContainDomainPath).
 77func (vm *versionManager) RegisterInitializer(_ int, rlm realm, initializer func(_ int, rlm realm, store any) any) error {
 78	if !rlm.IsCurrent() {
 79		return ErrSpoofedRealm
 80	}
 81
 82	// Validate initializer is not nil to prevent panic during initialization
 83	if initializer == nil {
 84		return errors.New("version_manager: initializer cannot be nil")
 85	}
 86
 87	// Ensure the caller is within the domain path (e.g., protocol_fee/v1, protocol_fee/v2).
 88	// rlm.Previous() corresponds to v1's runtime.PreviousRealm().
 89	previousRealm := rlm.Previous()
 90	if previousRealm.IsUser() {
 91		return errors.New("version_manager: caller cannot be user")
 92	}
 93
 94	targetPackagePath := previousRealm.PkgPath()
 95	if !vm.isContainDomainPath(targetPackagePath) {
 96		return errors.New("version_manager: caller is not in the domain path")
 97	}
 98
 99	// Check if this package path has already been registered
100	if _, ok := vm.initializers[targetPackagePath]; ok {
101		return errors.New("version_manager: initializer already registered")
102	}
103
104	// Register the initializer function for this package path
105	vm.initializers[targetPackagePath] = initializer
106
107	chain.Emit(
108		"RegisterInitializer",
109		"domainPath", vm.domainPath,
110		"registeredPackagePath", targetPackagePath,
111	)
112
113	// Initialize the current implementation if it hasn't been done yet
114	if vm.currentPackagePath == "" || vm.currentImplementation == nil {
115		vm.currentPackagePath = targetPackagePath
116		vm.currentImplementation = initializer(0, rlm, vm.initializeDomainStoreFn(0, rlm, vm.domainKVStore))
117
118		chain.Emit(
119			"InitializeImplementation",
120			"domainPath", vm.domainPath,
121			"newPackagePath", targetPackagePath,
122		)
123	}
124
125	return nil
126}
127
128// ChangeImplementation performs a hot-swap to a different version implementation.
129// This enables zero-downtime upgrades by switching the active implementation at runtime.
130//
131// The switching process:
132//  1. Validates the realm token is the live crossing frame (rejects spoofed tokens)
133//  2. Validates the target version has been registered via RegisterInitializer
134//  3. Retrieves and executes the target version's initializer
135//
136// Authorization is the caller realm's responsibility — version_manager only rejects
137// spoofed realm tokens. Upgrade ACLs (admin / governance) live in the wrapping /r/ realm
138// (see each module's upgrade.gno).
139//
140// Parameters:
141//   - rlm: the live realm token threaded in by the caller. The leading `_ int` is a v2
142//     sentinel that surfaces as a `0` at every call site so the realm-threading is visible.
143//   - packagePath: The full package path of the target version (e.g., "gno.land/r/gnoswap/protocol_fee/v2")
144//
145// Returns:
146//   - error: If realm token is spoofed, target version is not registered, or initializer is invalid
147func (vm *versionManager) ChangeImplementation(_ int, rlm realm, packagePath string) error {
148	if !rlm.IsCurrent() {
149		return ErrSpoofedRealm
150	}
151
152	// Retrieve the registered initializer function
153	initializer, ok := vm.initializers[packagePath]
154	if !ok {
155		return errors.New("version_manager: initializer not found for package path:" + packagePath)
156	}
157
158	if initializer == nil {
159		return errors.New("version_manager: initializer is not a function")
160	}
161
162	prevPackagePath := vm.currentPackagePath
163	vm.currentPackagePath = packagePath
164	vm.currentImplementation = initializer(0, rlm, vm.initializeDomainStoreFn(0, rlm, vm.domainKVStore))
165
166	chain.Emit(
167		"ChangeImplementation",
168		"domainPath", vm.domainPath,
169		"previousPackagePath", prevPackagePath,
170		"newPackagePath", packagePath,
171	)
172
173	return nil
174}
175
176// GetDomainPath returns the base domain path for this version manager.
177// Example: "gno.land/r/gnoswap/protocol_fee"
178func (vm *versionManager) GetDomainPath() string {
179	return vm.domainPath
180}
181
182// GetInitializers returns the map containing all registered initializer functions.
183// Keys are package paths, values are initializer functions.
184// Useful for inspecting which versions are available.
185func (vm *versionManager) GetInitializers() map[string]func(_ int, rlm realm, store any) any {
186	return vm.initializers
187}
188
189// GetCurrentPackagePath returns the package path of the currently active implementation.
190func (vm *versionManager) GetCurrentPackagePath() string {
191	return vm.currentPackagePath
192}
193
194// GetCurrentImplementation returns the instance of the currently active version.
195// The returned value should be type-asserted to the domain-specific interface.
196func (vm *versionManager) GetCurrentImplementation() any {
197	return vm.currentImplementation
198}
199
200// isContainDomainPath checks if the calling contract is within the authorized domain path.
201// This is a critical security check that prevents unauthorized external contracts from
202// registering implementations.
203//
204// Validation rules:
205//   - Package path must start with domainPath + "/"
206//
207// Example:
208//   - domainPath: "gno.land/r/gnoswap/protocol_fee"
209//   - Valid callers: "gno.land/r/gnoswap/protocol_fee/v1", "gno.land/r/gnoswap/protocol_fee/v2"
210//   - Invalid callers: "gno.land/r/gnoswap/other", "gno.land/r/attacker/malicious"
211func (vm *versionManager) isContainDomainPath(targetPackagePath string) bool {
212	// `domainPath` is set via the current realm's PkgPath in each contract.
213	// Therefore, there is no need for a separate trailing slash check,
214	// and the prefix is determined by directly appending `/` for version detection.
215	prefix := vm.domainPath + "/"
216
217	return strings.HasPrefix(targetPackagePath, prefix)
218}
219
220// NewVersionManager creates a new version manager instance for a specific domain.
221// This should be called once per domain during system initialization.
222//
223// Parameters:
224//
225//   - domainPath: The base package path for the domain (e.g., "gno.land/r/gnoswap/protocol_fee")
226//     Used for access control to ensure only authorized packages can register
227//
228//   - kvStore: The shared key-value store that all versions will access
229//     The domain realm (proxy) is the owner and has write permission to this store
230//
231//   - initializeDomainStoreFn: A factory function that wraps the KVStore into a domain-specific storage interface
232//     This abstraction allows each version to work with a familiar storage API
233//     Example: func(_ int, rlm realm, kvStore store.KVStore) any { return NewProtocolFeeStore(kvStore) }
234//
235// Returns:
236//   - VersionManager: An initialized version manager ready to accept implementation registrations
237//
238// Usage Pattern:
239//  1. Create version manager in parent domain package
240//  2. Each version (v1, v2, v3) calls RegisterInitializer during their init()
241//  3. Use ChangeImplementation to switch between versions at runtime
242func NewVersionManager(
243	domainPath string,
244	kvStore store.KVStore,
245	initializeDomainStoreFn func(_ int, rlm realm, kvStore store.KVStore) any,
246) VersionManager {
247	return &versionManager{
248		domainPath:              domainPath,
249		domainKVStore:           kvStore,
250		initializeDomainStoreFn: initializeDomainStoreFn,
251		initializers:            make(map[string]func(_ int, rlm realm, store any) any),
252		currentPackagePath:      "",
253		currentImplementation:   nil,
254	}
255}