Skip to content

Values of function-local declared types are unloadable after persistence #6063

Description

@omarsy

Summary

A value whose type is declared inside a function can be written into realm state
successfully, but any later transaction that reads it aborts with unexpected type with id …. The write commits and the failure only surfaces on a subsequent
read, leaving the realm holding state it can no longer load.

Verified at master 33763e8269.

Reproducer

Drop into gno.land/pkg/integration/testdata/:

gnoland start

gnokey maketx addpkg -pkgdir $WORK/lt -pkgpath gno.land/r/$test1_user_addr/lt -gas-fee 1000000ugnot -gas-wanted 100000000 -chainid=tendermint_test test1

gnokey maketx call -pkgpath gno.land/r/$test1_user_addr/lt -func SetFunc -gas-fee 1000000ugnot -gas-wanted 100000000 -send "" -broadcast -chainid=tendermint_test test1
stdout OK!

gnokey maketx call -pkgpath gno.land/r/$test1_user_addr/lt -func Read -gas-fee 1000000ugnot -gas-wanted 100000000 -send "" -broadcast -chainid=tendermint_test test1
stdout OK!

-- lt/gnomod.toml --
module = "lt"
gno = "0.9"

-- lt/lt.gno --
package lt

var FromFunc interface{}

func SetFunc(cur realm) {
	type ft struct{ N int }
	FromFunc = ft{1}
}

func Read(cur realm) string {
	if FromFunc != nil {
		return "func-ok"
	}
	return "nil"
}

Expected: both calls succeed, Read returns func-ok.

Actual: SetFunc succeeds; Read aborts:

VM panic: unexpected type with id gno.land/r/g1…/lt[gno.land/r/g1…/lt/lt.gno:5:1-8:2].ft

5:1-8:2 is the span of SetFunc — the ParentLoc baked into the local type's
TypeID.

Cause

saveNewPackageValuesAndTypes registers declared types by walking the package
block
only (gnovm/pkg/gnolang/machine.go:905-918 @ 33763e8269):

if bv, ok := pv.Block.(*Block); ok {
	for _, tv := range bv.Values {
		if tvv, ok := tv.V.(TypeValue); ok {
			if dt, ok := tvv.Type.(*DeclaredType); ok && dt.PkgPath == pv.PkgPath {
				m.Store.SetType(dt)
			}
		}
	}
}

A function-local *DeclaredType never appears in the package block, so it is
never SetType'd. Persisting a value of that type writes the TypeID into the
object image anyway; the next GetType(tid) finds nothing in cache or backend
and panics (store.go:793-796).

Nothing rejects the write at preprocess or commit time, so the corruption is
silent until read.

Scope

  • Not a regression; reproduces at 33763e8269 and at 7547806.
  • Verified via an interface-typed package var. Any escape route that lands the
    value in persisted realm state should behave the same, though I only tested
    this one.
  • Values that never leave the transaction are unaffected.
  • fix(gnovm): allow type decls in nested blocks; make const checks respect decl position #6060 legalises type declarations in nested blocks, which widens the set of
    code shapes that can reach this, but is not the cause — the reproducer uses a
    plain function-body declaration that already compiles on master.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions