Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions gno.land/adr/pr5885_phase2_inert_packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ADR: Phase 2 — Inert Package Storage with Oracle Activation

Companion to [PR #5885](https://github.com/gnolang/gno/pull/5885) (Phase 1: permissioned
code submission policy).

## Context

Phase 1 adds a `permissioned` policy that restricts `MsgAddPackage` / `MsgRun` to an
allowlist. Phase 2 enables permissionless submission while keeping the Go typechecker
off the critical path: packages are stored in an **inert** state (no typechecking, no
execution) and activated later by a trusted approver — possibly an off-chain oracle.

## Decision

### New policy value

`code_submission_policy = "inert"`: any address may submit a package, but it is stored
in a separate key space (`inert_pkg:<path>` in iavlStore) that is invisible to the
normal package resolver. The package is not typechecked or executed at submission time.

### New param

| Param | Type | Default | Description |
|---|---|---|---|
| `pkg_approvers` | []address | `[]` | Who may call `MsgEnablePackage` / `MsgDisablePackage` |

### New messages

**`MsgEnablePackage { Approver, PkgPath }`**
Approver must be in `pkg_approvers`. Chain retrieves the inert package, runs the
typechecker (oracle is untrusted for correctness), executes initialization, and moves
the package to the active store. After this point the package is importable.

**`MsgDisablePackage { Approver, PkgPath }`**
Interface stub. Full disable requires evicting executed objects from the base store;
not yet implemented. Returns an error until a follow-up PR delivers it.

### Store layer

`gnovm.Store` gains three new methods:
- `AddInertPackage(mpkg)` — store at `inert_pkg:<path>` in iavlStore
- `GetInertPackage(path)` — read from `inert_pkg:<path>`
- `DelInertPackage(path)` — remove from `inert_pkg:<path>`

These keys are disjoint from `pkg:<path>` so normal `GetPackage` / `GetMemPackage`
never see inert packages.

## Consequences

- **Permissionless submission, deferred typechecking**: the DoS surface from the
typechecker is removed from block execution time.
- **On-chain correctness guarantee**: the chain re-runs the typechecker at
`MsgEnablePackage`; the oracle cannot activate a package that fails typecheck.
- **Default unaffected**: `code_submission_policy` still defaults to
`"permissionless"`. Chains that don't opt in see no change.
- **Disable deferred**: MsgDisablePackage is stubbed; implementation requires a
strategy for cleaning up executed objects from the base store.
2 changes: 1 addition & 1 deletion gno.land/pkg/sdk/vm/apphash_crossrealm38_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import (
// their *_test.gno source bytes), which shifts the iavlStore Merkle root. This
// is the only consensus-relevant change in that PR; verified by bisection that
// no other change in the PR moves this hash. The shift is therefore expected.
const expectedCrossrealm38Hash = "28f55f0ad9842bc3c4d8984f1a63a709203a1e99fae7e816786825e26629f618"
const expectedCrossrealm38Hash = "133a70aa761f36a99f57f68ed1249ff56cbc42b77fa654534a922dd7993db887"

func TestAppHashCrossrealm38(t *testing.T) {
env := setupTestEnv()
Expand Down
22 changes: 22 additions & 0 deletions gno.land/pkg/sdk/vm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func (vh vmHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result {
return vh.handleMsgCall(ctx, msg)
case MsgRun:
return vh.handleMsgRun(ctx, msg)
case MsgEnablePackage:
return vh.handleMsgEnablePackage(ctx, msg)
case MsgDisablePackage:
return vh.handleMsgDisablePackage(ctx, msg)
default:
errMsg := fmt.Sprintf("unrecognized vm message type: %T", msg)
return abciResult(std.ErrUnknownRequest(errMsg))
Expand Down Expand Up @@ -66,6 +70,24 @@ func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) {
return
}

// Handle MsgEnablePackage.
func (vh vmHandler) handleMsgEnablePackage(ctx sdk.Context, msg MsgEnablePackage) sdk.Result {
err := vh.vm.EnablePackage(ctx, msg)
if err != nil {
return abciResult(err)
}
return sdk.Result{}
}

// Handle MsgDisablePackage.
func (vh vmHandler) handleMsgDisablePackage(ctx sdk.Context, msg MsgDisablePackage) sdk.Result {
err := vh.vm.DisablePackage(ctx, msg)
if err != nil {
return abciResult(err)
}
return sdk.Result{}
}

// ----------------------------------------
// Query

Expand Down
109 changes: 109 additions & 0 deletions gno.land/pkg/sdk/vm/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,31 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
if _, ok := gno.IsGnoRunPath(pkgPath); ok {
return ErrInvalidPkgPath("reserved package name: " + pkgPath)
}
// If the chain is operating in "inert" submission mode, store the package
// without typechecking or execution. It becomes callable only after an
// approver sends MsgEnablePackage.
if vm.GetParams(ctx).CodeSubmissionPolicy == CodeSubmissionPolicyInert {
gm, err := gnomod.ParseMemPackage(memPkg)
if err != nil {
return ErrInvalidPackage(err.Error())
}
gm.Module = pkgPath
gm.AddPkg.Creator = creator.String()
gm.AddPkg.Height = int(ctx.BlockHeight())
memPkg.SetFile("gnomod.toml", gm.WriteString())
if err := vm.checkNamespacePermission(ctx, creator, pkgPath); err != nil {
return err
}
if err := vm.checkCLASignature(ctx, creator); err != nil {
return err
}
if err := vm.bank.SendCoins(ctx, creator, gno.DerivePkgCryptoAddr(pkgPath), send); err != nil {
return err
}
gnostore.AddInertPackage(memPkg)
return nil
}

opts := gno.TypeCheckOptions{
Getter: gnostore,
TestGetter: vm.testStdlibCache.memPackageGetter(gnostore),
Expand Down Expand Up @@ -772,6 +797,90 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
return nil
}

// EnablePackage activates an inert package: runs the typechecker and
// initializes the package so it becomes importable and callable on-chain.
// Only addresses listed in Params.PkgApprovers may call this.
func (vm *VMKeeper) EnablePackage(ctx sdk.Context, msg MsgEnablePackage) (err error) {
params := vm.GetParams(ctx)
if !isApprover(params.PkgApprovers, msg.Approver) {
return std.ErrUnauthorized(fmt.Sprintf(
"address %s is not a pkg approver", msg.Approver))
}
gnostore := vm.getGnoTransactionStore(ctx)
memPkg := gnostore.GetInertPackage(msg.PkgPath)
if memPkg == nil {
return ErrInvalidPkgPath("no inert package at path: " + msg.PkgPath)
}
// Typecheck the stored package.
opts := gno.TypeCheckOptions{
Getter: gnostore,
TestGetter: vm.testStdlibCache.memPackageGetter(gnostore),
Mode: gno.TCLatestStrict,
Cache: vm.getTypeCheckCache(ctx),
}
if _, err = gno.TypeCheckMemPackage(memPkg, opts); err != nil {
return ErrTypeCheck(err)
}
// Execute and persist the package.
ctx = ContextWithParamsAccum(ctx)
msgCtx := stdlibs.ExecContext{
ChainID: ctx.ChainID(),
ChainDomain: vm.getChainDomainParam(ctx),
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
OriginCaller: msg.Approver.Bech32(),
OriginSend: std.Coins{},
OriginSendSpent: new(std.Coins),
Banker: NewSDKBanker(vm, ctx),
Params: NewSDKParams(vm.prmk, ctx),
EventLogger: ctx.EventLogger(),
SessionAccount: getSessionAccount(ctx, msg.Approver),
}
m2 := gno.NewMachineWithOptions(gno.MachineOptions{
PkgPath: "",
Output: vm.Output,
Store: gnostore,
Alloc: gnostore.GetAllocator(),
Context: msgCtx,
GasMeter: ctx.GasMeter(),
BoundedPanicRender: true,
})
defer m2.Release()
defer doRecover(m2, &err)
preAlloc := gno.NewAllocator(maxAllocTx)
preAlloc.SetGasMeter(ctx.GasMeter())
gnostore.SetPreprocessAllocator(preAlloc)
defer gnostore.SetPreprocessAllocator(nil)
m2.RunMemPackage(memPkg, true)
// Remove from inert store now that it is active.
gnostore.DelInertPackage(msg.PkgPath)
return nil
}

// isApprover reports whether addr is in the approvers list.
func isApprover(approvers []crypto.Address, addr crypto.Address) bool {
for _, a := range approvers {
if a == addr {
return true
}
}
return false
}

// DisablePackage moves an active package back to inert state.
// NOTE: full disable requires evicting executed objects from the base store,
// which is not yet implemented. This stub is provided for interface completeness.
func (vm *VMKeeper) DisablePackage(ctx sdk.Context, msg MsgDisablePackage) error {
params := vm.GetParams(ctx)
if !isApprover(params.PkgApprovers, msg.Approver) {
return std.ErrUnauthorized(fmt.Sprintf(
"address %s is not a pkg approver", msg.Approver))
}
// TODO: evict executed package objects from baseStore and move source back
// to inert_pkg key. Tracked in a follow-up PR.
return std.ErrUnknownRequest("disable_package is not yet implemented")
}

// Call calls a public Gno function (for delivertx).
func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
// Session spend on msg.Send is enforced inside bank.Keeper.SendCoins
Expand Down
78 changes: 78 additions & 0 deletions gno.land/pkg/sdk/vm/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,81 @@ func (msg MsgRun) SpendForSigner(signer crypto.Address) std.Coins {
}
return msg.Send
}

//----------------------------------------
// MsgEnablePackage

// MsgEnablePackage activates an inert package: runs typecheck and init,
// then makes the package importable on-chain.
// Only addresses listed in Params.PkgApprovers may send this message.
type MsgEnablePackage struct {
Approver crypto.Address `json:"approver" yaml:"approver"`
PkgPath string `json:"pkg_path" yaml:"pkg_path"`
}

var _ std.Msg = MsgEnablePackage{}

func (msg MsgEnablePackage) Route() string { return RouterKey }
func (msg MsgEnablePackage) Type() string { return "enable_package" }

func (msg MsgEnablePackage) ValidateBasic() error {
if msg.Approver.IsZero() {
return std.ErrInvalidAddress("missing approver address")
}
if msg.PkgPath == "" {
return ErrInvalidPkgPath("missing package path")
}
return nil
}

func (msg MsgEnablePackage) GetSignBytes() []byte {
return std.MustSortJSON(amino.MustMarshalJSON(msg))
}

func (msg MsgEnablePackage) GetSigners() []crypto.Address {
return []crypto.Address{msg.Approver}
}

func (msg MsgEnablePackage) GetReceived() std.Coins { return nil }

func (msg MsgEnablePackage) SpendForSigner(_ crypto.Address) std.Coins { return nil }

//----------------------------------------
// MsgDisablePackage

// MsgDisablePackage moves an active package back to inert state, preventing
// further calls. Only addresses listed in Params.PkgApprovers may send this.
//
// NOTE: full disable (cleaning up executed objects from the base store) is not
// yet implemented; the handler returns an error until a follow-up PR completes it.
type MsgDisablePackage struct {
Approver crypto.Address `json:"approver" yaml:"approver"`
PkgPath string `json:"pkg_path" yaml:"pkg_path"`
}

var _ std.Msg = MsgDisablePackage{}

func (msg MsgDisablePackage) Route() string { return RouterKey }
func (msg MsgDisablePackage) Type() string { return "disable_package" }

func (msg MsgDisablePackage) ValidateBasic() error {
if msg.Approver.IsZero() {
return std.ErrInvalidAddress("missing approver address")
}
if msg.PkgPath == "" {
return ErrInvalidPkgPath("missing package path")
}
return nil
}

func (msg MsgDisablePackage) GetSignBytes() []byte {
return std.MustSortJSON(amino.MustMarshalJSON(msg))
}

func (msg MsgDisablePackage) GetSigners() []crypto.Address {
return []crypto.Address{msg.Approver}
}

func (msg MsgDisablePackage) GetReceived() std.Coins { return nil }

func (msg MsgDisablePackage) SpendForSigner(_ crypto.Address) std.Coins { return nil }
2 changes: 2 additions & 0 deletions gno.land/pkg/sdk/vm/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var Package = amino.RegisterPackage(amino.NewPackage(
MsgCall{}, "m_call",
MsgRun{}, "m_run",
MsgAddPackage{}, "m_addpkg", // TODO rename both to MsgAddPkg?
MsgEnablePackage{}, "m_enable_pkg",
MsgDisablePackage{}, "m_disable_pkg",

// errors
InvalidPkgPathError{}, "InvalidPkgPathError",
Expand Down
Loading
Loading