diff --git a/gno.land/pkg/integration/testdata/addpkg_private_basic.txtar b/gno.land/pkg/integration/testdata/addpkg_private_basic.txtar new file mode 100644 index 00000000000..23c8bbbb12e --- /dev/null +++ b/gno.land/pkg/integration/testdata/addpkg_private_basic.txtar @@ -0,0 +1,54 @@ +# Test private realm basic functionality + +# start a new node +gnoland start + + +# add a private realm +gnokey maketx addpkg -pkgdir $WORK/privaterealm -pkgpath gno.land/r/foobar/privaterealm -gas-fee 10000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test $test1_user_addr +stdout OK! + + +# update the edited private realm over the original +gnokey maketx addpkg -pkgdir $WORK/privaterealmedited -pkgpath gno.land/r/foobar/privaterealm -gas-fee 10000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test $test1_user_addr +stdout OK! + + +-- privaterealm/gnomod.toml -- +module = "gno.land/r/test" +gno = "0.9" +private = true + +-- privaterealm/privaterealm.gno -- +package test + +var root any +var root2 any +var root3 any + +func Echo(cur realm) string { + return "hello private world" +} + +-- privaterealmedited/gnomod.toml -- +module = "gno.land/r/test" +gno = "0.9" +private = true + +-- privaterealmedited/privaterealm.gno -- +package test + +func Echo(cur realm) string { + return "hello private edited world" +} + +-- publicrealm/gnomod.toml -- +module = "gno.land/r/foobar/publicrealm" +gno = "0.9" + +-- publicrealm/publicrealm.gno -- +package publicrealm + +func Echo(cur realm) string { + return "hello public world" +} \ No newline at end of file diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 7e058a97bca..7acfc00aa08 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -440,9 +440,12 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { if !strings.HasPrefix(pkgPath, chainDomain+"/") { return ErrInvalidPkgPath("invalid domain: " + pkgPath) } - if pv := gnostore.GetPackage(pkgPath, false); pv != nil { + + pv := gnostore.GetPackage(pkgPath, false) + if pv != nil && !pv.Private { return ErrPkgAlreadyExists("package already exists: " + pkgPath) } + if !gno.IsRealmPath(pkgPath) && !gno.IsPPackagePath(pkgPath) { return ErrInvalidPkgPath("package path must be valid realm or p package path") } @@ -476,6 +479,9 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { if gm.HasReplaces() { return ErrInvalidPackage("development packages are not allowed") } + if pv != nil && pv.Private && !gm.Private { + return ErrInvalidPackage("a private package cannot be overridden by a public package") + } if gm.Private && !gno.IsRealmPath(pkgPath) { return ErrInvalidPackage("private packages must be realm packages") } diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 464c68df971..381a84bf5e9 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -260,6 +260,75 @@ func Echo(cur realm) string { assert.NoError(t, err) } +func TestVMKeeperAddPackage_UpdatePrivatePackage(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bankk.SetCoins(ctx, addr, initialBalance) + assert.True(t, env.bankk.GetCoins(ctx, addr).IsEqual(initialBalance)) + + // Create private test package. + const pkgPath = "gno.land/r/test" + files := []*std.MemFile{ + { + Name: "gnomod.toml", + Body: `module = "gno.land/r/test" +gno = "0.9" +private = true`, + }, + { + Name: "test.gno", + Body: `package test + +func Echo(cur realm) string { + return "hello world" +}`, + }, + } + + msg1 := NewMsgAddPackage(addr, pkgPath, files) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + err := env.vmk.AddPackage(ctx, msg1) + assert.NoError(t, err) + + // Re-upload the same private package with updated content. + files2 := []*std.MemFile{ + { + Name: "gnomod.toml", + Body: `module = "gno.land/r/test" +gno = "0.9" +private = true`, + }, + { + Name: "test.gno", + Body: `package test + +func Echo(cur realm) string { + return "hello updated world" +}`, + }, + } + + msg2 := NewMsgAddPackage(addr, pkgPath, files2) + err = env.vmk.AddPackage(ctx, msg2) + assert.NoError(t, err) + + // Verify the package was updated with the new content. + store := env.vmk.getGnoTransactionStore(ctx) + memFile := store.GetMemFile(pkgPath, "test.gno") + assert.NotNil(t, memFile) + expected := `package test + +func Echo(cur realm) string { + return "hello updated world" +}` + assert.Equal(t, expected, memFile.Body) +} + func TestVMKeeperAddPackage_ImportPrivate(t *testing.T) { env := setupTestEnv() ctx := env.vmk.MakeGnoTransactionStore(env.ctx) @@ -315,6 +384,111 @@ func Echo(cur realm) string { assert.Error(t, err, ErrTypeCheck(gnolang.ImportPrivateError{PkgPath: pkgPath})) } +func TestVMKeeperAddPackage_ChangePublicToPrivate(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bankk.SetCoins(ctx, addr, initialBalance) + assert.True(t, env.bankk.GetCoins(ctx, addr).IsEqual(initialBalance)) + + const pkgPath = "gno.land/r/test" + files := []*std.MemFile{ + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(pkgPath)}, + { + Name: "test.gno", + Body: `package test + +func Echo(cur realm) string { + return "hello world" +}`, + }, + } + + msg1 := NewMsgAddPackage(addr, pkgPath, files) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + err := env.vmk.AddPackage(ctx, msg1) + assert.NoError(t, err) + + // Try to upload a private version of the same package. + files2 := []*std.MemFile{ + { + Name: "gnomod.toml", + Body: `module = "gno.land/r/test" +gno = "0.9" +private = true`, + }, + { + Name: "test.gno", + Body: `package test + +func Echo(cur realm) string { + return "hello private world" +}`, + }, + } + + msg2 := NewMsgAddPackage(addr, pkgPath, files2) + err = env.vmk.AddPackage(ctx, msg2) + assert.Error(t, err, ErrInvalidPackage("")) +} + +func TestVMKeeperAddPackage_ChangePrivateToPublic(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bankk.SetCoins(ctx, addr, initialBalance) + assert.True(t, env.bankk.GetCoins(ctx, addr).IsEqual(initialBalance)) + + // Create a private test package first. + const pkgPath = "gno.land/r/test" + files := []*std.MemFile{ + { + Name: "gnomod.toml", + Body: `module = "gno.land/r/test" +gno = "0.9" +private = true`, + }, + { + Name: "test.gno", + Body: `package test + +func Echo(cur realm) string { + return "hello private world" +}`, + }, + } + + msg1 := NewMsgAddPackage(addr, pkgPath, files) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + err := env.vmk.AddPackage(ctx, msg1) + assert.NoError(t, err) + + // Try to upload a public version of the same package. + files2 := []*std.MemFile{ + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(pkgPath)}, + { + Name: "test.gno", + Body: `package test + +func Echo(cur realm) string { + return "hello public world" +}`, + }, + } + + msg2 := NewMsgAddPackage(addr, pkgPath, files2) + err = env.vmk.AddPackage(ctx, msg2) + assert.Error(t, err, ErrInvalidPackage("")) +} + // Sending total send amount succeeds. func TestVMKeeperOriginSend1(t *testing.T) { env := setupTestEnv() diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 46e091dd18e..ea30bbb011d 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -254,6 +254,7 @@ func (m *Machine) RunMemPackageWithOverrides(mpkg *std.MemPackage, save bool) (* } func (m *Machine) runMemPackage(mpkg *std.MemPackage, save, overrides bool) (*PackageNode, *PackageValue) { + // fmt.Println("======runMemPacakge, path: ", mpkg.Path) // validate mpkg.Type. mptype := mpkg.Type.(MemPackageType) if save && !mptype.IsStorable() { @@ -273,6 +274,56 @@ func (m *Machine) runMemPackage(mpkg *std.MemPackage, save, overrides bool) (*Pa private = mod.Private } + if oid := ObjectIDFromPkgPath(mpkg.Path); m.Store.HasObject(oid) && private { + // idx of the old package + pkgidx := m.Store.GetPackageIndexCounter(oid.PkgID) + // fmt.Println("======pkgidx: ", pkgidx-1) + objidx := m.Store.GetObjectIndexCounter(backendObjectIndexKey(oid.PkgID, pkgidx)) + // fmt.Println("======objidx: ", objidx) + + // fmt.Println("======prepare for cleaning MemPackage here...") + ctr2 := m.Store.GetPackageIndexCounter(ObjectIDFromPkgPath(mpkg.Path).PkgID) + // fmt.Println("======ctr2: ", ctr2) + idxkey := []byte(backendPackageIndexKey(ctr2)) + if m.Store.HasMemPackage(idxkey) { + // fmt.Println("======found mempackage, clean it... idxkey: ", string(idxkey)) + m.Store.DelMemPackage(idxkey) + } + //=========================================================== + defer func() { + // fmt.Println("======defer...") + // idx of new package revision + pkgidx2 := m.Store.GetPackageIndexCounter(oid.PkgID) + // fmt.Println("======pkgidx2: ", pkgidx2) + objidx2 := m.Store.GetObjectIndexCounter(backendObjectIndexKey(oid.PkgID, pkgidx2)) + // fmt.Println("======objidx2: ", objidx2) + + if objidx2 >= objidx { + // fmt.Println("======nothing to clean") + return + } + + // fmt.Println("======do clean..., num: ", objidx-objidx2) + // fmt.Println("======objidx: ", objidx) + for i := objidx2 + 1; ; i++ { + oid := ObjectID{PkgID: oid.PkgID, NewTime: i} + if m.Store.HasObject(oid) { + m.Store.DelObjectByID(oid) + } else { + // fmt.Println("============clean over...") + break + } + } + }() + } + + // inc counter + ctr := m.Store.NextGlobalID() // global + m.Store.NextPackageRevision(ObjectIDFromPkgPath(mpkg.Path).PkgID, ctr) // per package + + // if oid := ObjectIDFromPkgPath(mpkg.Path); m.Store.HasObject(oid) && private { + // } + // make and set package if doesn't exist. pn := (*PackageNode)(nil) pv := (*PackageValue)(nil) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 6cfa8f442e3..99dc2dfc97e 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -48,9 +48,11 @@ type Store interface { GetObject(oid ObjectID) Object GetObjectSafe(oid ObjectID) Object SetObject(Object) int64 // returns size difference of the object + HasObject(oid ObjectID) bool GetStagingPackage() *PackageValue SetStagingPackage(pv *PackageValue) DelObject(Object) int64 // returns size difference of the object + DelObjectByID(oid ObjectID) GetType(tid TypeID) Type GetTypeSafe(tid TypeID) Type SetCacheType(Type) @@ -65,10 +67,21 @@ type Store interface { GetAllocator() *Allocator SetAllocator(alloc *Allocator) NumMemPackages() int64 + + // Generates the next global sequence ID. + NextGlobalID() uint64 + GetGlobalID() uint64 + + // Increments the revision number for the specific package. + NextPackageRevision(pid PkgID, currentRev uint64) + GetPackageIndexCounter(pid PkgID) uint64 + GetObjectIndexCounter(key string) uint64 // Upon restart, all packages will be re-preprocessed; This // loads BlockNodes and Types onto the store for persistence // version 1. AddMemPackage(mpkg *std.MemPackage, mptype MemPackageType) + DelMemPackage(key []byte) + HasMemPackage(key []byte) bool GetMemPackage(path string) *std.MemPackage GetMemFile(path string, name string) *std.MemFile FindPathsByPrefix(prefix string) iter.Seq[string] @@ -446,6 +459,20 @@ func (ds *defaultStore) GetObjectSafe(oid ObjectID) Object { return nil } +func (ds *defaultStore) HasObject(oid ObjectID) bool { + if bm.OpsEnabled { + bm.PauseOpCode() + defer bm.ResumeOpCode() + } + // check cache. + if _, exists := ds.cacheObjects[oid]; exists { + return true + } + key := backendObjectKey(oid) + hashbz := ds.baseStore.Get([]byte(key)) + return hashbz != nil +} + // loads and caches an object. // CONTRACT: object isn't already in the cache. func (ds *defaultStore) loadObjectSafe(oid ObjectID) Object { @@ -702,6 +729,12 @@ func (ds *defaultStore) SetObject(oo Object) int64 { value = hash.Bytes() ds.iavlStore.Set(key, value) } + + pid := oid.PkgID + pkgidx := ds.GetPackageIndexCounter(pid) + // fmt.Println("======SetObject, pkgidx: ", pkgidx) + ds.incObjectIndexCounter(backendObjectIndexKey(pid, pkgidx)) + // fmt.Println("======objidx after increase: ", objidx) return diff } @@ -719,6 +752,7 @@ func (ds *defaultStore) loadForLog(oid ObjectID) Object { } func (ds *defaultStore) DelObject(oo Object) int64 { + fmt.Println("======DelObject, oo: ", oo.GetObjectID()) if bm.OpsEnabled { bm.PauseOpCode() defer bm.ResumeOpCode() @@ -747,6 +781,18 @@ func (ds *defaultStore) DelObject(oo Object) int64 { return size } +// XXX, consume gas +func (ds *defaultStore) DelObjectByID(oid ObjectID) { + // fmt.Println("======DelObjectByID, oo: ", oid) + // delete from cache. + delete(ds.cacheObjects, oid) + // delete from backend. + if ds.baseStore != nil { + key := backendObjectKey(oid) + ds.baseStore.Delete([]byte(key)) + } +} + // NOTE: not used quite yet. // NOTE: The implementation matches that of GetObject() in anticipation of what // the persistent type system might work like. @@ -911,6 +957,7 @@ func (ds *defaultStore) SetBlockNode(bn BlockNode) { // XXX } +// XXX, filter out empty paths. func (ds *defaultStore) NumMemPackages() int64 { ctrkey := []byte(backendPackageIndexCtrKey()) ctrbz := ds.baseStore.Get(ctrkey) @@ -925,7 +972,8 @@ func (ds *defaultStore) NumMemPackages() int64 { } } -func (ds *defaultStore) incGetPackageIndexCounter() uint64 { +// index all packages +func (ds *defaultStore) NextGlobalID() uint64 { ctrkey := []byte(backendPackageIndexCtrKey()) ctrbz := ds.baseStore.Get(ctrkey) if ctrbz == nil { @@ -943,6 +991,77 @@ func (ds *defaultStore) incGetPackageIndexCounter() uint64 { } } +func (ds *defaultStore) GetGlobalID() uint64 { + ctrkey := []byte(backendPackageIndexCtrKey()) + ctrbz := ds.baseStore.Get(ctrkey) + if ctrbz == nil { + return 0 + } else { + ctr, err := strconv.Atoi(string(ctrbz)) + if err != nil { + panic(err) + } + return uint64(ctr) + } +} + +// index per package +func (ds *defaultStore) NextPackageRevision(pid PkgID, ctr uint64) { + // fmt.Printf("======incPackageIndexCounter, pid: %v, ctr: %d\n", pid, ctr) + ctrkey := pid.Hashlet[:] + ds.baseStore.Set(ctrkey, []byte(strconv.Itoa(int(ctr)))) +} + +func (ds *defaultStore) GetPackageIndexCounter(pid PkgID) uint64 { + ctrkey := pid.Hashlet[:] + ctrbz := ds.baseStore.Get(ctrkey) + if ctrbz == nil { + return 0 + } else { + ctr, err := strconv.Atoi(string(ctrbz)) + if err != nil { + panic(err) + } + return uint64(ctr) + } +} + +// invode in SetObject +// key format: pkgID:counter +// e.g. 0000:1, 0000:2, same packages with different index +// value is index of objects that belogs to a package +func (ds *defaultStore) incObjectIndexCounter(key string) uint64 { + ctrkey := []byte(key) + ctrbz := ds.baseStore.Get(ctrkey) + if ctrbz == nil { + nextbz := strconv.Itoa(1) + ds.baseStore.Set(ctrkey, []byte(nextbz)) + return 1 + } else { + ctr, err := strconv.Atoi(string(ctrbz)) + if err != nil { + panic(err) + } + nextbz := strconv.Itoa(ctr + 1) + ds.baseStore.Set(ctrkey, []byte(nextbz)) + return uint64(ctr) + 1 + } +} + +func (ds *defaultStore) GetObjectIndexCounter(key string) uint64 { + ctrkey := []byte(key) + ctrbz := ds.baseStore.Get(ctrkey) + if ctrbz == nil { + return 0 + } else { + ctr, err := strconv.Atoi(string(ctrbz)) + if err != nil { + panic(err) + } + return uint64(ctr) + } +} + // mptype is passed in as a redundant parameter as convenience to assert that // mpkg.Type is what is expected. // If MPAnyAll, mpkg may be either MPStdlibAll or MPProdAll, and likewise for @@ -974,7 +1093,8 @@ func (ds *defaultStore) AddMemPackage(mpkg *std.MemPackage, mptype MemPackageTyp if err != nil { panic(fmt.Errorf("invalid mempackage: %w", err)) } - ctr := ds.incGetPackageIndexCounter() + + ctr := ds.GetGlobalID() idxkey := []byte(backendPackageIndexKey(ctr)) bz := amino.MustMarshal(mpkg) gas := overflow.Mulp(ds.gasConfig.GasAddMemPackage, store.Gas(len(bz))) @@ -985,6 +1105,14 @@ func (ds *defaultStore) AddMemPackage(mpkg *std.MemPackage, mptype MemPackageTyp size = len(bz) } +func (ds *defaultStore) DelMemPackage(key []byte) { + ds.baseStore.Delete(key) +} + +func (ds *defaultStore) HasMemPackage(key []byte) bool { + return ds.baseStore.Has(key) +} + // GetMemPackage retrieves the MemPackage at the given path. // It returns nil if the package could not be found. func (ds *defaultStore) GetMemPackage(path string) *std.MemPackage { @@ -1082,6 +1210,7 @@ func (ds *defaultStore) IterMemPackage() <-chan *std.MemPackage { idxkey := []byte(backendPackageIndexKey(i)) path := ds.baseStore.Get(idxkey) if path == nil { + // XXX, this is possible.. panic(fmt.Sprintf( "missing package index %d", i)) } @@ -1208,6 +1337,10 @@ func backendPackageIndexKey(index uint64) string { return fmt.Sprintf("pkgidx:%020d", index) } +func backendObjectIndexKey(pid PkgID, index uint64) string { + return fmt.Sprintf("%s:%020d", pid, index) +} + // We need to prefix stdlibs path with `_` to maitain them lexicographically // ordered with domain path func backendPackagePathKey(path string) string {