diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go index 0ab5724154e..53c0bb4b686 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -18,7 +18,6 @@ import ( const ( defaultAccount_Name = "test1" - defaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" defaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" defaultAccount_publicKey = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj" ) diff --git a/contribs/gnomigrate/internal/txs/txs.go b/contribs/gnomigrate/internal/txs/txs.go index 4c65ca6ef0b..231428d5064 100644 --- a/contribs/gnomigrate/internal/txs/txs.go +++ b/contribs/gnomigrate/internal/txs/txs.go @@ -184,7 +184,7 @@ func processFile(ctx context.Context, io commands.IO, source, destination string continue } - if _, err = outputFile.WriteString(fmt.Sprintf("%s\n", string(marshaledData))); err != nil { + if _, err = fmt.Fprintf(outputFile, "%s\n", marshaledData); err != nil { io.ErrPrintfln("unable to save to output file, %s", err) } } diff --git a/examples/gno.land/p/moul/addrset/addrset.gno b/examples/gno.land/p/moul/addrset/addrset.gno new file mode 100644 index 00000000000..0bb8165f9fe --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset.gno @@ -0,0 +1,100 @@ +// Package addrset provides a specialized set data structure for managing unique Gno addresses. +// +// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order. +// This package is particularly useful when you need to: +// - Track a collection of unique addresses (e.g., for whitelists, participants, etc.) +// - Efficiently check address membership +// - Support pagination when displaying addresses +// +// Example usage: +// +// import ( +// "std" +// "gno.land/p/moul/addrset" +// ) +// +// func MyHandler() { +// // Create a new address set +// var set addrset.Set +// +// // Add some addresses +// addr1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +// addr2 := std.Address("g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth") +// +// set.Add(addr1) // returns true (newly added) +// set.Add(addr2) // returns true (newly added) +// set.Add(addr1) // returns false (already exists) +// +// // Check membership +// if set.Has(addr1) { +// // addr1 is in the set +// } +// +// // Get size +// size := set.Size() // returns 2 +// +// // Iterate with pagination (10 items per page, starting at offset 0) +// set.IterateByOffset(0, 10, func(addr std.Address) bool { +// // Process addr +// return false // continue iteration +// }) +// +// // Remove an address +// set.Remove(addr1) // returns true (was present) +// set.Remove(addr1) // returns false (not present) +// } +package addrset + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Set struct { + tree avl.Tree +} + +// Add inserts an address into the set. +// Returns true if the address was newly added, false if it already existed. +func (s *Set) Add(addr std.Address) bool { + return !s.tree.Set(string(addr), nil) +} + +// Remove deletes an address from the set. +// Returns true if the address was found and removed, false if it didn't exist. +func (s *Set) Remove(addr std.Address) bool { + _, removed := s.tree.Remove(string(addr)) + return removed +} + +// Has checks if an address exists in the set. +func (s *Set) Has(addr std.Address) bool { + return s.tree.Has(string(addr)) +} + +// Size returns the number of addresses in the set. +func (s *Set) Size() int { + return s.tree.Size() +} + +// IterateByOffset walks through addresses starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) IterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.IterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.ReverseIterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// Tree returns the underlying AVL tree for advanced usage. +func (s *Set) Tree() avl.ITree { + return &s.tree +} diff --git a/examples/gno.land/p/moul/addrset/addrset_test.gno b/examples/gno.land/p/moul/addrset/addrset_test.gno new file mode 100644 index 00000000000..c3e27eab1df --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset_test.gno @@ -0,0 +1,174 @@ +package addrset + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestSet(t *testing.T) { + addr1 := std.Address("addr1") + addr2 := std.Address("addr2") + addr3 := std.Address("addr3") + + tests := []struct { + name string + actions func(s *Set) + size int + has map[std.Address]bool + addrs []std.Address // for iteration checks + }{ + { + name: "empty set", + actions: func(s *Set) {}, + size: 0, + has: map[std.Address]bool{addr1: false}, + }, + { + name: "single address", + actions: func(s *Set) { + s.Add(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: true, + addr2: false, + }, + addrs: []std.Address{addr1}, + }, + { + name: "multiple addresses", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Add(addr3) + }, + size: 3, + has: map[std.Address]bool{ + addr1: true, + addr2: true, + addr3: true, + }, + addrs: []std.Address{addr1, addr2, addr3}, + }, + { + name: "remove address", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Remove(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: false, + addr2: true, + }, + addrs: []std.Address{addr2}, + }, + { + name: "duplicate adds", + actions: func(s *Set) { + uassert.True(t, s.Add(addr1)) // first add returns true + uassert.False(t, s.Add(addr1)) // second add returns false + uassert.True(t, s.Remove(addr1)) // remove existing returns true + uassert.False(t, s.Remove(addr1)) // remove non-existing returns false + }, + size: 0, + has: map[std.Address]bool{ + addr1: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + + // Execute test actions + tt.actions(&set) + + // Check size + uassert.Equal(t, tt.size, set.Size()) + + // Check existence + for addr, expected := range tt.has { + uassert.Equal(t, expected, set.Has(addr)) + } + + // Check iteration if addresses are specified + if tt.addrs != nil { + collected := []std.Address{} + set.IterateByOffset(0, 10, func(addr std.Address) bool { + collected = append(collected, addr) + return false + }) + + // Check length + uassert.Equal(t, len(tt.addrs), len(collected)) + + // Check each address + for i, addr := range tt.addrs { + uassert.Equal(t, addr, collected[i]) + } + } + }) + } +} + +func TestSetIterationLimits(t *testing.T) { + tests := []struct { + name string + addrs []std.Address + offset int + limit int + expected int + }{ + { + name: "zero offset full list", + addrs: []std.Address{"a1", "a2", "a3"}, + offset: 0, + limit: 10, + expected: 3, + }, + { + name: "offset with limit", + addrs: []std.Address{"a1", "a2", "a3", "a4"}, + offset: 1, + limit: 2, + expected: 2, + }, + { + name: "offset beyond size", + addrs: []std.Address{"a1", "a2"}, + offset: 3, + limit: 1, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + for _, addr := range tt.addrs { + set.Add(addr) + } + + // Test forward iteration + count := 0 + set.IterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + + // Test reverse iteration + count = 0 + set.ReverseIterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + }) + } +} diff --git a/examples/gno.land/p/moul/addrset/gno.mod b/examples/gno.land/p/moul/addrset/gno.mod new file mode 100644 index 00000000000..45bb53b399c --- /dev/null +++ b/examples/gno.land/p/moul/addrset/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/addrset diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao.gno b/examples/gno.land/r/demo/btree_dao/btree_dao.gno new file mode 100644 index 00000000000..c90742eb29b --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao.gno @@ -0,0 +1,209 @@ +package btree_dao + +import ( + "errors" + "std" + "strings" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" +) + +// RegistrationDetails holds the details of a user's registration in the BTree DAO. +// It stores the user's address, registration time, their B-Tree if they planted one, +// and their NFT ID. +type RegistrationDetails struct { + Address std.Address + RegTime time.Time + UserBTree *btree.BTree + NFTID string +} + +// Less implements the btree.Record interface for RegistrationDetails. +// It compares two RegistrationDetails based on their registration time. +// Returns true if the current registration time is before the other registration time. +func (rd *RegistrationDetails) Less(than btree.Record) bool { + other := than.(*RegistrationDetails) + return rd.RegTime.Before(other.RegTime) +} + +var ( + dao = grc721.NewBasicNFT("BTree DAO", "BTDAO") + tokenID = 0 + members = btree.New() +) + +// PlantTree allows a user to plant their B-Tree in the DAO forest. +// It mints an NFT to the user and registers their tree in the DAO. +// Returns an error if the tree is already planted, empty, or if NFT minting fails. +func PlantTree(userBTree *btree.BTree) error { + return plantImpl(userBTree, "") +} + +// PlantSeed allows a user to register as a seed in the DAO with a message. +// It mints an NFT to the user and registers them as a seed member. +// Returns an error if the message is empty or if NFT minting fails. +func PlantSeed(message string) error { + return plantImpl(nil, message) +} + +// plantImpl is the internal implementation that handles both tree planting and seed registration. +// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty. +// For seed planting (userBTree == nil), it verifies the seed message isn't empty. +// In both cases, it mints an NFT to the user and adds their registration details to the members tree. +// Returns an error if any validation fails or if NFT minting fails. +func plantImpl(userBTree *btree.BTree, seedMessage string) error { + // Get the caller's address + userAddress := std.GetOrigCaller() + + var nftID string + var regDetails *RegistrationDetails + + if userBTree != nil { + // Handle tree planting + var treeExists bool + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == userBTree { + treeExists = true + return false + } + return true + }) + if treeExists { + return errors.New("tree is already planted in the forest") + } + + if userBTree.Len() == 0 { + return errors.New("cannot plant an empty tree") + } + + nftID = ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: userBTree, + NFTID: nftID, + } + } else { + // Handle seed planting + if seedMessage == "" { + return errors.New("seed message cannot be empty") + } + nftID = "seed_" + ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: nil, + NFTID: nftID, + } + } + + // Mint an NFT to the user + err := dao.Mint(userAddress, grc721.TokenID(nftID)) + if err != nil { + return err + } + + members.Insert(regDetails) + tokenID++ + return nil +} + +// Render generates a Markdown representation of the DAO members. +// It displays: +// - Total number of NFTs minted +// - Total number of members +// - Size of the biggest planted tree +// - The first 3 members (OGs) +// - The latest 10 members +// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds). +// The path parameter is currently unused. +// Returns a formatted Markdown string. +func Render(path string) string { + var latestMembers []string + var ogMembers []string + + // Get total size and first member + totalSize := members.Len() + biggestTree := 0 + if maxMember := members.Max(); maxMember != nil { + if userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil { + biggestTree = userBTree.Len() + } + } + + // Collect the latest 10 members + members.Descend(func(record btree.Record) bool { + if len(latestMembers) < 10 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "🌱#" + regDetails.NFTID + } else { + nftList += "🌳#" + regDetails.NFTID + } + } + nftList += ")" + } + latestMembers = append(latestMembers, string(addr)+nftList) + return true + } + return false + }) + + // Collect the first 3 members (OGs) + members.Ascend(func(record btree.Record) bool { + if len(ogMembers) < 3 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "🌱#" + regDetails.NFTID + } else { + nftList += "🌳#" + regDetails.NFTID + } + } + nftList += ")" + } + ogMembers = append(ogMembers, string(addr)+nftList) + return true + } + return false + }) + + var sb strings.Builder + + sb.WriteString(md.H1("B-Tree DAO Members")) + sb.WriteString(md.H2("Total NFTs Minted")) + sb.WriteString(ufmt.Sprintf("Total NFTs minted: %d\n\n", dao.TokenCount())) + sb.WriteString(md.H2("Member Stats")) + sb.WriteString(ufmt.Sprintf("Total members: %d\n", totalSize)) + if biggestTree > 0 { + sb.WriteString(ufmt.Sprintf("Biggest tree size: %d\n", biggestTree)) + } + sb.WriteString(md.H2("OG Members")) + sb.WriteString(md.BulletList(ogMembers)) + sb.WriteString(md.H2("Latest Members")) + sb.WriteString(md.BulletList(latestMembers)) + + return sb.String() +} diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno new file mode 100644 index 00000000000..0514f52f7b4 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno @@ -0,0 +1,97 @@ +package btree_dao + +import ( + "std" + "strings" + "testing" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) + members = btree.New() +} + +type TestElement struct { + value int +} + +func (te *TestElement) Less(than btree.Record) bool { + return te.value < than.(*TestElement).value +} + +func TestPlantTree(t *testing.T) { + setupTest() + + tree := btree.New() + elements := []int{30, 10, 50, 20, 40} + for _, val := range elements { + tree.Insert(&TestElement{value: val}) + } + + err := PlantTree(tree) + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == tree { + found = true + return false + } + return true + }) + uassert.True(t, found) + + err = PlantTree(tree) + uassert.Error(t, err) + + emptyTree := btree.New() + err = PlantTree(emptyTree) + uassert.Error(t, err) +} + +func TestPlantSeed(t *testing.T) { + setupTest() + + err := PlantSeed("Hello DAO!") + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == nil { + found = true + uassert.NotEmpty(t, regDetails.NFTID) + uassert.True(t, strings.Contains(regDetails.NFTID, "seed_")) + return false + } + return true + }) + uassert.True(t, found) + + err = PlantSeed("") + uassert.Error(t, err) +} + +func TestRegistrationDetailsOrdering(t *testing.T) { + setupTest() + + rd1 := &RegistrationDetails{ + Address: std.Address("test1"), + RegTime: time.Now(), + NFTID: "0", + } + rd2 := &RegistrationDetails{ + Address: std.Address("test2"), + RegTime: time.Now().Add(time.Hour), + NFTID: "1", + } + + uassert.True(t, rd1.Less(rd2)) + uassert.False(t, rd2.Less(rd1)) +} diff --git a/examples/gno.land/r/demo/btree_dao/gno.mod b/examples/gno.land/r/demo/btree_dao/gno.mod new file mode 100644 index 00000000000..01b99acc300 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/btree_dao diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index cb5d54a513a..4f380031be4 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -45,22 +45,20 @@ var startGraphic = strings.ReplaceAll(` /___/ `, "'", "`") -var ( - // Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go - genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -) +// Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go +var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) type startCfg struct { - gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisFile string - chainID string - dataDir string - lazyInit bool + gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + skipGenesisSigVerification bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisFile string + chainID string + dataDir string + lazyInit bool logLevel string logFormat string @@ -86,7 +84,6 @@ func newStartCmd(io commands.IO) *commands.Command { func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { gnoroot := gnoenv.RootDir() defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") - defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl") fs.BoolVar( &c.skipFailingGenesisTxs, @@ -95,6 +92,13 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "don't panic when replaying invalid genesis txs", ) + fs.BoolVar( + &c.skipGenesisSigVerification, + "skip-genesis-sig-verification", + false, + "don't panic when replaying invalidly signed genesis txs", + ) + fs.StringVar( &c.genesisBalancesFile, "genesis-balances-file", @@ -105,7 +109,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.genesisTxsFile, "genesis-txs-file", - defaultGenesisTxsFile, + "", "initial txs to replay", ) @@ -218,7 +222,7 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { ) // Init a new genesis.json - if err := lazyInitGenesis(io, c, genesisPath, privateKey.GetPubKey()); err != nil { + if err := lazyInitGenesis(io, c, genesisPath, privateKey.Key.PrivKey); err != nil { return fmt.Errorf("unable to initialize genesis.json, %w", err) } } @@ -238,7 +242,16 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { minGasPrices := cfg.Application.MinGasPrices // Create application and node - cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, evsw, logger, minGasPrices) + cfg.LocalApp, err = gnoland.NewApp( + nodeDir, + gnoland.GenesisAppConfig{ + SkipFailingTxs: c.skipFailingGenesisTxs, + SkipSigVerification: c.skipGenesisSigVerification, + }, + evsw, + logger, + minGasPrices, + ) if err != nil { return fmt.Errorf("unable to create the Gnoland app, %w", err) } @@ -334,7 +347,7 @@ func lazyInitGenesis( io commands.IO, c *startCfg, genesisPath string, - publicKey crypto.PubKey, + privateKey crypto.PrivKey, ) error { // Check if the genesis.json is present if osm.FileExists(genesisPath) { @@ -342,7 +355,7 @@ func lazyInitGenesis( } // Generate the new genesis.json file - if err := generateGenesisFile(genesisPath, publicKey, c); err != nil { + if err := generateGenesisFile(genesisPath, privateKey, c); err != nil { return fmt.Errorf("unable to generate genesis file, %w", err) } @@ -367,7 +380,21 @@ func initializeLogger(io io.WriteCloser, logLevel, logFormat string) (*zap.Logge return log.GetZapLoggerFn(format)(io, level), nil } -func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error { +func generateGenesisFile(genesisFile string, privKey crypto.PrivKey, c *startCfg) error { + var ( + pubKey = privKey.PubKey() + // There is an active constraint for gno.land transactions: + // + // All transaction messages' (MsgSend, MsgAddPkg...) "author" field, + // specific to the message type ("creator", "sender"...), must match + // the signature address contained in the transaction itself. + // This means that if MsgSend is originating from address A, + // the owner of the private key for address A needs to sign the transaction + // containing the message. Every message in a transaction needs to + // originate from the same account that signed the transaction + txSender = pubKey.Address() + ) + gen := &bft.GenesisDoc{} gen.GenesisTime = time.Now() gen.ChainID = c.chainID @@ -383,8 +410,8 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro gen.Validators = []bft.GenesisValidator{ { - Address: pk.Address(), - PubKey: pk, + Address: pubKey.Address(), + PubKey: pubKey, Power: 10, Name: "testvalidator", }, @@ -398,22 +425,43 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro // Load examples folder examplesDir := filepath.Join(c.gnoRootDir, "examples") - pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, genesisDeployAddress, genesisDeployFee) + pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, txSender, genesisDeployFee) if err != nil { return fmt.Errorf("unable to load examples folder: %w", err) } // Load Genesis TXs - genesisTxs, err := gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote) - if err != nil { - return fmt.Errorf("unable to load genesis txs file: %w", err) + var genesisTxs []gnoland.TxWithMetadata + + if c.genesisTxsFile != "" { + genesisTxs, err = gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote) + if err != nil { + return fmt.Errorf("unable to load genesis txs file: %w", err) + } } genesisTxs = append(pkgsTxs, genesisTxs...) + // Sign genesis transactions, with the default key (test1) + if err = gnoland.SignGenesisTxs(genesisTxs, privKey, c.chainID); err != nil { + return fmt.Errorf("unable to sign genesis txs: %w", err) + } + + // Make sure the genesis transaction author has sufficient + // balance to cover transaction deployments in genesis. + // + // During the init-chainer process, the account that authors the + // genesis transactions needs to have a sufficient balance + // to cover outstanding transaction costs. + // Since the cost can't be estimated upfront at this point, the balance + // set is an arbitrary value based on a "best guess" basis. + // There should be a larger discussion if genesis transactions should consume gas, at all + deployerBalance := int64(len(genesisTxs)) * 10_000_000 // ~10 GNOT per tx + balances.Set(txSender, std.NewCoins(std.NewCoin("ugnot", deployerBalance))) + // Construct genesis AppState. defaultGenState := gnoland.DefaultGenState() - defaultGenState.Balances = balances + defaultGenState.Balances = balances.List() defaultGenState.Txs = genesisTxs gen.AppState = defaultGenState diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 80c58e9e982..0826071b9f5 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -182,10 +182,25 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { return baseApp, nil } +// GenesisAppConfig wraps the most important +// genesis params relating to the App +type GenesisAppConfig struct { + SkipFailingTxs bool // does not stop the chain from starting if any tx fails + SkipSigVerification bool // does not verify the transaction signatures in genesis +} + +// NewTestGenesisAppConfig returns a testing genesis app config +func NewTestGenesisAppConfig() GenesisAppConfig { + return GenesisAppConfig{ + SkipFailingTxs: true, + SkipSigVerification: true, + } +} + // NewApp creates the gno.land application. func NewApp( dataRootDir string, - skipFailingGenesisTxs bool, + genesisCfg GenesisAppConfig, evsw events.EventSwitch, logger *slog.Logger, minGasPrices string, @@ -199,9 +214,10 @@ func NewApp( GenesisTxResultHandler: PanicOnFailingTxResultHandler, StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), }, - MinGasPrices: minGasPrices, + MinGasPrices: minGasPrices, + SkipGenesisVerification: genesisCfg.SkipSigVerification, } - if skipFailingGenesisTxs { + if genesisCfg.SkipFailingTxs { cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler } diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 56a15fed5a9..361d7505157 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -134,7 +134,7 @@ func TestNewApp(t *testing.T) { // NewApp should have good defaults and manage to run InitChain. td := t.TempDir() - app, err := NewApp(td, true, events.NewEventSwitch(), log.NewNoopLogger(), "") + app, err := NewApp(td, NewTestGenesisAppConfig(), events.NewEventSwitch(), log.NewNoopLogger(), "") require.NoError(t, err, "NewApp should be successful") resp := app.InitChain(abci.RequestInitChain{ diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index d7844d77b57..a754e7a4644 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -20,7 +20,7 @@ import ( const initGasPrice = "1ugnot/1000gas" // LoadGenesisBalancesFile loads genesis balances from the provided file path. -func LoadGenesisBalancesFile(path string) ([]Balance, error) { +func LoadGenesisBalancesFile(path string) (Balances, error) { // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot content, err := osm.ReadFile(path) if err != nil { @@ -28,7 +28,7 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { } lines := strings.Split(string(content), "\n") - balances := make([]Balance, 0, len(lines)) + balances := make(Balances, len(lines)) for _, line := range lines { line = strings.TrimSpace(line) @@ -56,10 +56,7 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { return nil, fmt.Errorf("invalid balance coins %s: %w", parts[1], err) } - balances = append(balances, Balance{ - Address: addr, - Amount: coins, - }) + balances.Set(addr, coins) } return balances, nil diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index ed35c4141f4..66fb2f54e8a 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -8,6 +8,7 @@ import ( "os" "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -86,3 +87,31 @@ func ReadGenesisTxs(ctx context.Context, path string) ([]TxWithMetadata, error) return txs, nil } + +// SignGenesisTxs will sign all txs passed as argument using the private key. +// This signature is only valid for genesis transactions as the account number and sequence are 0 +func SignGenesisTxs(txs []TxWithMetadata, privKey crypto.PrivKey, chainID string) error { + for index, tx := range txs { + // Upon verifying genesis transactions, the account number and sequence are considered to be 0. + // The reason for this is that it is not possible to know the account number (or sequence!) in advance + // when generating the genesis transaction signature + bytes, err := tx.Tx.GetSignBytes(chainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to get sign bytes for transaction, %w", err) + } + + signature, err := privKey.Sign(bytes) + if err != nil { + return fmt.Errorf("unable to sign genesis transaction, %w", err) + } + + txs[index].Tx.Signatures = []std.Signature{ + { + PubKey: privKey.PubKey(), + Signature: signature, + }, + } + } + + return nil +} diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go index b4625d6d7d6..c501325bc3e 100644 --- a/gno.land/pkg/gnoland/types_test.go +++ b/gno.land/pkg/gnoland/types_test.go @@ -11,6 +11,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" @@ -129,3 +130,29 @@ func TestReadGenesisTxs(t *testing.T) { } }) } + +func TestSignGenesisTx(t *testing.T) { + t.Parallel() + + var ( + txs = generateTxs(t, 100) + privKey = secp256k1.GenPrivKey() + pubKey = privKey.PubKey() + chainID = "testing" + ) + + // Make sure the transactions are properly signed + require.NoError(t, SignGenesisTxs(txs, privKey, chainID)) + + // Make sure the signatures are valid + for _, tx := range txs { + payload, err := tx.Tx.GetSignBytes(chainID, 0, 0) + require.NoError(t, err) + + sigs := tx.Tx.GetSignatures() + require.Len(t, sigs, 1) + + assert.True(t, pubKey.Equals(sigs[0].PubKey)) + assert.True(t, pubKey.VerifyBytes(payload, sigs[0].Signature)) + } +} diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go index 3e09d627c9a..d93d4607a59 100644 --- a/gno.land/pkg/integration/doc.go +++ b/gno.land/pkg/integration/doc.go @@ -102,11 +102,17 @@ // The path where the gnoland node stores its configuration and data. It's // set only if the node has started. // -// - USER_SEED_test1: -// Contains the seed for the test1 account. +// - xxx_user_seed: +// Where `xxx` is the account name; Contains the seed for the test1 account. // -// - USER_ADDR_test1: -// Contains the address for the test1 account. +// - xxx_user_addr: +// Where `xxx` is the account name; Contains the address for the test1 account. +// +// - xxx_account_num: +// Where `xxx` is the account name; Contains the account number for the test1 account. +// +// - xxx_account_seq: +// Where `xxx` is the account name; Contains the address for the test1 account. // // - RPC_ADDR: // Points to the gnoland node's remote address. It's set only if the node has started. diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go index 7965f228fc2..edcf53de5d3 100644 --- a/gno.land/pkg/integration/node_testing.go +++ b/gno.land/pkg/integration/node_testing.go @@ -148,7 +148,7 @@ func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balanc genesisBalances, err := gnoland.LoadGenesisBalancesFile(balanceFile) require.NoError(t, err) - return genesisBalances + return genesisBalances.List() } // LoadDefaultGenesisParamFile loads the default genesis balance file for testing. diff --git a/gno.land/pkg/integration/pkgloader.go b/gno.land/pkg/integration/pkgloader.go index 7e7e817dd92..71b1491b2a8 100644 --- a/gno.land/pkg/integration/pkgloader.go +++ b/gno.land/pkg/integration/pkgloader.go @@ -77,8 +77,7 @@ func (pl *PkgsLoader) LoadPackages(creatorKey crypto.PrivKey, fee std.Fee, depos } } - err = SignTxs(txs, creatorKey, "tendermint_test") - if err != nil { + if err = gnoland.SignGenesisTxs(txs, creatorKey, "tendermint_test"); err != nil { return nil, fmt.Errorf("unable to sign txs: %w", err) } @@ -131,10 +130,11 @@ func (pl *PkgsLoader) LoadPackage(modroot string, path, name string) error { if err != nil { return fmt.Errorf("unable to read package at %q: %w", currentPkg.Dir, err) } - imports, err := packages.Imports(pkg, nil) + importsMap, err := packages.Imports(pkg, nil) if err != nil { return fmt.Errorf("unable to load package imports in %q: %w", currentPkg.Dir, err) } + imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindFiletest) for _, imp := range imports { if imp.PkgPath == currentPkg.Name || gnolang.IsStdlib(imp.PkgPath) { continue diff --git a/gno.land/pkg/integration/signer.go b/gno.land/pkg/integration/signer.go deleted file mode 100644 index b32cd9c59bc..00000000000 --- a/gno.land/pkg/integration/signer.go +++ /dev/null @@ -1,33 +0,0 @@ -package integration - -import ( - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" -) - -// SignTxs will sign all txs passed as argument using the private key -// this signature is only valid for genesis transactions as accountNumber and sequence are 0 -func SignTxs(txs []gnoland.TxWithMetadata, privKey crypto.PrivKey, chainID string) error { - for index, tx := range txs { - bytes, err := tx.Tx.GetSignBytes(chainID, 0, 0) - if err != nil { - return fmt.Errorf("unable to get sign bytes for transaction, %w", err) - } - signature, err := privKey.Sign(bytes) - if err != nil { - return fmt.Errorf("unable to sign transaction, %w", err) - } - - txs[index].Tx.Signatures = []std.Signature{ - { - PubKey: privKey.PubKey(), - Signature: signature, - }, - } - } - return nil -} diff --git a/gno.land/pkg/integration/testdata/addpkg.txtar b/gno.land/pkg/integration/testdata/addpkg.txtar index 8594e6596ce..15e8ad222f2 100644 --- a/gno.land/pkg/integration/testdata/addpkg.txtar +++ b/gno.land/pkg/integration/testdata/addpkg.txtar @@ -4,7 +4,7 @@ gnoland start ## deploy realm -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/hello -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/hello -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 ## check output stdout OK! @@ -15,7 +15,7 @@ stdout 'EVENTS: \[\]' stdout 'TX HASH: ' ## call added realm -gnokey maketx call -pkgpath gno.land/r/$USER_ADDR_test1/hello -chainid=tendermint_test -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast test1 +gnokey maketx call -pkgpath gno.land/r/$test1_user_addr/hello -chainid=tendermint_test -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast test1 ## check output stdout '\("hello world!" string\)' diff --git a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar index 89da8a51820..f529c176f36 100644 --- a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar +++ b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar @@ -4,7 +4,7 @@ loadpkg gno.land/r/sys/users adduser admin adduser gui -patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $USER_ADDR_admin # use our custom admin +patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $admin_user_addr # use our custom admin gnoland start @@ -20,7 +20,7 @@ stdout 'false' # Gui should be able to addpkg on test1 addr # gui addpkg -> gno.land/r//mysuperpkg -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 400000 -broadcast -chainid=tendermint_test gui +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 400000 -broadcast -chainid=tendermint_test gui stdout 'OK!' # Gui should be able to addpkg on random name @@ -43,12 +43,12 @@ stdout 'true' # Try to add a pkg an with unregistered user # gui addpkg -> gno.land/r//one -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/one -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test gui +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/one -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test gui stderr 'unauthorized user' # Try to add a pkg with an unregistered user, on their own address as namespace # gui addpkg -> gno.land/r//one -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_gui/one -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test gui +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$gui_user_addr/one -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test gui stdout 'OK!' ## Test unregistered namespace @@ -63,12 +63,12 @@ stderr 'unauthorized user' # Test admin invites gui # admin call -> demo/users.Invite -gnokey maketx call -pkgpath gno.land/r/demo/users -func Invite -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $USER_ADDR_gui admin +gnokey maketx call -pkgpath gno.land/r/demo/users -func Invite -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $gui_user_addr admin stdout 'OK!' # test gui register namespace # gui call -> demo/users.Register -gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $USER_ADDR_admin -args 'guiland' -args 'im gui' gui +gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $admin_user_addr -args 'guiland' -args 'im gui' gui stdout 'OK!' # Test gui publishing on guiland/one diff --git a/gno.land/pkg/integration/testdata/adduserfrom.txtar b/gno.land/pkg/integration/testdata/adduserfrom.txtar index 47ec70b00e6..8bbfaa738fd 100644 --- a/gno.land/pkg/integration/testdata/adduserfrom.txtar +++ b/gno.land/pkg/integration/testdata/adduserfrom.txtar @@ -14,13 +14,13 @@ stdout 'g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp' gnoland start ## check users initial balance -gnokey query bank/balances/${USER_ADDR_user1} +gnokey query bank/balances/$user1_user_addr stdout '10000000ugnot' gnokey query bank/balances/g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4 stdout '10000000ugnot' -gnokey query auth/accounts/${USER_ADDR_user3} +gnokey query auth/accounts/$user3_user_addr stdout 'height: 0' stdout 'data: {' stdout ' "BaseAccount": {' diff --git a/gno.land/pkg/integration/testdata/event_multi_msg.txtar b/gno.land/pkg/integration/testdata/event_multi_msg.txtar index 13a448e7f8c..3c5667b73b0 100644 --- a/gno.land/pkg/integration/testdata/event_multi_msg.txtar +++ b/gno.land/pkg/integration/testdata/event_multi_msg.txtar @@ -1,29 +1,30 @@ # load the package from $WORK directory loadpkg gno.land/r/demo/simple_event $WORK/event +# add a random user +adduserfrom user1 'success myself purchase tray reject demise scene little legend someone lunar hope media goat regular test area smart save flee surround attack rapid smoke' +stdout 'g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0' + # start a new node gnoland start -## test1 account should be available on default -gnokey query auth/accounts/${USER_ADDR_test1} +## account should be available since it has an initial balance +gnokey query auth/accounts/g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0 stdout 'height: 0' stdout 'data: {' stdout ' "BaseAccount": {' -stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' +stdout ' "address": "'${user1_user_addr}'",' stdout ' "coins": "[0-9]*ugnot",' # dynamic -stdout ' "public_key": {' -stdout ' "@type": "/tm.PubKeySecp256k1",' -stdout ' "value": "A\+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"' -stdout ' },' -stdout ' "account_number": "0",' -stdout ' "sequence": "1"' +stdout ' "public_key": null,' +stdout ' "account_number": "'${user1_account_num}'",' +stdout ' "sequence": "'${user1_account_seq}'"' stdout ' }' stdout '}' ! stderr '.+' # empty ## sign -gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number 0 -account-sequence 1 test1 +gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number $user1_account_num -account-sequence $user1_account_seq user1 stdout 'Tx successfully signed and saved to ' ## broadcast @@ -49,5 +50,4 @@ func Event(value string) { } -- multi/multi_msg.tx -- -{"msg":[{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value11"]},{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value22"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":null,"memo":""} - +{"msg":[{"@type":"/vm.m_call","caller":"g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value11"]},{"@type":"/vm.m_call","caller":"g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value22"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":null,"memo":""} diff --git a/gno.land/pkg/integration/testdata/gnokey.txtar b/gno.land/pkg/integration/testdata/gnokey.txtar index 123a0ce291c..3268782b1ca 100644 --- a/gno.land/pkg/integration/testdata/gnokey.txtar +++ b/gno.land/pkg/integration/testdata/gnokey.txtar @@ -1,19 +1,22 @@ # test basic gnokey integrations commands # golden files have been generated using UPDATE_SCRIPTS=true +# add a random user +adduser user1 + # start gnoland gnoland start ## test1 account should be available on default -gnokey query auth/accounts/${USER_ADDR_test1} +gnokey query auth/accounts/$user1_user_addr stdout 'height: 0' stdout 'data: {' stdout ' "BaseAccount": {' -stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' +stdout ' "address": "'${user1_user_addr}'",' stdout ' "coins": "[0-9]*ugnot",' # dynamic stdout ' "public_key": null,' -stdout ' "account_number": "0",' -stdout ' "sequence": "0"' +stdout ' "account_number": "'${user1_account_num}'",' +stdout ' "sequence": "'${user1_account_seq}'"' stdout ' }' stdout '}' ! stderr '.+' # empty diff --git a/gno.land/pkg/integration/testdata/gnokey_simulate.txtar b/gno.land/pkg/integration/testdata/gnokey_simulate.txtar index db3cd527eb3..31b2249f8bb 100644 --- a/gno.land/pkg/integration/testdata/gnokey_simulate.txtar +++ b/gno.land/pkg/integration/testdata/gnokey_simulate.txtar @@ -6,41 +6,41 @@ loadpkg gno.land/r/hello $WORK/hello gnoland start # Initial state: assert that sequence == 0. -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "1"' # attempt adding the "test" package. # the package has a syntax error; simulation should catch this ahead of time and prevent the tx. # -simulate test ! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate test test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "1"' # -simulate only ! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "1"' # -simulate skip ! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "2"' # attempt calling hello.SetName correctly. # -simulate test and skip should do it successfully, -simulate only should not. # -simulate test gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args John -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate test test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "3"' gnokey query vm/qeval --data "gno.land/r/hello.Hello()" stdout 'Hello, John!' # -simulate only gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args Paul -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "3"' gnokey query vm/qeval --data "gno.land/r/hello.Hello()" stdout 'Hello, John!' # -simulate skip gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args George -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate skip test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "4"' gnokey query vm/qeval --data "gno.land/r/hello.Hello()" stdout 'Hello, George!' @@ -50,19 +50,19 @@ stdout 'Hello, George!' # none should change the name (ie. panic rollbacks). # -simulate test ! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate test test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "4"' gnokey query vm/qeval --data "gno.land/r/hello.Hello()" stdout 'Hello, George!' # -simulate only ! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "4"' gnokey query vm/qeval --data "gno.land/r/hello.Hello()" stdout 'Hello, George!' # -simulate skip ! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate skip test1 -gnokey query auth/accounts/$USER_ADDR_test1 +gnokey query auth/accounts/$test1_user_addr stdout '"sequence": "5"' gnokey query vm/qeval --data "gno.land/r/hello.Hello()" stdout 'Hello, George!' diff --git a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar index 02bd8058214..838db121442 100644 --- a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar +++ b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar @@ -4,32 +4,33 @@ # load the package from $WORK directory loadpkg gno.land/r/demo/echo +# add a random user +adduserfrom user1 'lamp any denial pulse used shoot gap error denial mansion hurry foot solution grab winner congress drastic cat bamboo chicken color digital coffee unknown' +stdout 'g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva' + # start the node gnoland start # Query account -gnokey query auth/accounts/${USER_ADDR_test1} +gnokey query auth/accounts/g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva stdout 'height: 0' stdout 'data: {' stdout ' "BaseAccount": {' -stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' +stdout ' "address": "g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva",' stdout ' "coins": "[0-9]*ugnot",' # dynamic -stdout ' "public_key": {' -stdout ' "@type": "/tm.PubKeySecp256k1",' -stdout ' "value": "A\+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"' -stdout ' },' -stdout ' "account_number": "0",' -stdout ' "sequence": "4"' +stdout ' "public_key": null,' +stdout ' "account_number": "57",' +stdout ' "sequence": "0"' stdout ' }' stdout '}' ! stderr '.+' # empty # Create transaction -gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -args "HELLO" test1 +gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -args "HELLO" user1 cp stdout call.tx # Sign -gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 0 -account-sequence 4 test1 +gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 57 -account-sequence 0 user1 cmpenv stdout sign.stdout.golden gnokey broadcast $WORK/call.tx diff --git a/gno.land/pkg/integration/testdata/patchpkg.txtar b/gno.land/pkg/integration/testdata/patchpkg.txtar index c5962709625..0a1a7fa993d 100644 --- a/gno.land/pkg/integration/testdata/patchpkg.txtar +++ b/gno.land/pkg/integration/testdata/patchpkg.txtar @@ -2,13 +2,13 @@ loadpkg gno.land/r/dev/admin $WORK adduser dev -patchpkg "g1abcde" $USER_ADDR_dev +patchpkg "g1abcde" $dev_user_addr gnoland start gnokey maketx call -pkgpath gno.land/r/dev/admin -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 ! stdout g1abcde -stdout $USER_ADDR_dev +stdout $dev_user_addr -- admin.gno -- package admin diff --git a/gno.land/pkg/integration/testdata/prevrealm.txtar b/gno.land/pkg/integration/testdata/prevrealm.txtar index 20317d87345..4bbe16c3205 100644 --- a/gno.land/pkg/integration/testdata/prevrealm.txtar +++ b/gno.land/pkg/integration/testdata/prevrealm.txtar @@ -30,65 +30,65 @@ loadpkg gno.land/p/demo/bar $WORK/p/demo/bar ## start a new node gnoland start -env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 +env RFOO_USER_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 # Test cases ## 1. MsgCall -> myrlm.A: user address gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 700000 -broadcast -chainid tendermint_test test1 -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} ## 2. MsgCall -> myrealm.B -> myrlm.A: user address gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 800000 -broadcast -chainid tendermint_test test1 -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} ## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 800000 -broadcast -chainid tendermint_test test1 -stdout ${RFOO_ADDR} +stdout ${RFOO_USER_ADDR} ## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 800000 -broadcast -chainid tendermint_test test1 -stdout ${RFOO_ADDR} +stdout ${RFOO_USER_ADDR} ## remove due to update to maketx call can only call realm (case 5, 6, 13) ## 5. MsgCall -> p/demo/bar.A: user address ## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 -## stdout ${USER_ADDR_test1} +## stdout ${test1_user_addr} ## 6. MsgCall -> p/demo/bar.B: user address ## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 -## stdout ${USER_ADDR_test1} +## stdout ${test1_user_addr} ## 7. MsgRun -> myrlm.A: user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} ## 8. MsgRun -> myrealm.B -> myrlm.A: user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} ## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno -stdout ${RFOO_ADDR} +stdout ${RFOO_USER_ADDR} ## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno -stdout ${RFOO_ADDR} +stdout ${RFOO_USER_ADDR} ## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} ## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} ## 13. MsgCall -> std.PrevRealm(): user address ## gnokey maketx call -pkgpath std -func PrevRealm -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 -## stdout ${USER_ADDR_test1} +## stdout ${test1_user_addr} ## 14. MsgRun -> std.PrevRealm(): user address gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno -stdout ${USER_ADDR_test1} +stdout ${test1_user_addr} -- r/myrlm/myrlm.gno -- package myrlm diff --git a/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar index be9a686bac6..a55604267ae 100644 --- a/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar +++ b/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar @@ -19,31 +19,31 @@ gnokey maketx addpkg -pkgdir $WORK/invalid_realm_denom -pkgpath gno.land/r/test/ gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 ## check test2 balance -gnokey query bank/balances/${USER_ADDR_test2} +gnokey query bank/balances/${test2_user_addr} stdout '' ## mint coin from banker -gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args ${USER_ADDR_test2} -args "ugnot" -args "31337" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args ${test2_user_addr} -args "ugnot" -args "31337" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 ## check balance after minting, without patching banker will return '31337ugnot' -gnokey query bank/balances/${USER_ADDR_test2} +gnokey query bank/balances/${test2_user_addr} stdout '"31337/gno.land/r/test/realm_banker:ugnot"' ## burn coin -gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args ${USER_ADDR_test2} -args "ugnot" -args "7" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args ${test2_user_addr} -args "ugnot" -args "7" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 ## check balance after burning -gnokey query bank/balances/${USER_ADDR_test2} +gnokey query bank/balances/${test2_user_addr} stdout '"31330/gno.land/r/test/realm_banker:ugnot"' ## transfer 1ugnot to test2 for gas-fee of below tx -gnokey maketx send -send "1ugnot" -to ${USER_ADDR_test2} -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx send -send "1ugnot" -to ${test2_user_addr} -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 ## transfer coin gnokey maketx send -send "1330/gno.land/r/test/realm_banker:ugnot" -to g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 ## check sender balance -gnokey query bank/balances/${USER_ADDR_test2} +gnokey query bank/balances/${test2_user_addr} stdout '"30000/gno.land/r/test/realm_banker:ugnot"' ## check receiver balance @@ -121,4 +121,4 @@ func Mint(addr std.Address, denom string, amount int64) { func Burn(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) banker.RemoveCoin(addr, denom, amount) -} \ No newline at end of file +} diff --git a/gno.land/pkg/integration/testdata/restart_missing_type.txtar b/gno.land/pkg/integration/testdata/restart_missing_type.txtar index 09e1a27d6f4..cc8ed702734 100644 --- a/gno.land/pkg/integration/testdata/restart_missing_type.txtar +++ b/gno.land/pkg/integration/testdata/restart_missing_type.txtar @@ -1,3 +1,7 @@ +# add a random user +adduserfrom user1 'bone make joy hospital hawk crew civil relief maple alter always frozen category emerge fun inflict room sphere casino vital scheme basket omit wrap' +stdout 'g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4' + # This txtar is a regression test for a bug, whereby a type is committed to # the defaultStore.cacheTypes map, but not to the underlying store (due to a # failing transaction). @@ -5,15 +9,15 @@ loadpkg gno.land/p/demo/avl gnoland start -gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 1 test1 +gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 -account-number 57 user1 ! gnokey broadcast $WORK/tx1.tx stderr 'out of gas' -gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 2 test1 +gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 -account-number 57 user1 gnokey broadcast $WORK/tx2.tx stdout 'OK!' -gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 3 test1 +gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 -account-number 57 user1 gnokey broadcast $WORK/tx3.tx stdout 'OK!' @@ -24,7 +28,7 @@ gnoland restart "msg": [ { "@type": "/vm.m_addpkg", - "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "creator": "g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4", "package": { "name": "zentasktic", "path": "gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic", @@ -99,7 +103,7 @@ gnoland restart "msg": [ { "@type": "/vm.m_addpkg", - "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "creator": "g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4", "package": { "name": "zentasktic", "path": "gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic", @@ -174,7 +178,7 @@ gnoland restart "msg": [ { "@type": "/vm.m_addpkg", - "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "creator": "g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4", "package": { "name": "zentasktic_core", "path": "gno.land/r/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic_core", diff --git a/gno.land/pkg/integration/testdata/simulate_gas.txtar b/gno.land/pkg/integration/testdata/simulate_gas.txtar index 4c5213da345..57be82b75ff 100644 --- a/gno.land/pkg/integration/testdata/simulate_gas.txtar +++ b/gno.land/pkg/integration/testdata/simulate_gas.txtar @@ -6,11 +6,11 @@ gnoland start # simulate only gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 -stdout 'GAS USED: 99015' +stdout 'GAS USED: 99339' # simulate skip gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 -stdout 'GAS USED: 99015' # same as simulate only +stdout 'GAS USED: 99339' # same as simulate only -- package/package.gno -- diff --git a/gno.land/pkg/integration/testscript_gnoland.go b/gno.land/pkg/integration/testscript_gnoland.go index 9781799ea7d..1531b83dfef 100644 --- a/gno.land/pkg/integration/testscript_gnoland.go +++ b/gno.land/pkg/integration/testscript_gnoland.go @@ -20,6 +20,9 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/keyscli" "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/amino" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -45,6 +48,7 @@ const ( envKeyPrivValKey envKeyExecCommand envKeyExecBin + envKeyBase ) type commandkind int @@ -158,13 +162,18 @@ func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error { } kb.ImportPrivKey(DefaultAccount_Name, defaultPK, "") - env.Setenv("USER_SEED_"+DefaultAccount_Name, DefaultAccount_Seed) - env.Setenv("USER_ADDR_"+DefaultAccount_Name, DefaultAccount_Address) + env.Setenv(DefaultAccount_Name+"_user_seed", DefaultAccount_Seed) + env.Setenv(DefaultAccount_Name+"_user_addr", DefaultAccount_Address) // New private key env.Values[envKeyPrivValKey] = ed25519.GenPrivKey() + + // Set gno dbdir env.Setenv("GNO_DBDIR", dbdir) + // Setup account store + env.Values[envKeyBase] = kb + // Generate node short id var sid string { @@ -215,6 +224,7 @@ func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error { "adduserfrom": adduserfromCmd(nodesManager), "patchpkg": patchpkgCmd(), "loadpkg": loadpkgCmd(gnoRootDir), + "scanf": loadpkgCmd(gnoRootDir), } // Initialize cmds map if needed @@ -305,8 +315,11 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun }) nodesManager.Set(sid, &tNodeProcess{NodeProcess: nodep, cfg: cfg}) - ts.Setenv("RPC_ADDR", nodep.Address()) + + // Load user infos + loadUserEnv(ts, nodep.Address()) + fmt.Fprintln(ts.Stdout(), "node started successfully") case "restart": @@ -337,6 +350,9 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun ts.Setenv("RPC_ADDR", nodep.Address()) nodesManager.Set(sid, &tNodeProcess{NodeProcess: nodep, cfg: node.cfg}) + // Load user infos + loadUserEnv(ts, nodep.Address()) + fmt.Fprintln(ts.Stdout(), "node restarted successfully") case "stop": @@ -534,6 +550,64 @@ func loadpkgCmd(gnoRootDir string) func(ts *testscript.TestScript, neg bool, arg } } +func loadUserEnv(ts *testscript.TestScript, remote string) error { + const path = "auth/accounts" + + // List all accounts + kb := ts.Value(envKeyBase).(keys.Keybase) + accounts, err := kb.List() + if err != nil { + ts.Fatalf("query accounts: unable to list keys: %s", err) + } + + cli, err := rpcclient.NewHTTPClient(remote) + if err != nil { + return fmt.Errorf("unable create rpc client %q: %w", remote, err) + } + + batch := cli.NewBatch() + for _, account := range accounts { + accountPath := filepath.Join(path, account.GetAddress().String()) + if err := batch.ABCIQuery(accountPath, []byte{}); err != nil { + return fmt.Errorf("unable to create query request: %w", err) + } + } + + batchRes, err := batch.Send(context.Background()) + if err != nil { + return fmt.Errorf("unable to query accounts: %w", err) + } + + if len(batchRes) != len(accounts) { + ts.Fatalf("query accounts: len(res) != len(accounts)") + } + + for i, res := range batchRes { + account := accounts[i] + name := account.GetName() + qres := res.(*ctypes.ResultABCIQuery) + + if err := qres.Response.Error; err != nil { + ts.Fatalf("query account %q error: %s", account.GetName(), err.Error()) + } + + var qret struct{ BaseAccount std.BaseAccount } + if err = amino.UnmarshalJSON(qres.Response.Data, &qret); err != nil { + ts.Fatalf("query account %q unarmshal error: %s", account.GetName(), err.Error()) + } + + strAccountNumber := strconv.Itoa(int(qret.BaseAccount.GetAccountNumber())) + ts.Setenv(name+"_account_num", strAccountNumber) + ts.Logf("[%q] account number: %s", name, strAccountNumber) + + strAccountSequence := strconv.Itoa(int(qret.BaseAccount.GetSequence())) + ts.Setenv(name+"_account_seq", strAccountSequence) + ts.Logf("[%q] account sequence: %s", name, strAccountNumber) + } + + return nil +} + type tsLogWriter struct { ts *testscript.TestScript } @@ -589,94 +663,8 @@ func setupNode(ts *testscript.TestScript, ctx context.Context, cfg *ProcessNodeC return nil } -// `unquote` takes a slice of strings, resulting from splitting a string block by spaces, and -// processes them. The function handles quoted phrases and escape characters within these strings. -func unquote(args []string) ([]string, error) { - const quote = '"' - - parts := []string{} - var inQuote bool - - var part strings.Builder - for _, arg := range args { - var escaped bool - for _, c := range arg { - if escaped { - // If the character is meant to be escaped, it is processed with Unquote. - // We use `Unquote` here for two main reasons: - // 1. It will validate that the escape sequence is correct - // 2. It converts the escaped string to its corresponding raw character. - // For example, "\\t" becomes '\t'. - uc, err := strconv.Unquote(`"\` + string(c) + `"`) - if err != nil { - return nil, fmt.Errorf("unhandled escape sequence `\\%c`: %w", c, err) - } - - part.WriteString(uc) - escaped = false - continue - } - - // If we are inside a quoted string and encounter an escape character, - // flag the next character as `escaped` - if inQuote && c == '\\' { - escaped = true - continue - } - - // Detect quote and toggle inQuote state - if c == quote { - inQuote = !inQuote - continue - } - - // Handle regular character - part.WriteRune(c) - } - - // If we're inside a quote, add a single space. - // It reflects one or multiple spaces between args in the original string. - if inQuote { - part.WriteRune(' ') - continue - } - - // Finalize part, add to parts, and reset for next part - parts = append(parts, part.String()) - part.Reset() - } - - // Check if a quote is left open - if inQuote { - return nil, errors.New("unfinished quote") - } - - return parts, nil -} - -func getNodeSID(ts *testscript.TestScript) string { - return ts.Getenv("SID") -} - -func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { - if err != nil { - fmt.Fprintf(ts.Stderr(), "%q error: %+v\n", cmd, err) - if !neg { - ts.Fatalf("unexpected %q command failure: %s", cmd, err) - } - } else { - if neg { - ts.Fatalf("unexpected %q command success", cmd) - } - } -} - -type envSetter interface { - Setenv(key, value string) -} - // createAccount creates a new account with the given name and adds it to the keybase. -func createAccount(env envSetter, kb keys.Keybase, accountName string) (gnoland.Balance, error) { +func createAccount(ts *testscript.TestScript, kb keys.Keybase, accountName string) (gnoland.Balance, error) { var balance gnoland.Balance entropy, err := bip39.NewEntropy(256) if err != nil { @@ -688,23 +676,11 @@ func createAccount(env envSetter, kb keys.Keybase, accountName string) (gnoland. return balance, fmt.Errorf("error generating mnemonic: %w", err) } - var keyInfo keys.Info - if keyInfo, err = kb.CreateAccount(accountName, mnemonic, "", "", 0, 0); err != nil { - return balance, fmt.Errorf("unable to create account: %w", err) - } - - address := keyInfo.GetAddress() - env.Setenv("USER_SEED_"+accountName, mnemonic) - env.Setenv("USER_ADDR_"+accountName, address.String()) - - return gnoland.Balance{ - Address: address, - Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)}, - }, nil + return createAccountFrom(ts, kb, accountName, mnemonic, 0, 0) } // createAccountFrom creates a new account with the given metadata and adds it to the keybase. -func createAccountFrom(env envSetter, kb keys.Keybase, accountName, mnemonic string, account, index uint32) (gnoland.Balance, error) { +func createAccountFrom(ts *testscript.TestScript, kb keys.Keybase, accountName, mnemonic string, account, index uint32) (gnoland.Balance, error) { var balance gnoland.Balance // check if mnemonic is valid @@ -718,8 +694,8 @@ func createAccountFrom(env envSetter, kb keys.Keybase, accountName, mnemonic str } address := keyInfo.GetAddress() - env.Setenv("USER_SEED_"+accountName, mnemonic) - env.Setenv("USER_ADDR_"+accountName, address.String()) + ts.Setenv(accountName+"_user_seed", mnemonic) + ts.Setenv(accountName+"_user_addr", address.String()) return gnoland.Balance{ Address: address, @@ -787,3 +763,20 @@ func GeneratePrivKeyFromMnemonic(mnemonic, bip39Passphrase string, account, inde privKey := secp256k1.PrivKeySecp256k1(derivedPriv) return privKey, nil } + +func getNodeSID(ts *testscript.TestScript) string { + return ts.Getenv("SID") +} + +func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { + if err != nil { + fmt.Fprintf(ts.Stderr(), "%q error: %+v\n", cmd, err) + if !neg { + ts.Fatalf("unexpected %q command failure: %s", cmd, err) + } + } else { + if neg { + ts.Fatalf("unexpected %q command success", cmd) + } + } +} diff --git a/gno.land/pkg/integration/utils.go b/gno.land/pkg/integration/utils.go new file mode 100644 index 00000000000..bc9e7f1e220 --- /dev/null +++ b/gno.land/pkg/integration/utils.go @@ -0,0 +1,73 @@ +package integration + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +// `unquote` takes a slice of strings, resulting from splitting a string block by spaces, and +// processes them. The function handles quoted phrases and escape characters within these strings. +func unquote(args []string) ([]string, error) { + const quote = '"' + + parts := []string{} + var inQuote bool + + var part strings.Builder + for _, arg := range args { + var escaped bool + for _, c := range arg { + if escaped { + // If the character is meant to be escaped, it is processed with Unquote. + // We use `Unquote` here for two main reasons: + // 1. It will validate that the escape sequence is correct + // 2. It converts the escaped string to its corresponding raw character. + // For example, "\\t" becomes '\t'. + uc, err := strconv.Unquote(`"\` + string(c) + `"`) + if err != nil { + return nil, fmt.Errorf("unhandled escape sequence `\\%c`: %w", c, err) + } + + part.WriteString(uc) + escaped = false + continue + } + + // If we are inside a quoted string and encounter an escape character, + // flag the next character as `escaped` + if inQuote && c == '\\' { + escaped = true + continue + } + + // Detect quote and toggle inQuote state + if c == quote { + inQuote = !inQuote + continue + } + + // Handle regular character + part.WriteRune(c) + } + + // If we're inside a quote, add a single space. + // It reflects one or multiple spaces between args in the original string. + if inQuote { + part.WriteRune(' ') + continue + } + + // Finalize part, add to parts, and reset for next part + parts = append(parts, part.String()) + part.Reset() + } + + // Check if a quote is left open + if inQuote { + return nil, errors.New("unfinished quote") + } + + return parts, nil +} diff --git a/gno.land/pkg/integration/testscript_gnoland_test.go b/gno.land/pkg/integration/utils_test.go similarity index 100% rename from gno.land/pkg/integration/testscript_gnoland_test.go rename to gno.land/pkg/integration/utils_test.go diff --git a/gnovm/cmd/gno/download_deps.go b/gnovm/cmd/gno/download_deps.go index 5a8c50be20b..4e638eb4970 100644 --- a/gnovm/cmd/gno/download_deps.go +++ b/gnovm/cmd/gno/download_deps.go @@ -25,10 +25,11 @@ func downloadDeps(io commands.IO, pkgDir string, gnoMod *gnomod.File, fetcher pk if err != nil { return fmt.Errorf("read package at %q: %w", pkgDir, err) } - imports, err := packages.Imports(pkg, nil) + importsMap, err := packages.Imports(pkg, nil) if err != nil { return fmt.Errorf("read imports at %q: %w", pkgDir, err) } + imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest) for _, pkgPath := range imports { resolved := gnoMod.Resolve(module.Version{Path: pkgPath.PkgPath}) diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go index b287fd20708..4c481324e9a 100644 --- a/gnovm/pkg/doc/dirs.go +++ b/gnovm/pkg/doc/dirs.go @@ -105,11 +105,12 @@ func packageImportsRecursive(root string, pkgPath string) []string { pkg = &gnovm.MemPackage{} } - resRaw, err := packages.Imports(pkg, nil) + importsMap, err := packages.Imports(pkg, nil) if err != nil { // ignore packages with invalid imports - resRaw = nil + importsMap = nil } + resRaw := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest) res := make([]string, len(resRaw)) for idx, imp := range resRaw { res[idx] = imp.PkgPath diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go index 89458667997..5a8c6faf315 100644 --- a/gnovm/pkg/gnolang/gno_test.go +++ b/gnovm/pkg/gnolang/gno_test.go @@ -36,6 +36,8 @@ func setupMachine(b *testing.B, numValues, numStmts, numExprs, numBlocks, numFra func BenchmarkStringLargeData(b *testing.B) { m := setupMachine(b, 10000, 5000, 5000, 2000, 3000, 1000) + b.ResetTimer() + b.ReportAllocs() for i := 0; i < b.N; i++ { _ = m.String() diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 4480a89d16f..75d12ac5402 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -2117,6 +2117,10 @@ func (m *Machine) Printf(format string, args ...interface{}) { } func (m *Machine) String() string { + if m == nil { + return "Machine:nil" + } + // Calculate some reasonable total length to avoid reallocation // Assuming an average length of 32 characters per string var ( @@ -2131,25 +2135,26 @@ func (m *Machine) String() string { totalLength = vsLength + ssLength + xsLength + bsLength + obsLength + fsLength + exceptionsLength ) - var builder strings.Builder + var sb strings.Builder + builder := &sb // Pointer for use in fmt.Fprintf. builder.Grow(totalLength) - builder.WriteString(fmt.Sprintf("Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues)) + fmt.Fprintf(builder, "Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues) for i := m.NumValues - 1; i >= 0; i-- { - builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Values[i])) + fmt.Fprintf(builder, " #%d %v\n", i, m.Values[i]) } builder.WriteString(" Exprs:\n") for i := len(m.Exprs) - 1; i >= 0; i-- { - builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Exprs[i])) + fmt.Fprintf(builder, " #%d %v\n", i, m.Exprs[i]) } builder.WriteString(" Stmts:\n") for i := len(m.Stmts) - 1; i >= 0; i-- { - builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Stmts[i])) + fmt.Fprintf(builder, " #%d %v\n", i, m.Stmts[i]) } builder.WriteString(" Blocks:\n") @@ -2166,17 +2171,17 @@ func (m *Machine) String() string { if pv, ok := b.Source.(*PackageNode); ok { // package blocks have too much, so just // print the pkgpath. - builder.WriteString(fmt.Sprintf(" %s(%d) %s\n", gens, gen, pv.PkgPath)) + fmt.Fprintf(builder, " %s(%d) %s\n", gens, gen, pv.PkgPath) } else { bsi := b.StringIndented(" ") - builder.WriteString(fmt.Sprintf(" %s(%d) %s\n", gens, gen, bsi)) + fmt.Fprintf(builder, " %s(%d) %s\n", gens, gen, bsi) if b.Source != nil { sb := b.GetSource(m.Store).GetStaticBlock().GetBlock() - builder.WriteString(fmt.Sprintf(" (s vals) %s(%d) %s\n", gens, gen, sb.StringIndented(" "))) + fmt.Fprintf(builder, " (s vals) %s(%d) %s\n", gens, gen, sb.StringIndented(" ")) sts := b.GetSource(m.Store).GetStaticBlock().Types - builder.WriteString(fmt.Sprintf(" (s typs) %s(%d) %s\n", gens, gen, sts)) + fmt.Fprintf(builder, " (s typs) %s(%d) %s\n", gens, gen, sts) } } @@ -2187,7 +2192,7 @@ func (m *Machine) String() string { case *Block: b = bp case RefValue: - builder.WriteString(fmt.Sprintf(" (block ref %v)\n", bp.ObjectID)) + fmt.Fprintf(builder, " (block ref %v)\n", bp.ObjectID) b = nil default: panic("should not happen") @@ -2206,12 +2211,12 @@ func (m *Machine) String() string { if _, ok := b.Source.(*PackageNode); ok { break // done, skip *PackageNode. } else { - builder.WriteString(fmt.Sprintf(" #%d %s\n", i, - b.StringIndented(" "))) + fmt.Fprintf(builder, " #%d %s\n", i, + b.StringIndented(" ")) if b.Source != nil { sb := b.GetSource(m.Store).GetStaticBlock().GetBlock() - builder.WriteString(fmt.Sprintf(" (static) #%d %s\n", i, - sb.StringIndented(" "))) + fmt.Fprintf(builder, " (static) #%d %s\n", i, + sb.StringIndented(" ")) } } } @@ -2219,17 +2224,17 @@ func (m *Machine) String() string { builder.WriteString(" Frames:\n") for i := len(m.Frames) - 1; i >= 0; i-- { - builder.WriteString(fmt.Sprintf(" #%d %s\n", i, m.Frames[i])) + fmt.Fprintf(builder, " #%d %s\n", i, m.Frames[i]) } if m.Realm != nil { - builder.WriteString(fmt.Sprintf(" Realm:\n %s\n", m.Realm.Path)) + fmt.Fprintf(builder, " Realm:\n %s\n", m.Realm.Path) } builder.WriteString(" Exceptions:\n") for _, ex := range m.Exceptions { - builder.WriteString(fmt.Sprintf(" %s\n", ex.Sprint(m))) + fmt.Fprintf(builder, " %s\n", ex.Sprint(m)) } return builder.String() diff --git a/gnovm/pkg/gnolang/machine_test.go b/gnovm/pkg/gnolang/machine_test.go index c3b2118f099..c2ab8ea12c5 100644 --- a/gnovm/pkg/gnolang/machine_test.go +++ b/gnovm/pkg/gnolang/machine_test.go @@ -9,6 +9,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/dbadapter" "github.com/gnolang/gno/tm2/pkg/store/iavl" stypes "github.com/gnolang/gno/tm2/pkg/store/types" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" ) @@ -56,3 +57,61 @@ func TestRunMemPackageWithOverrides_revertToOld(t *testing.T) { assert.Equal(t, StringKind, v.T.Kind()) assert.Equal(t, StringValue("1"), v.V) } + +func TestMachineString(t *testing.T) { + cases := []struct { + name string + in *Machine + want string + }{ + { + "nil Machine", + nil, + "Machine:nil", + }, + { + "created with defaults", + NewMachineWithOptions(MachineOptions{}), + "Machine:\n PreprocessorMode: false\n Op: []\n Values: (len: 0)\n Exprs:\n Stmts:\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n", + }, + { + "created with store and defaults", + func() *Machine { + db := memdb.NewMemDB() + baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{}) + iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{}) + store := NewStore(nil, baseStore, iavlStore) + return NewMachine("std", store) + }(), + "Machine:\n PreprocessorMode: false\n Op: []\n Values: (len: 0)\n Exprs:\n Stmts:\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n", + }, + { + "filled in", + func() *Machine { + db := memdb.NewMemDB() + baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{}) + iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{}) + store := NewStore(nil, baseStore, iavlStore) + m := NewMachine("std", store) + m.PushOp(OpHalt) + m.PushExpr(&BasicLitExpr{ + Kind: INT, + Value: "100", + }) + m.Blocks = make([]*Block, 1, 1) + m.PushStmts(S(Call(X("Redecl"), 11))) + return m + }(), + "Machine:\n PreprocessorMode: false\n Op: [OpHalt]\n Values: (len: 0)\n Exprs:\n #0 100\n Stmts:\n #0 Redecl(11)\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got := tt.in.String() + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Fatalf("Mismatch: got - want +\n%s", diff) + } + }) + } +} diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 0496d37ed72..b85d1ac7026 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -153,10 +153,8 @@ const ( ATTR_TYPE_VALUE GnoAttribute = "ATTR_TYPE_VALUE" ATTR_TYPEOF_VALUE GnoAttribute = "ATTR_TYPEOF_VALUE" ATTR_IOTA GnoAttribute = "ATTR_IOTA" - ATTR_LOCATIONED GnoAttribute = "ATTR_LOCATIONE" // XXX DELETE - ATTR_GOTOLOOP_STMT GnoAttribute = "ATTR_GOTOLOOP_STMT" // XXX delete? - ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops. - ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used. + ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops. + ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used. ATTR_SHIFT_RHS GnoAttribute = "ATTR_SHIFT_RHS" ATTR_LAST_BLOCK_STMT GnoAttribute = "ATTR_LAST_BLOCK_STMT" ) diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 79695d8888a..ddfd1851989 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2723,17 +2723,10 @@ func findGotoLoopDefines(ctx BlockNode, bn BlockNode) { // Otherwise mark stmt as gotoloop. case Stmt: // we're done if we - // re-encounter origGotoStmtm. + // re-encounter origGotoStmt. if n == origGoto { - n.SetAttribute( - ATTR_GOTOLOOP_STMT, - true) return n, TRANS_EXIT // done } - // otherwise set attribute. - n.SetAttribute( - ATTR_GOTOLOOP_STMT, - true) return n, TRANS_CONTINUE // Special case, maybe convert // NameExprTypeDefine to @@ -4804,9 +4797,6 @@ func setNodeLines(n Node) { // based on sparse expectations on block nodes, and ensures uniqueness of BlockNode.Locations. // Ensures uniqueness of BlockNode.Locations. func setNodeLocations(pkgPath string, fileName string, n Node) { - if n.GetAttribute(ATTR_LOCATIONED) == true { - return // locations already set (typically n is a filenode). - } if pkgPath == "" || fileName == "" { panic("missing package path or file name") } diff --git a/gnovm/pkg/gnomod/pkg.go b/gnovm/pkg/gnomod/pkg.go index a0831d494b0..85f1d31442d 100644 --- a/gnovm/pkg/gnomod/pkg.go +++ b/gnovm/pkg/gnomod/pkg.go @@ -121,11 +121,13 @@ func ListPkgs(root string) (PkgList, error) { pkg = &gnovm.MemPackage{} } - importsRaw, err := packages.Imports(pkg, nil) + importsMap, err := packages.Imports(pkg, nil) if err != nil { // ignore imports on error - importsRaw = nil + importsMap = nil } + importsRaw := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest) + imports := make([]string, 0, len(importsRaw)) for _, imp := range importsRaw { // remove self and standard libraries from imports diff --git a/gnovm/pkg/packages/filekind.go b/gnovm/pkg/packages/filekind.go new file mode 100644 index 00000000000..ed2ca84b7d0 --- /dev/null +++ b/gnovm/pkg/packages/filekind.go @@ -0,0 +1,56 @@ +package packages + +import ( + "fmt" + "go/parser" + "go/token" + "strings" +) + +// FileKind represent the category a gno source file falls in, can be one of: +// +// - [FileKindPackageSource] -> A *.gno file that will be included in the gnovm package +// +// - [FileKindTest] -> A *_test.gno file that will be used for testing +// +// - [FileKindXTest] -> A *_test.gno file with a package name ending in _test that will be used for blackbox testing +// +// - [FileKindFiletest] -> A *_filetest.gno file that will be used for snapshot testing +type FileKind uint + +const ( + FileKindUnknown FileKind = iota + FileKindPackageSource + FileKindTest + FileKindXTest + FileKindFiletest +) + +// GetFileKind analyzes a file's name and body to get it's [FileKind], fset is optional +func GetFileKind(filename string, body string, fset *token.FileSet) (FileKind, error) { + if !strings.HasSuffix(filename, ".gno") { + return FileKindUnknown, fmt.Errorf("%s:1:1: not a gno file", filename) + } + + if strings.HasSuffix(filename, "_filetest.gno") { + return FileKindFiletest, nil + } + + if !strings.HasSuffix(filename, "_test.gno") { + return FileKindPackageSource, nil + } + + if fset == nil { + fset = token.NewFileSet() + } + ast, err := parser.ParseFile(fset, filename, body, parser.PackageClauseOnly) + if err != nil { + return FileKindUnknown, err + } + packageName := ast.Name.Name + + if strings.HasSuffix(packageName, "_test") { + return FileKindXTest, nil + } + return FileKindTest, nil +} diff --git a/gnovm/pkg/packages/filekind_test.go b/gnovm/pkg/packages/filekind_test.go new file mode 100644 index 00000000000..bd06b49fb45 --- /dev/null +++ b/gnovm/pkg/packages/filekind_test.go @@ -0,0 +1,63 @@ +package packages + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetFileKind(t *testing.T) { + tcs := []struct { + name string + filename string + body string + fileKind FileKind + errorContains string + }{ + { + name: "compiled", + filename: "foo.gno", + fileKind: FileKindPackageSource, + }, + { + name: "test", + filename: "foo_test.gno", + body: "package foo", + fileKind: FileKindTest, + }, + { + name: "xtest", + filename: "foo_test.gno", + body: "package foo_test", + fileKind: FileKindXTest, + }, + { + name: "filetest", + filename: "foo_filetest.gno", + fileKind: FileKindFiletest, + }, + { + name: "err_badpkgclause", + filename: "foo_test.gno", + body: "pakage foo", + errorContains: "foo_test.gno:1:1: expected 'package', found pakage", + }, + { + name: "err_notgnofile", + filename: "foo.gno.bck", + errorContains: `foo.gno.bck:1:1: not a gno file`, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + out, err := GetFileKind(tc.filename, tc.body, nil) + if len(tc.errorContains) != 0 { + require.ErrorContains(t, err, tc.errorContains) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.fileKind, out) + }) + } +} diff --git a/gnovm/pkg/packages/imports.go b/gnovm/pkg/packages/imports.go index 201965bc588..3bc60be6664 100644 --- a/gnovm/pkg/packages/imports.go +++ b/gnovm/pkg/packages/imports.go @@ -14,33 +14,40 @@ import ( // Imports returns the list of gno imports from a [gnovm.MemPackage]. // fset is optional. -func Imports(pkg *gnovm.MemPackage, fset *token.FileSet) ([]FileImport, error) { - allImports := make([]FileImport, 0, 16) - seen := make(map[string]struct{}, 16) +func Imports(pkg *gnovm.MemPackage, fset *token.FileSet) (ImportsMap, error) { + res := make(ImportsMap, 16) + seen := make(map[FileKind]map[string]struct{}, 16) + for _, file := range pkg.Files { if !strings.HasSuffix(file.Name, ".gno") { continue } - if strings.HasSuffix(file.Name, "_filetest.gno") { - continue + + fileKind, err := GetFileKind(file.Name, file.Body, fset) + if err != nil { + return nil, err } imports, err := FileImports(file.Name, file.Body, fset) if err != nil { return nil, err } for _, im := range imports { - if _, ok := seen[im.PkgPath]; ok { + if _, ok := seen[fileKind][im.PkgPath]; ok { continue } - allImports = append(allImports, im) - seen[im.PkgPath] = struct{}{} + res[fileKind] = append(res[fileKind], im) + if _, ok := seen[fileKind]; !ok { + seen[fileKind] = make(map[string]struct{}, 16) + } + seen[fileKind][im.PkgPath] = struct{}{} } } - sort.Slice(allImports, func(i, j int) bool { - return allImports[i].PkgPath < allImports[j].PkgPath - }) - return allImports, nil + for _, imports := range res { + sortImports(imports) + } + + return res, nil } // FileImport represents an import @@ -75,3 +82,31 @@ func FileImports(filename string, src string, fset *token.FileSet) ([]FileImport } return res, nil } + +type ImportsMap map[FileKind][]FileImport + +// Merge merges imports, it removes duplicates and sorts the result +func (imap ImportsMap) Merge(kinds ...FileKind) []FileImport { + res := make([]FileImport, 0, 16) + seen := make(map[string]struct{}, 16) + + for _, kind := range kinds { + for _, im := range imap[kind] { + if _, ok := seen[im.PkgPath]; ok { + continue + } + seen[im.PkgPath] = struct{}{} + + res = append(res, im) + } + } + + sortImports(res) + return res +} + +func sortImports(imports []FileImport) { + sort.Slice(imports, func(i, j int) bool { + return imports[i].PkgPath < imports[j].PkgPath + }) +} diff --git a/gnovm/pkg/packages/imports_test.go b/gnovm/pkg/packages/imports_test.go index 3750aa9108c..f9f58b967c8 100644 --- a/gnovm/pkg/packages/imports_test.go +++ b/gnovm/pkg/packages/imports_test.go @@ -58,13 +58,26 @@ func TestImports(t *testing.T) { ) `, }, + { + name: "file2_test.gno", + data: ` + package tmp_test + + import ( + "testing" + + "gno.land/p/demo/testpkg" + "gno.land/p/demo/xtestdep" + ) + `, + }, { name: "z_0_filetest.gno", data: ` package main import ( - "gno.land/p/demo/filetestpkg" + "gno.land/p/demo/filetestdep" ) `, }, @@ -95,17 +108,28 @@ func TestImports(t *testing.T) { }, } - // Expected list of imports + // Expected lists of imports // - ignore subdirs // - ignore duplicate - // - ignore *_filetest.gno // - should be sorted - expected := []string{ - "gno.land/p/demo/pkg1", - "gno.land/p/demo/pkg2", - "gno.land/p/demo/testpkg", - "std", - "testing", + expected := map[FileKind][]string{ + FileKindPackageSource: { + "gno.land/p/demo/pkg1", + "gno.land/p/demo/pkg2", + "std", + }, + FileKindTest: { + "gno.land/p/demo/testpkg", + "testing", + }, + FileKindXTest: { + "gno.land/p/demo/testpkg", + "gno.land/p/demo/xtestdep", + "testing", + }, + FileKindFiletest: { + "gno.land/p/demo/filetestdep", + }, } // Create subpkg dir @@ -120,12 +144,19 @@ func TestImports(t *testing.T) { pkg, err := gnolang.ReadMemPackage(tmpDir, "test") require.NoError(t, err) - imports, err := Imports(pkg, nil) + + importsMap, err := Imports(pkg, nil) require.NoError(t, err) - importsStrings := make([]string, len(imports)) - for idx, imp := range imports { - importsStrings[idx] = imp.PkgPath + + // ignore specs + got := map[FileKind][]string{} + for key, vals := range importsMap { + stringVals := make([]string, len(vals)) + for i, val := range vals { + stringVals[i] = val.PkgPath + } + got[key] = stringVals } - require.Equal(t, expected, importsStrings) + require.Equal(t, expected, got) } diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index 8b24fdeaa77..a8dd709e501 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -242,10 +242,11 @@ func LoadImports(store gno.Store, memPkg *gnovm.MemPackage) (err error) { }() fset := token.NewFileSet() - imports, err := packages.Imports(memPkg, fset) + importsMap, err := packages.Imports(memPkg, fset) if err != nil { return err } + imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest) for _, imp := range imports { if gno.IsRealmPath(imp.PkgPath) { // Don't eagerly load realms. diff --git a/go.mod b/go.mod index 280ca3ae602..cd038e2ae65 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fortytw2/leaktest v1.3.0 + github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 github.com/gorilla/websocket v1.5.3 github.com/libp2p/go-buffer-pool v0.1.0 diff --git a/misc/docs-linter/jsx.go b/misc/docs-linter/jsx.go index d0307680a0c..eb041a78386 100644 --- a/misc/docs-linter/jsx.go +++ b/misc/docs-linter/jsx.go @@ -50,7 +50,7 @@ func lintJSX(filepathToJSX map[string][]string) (string, error) { found = true } - output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", tag, filePath)) + fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", tag, filePath) } } diff --git a/misc/docs-linter/links.go b/misc/docs-linter/links.go index 744917d8dfb..e34d35d9f58 100644 --- a/misc/docs-linter/links.go +++ b/misc/docs-linter/links.go @@ -80,7 +80,7 @@ func lintLocalLinks(filepathToLinks map[string][]string, docsPath string) (strin found = true } - output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", link, filePath)) + fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", link, filePath) } } } diff --git a/misc/docs-linter/main.go b/misc/docs-linter/main.go index 97d80316108..5d7cdf37982 100644 --- a/misc/docs-linter/main.go +++ b/misc/docs-linter/main.go @@ -61,8 +61,8 @@ func execLint(cfg *cfg, ctx context.Context) (string, error) { } // Main buffer to write to the end user after linting - var output bytes.Buffer - output.WriteString(fmt.Sprintf("Linting %s...\n", absPath)) + var output bytes.Buffer + fmt.Fprintf(&output, "Linting %s...\n", absPath) // Find docs files to lint mdFiles, err := findFilePaths(cfg.docsPath) diff --git a/misc/docs-linter/urls.go b/misc/docs-linter/urls.go index 093e624d81e..098d0a05524 100644 --- a/misc/docs-linter/urls.go +++ b/misc/docs-linter/urls.go @@ -66,7 +66,7 @@ func lintURLs(filepathToURLs map[string][]string, ctx context.Context) (string, found = true } - output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", url, filePath)) + fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", url, filePath) lock.Unlock() } diff --git a/misc/loop/scripts/start.sh b/misc/loop/scripts/start.sh index db36de39f2a..bdabd2ac40f 100755 --- a/misc/loop/scripts/start.sh +++ b/misc/loop/scripts/start.sh @@ -11,10 +11,11 @@ GENESIS_BALANCES_FILE=${GENESIS_BALANCES_FILE:-""} SEEDS=${SEEDS:-""} PERSISTENT_PEERS=${PERSISTENT_PEERS:-""} +FINAL_GENESIS_TXS_SHEET="/gnoroot/gno.land/genesis/genesis_txs.jsonl" -echo "" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +echo "" >> $FINAL_GENESIS_TXS_SHEET echo "" >> /gnoroot/gno.land/genesis/genesis_balances.jsonl -cat "${GENESIS_BACKUP_FILE}" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +cat "${GENESIS_BACKUP_FILE}" >> $FINAL_GENESIS_TXS_SHEET cat "${GENESIS_BALANCES_FILE}" >> /gnoroot/gno.land/genesis/genesis_balances.jsonl # Initialize the secrets @@ -35,5 +36,7 @@ gnoland config set p2p.persistent_peers "${PERSISTENT_PEERS}" # reading and piping to the gnoland genesis commands exec gnoland start \ --chainid="${CHAIN_ID}" \ + --genesis-txs-file="${FINAL_GENESIS_TXS_SHEET}" \ --lazy \ - --skip-failing-genesis-txs + --skip-failing-genesis-txs \ + --skip-genesis-sig-verification diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 88ee26da4a9..9e10596a975 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -939,7 +939,7 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st for _, name := range noArgNames { link := fmt.Sprintf("//%s/%s", r.Host, name) - buf.WriteString(fmt.Sprintf("%s
", link, link)) + fmt.Fprintf(buf, "%s
", link, link) } buf.WriteString("
Endpoints that require arguments:
") @@ -952,7 +952,7 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st link += "&" } } - buf.WriteString(fmt.Sprintf("%s
", link, link)) + fmt.Fprintf(buf, "%s
", link, link) } buf.WriteString("") w.Header().Set("Content-Type", "text/html") diff --git a/tm2/pkg/bft/rpc/lib/server/write_endpoints_test.go b/tm2/pkg/bft/rpc/lib/server/write_endpoints_test.go new file mode 100644 index 00000000000..b01144f9273 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/server/write_endpoints_test.go @@ -0,0 +1,33 @@ +package rpcserver + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +func TestWriteListOfEndpoints(t *testing.T) { + funcMap := map[string]*RPCFunc{ + "c": NewWSRPCFunc(func(ctx *types.Context, s string, i int) (string, error) { return "foo", nil }, "s,i"), + "d": {}, + } + + req, _ := http.NewRequest("GET", "http://localhost/", nil) + rec := httptest.NewRecorder() + writeListOfEndpoints(rec, req, funcMap) + res := rec.Result() + assert.Equal(t, res.StatusCode, 200, "Should always return 200") + blob, err := io.ReadAll(res.Body) + assert.NoError(t, err) + gotResp := string(blob) + wantResp := `
Available endpoints:
//localhost/d

Endpoints that require arguments:
//localhost/c?s=_&i=_
` + if diff := cmp.Diff(gotResp, wantResp); diff != "" { + t.Fatalf("Mismatch response: got - want +\n%s", diff) + } +} diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index f05a8eff0a7..f941f398b17 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -215,7 +215,7 @@ func processSig( ctx sdk.Context, acc std.Account, sig std.Signature, signBytes []byte, simulate bool, params Params, sigGasConsumer SignatureVerificationGasConsumer, ) (updatedAcc std.Account, res sdk.Result) { - pubKey, res := ProcessPubKey(acc, sig, simulate) + pubKey, res := ProcessPubKey(acc, sig) if !res.IsOK() { return nil, res } @@ -243,7 +243,7 @@ func processSig( // ProcessPubKey verifies that the given account address matches that of the // std.Signature. In addition, it will set the public key of the account if it // has not been set. -func ProcessPubKey(acc std.Account, sig std.Signature, simulate bool) (crypto.PubKey, sdk.Result) { +func ProcessPubKey(acc std.Account, sig std.Signature) (crypto.PubKey, sdk.Result) { // If pubkey is not known for account, set it from the std.Signature. pubKey := acc.GetPubKey() if pubKey == nil { @@ -271,7 +271,7 @@ func DefaultSigVerificationGasConsumer( switch pubkey := pubkey.(type) { case ed25519.PubKeyEd25519: meter.ConsumeGas(params.SigVerifyCostED25519, "ante verify: ed25519") - return abciResult(std.ErrInvalidPubKey("ED25519 public keys are unsupported")) + return sdk.Result{} case secp256k1.PubKeySecp256k1: meter.ConsumeGas(params.SigVerifyCostSecp256k1, "ante verify: secp256k1") diff --git a/tm2/pkg/sdk/auth/ante_test.go b/tm2/pkg/sdk/auth/ante_test.go index 7c6ace51e4e..430954a0867 100644 --- a/tm2/pkg/sdk/auth/ante_test.go +++ b/tm2/pkg/sdk/auth/ante_test.go @@ -623,7 +623,7 @@ func TestProcessPubKey(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - _, err := ProcessPubKey(tt.args.acc, tt.args.sig, tt.args.simulate) + _, err := ProcessPubKey(tt.args.acc, tt.args.sig) require.Equal(t, tt.wantErr, !err.IsOK()) }) } @@ -655,7 +655,7 @@ func TestConsumeSignatureVerificationGas(t *testing.T) { gasConsumed int64 shouldErr bool }{ - {"PubKeyEd25519", args{store.NewInfiniteGasMeter(), nil, ed25519.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostED25519, true}, + {"PubKeyEd25519", args{store.NewInfiniteGasMeter(), nil, ed25519.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostED25519, false}, {"PubKeySecp256k1", args{store.NewInfiniteGasMeter(), nil, secp256k1.GenPrivKey().PubKey(), params}, DefaultSigVerifyCostSecp256k1, false}, {"Multisig", args{store.NewInfiniteGasMeter(), amino.MustMarshal(multisignature1), multisigKey1, params}, expectedCost1, false}, {"unknown key", args{store.NewInfiniteGasMeter(), nil, nil, params}, 0, true}, diff --git a/tm2/pkg/sdk/auth/params.go b/tm2/pkg/sdk/auth/params.go index 3fe08ed444d..fda85c7a3d6 100644 --- a/tm2/pkg/sdk/auth/params.go +++ b/tm2/pkg/sdk/auth/params.go @@ -69,15 +69,16 @@ func DefaultParams() Params { // String implements the stringer interface. func (p Params) String() string { - var sb strings.Builder + var builder strings.Builder + sb := &builder // Pointer for use with fmt.Fprintf sb.WriteString("Params: \n") - sb.WriteString(fmt.Sprintf("MaxMemoBytes: %d\n", p.MaxMemoBytes)) - sb.WriteString(fmt.Sprintf("TxSigLimit: %d\n", p.TxSigLimit)) - sb.WriteString(fmt.Sprintf("TxSizeCostPerByte: %d\n", p.TxSizeCostPerByte)) - sb.WriteString(fmt.Sprintf("SigVerifyCostED25519: %d\n", p.SigVerifyCostED25519)) - sb.WriteString(fmt.Sprintf("SigVerifyCostSecp256k1: %d\n", p.SigVerifyCostSecp256k1)) - sb.WriteString(fmt.Sprintf("GasPricesChangeCompressor: %d\n", p.GasPricesChangeCompressor)) - sb.WriteString(fmt.Sprintf("TargetGasRatio: %d\n", p.TargetGasRatio)) + fmt.Fprintf(sb, "MaxMemoBytes: %d\n", p.MaxMemoBytes) + fmt.Fprintf(sb, "TxSigLimit: %d\n", p.TxSigLimit) + fmt.Fprintf(sb, "TxSizeCostPerByte: %d\n", p.TxSizeCostPerByte) + fmt.Fprintf(sb, "SigVerifyCostED25519: %d\n", p.SigVerifyCostED25519) + fmt.Fprintf(sb, "SigVerifyCostSecp256k1: %d\n", p.SigVerifyCostSecp256k1) + fmt.Fprintf(sb, "GasPricesChangeCompressor: %d\n", p.GasPricesChangeCompressor) + fmt.Fprintf(sb, "TargetGasRatio: %d\n", p.TargetGasRatio) return sb.String() } diff --git a/tm2/pkg/sdk/auth/params_test.go b/tm2/pkg/sdk/auth/params_test.go index 4b5a6b15789..36a52ac9001 100644 --- a/tm2/pkg/sdk/auth/params_test.go +++ b/tm2/pkg/sdk/auth/params_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" ) @@ -105,3 +106,26 @@ func TestNewParams(t *testing.T) { t.Errorf("NewParams() = %+v, want %+v", params, expectedParams) } } + +func TestParamsString(t *testing.T) { + cases := []struct { + name string + params Params + want string + }{ + {"blank params", Params{}, "Params: \nMaxMemoBytes: 0\nTxSigLimit: 0\nTxSizeCostPerByte: 0\nSigVerifyCostED25519: 0\nSigVerifyCostSecp256k1: 0\nGasPricesChangeCompressor: 0\nTargetGasRatio: 0\n"}, + {"some values", Params{ + MaxMemoBytes: 1_000_000, + TxSizeCostPerByte: 8192, + }, "Params: \nMaxMemoBytes: 1000000\nTxSigLimit: 0\nTxSizeCostPerByte: 8192\nSigVerifyCostED25519: 0\nSigVerifyCostSecp256k1: 0\nGasPricesChangeCompressor: 0\nTargetGasRatio: 0\n"}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got := tt.params.String() + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Fatalf("Mismatch: got - want +\n%s", diff) + } + }) + } +}