diff --git a/contribs/gnodev/pkg/packages/resolver_test.go b/contribs/gnodev/pkg/packages/resolver_test.go index 2463deec63e..850dfc6f244 100644 --- a/contribs/gnodev/pkg/packages/resolver_test.go +++ b/contribs/gnodev/pkg/packages/resolver_test.go @@ -253,7 +253,10 @@ func TestResolver_ResolveRemote(t *testing.T) { pkg, err := remoteResolver.Resolve(token.NewFileSet(), mempkg.Path) require.NoError(t, err) require.NotNil(t, pkg) - assert.Equal(t, mempkg, pkg.MemPackage) + // The files will be slightly different, because addpkg adds information + // to the gnomod.toml about the creator. + assert.Equal(t, mempkg.Name, pkg.MemPackage.Name) + assert.Equal(t, mempkg.Path, pkg.MemPackage.Path) }) t.Run("invalid package", func(t *testing.T) { diff --git a/contribs/gnofaucet/github/fetcher.go b/contribs/gnofaucet/github/fetcher.go index dd6e53db176..6541142b6c8 100644 --- a/contribs/gnofaucet/github/fetcher.go +++ b/contribs/gnofaucet/github/fetcher.go @@ -25,7 +25,8 @@ func NewGHFetcher( rClient *redis.Client, repos map[string][]string, logger *slog.Logger, - interval time.Duration) *GHFetcher { + interval time.Duration, +) *GHFetcher { return &GHFetcher{ ghClient: ghClient, redisClient: rClient, diff --git a/contribs/gnogenesis/genesis.go b/contribs/gnogenesis/genesis.go index 2bf27b32c85..a43a277e414 100644 --- a/contribs/gnogenesis/genesis.go +++ b/contribs/gnogenesis/genesis.go @@ -2,6 +2,7 @@ package main import ( "github.com/gnolang/contribs/gnogenesis/internal/balances" + "github.com/gnolang/contribs/gnogenesis/internal/fork" "github.com/gnolang/contribs/gnogenesis/internal/generate" "github.com/gnolang/contribs/gnogenesis/internal/params" "github.com/gnolang/contribs/gnogenesis/internal/txs" @@ -28,6 +29,7 @@ func newGenesisCmd(io commands.IO) *commands.Command { balances.NewBalancesCmd(io), txs.NewTxsCmd(io), params.NewParamsCmd(io), + fork.NewForkCmd(io), ) return cmd diff --git a/contribs/gnogenesis/internal/fork/fork.go b/contribs/gnogenesis/internal/fork/fork.go new file mode 100644 index 00000000000..a97b71f1a81 --- /dev/null +++ b/contribs/gnogenesis/internal/fork/fork.go @@ -0,0 +1,48 @@ +// Package fork provides the `gnogenesis fork` subcommands for building and +// smoke-testing hardfork genesis files. +// +// A hardfork genesis is built from: +// 1. SOURCE CHAIN — provides historical state (genesis + tx history) +// 2. NEW BINARY — the updated gnoland built from this repo +// +// Source modes (auto-detected from --source): +// +// http(s)://... RPC of a running or recently-halted node +// /path/to/dir local node data directory (must contain config/genesis.json) +// /path/to/file exported file: genesis.json (no txs) or .jsonl (txs) or .tar.gz +package fork + +import ( + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// NewForkCmd returns the `gnogenesis fork` parent command with its +// subcommands (`generate`, `test`) attached. +func NewForkCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "fork", + ShortUsage: " [flags] [...]", + ShortHelp: "build and smoke-test hardfork genesis files", + LongHelp: `Build a hardfork genesis from a source chain and smoke-test it locally. + +Subcommands: + generate Assemble a new-chain genesis.json from a source chain's state + tx history. + test Run an in-memory InitChain replay against a genesis.json (fast smoke-test). + +Source modes (auto-detected from --source): + http(s)://... RPC of a running or recently-halted node + /path/to/dir local node data directory (must contain config/genesis.json) + /path/to/file exported file: genesis.json (no txs) or .jsonl (txs) or .tar.gz`, + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newGenerateCmd(io), + newTestCmd(io), + ) + + return cmd +} diff --git a/contribs/gnogenesis/internal/fork/generate.go b/contribs/gnogenesis/internal/fork/generate.go new file mode 100644 index 00000000000..8028d821525 --- /dev/null +++ b/contribs/gnogenesis/internal/fork/generate.go @@ -0,0 +1,501 @@ +package fork + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + bftypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type generateCfg struct { + source string + chainID string + originalChainID string + haltHeight int64 + output string + txsOutput string + patchRealms patchRealmList + migrationTxs stringList + skipTxs bool + noVerify bool +} + +// patchRealmList accepts repeated --patch-realm flags. Each value is +// "pkgpath=srcdir"; the tool rewrites the matching genesis-mode addpkg +// tx's Package.Files with the contents of srcdir. +type patchRealmList []string + +func (p *patchRealmList) String() string { return strings.Join(*p, ",") } +func (p *patchRealmList) Set(v string) error { + *p = append(*p, v) + return nil +} + +// stringList accepts repeated string flags. +type stringList []string + +func (s *stringList) String() string { return strings.Join(*s, ",") } +func (s *stringList) Set(v string) error { + *s = append(*s, v) + return nil +} + +func newGenerateCmd(io commands.IO) *commands.Command { + cfg := &generateCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "generate", + ShortUsage: "generate [flags]", + ShortHelp: "assemble a hardfork genesis from a source chain", + LongHelp: `Generates a hardfork genesis.json by extracting state from a source chain +and assembling it with the hardfork parameters (original_chain_id, initial_height). + +The source chain provides the base genesis (balances, validators, auth state) +and the historical transaction history. Both are embedded in the new genesis +so the new chain can replay all historical activity starting from the halt height. + +Examples: + + # From a running or recently-halted node via RPC: + gnogenesis fork generate --source http://rpc.gno.land:26657 --chain-id gnoland-1 + + # From a local node data directory (offline, reads block store): + gnogenesis fork generate --source /var/lib/gnoland --chain-id gnoland-1 + + # From a pre-exported tarball (genesis.json + txs.jsonl): + gnogenesis fork generate --source /tmp/gnoland1-export.tar.gz --chain-id gnoland-1 + + # Preview only (skip tx export — fast summary of genesis structure): + gnogenesis fork generate --source http://rpc.gno.land:26657 --skip-txs`, + }, + cfg, + func(ctx context.Context, args []string) error { + return execGenerate(ctx, cfg, io) + }, + ) +} + +func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar(&c.source, "source", "", "source: RPC URL, local data dir, or exported file (.json/.jsonl/.tar.gz)") + fs.StringVar(&c.chainID, "chain-id", "gnoland-1", "new chain ID") + fs.StringVar(&c.originalChainID, "original-chain-id", "", "source chain ID for signature verification (auto-detected from source genesis if empty)") + fs.Int64Var(&c.haltHeight, "halt-height", 0, "block height at which source chain halted (auto-detected from source if 0)") + fs.StringVar(&c.output, "output", "genesis.json", "output genesis file path") + fs.StringVar(&c.txsOutput, "txs-output", "", "also write extracted txs to this .jsonl file (optional)") + fs.Var(&c.migrationTxs, "migration-tx", "append migration txs at the END of appState.Txs "+ + "(after historical replay). Repeatable. FILE is a .jsonl where each "+ + "line is an amino-JSON gnoland.TxWithMetadata. These are genesis-mode "+ + "txs (BlockHeight==0) that run through the same --skip-genesis-sig-"+ + "verification code path as original genesis-mode txs, but are placed "+ + "after the historical stream so they can mutate replayed state "+ + "(e.g. govDAO prop to update r/sys/validators/v2 to the new valset).") + fs.Var(&c.patchRealms, "patch-realm", "patch a genesis-mode addpkg tx in place: repeatable, PKGPATH=SRCDIR. "+ + "Replaces Package.Files with the *.gno + gnomod.toml files found in SRCDIR. "+ + "Source genesis on disk is NOT modified; the patch is applied in memory "+ + "before writing the hardfork genesis. Use this to deliver realm upgrades "+ + "as part of the fork (e.g. adding a new .gno file to an existing realm).") + fs.BoolVar(&c.skipTxs, "skip-txs", false, "skip tx export (only copy genesis structure — useful for quick preview)") + fs.BoolVar(&c.noVerify, "no-verify", false, "skip genesis verification after assembly") +} + +func execGenerate(ctx context.Context, cfg *generateCfg, io commands.IO) error { + if cfg.source == "" { + return errors.New("--source is required (RPC URL, local data dir, or exported file)") + } + + src, err := openSource(cfg.source) + if err != nil { + return fmt.Errorf("opening source %q: %w", cfg.source, err) + } + defer src.Close() + + io.Printf("Source: %s (%s)\n", src.Description(), cfg.source) + + // ------------------------------------------------------------------------- + // Step 1: Fetch base genesis from source + // ------------------------------------------------------------------------- + io.Println("Step 1/4: Fetching base genesis...") + + baseGenDoc, err := src.FetchGenesis(ctx) + if err != nil { + return fmt.Errorf("fetching genesis: %w", err) + } + + sourceChainID := baseGenDoc.ChainID + io.Printf(" Source chain ID: %s\n", sourceChainID) + io.Printf(" Source genesis time: %s\n", baseGenDoc.GenesisTime) + + // Use auto-detected chain ID if not explicitly provided + if cfg.originalChainID == "" { + cfg.originalChainID = sourceChainID + io.Printf(" Original chain ID (auto-detected): %s\n", cfg.originalChainID) + } + + // Auto-detect halt height from source + if cfg.haltHeight == 0 { + h, err := src.LatestHeight(ctx) + if err != nil { + return fmt.Errorf("detecting halt height: %w", err) + } + cfg.haltHeight = h + io.Printf(" Halt height (auto-detected): %d\n", cfg.haltHeight) + } else { + io.Printf(" Halt height: %d\n", cfg.haltHeight) + } + + // ------------------------------------------------------------------------- + // Step 2: Fetch historical transactions + // ------------------------------------------------------------------------- + var txs []gnoland.TxWithMetadata + + if !cfg.skipTxs { + io.Printf("Step 2/4: Fetching historical transactions (height 1..%d)...\n", cfg.haltHeight) + + txs, err = src.FetchTxs(ctx, 1, cfg.haltHeight, io) + if err != nil { + return fmt.Errorf("fetching transactions: %w", err) + } + + io.Printf(" Fetched %d successful transactions\n", len(txs)) + + // Write txs to separate file if requested + if cfg.txsOutput != "" { + if err := writeTxsJSONL(cfg.txsOutput, txs); err != nil { + return fmt.Errorf("writing txs output: %w", err) + } + io.Printf(" Txs written to: %s\n", cfg.txsOutput) + } + } else { + io.Println("Step 2/4: Skipping tx export (--skip-txs)") + } + + // ------------------------------------------------------------------------- + // Step 3: Assemble hardfork genesis + // ------------------------------------------------------------------------- + io.Println("Step 3/4: Assembling hardfork genesis...") + + initialHeight := cfg.haltHeight + 1 + + newGenDoc, appState, err := buildHardforkGenesis(baseGenDoc, txs, cfg.chainID, cfg.originalChainID, initialHeight) + if err != nil { + return fmt.Errorf("building hardfork genesis: %w", err) + } + + // Apply --patch-realm rewrites on genesis-mode addpkg txs (in-memory only). + for _, spec := range cfg.patchRealms { + parts := strings.SplitN(spec, "=", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("--patch-realm needs PKGPATH=SRCDIR, got %q", spec) + } + pkgPath, srcDir := parts[0], parts[1] + n, err := patchGenesisModeAddPkg(appState, pkgPath, srcDir) + if err != nil { + return fmt.Errorf("patch %s: %w", pkgPath, err) + } + if n == 0 { + io.Printf(" WARNING: --patch-realm %s did not match any genesis-mode addpkg tx\n", pkgPath) + } else { + io.Printf(" patched %s from %s (%d tx rewritten)\n", pkgPath, srcDir, n) + } + } + // Append --migration-tx files at the END of appState.Txs (post-history). + // Each file is a .jsonl of gnoland.TxWithMetadata. We force BlockHeight=0 + // so they go through the genesis-mode path (chain-id via PastChainIDs[0], + // sig verify skipped under --skip-genesis-sig-verification). + for _, path := range cfg.migrationTxs { + migTxs, err := readMigrationTxs(path) + if err != nil { + return fmt.Errorf("migration-tx %s: %w", path, err) + } + appState.Txs = append(appState.Txs, migTxs...) + io.Printf(" appended %d migration tx(s) from %s\n", len(migTxs), path) + } + newGenDoc.AppState = *appState + + // ------------------------------------------------------------------------- + // Step 4: Write and verify output + // ------------------------------------------------------------------------- + io.Println("Step 4/4: Writing genesis...") + + if err := writeGenesis(cfg.output, newGenDoc, appState); err != nil { + return fmt.Errorf("writing genesis: %w", err) + } + + stat, _ := os.Stat(cfg.output) + io.Printf(" Written: %s", cfg.output) + if stat != nil { + io.Printf(" (%.1f MB)", float64(stat.Size())/(1024*1024)) + } + io.Println() + + if !cfg.noVerify { + if err := verifyGenesisFile(cfg.output); err != nil { + return fmt.Errorf("genesis verification failed: %w (use --no-verify to skip)", err) + } + io.Println(" Verification: OK") + } + + // Summary + io.Println() + io.Println("=== Hardfork Genesis Summary ===") + io.Printf(" New chain ID: %s\n", cfg.chainID) + io.Printf(" Original chain ID: %s\n", cfg.originalChainID) + io.Printf(" Initial height: %d\n", initialHeight) + io.Printf(" Halt height: %d\n", cfg.haltHeight) + io.Printf(" Genesis-mode txs: %d (from source genesis, no metadata)\n", len(baseGenesisModeTxs(appState))) + io.Printf(" Historical txs: %d (with block_height metadata)\n", len(txs)) + io.Printf(" Total txs: %d\n", len(appState.Txs)) + io.Printf(" Output: %s\n", cfg.output) + io.Println() + io.Println("Next steps:") + io.Printf(" 1. Test locally (in-process replay):\n") + io.Printf(" hardfork test --genesis %s\n", cfg.output) + io.Printf(" 2. Verify with other validators (share SHA-256):\n") + io.Printf(" sha256: $(sha256sum %s | cut -d' ' -f1)\n", cfg.output) + + _ = appState // suppress unused warning (used in summary above) + return nil +} + +// buildHardforkGenesis constructs the new genesis document. +// It takes the source chain's genesis as the base, injects the hardfork +// parameters, and appends historical txs (with block_height metadata). +func buildHardforkGenesis( + srcGenDoc *bftypes.GenesisDoc, + historicalTxs []gnoland.TxWithMetadata, + newChainID string, + originalChainID string, + initialHeight int64, +) (*bftypes.GenesisDoc, *gnoland.GnoGenesisState, error) { + // Extract app state from source genesis + appState, ok := srcGenDoc.AppState.(gnoland.GnoGenesisState) + if !ok { + // Try amino JSON round-trip if the app state is a raw json.RawMessage + raw, err := amino.MarshalJSON(srcGenDoc.AppState) + if err != nil { + return nil, nil, fmt.Errorf("marshalling source app state: %w", err) + } + if err := amino.UnmarshalJSON(raw, &appState); err != nil { + return nil, nil, fmt.Errorf("unmarshalling source app state as GnoGenesisState: %w", err) + } + } + + // Inject hardfork fields + appState.PastChainIDs = []string{originalChainID} + appState.InitialHeight = initialHeight + + // Append historical txs after existing genesis-mode txs + // Genesis-mode txs (no metadata or BlockHeight==0): package deploys, setup + // Historical txs (BlockHeight > 0): replayed with original chain ID context + appState.Txs = append(appState.Txs, historicalTxs...) + + // Build the new genesis doc + newGenDoc := *srcGenDoc // copy + newGenDoc.ChainID = newChainID + newGenDoc.InitialHeight = initialHeight + newGenDoc.AppState = appState + + return &newGenDoc, &appState, nil +} + +// writeGenesis serializes and writes the genesis to a file. +func writeGenesis(path string, genDoc *bftypes.GenesisDoc, _ *gnoland.GnoGenesisState) error { + data, err := amino.MarshalJSONIndent(genDoc, "", " ") + if err != nil { + return fmt.Errorf("marshalling genesis: %w", err) + } + return os.WriteFile(path, data, 0o644) +} + +// readMigrationTxs reads a .jsonl file of gnoland.TxWithMetadata entries. +// BlockHeight is forced to 0 so each line is treated as a genesis-mode tx +// when replayed (uses PastChainIDs[0] for chain-id; sig verify skipped under +// --skip-genesis-sig-verification). Blank lines and # comments are ignored. +func readMigrationTxs(path string) ([]gnoland.TxWithMetadata, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var out []gnoland.TxWithMetadata + for i, line := range strings.Split(string(data), "\n") { + trim := strings.TrimSpace(line) + if trim == "" || strings.HasPrefix(trim, "#") { + continue + } + var tx gnoland.TxWithMetadata + if err := amino.UnmarshalJSON([]byte(line), &tx); err != nil { + return nil, fmt.Errorf("line %d: %w", i+1, err) + } + if tx.Metadata == nil { + tx.Metadata = &gnoland.GnoTxMetadata{} + } + tx.Metadata.BlockHeight = 0 // always genesis-mode + out = append(out, tx) + } + return out, nil +} + +// writeTxsJSONL writes transactions to a file, one amino JSON per line. +// Uses amino.MarshalJSON to preserve interface type information (e.g. std.Msg). +func writeTxsJSONL(path string, txs []gnoland.TxWithMetadata) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + for _, tx := range txs { + data, err := amino.MarshalJSON(tx) + if err != nil { + return err + } + data = append(data, '\n') + if _, err := f.Write(data); err != nil { + return err + } + } + return nil +} + +// verifyGenesisFile runs basic validation on the written genesis file. +func verifyGenesisFile(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + var genDoc bftypes.GenesisDoc + if err := amino.UnmarshalJSON(data, &genDoc); err != nil { + return fmt.Errorf("parse: %w", err) + } + + return genDoc.ValidateAndComplete() +} + +// baseGenesisModeTxs returns only the genesis-mode txs (BlockHeight == 0) from app state. +func baseGenesisModeTxs(appState *gnoland.GnoGenesisState) []gnoland.TxWithMetadata { + var out []gnoland.TxWithMetadata + for _, tx := range appState.Txs { + if tx.Metadata == nil || tx.Metadata.BlockHeight == 0 { + out = append(out, tx) + } + } + return out +} + +// patchGenesisModeAddPkg rewrites every genesis-mode addpkg tx whose package +// path matches `pkgPath` in-place — replacing its Package.Files slice with +// the *.gno + gnomod.toml files read from `srcDir`. +// +// This is how realm upgrades ride along in a hardfork: instead of adding a +// new tx (which would run with a different caller + account state and may +// collide with existing state), we rewrite the tx that originally deployed +// the realm so the forked chain initialises it with the new source. +// +// The source genesis on disk is NOT touched — this operates on the in-memory +// GnoGenesisState that we assembled for the output. +// +// Returns the number of txs rewritten. +func patchGenesisModeAddPkg(appState *gnoland.GnoGenesisState, pkgPath, srcDir string) (int, error) { + files, err := loadGnoPackageFiles(srcDir) + if err != nil { + return 0, fmt.Errorf("load %s: %w", srcDir, err) + } + if len(files) == 0 { + return 0, fmt.Errorf("no .gno/.toml files in %s", srcDir) + } + + patched := 0 + for i := range appState.Txs { + txm := &appState.Txs[i] + if txm.Metadata != nil && txm.Metadata.BlockHeight > 0 { + continue // historical tx, leave alone + } + for mi, msg := range txm.Tx.Msgs { + addpkg, ok := msg.(vm.MsgAddPackage) + if !ok { + continue + } + if addpkg.Package == nil || addpkg.Package.Path != pkgPath { + continue + } + addpkg.Package.Files = files + // Refresh package name in case a .gno's `package ...` declaration + // matters downstream. + for _, f := range files { + if strings.HasSuffix(f.Name, ".gno") { + if name := gnoPackageNameFromFileBody(f.Name, f.Body); name != "" { + addpkg.Package.Name = name + } + break + } + } + txm.Tx.Msgs[mi] = addpkg + patched++ + } + } + return patched, nil +} + +// loadGnoPackageFiles reads *.gno and gnomod.toml files from srcDir +// (non-recursive) and returns them as ordered std.MemFile entries. +// Skips _test.gno, _filetest.gno, and hidden files. +func loadGnoPackageFiles(srcDir string) ([]*std.MemFile, error) { + entries, err := os.ReadDir(srcDir) + if err != nil { + return nil, err + } + var names []string + for _, e := range entries { + if e.IsDir() || strings.HasPrefix(e.Name(), ".") { + continue + } + n := e.Name() + if strings.HasSuffix(n, "_test.gno") || strings.HasSuffix(n, "_filetest.gno") { + continue + } + if strings.HasSuffix(n, ".gno") || n == "gnomod.toml" { + names = append(names, n) + } + } + sort.Strings(names) + + files := make([]*std.MemFile, 0, len(names)) + for _, n := range names { + body, err := os.ReadFile(filepath.Join(srcDir, n)) + if err != nil { + return nil, err + } + files = append(files, &std.MemFile{Name: n, Body: string(body)}) + } + return files, nil +} + +// gnoPackageNameFromFileBody extracts `package NAME` from the top of a .gno +// file. Returns "" if not found. (Intentionally lightweight — avoids pulling +// in the gnovm parser.) +func gnoPackageNameFromFileBody(_ string, body string) string { + for _, line := range strings.Split(body, "\n") { + l := strings.TrimSpace(line) + if strings.HasPrefix(l, "package ") { + rest := strings.TrimPrefix(l, "package ") + if i := strings.IndexAny(rest, " \t/"); i >= 0 { + rest = rest[:i] + } + return rest + } + } + return "" +} diff --git a/contribs/gnogenesis/internal/fork/generate_test.go b/contribs/gnogenesis/internal/fork/generate_test.go new file mode 100644 index 00000000000..5852adb7dcb --- /dev/null +++ b/contribs/gnogenesis/internal/fork/generate_test.go @@ -0,0 +1,104 @@ +package fork + +import ( + "bufio" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestWriteTxsJSONL_RoundTrip verifies that writeTxsJSONL produces output +// that can be read back by the dir source's JSONL reader. +// BUG: writeTxsJSONL uses encoding/json instead of amino, which loses type +// information for interface fields (std.Msg). The round-trip fails because +// the Msg type cannot be recovered from plain JSON. +func TestWriteTxsJSONL_RoundTrip(t *testing.T) { + t.Parallel() + + // Create a tx with a concrete Msg (bank.MsgSend). + msg := bank.MsgSend{ + FromAddress: crypto.AddressFromPreimage([]byte("sender")), + ToAddress: crypto.AddressFromPreimage([]byte("receiver")), + Amount: std.NewCoins(std.NewCoin("ugnot", 1000)), + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(50000, std.NewCoin("ugnot", 1000)), + } + original := []gnoland.TxWithMetadata{ + { + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: 1234567890, + BlockHeight: 42, + ChainID: "test-chain", + }, + }, + } + + // Write to JSONL. + dir := t.TempDir() + path := filepath.Join(dir, "txs.jsonl") + require.NoError(t, writeTxsJSONL(path, original)) + + // Read back line-by-line using amino.UnmarshalJSON (the correct decoder + // for amino-registered interfaces like std.Msg). + f, err := os.Open(path) + require.NoError(t, err) + defer f.Close() + + var decoded []gnoland.TxWithMetadata + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + var tx gnoland.TxWithMetadata + require.NoError(t, amino.UnmarshalJSON(line, &tx), "amino should unmarshal JSONL line") + decoded = append(decoded, tx) + } + require.NoError(t, scanner.Err()) + + require.Len(t, decoded, 1, "should decode exactly one tx") + + // The Msg should round-trip correctly with its type preserved. + require.Len(t, decoded[0].Tx.Msgs, 1, "should have one msg") + _, ok := decoded[0].Tx.Msgs[0].(bank.MsgSend) + require.True(t, ok, "Msg should be bank.MsgSend after round-trip, got %T", decoded[0].Tx.Msgs[0]) + + // Metadata should survive. + require.NotNil(t, decoded[0].Metadata) + assert.Equal(t, int64(42), decoded[0].Metadata.BlockHeight) + assert.Equal(t, "test-chain", decoded[0].Metadata.ChainID) +} + +// TestVerifyGenesisFile_Invalid verifies that verifyGenesisFile returns an +// error for a malformed genesis file (so the calling tool can abort). +func TestVerifyGenesisFile_Invalid(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + t.Run("missing file", func(t *testing.T) { + t.Parallel() + err := verifyGenesisFile(filepath.Join(dir, "does-not-exist.json")) + require.Error(t, err) + }) + + t.Run("malformed json", func(t *testing.T) { + t.Parallel() + path := filepath.Join(dir, "bad.json") + require.NoError(t, os.WriteFile(path, []byte(`{"not_valid": `), 0o644)) + err := verifyGenesisFile(path) + require.Error(t, err) + }) +} diff --git a/contribs/gnogenesis/internal/fork/source.go b/contribs/gnogenesis/internal/fork/source.go new file mode 100644 index 00000000000..967ae698a2b --- /dev/null +++ b/contribs/gnogenesis/internal/fork/source.go @@ -0,0 +1,75 @@ +package fork + +import ( + "context" + "fmt" + "net/url" + "os" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + bftypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// Source is a provider of chain state for hardfork genesis assembly. +type Source interface { + // Description returns a human-readable source type label. + Description() string + + // FetchGenesis returns the source chain's genesis document. + FetchGenesis(ctx context.Context) (*bftypes.GenesisDoc, error) + + // LatestHeight returns the latest committed block height. + // Used to auto-detect halt height when --halt-height is not specified. + LatestHeight(ctx context.Context) (int64, error) + + // FetchTxs fetches all successful transactions in [fromHeight, toHeight] + // with metadata (BlockHeight, Timestamp, ChainID populated). + // Progress is reported via io. + FetchTxs(ctx context.Context, fromHeight, toHeight int64, io commands.IO) ([]gnoland.TxWithMetadata, error) + + // Close releases any resources held by the source. + Close() error +} + +// openSource auto-detects the source type from the provided string and +// returns the appropriate Source implementation. +// +// Detection order: +// 1. http:// or https:// prefix → RPC source +// 2. directory path that exists → local directory source +// 3. file ending in .json → single genesis file source +// 4. file ending in .tar.gz/.tgz → tarball source (future) +func openSource(s string) (Source, error) { + // RPC source + if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") { + u, err := url.Parse(s) + if err != nil { + return nil, fmt.Errorf("invalid RPC URL: %w", err) + } + return newRPCSource(u.String()) + } + + // Local path + fi, err := os.Stat(s) + if err != nil { + return nil, fmt.Errorf("source path %q: %w", s, err) + } + + if fi.IsDir() { + return newDirSource(s) + } + + // Single genesis file + if strings.HasSuffix(s, ".json") { + return newFileSource(s) + } + + // Tarball (not yet implemented) + if strings.HasSuffix(s, ".tar.gz") || strings.HasSuffix(s, ".tgz") { + return nil, fmt.Errorf("tarball source not yet implemented; extract first and use --source /path/to/dir") + } + + return nil, fmt.Errorf("unrecognised source %q: expected http(s) URL, directory, .json file, or .tar.gz", s) +} diff --git a/contribs/gnogenesis/internal/fork/source_dir.go b/contribs/gnogenesis/internal/fork/source_dir.go new file mode 100644 index 00000000000..9deb76244cb --- /dev/null +++ b/contribs/gnogenesis/internal/fork/source_dir.go @@ -0,0 +1,191 @@ +package fork + +import ( + "bufio" + "context" + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + bftypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// dirSource reads chain state from a local directory. +// +// Expected directory layouts (tries both): +// +// /path/to/dir/ +// config/genesis.json ← gnoland default node layout +// genesis.json ← flat layout (e.g. manual export) +// txs.jsonl ← optional: pre-exported txs with metadata +// +// If txs.jsonl is present it is used directly (no block store access needed). +// If txs.jsonl is absent, FetchTxs returns an empty slice with a warning — +// reading directly from the block store will be added in a future version. +type dirSource struct { + dir string + genesisPath string // resolved path to genesis.json + txsPath string // resolved path to txs.jsonl (empty if not found) +} + +func newDirSource(dir string) (*dirSource, error) { + s := &dirSource{dir: dir} + + // Find genesis.json + candidates := []string{ + filepath.Join(dir, "config", "genesis.json"), + filepath.Join(dir, "genesis.json"), + } + for _, c := range candidates { + if _, err := os.Stat(c); err == nil { + s.genesisPath = c + break + } + } + if s.genesisPath == "" { + return nil, fmt.Errorf("genesis.json not found in %s (tried config/genesis.json and genesis.json)", dir) + } + + // Find txs.jsonl (optional) + txsCandidates := []string{ + filepath.Join(dir, "txs.jsonl"), + filepath.Join(dir, "historical-txs.jsonl"), + } + for _, c := range txsCandidates { + if _, err := os.Stat(c); err == nil { + s.txsPath = c + break + } + } + + return s, nil +} + +func (s *dirSource) Description() string { return "local directory" } +func (s *dirSource) Close() error { return nil } + +func (s *dirSource) FetchGenesis(ctx context.Context) (*bftypes.GenesisDoc, error) { + data, err := os.ReadFile(s.genesisPath) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", s.genesisPath, err) + } + + var genDoc bftypes.GenesisDoc + if err := amino.UnmarshalJSON(data, &genDoc); err != nil { + return nil, fmt.Errorf("parsing genesis: %w", err) + } + + return &genDoc, nil +} + +// LatestHeight returns the halt height from the genesis InitialHeight if set, +// otherwise falls back to -1 (user must specify --halt-height). +// +// For a proper auto-detect from a local node directory, reading the block store +// would be needed. That is tracked as a future enhancement. +func (s *dirSource) LatestHeight(_ context.Context) (int64, error) { + data, err := os.ReadFile(s.genesisPath) + if err != nil { + return 0, fmt.Errorf("reading genesis: %w", err) + } + + // Try to extract a height hint from the genesis file itself + var raw struct { + InitialHeight int64 `json:"initial_height"` + } + _ = amino.UnmarshalJSON(data, &raw) + if raw.InitialHeight > 1 { + // This is already a hardfork genesis — use InitialHeight-1 as the halt height + return raw.InitialHeight - 1, nil + } + + return 0, fmt.Errorf( + "cannot auto-detect halt height from local directory %s; " + + "please specify --halt-height explicitly, or point --source to a running node RPC", + s.dir, + ) +} + +// FetchTxs reads transactions from txs.jsonl if present. +// If no txs file is found, returns an empty slice with a warning. +// Full block-store reading will be added in a future version. +func (s *dirSource) FetchTxs(_ context.Context, fromHeight, toHeight int64, io commands.IO) ([]gnoland.TxWithMetadata, error) { + if s.txsPath == "" { + io.Println(" WARNING: no txs.jsonl found in source directory.") + io.Println(" Historical tx replay will be empty — only genesis-mode txs will be included.") + io.Println(" To include historical txs, provide a txs.jsonl file alongside genesis.json,") + io.Println(" or use --source with an RPC URL instead.") + return nil, nil + } + + io.Printf(" Reading txs from: %s\n", s.txsPath) + + f, err := os.Open(s.txsPath) + if err != nil { + return nil, fmt.Errorf("opening %s: %w", s.txsPath, err) + } + defer f.Close() + + var txs []gnoland.TxWithMetadata + scanner := bufio.NewScanner(f) + // Increase buffer for large tx lines. + scanner.Buffer(make([]byte, 0, 4096), 10*1024*1024) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + var tx gnoland.TxWithMetadata + if err := amino.UnmarshalJSON(line, &tx); err != nil { + return nil, fmt.Errorf("decoding tx: %w", err) + } + + // Filter to requested height range + if tx.Metadata != nil && tx.Metadata.BlockHeight > 0 { + if tx.Metadata.BlockHeight < fromHeight || tx.Metadata.BlockHeight > toHeight { + continue + } + } + + txs = append(txs, tx) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("reading txs: %w", err) + } + + return txs, nil +} + +// fileSource handles a single genesis.json file (no txs). +type fileSource struct { + path string +} + +func newFileSource(path string) (*fileSource, error) { + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf("file %q: %w", path, err) + } + return &fileSource{path: path}, nil +} + +func (s *fileSource) Description() string { return "genesis file" } +func (s *fileSource) Close() error { return nil } + +func (s *fileSource) FetchGenesis(ctx context.Context) (*bftypes.GenesisDoc, error) { + d := &dirSource{dir: filepath.Dir(s.path), genesisPath: s.path} + return d.FetchGenesis(ctx) +} + +func (s *fileSource) LatestHeight(ctx context.Context) (int64, error) { + d := &dirSource{dir: filepath.Dir(s.path), genesisPath: s.path} + return d.LatestHeight(ctx) +} + +func (s *fileSource) FetchTxs(_ context.Context, _, _ int64, io commands.IO) ([]gnoland.TxWithMetadata, error) { + io.Println(" WARNING: single genesis.json source — no historical txs available.") + io.Println(" Use --source with a directory (containing txs.jsonl) or an RPC URL.") + return nil, nil +} diff --git a/contribs/gnogenesis/internal/fork/source_rpc.go b/contribs/gnogenesis/internal/fork/source_rpc.go new file mode 100644 index 00000000000..901ffce906c --- /dev/null +++ b/contribs/gnogenesis/internal/fork/source_rpc.go @@ -0,0 +1,430 @@ +package fork + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + bftypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// rpcSource fetches chain state from a live (or recently-halted) node via RPC. +type rpcSource struct { + rpcURL string + client *rpcclient.RPCClient +} + +func newRPCSource(rpcURL string) (*rpcSource, error) { + client, err := rpcclient.NewHTTPClient(rpcURL) + if err != nil { + return nil, fmt.Errorf("creating RPC client for %s: %w", rpcURL, err) + } + return &rpcSource{rpcURL: rpcURL, client: client}, nil +} + +func (s *rpcSource) Description() string { return "RPC" } +func (s *rpcSource) Close() error { return s.client.Close() } + +func (s *rpcSource) FetchGenesis(ctx context.Context) (*bftypes.GenesisDoc, error) { + res, err := s.client.Genesis(ctx) + if err == nil { + return res.Genesis, nil + } + + // Fallback: large genesis docs can exceed the JSON-RPC client's response + // buffer. Fetch via raw HTTP with streaming decode instead. + genDoc, rawErr := s.fetchGenesisRawHTTP(ctx) + if rawErr != nil { + return nil, fmt.Errorf("RPC genesis call: %w (raw HTTP fallback also failed: %v)", err, rawErr) + } + return genDoc, nil +} + +// fetchGenesisRawHTTP fetches the genesis doc directly via HTTP, streaming the +// response body to handle arbitrarily large genesis documents (e.g. betanet +// with hundreds of thousands of genesis txs). +func (s *rpcSource) fetchGenesisRawHTTP(ctx context.Context) (*bftypes.GenesisDoc, error) { + // Build the HTTP URL from the RPC URL. + baseURL := strings.TrimRight(s.rpcURL, "/") + url := baseURL + "/genesis" + + // Force HTTP/1.1 — large responses (100+ MB) can trigger HTTP/2 stream + // errors with some reverse proxies / CDNs. + transport := &http.Transport{ + ForceAttemptHTTP2: false, + TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), + } + httpClient := &http.Client{ + Timeout: 10 * time.Minute, + Transport: transport, + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP GET %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP GET %s: status %d", url, resp.StatusCode) + } + + // The response is a JSON-RPC envelope: {"jsonrpc":"2.0","id":"","result":{"genesis":{...}}} + // Stream-decode to avoid buffering the entire response. + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading response body: %w", err) + } + + var envelope struct { + Result struct { + Genesis json.RawMessage `json:"genesis"` + } `json:"result"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + return nil, fmt.Errorf("decoding JSON-RPC envelope: %w", err) + } + + var genDoc bftypes.GenesisDoc + if err := amino.UnmarshalJSON(envelope.Result.Genesis, &genDoc); err != nil { + return nil, fmt.Errorf("decoding genesis doc: %w", err) + } + + return &genDoc, nil +} + +func (s *rpcSource) LatestHeight(ctx context.Context) (int64, error) { + res, err := s.client.Status(ctx, nil) + if err != nil { + return 0, fmt.Errorf("RPC status call: %w", err) + } + return res.SyncInfo.LatestBlockHeight, nil +} + +// signerState tracks per-signer sequence resolution during export. +type signerState struct { + accNum uint64 + finalSeq uint64 // from RPC query at halt_height + seq uint64 // current pre-tx sequence counter + initialized bool // true after first brute-force resolves starting seq + pendingFails []*pendingFailedTx +} + +type pendingFailedTx struct { + txIndex int // index in the output txs slice, for back-patching SignerInfo + signerI int // index of this signer within the tx's signers +} + +// FetchTxs fetches all transactions in [fromHeight, toHeight] with metadata. +// Includes both successful and failed txs. Failed txs are marked with +// Failed: true and are not re-executed during replay, but their sequence +// impact is tracked. +func (s *rpcSource) FetchTxs(ctx context.Context, fromHeight, toHeight int64, io commands.IO) ([]gnoland.TxWithMetadata, error) { + var txs []gnoland.TxWithMetadata + + // Get chain ID from genesis (needed for metadata) + genesis, err := s.FetchGenesis(ctx) + if err != nil { + return nil, err + } + chainID := genesis.ChainID + + // Per-signer state for sequence tracking + signerStates := map[crypto.Address]*signerState{} + + getOrCreateSignerState := func(addr crypto.Address) *signerState { + if ss, ok := signerStates[addr]; ok { + return ss + } + // Query account at halt_height + acc := s.queryAccountAtHeight(ctx, addr, toHeight, io) + ss := &signerState{} + if acc != nil { + ss.accNum = acc.GetAccountNumber() + ss.finalSeq = acc.GetSequence() + } + signerStates[addr] = ss + return ss + } + + total := toHeight - fromHeight + 1 + var processed, txCount int64 + + for h := fromHeight; h <= toHeight; h++ { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + processed++ + if processed%1000 == 0 || processed == total { + io.Printf("\r Blocks: %d/%d Txs: %d", processed, total, txCount) + } + + // Fetch block + block, err := s.client.Block(ctx, &h) + if err != nil { + return nil, fmt.Errorf("fetching block %d: %w", h, err) + } + + if len(block.Block.Data.Txs) == 0 { + continue + } + + // Fetch block results to check success/failure + results, err := s.client.BlockResults(ctx, &h) + if err != nil { + return nil, fmt.Errorf("fetching block results %d: %w", h, err) + } + + timestamp := block.Block.Header.Time.Unix() + + for i, rawTx := range block.Block.Data.Txs { + // Decode the raw transaction bytes + var stdTx std.Tx + if err := amino.Unmarshal(rawTx, &stdTx); err != nil { + io.Printf("\n WARNING: could not decode tx at height %d index %d: %v\n", h, i, err) + continue + } + + failed := false + if i < len(results.Results.DeliverTxs) && results.Results.DeliverTxs[i].IsErr() { + failed = true + } + + signers := stdTx.GetSigners() + sigs := stdTx.GetSignatures() + + txIdx := len(txs) // index in output slice + + // Build signer info + signerInfos := make([]gnoland.SignerAccountInfo, len(signers)) + for j, signer := range signers { + ss := getOrCreateSignerState(signer) + signerInfos[j] = gnoland.SignerAccountInfo{ + Address: signer, + AccountNum: ss.accNum, + Sequence: 0, // filled below + } + } + + if !failed { + // Successful tx: resolve sequences + for j, signer := range signers { + ss := signerStates[signer] + + if !ss.initialized || len(ss.pendingFails) > 0 { + // Brute-force to find this tx's pre-tx sequence. + lo := ss.seq + hi := ss.seq + uint64(len(ss.pendingFails)) + if !ss.initialized { + lo = 0 + hi = ss.finalSeq + } + + var sig std.Signature + if j < len(sigs) { + sig = sigs[j] + } + + resolvedSeq, err := bruteForceSignerSequence( + stdTx, sig, ss.accNum, lo, hi, chainID) + if err != nil { + io.Printf("\n WARNING: brute-force failed for signer %s at height %d: %v (using counter %d)\n", + signer, h, err, ss.seq) + resolvedSeq = ss.seq + } + + // Back-patch buffered failed txs (cosmetic/audit-only) + assignFailedTxSequences(txs, ss.pendingFails, ss.seq, resolvedSeq) + ss.pendingFails = nil + ss.seq = resolvedSeq + ss.initialized = true + } + + signerInfos[j].Sequence = ss.seq + ss.seq++ + } + } else { + // Failed tx: buffer for each signer + for j, signer := range signers { + ss := signerStates[signer] + ss.pendingFails = append(ss.pendingFails, &pendingFailedTx{ + txIndex: txIdx, + signerI: j, + }) + // Assign current counter as placeholder (will be back-patched) + signerInfos[j].Sequence = ss.seq + } + } + + txs = append(txs, gnoland.TxWithMetadata{ + Tx: stdTx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: timestamp, + BlockHeight: h, + ChainID: chainID, + Failed: failed, + SignerInfo: signerInfos, + }, + }) + txCount++ + } + } + + // Resolve trailing failures + for _, ss := range signerStates { + if len(ss.pendingFails) == 0 { + continue + } + + if !ss.initialized { + // Never had a successful tx. Cap consumed at len(pendingFails). + var consumed uint64 + if ss.finalSeq > ss.seq { + consumed = ss.finalSeq - ss.seq + } + if consumed > uint64(len(ss.pendingFails)) { + ss.seq = ss.finalSeq - uint64(len(ss.pendingFails)) + consumed = uint64(len(ss.pendingFails)) + } + assignTrailingFailedTxSequences(txs, ss.pendingFails, ss.seq, consumed) + } else { + var consumed uint64 + if ss.finalSeq > ss.seq { + consumed = ss.finalSeq - ss.seq + } + assignTrailingFailedTxSequences(txs, ss.pendingFails, ss.seq, consumed) + } + } + + io.Printf("\r Blocks: %d/%d Txs: %d\n", processed, total, txCount) + return txs, nil +} + +// queryAccountAtHeight queries an account's state at a specific block height. +func (s *rpcSource) queryAccountAtHeight( + ctx context.Context, addr crypto.Address, height int64, io commands.IO, +) std.Account { + path := fmt.Sprintf("auth/accounts/%s", addr) + res, err := s.client.ABCIQueryWithOptions(ctx, path, nil, rpcclient.ABCIQueryOptions{ + Height: height, + }) + if err != nil { + return nil + } + if res.Response.Error != nil { + return nil + } + if len(res.Response.Data) == 0 { + return nil + } + + // Response data is amino JSON (the auth query handler returns JSON). + // Try wrapped form first {"BaseAccount": {...}}, then direct. + var wrapper struct { + BaseAccount std.BaseAccount `json:"BaseAccount"` + } + if err := amino.UnmarshalJSON(res.Response.Data, &wrapper); err == nil { + return &wrapper.BaseAccount + } + + var acc std.BaseAccount + if err := amino.UnmarshalJSON(res.Response.Data, &acc); err != nil { + io.Printf("\n WARNING: could not decode account %s at height %d: %v\n", + addr, height, err) + return nil + } + return &acc +} + +// bruteForceSignerSequence tries sequences in [lo, hi] to find which makes +// the signature verify. Returns the pre-tx sequence (the value used in sign bytes). +func bruteForceSignerSequence( + tx std.Tx, sig std.Signature, accNum uint64, + lo, hi uint64, chainID string, +) (uint64, error) { + pubKey := sig.PubKey + if pubKey == nil { + return lo, fmt.Errorf("no pubkey in signature") + } + + for seq := lo; seq <= hi; seq++ { + signBytes, err := std.GetSignaturePayload(std.SignDoc{ + ChainID: chainID, + AccountNumber: accNum, + Sequence: seq, + Fee: tx.Fee, + Msgs: tx.Msgs, + Memo: tx.Memo, + }) + if err != nil { + continue + } + if pubKey.VerifyBytes(signBytes, sig.Signature) { + return seq, nil + } + } + + return lo, fmt.Errorf("no sequence in [%d, %d] verified for account %d", lo, hi, accNum) +} + +// assignFailedTxSequences back-patches sequence values on buffered failed txs. +// This is cosmetic/audit-only — failed txs are skipped during replay and the +// replay loop does not depend on their SignerInfo.Sequence values. +// +// Ordering within the gap is ambiguous: we cannot determine whether a failed tx +// was ante-fail (no sequence consumed) or msg-fail (sequence consumed) without +// re-verifying its signature, which may not be possible if the pubkey was not +// on-chain yet. We approximate by assuming msg-fails (consuming) come first in +// the gap, then ante-fails. +func assignFailedTxSequences( + txs []gnoland.TxWithMetadata, + pending []*pendingFailedTx, + startSeq, resolvedSeq uint64, +) { + consumed := resolvedSeq - startSeq + seq := startSeq + for i, pf := range pending { + if pf.txIndex < len(txs) && pf.signerI < len(txs[pf.txIndex].Metadata.SignerInfo) { + txs[pf.txIndex].Metadata.SignerInfo[pf.signerI].Sequence = seq + } + if uint64(i) < consumed { + seq++ + } + } +} + +// assignTrailingFailedTxSequences handles failed txs at the end of the chain +// with no subsequent success to anchor against. +func assignTrailingFailedTxSequences( + txs []gnoland.TxWithMetadata, + pending []*pendingFailedTx, + startSeq, consumed uint64, +) { + seq := startSeq + for i, pf := range pending { + if pf.txIndex < len(txs) && pf.signerI < len(txs[pf.txIndex].Metadata.SignerInfo) { + txs[pf.txIndex].Metadata.SignerInfo[pf.signerI].Sequence = seq + } + if uint64(i) < consumed { + seq++ + } + } +} diff --git a/contribs/gnogenesis/internal/fork/source_rpc_test.go b/contribs/gnogenesis/internal/fork/source_rpc_test.go new file mode 100644 index 00000000000..8b9e34a1505 --- /dev/null +++ b/contribs/gnogenesis/internal/fork/source_rpc_test.go @@ -0,0 +1,164 @@ +package fork + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "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" + "github.com/stretchr/testify/require" +) + +// signTxAt signs a tx as-if the signer had (accNum, seq) at sign time and +// returns the Signature. The returned Signature embeds the pubkey so +// bruteForceSignerSequence can verify it. +func signTxAt(t *testing.T, priv crypto.PrivKey, tx std.Tx, chainID string, accNum, seq uint64) std.Signature { + t.Helper() + payload, err := std.GetSignaturePayload(std.SignDoc{ + ChainID: chainID, + AccountNumber: accNum, + Sequence: seq, + Fee: tx.Fee, + Msgs: tx.Msgs, + Memo: tx.Memo, + }) + require.NoError(t, err) + + sig, err := priv.Sign(payload) + require.NoError(t, err) + + return std.Signature{ + PubKey: priv.PubKey(), + Signature: sig, + } +} + +func makeTestTx(t *testing.T, priv crypto.PrivKey) std.Tx { + t.Helper() + msg := bank.MsgSend{ + FromAddress: priv.PubKey().Address(), + ToAddress: priv.PubKey().Address(), // doesn't matter for sig test + Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + } + return std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(50000, std.NewCoin("ugnot", 1000)), + Memo: "test", + } +} + +func TestBruteForceSignerSequence(t *testing.T) { + t.Parallel() + + chainID := "test-chain" + priv := ed25519.GenPrivKey() + accNum := uint64(42) + + t.Run("finds correct sequence in range", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + actualSeq := uint64(7) + sig := signTxAt(t, priv, tx, chainID, accNum, actualSeq) + + resolved, err := bruteForceSignerSequence(tx, sig, accNum, 0, 20, chainID) + require.NoError(t, err) + assert.Equal(t, actualSeq, resolved) + }) + + t.Run("finds sequence at lo boundary", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 5) + + resolved, err := bruteForceSignerSequence(tx, sig, accNum, 5, 10, chainID) + require.NoError(t, err) + assert.Equal(t, uint64(5), resolved) + }) + + t.Run("finds sequence at hi boundary", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 10) + + resolved, err := bruteForceSignerSequence(tx, sig, accNum, 5, 10, chainID) + require.NoError(t, err) + assert.Equal(t, uint64(10), resolved) + }) + + t.Run("lo==hi with correct value", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 3) + + resolved, err := bruteForceSignerSequence(tx, sig, accNum, 3, 3, chainID) + require.NoError(t, err) + assert.Equal(t, uint64(3), resolved) + }) + + t.Run("sequence outside range returns error", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 100) + + _, err := bruteForceSignerSequence(tx, sig, accNum, 0, 20, chainID) + require.Error(t, err) + assert.Contains(t, err.Error(), "no sequence in") + }) + + t.Run("wrong account number returns error", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 5) + + // Sign says accNum=42 but we search assuming 99. + _, err := bruteForceSignerSequence(tx, sig, 99, 0, 20, chainID) + require.Error(t, err) + }) + + t.Run("wrong chain ID returns error", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 5) + + // Sign says chainID="test-chain" but we search with "other-chain". + _, err := bruteForceSignerSequence(tx, sig, accNum, 0, 20, "other-chain") + require.Error(t, err) + }) + + t.Run("nil pubkey returns error", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := std.Signature{PubKey: nil, Signature: []byte("dummy")} + + _, err := bruteForceSignerSequence(tx, sig, accNum, 0, 20, chainID) + require.Error(t, err) + assert.Contains(t, err.Error(), "no pubkey") + }) + + t.Run("secp256k1 key also works", func(t *testing.T) { + t.Parallel() + sPriv := secp256k1.GenPrivKey() + tx := makeTestTx(t, sPriv) + sig := signTxAt(t, sPriv, tx, chainID, accNum, 12) + + resolved, err := bruteForceSignerSequence(tx, sig, accNum, 0, 20, chainID) + require.NoError(t, err) + assert.Equal(t, uint64(12), resolved) + }) + + t.Run("tampered tx fee rejects all sequences", func(t *testing.T) { + t.Parallel() + tx := makeTestTx(t, priv) + sig := signTxAt(t, priv, tx, chainID, accNum, 5) + + // Tamper with the tx after signing. + tampered := tx + tampered.Fee = std.NewFee(99999, std.NewCoin("ugnot", 9999)) + + _, err := bruteForceSignerSequence(tampered, sig, accNum, 0, 20, chainID) + require.Error(t, err) + }) +} diff --git a/contribs/gnogenesis/internal/fork/test.go b/contribs/gnogenesis/internal/fork/test.go new file mode 100644 index 00000000000..08acde42a1f --- /dev/null +++ b/contribs/gnogenesis/internal/fork/test.go @@ -0,0 +1,284 @@ +package fork + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + "path/filepath" + "sync/atomic" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/amino" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type testCfg struct { + genesis string + timeout time.Duration + verbose bool + keepRunning bool +} + +func newTestCmd(io commands.IO) *commands.Command { + cfg := &testCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "test", + ShortUsage: "test [flags]", + ShortHelp: "smoke-test a hardfork genesis by replaying it in-process", + LongHelp: `Smoke-tests a hardfork genesis by loading it into an in-memory gnoland node +and replaying all transactions (genesis-mode and historical). + +A fresh single-validator identity is generated for the test — it replaces the +real validators in the genesis so the node can produce blocks without requiring +the actual validator keys. The app state (txs, balances, packages) is kept +exactly as-is. + +SkipGenesisSigVerification is enabled for genesis-mode txs. Historical txs +(those with block_height > 0) go through the normal ante handler using the +original_chain_id from the genesis to verify signatures. + +Exit code: 0 on success (all txs replayed, first block produced), non-zero on failure. + +Examples: + + # Smoke-test the default output of hardfork genesis: + hardfork test --genesis genesis.json + + # With a longer timeout and verbose tx logging: + hardfork test --genesis genesis.json --timeout 2h --verbose + + # Keep the node running after replay for manual inspection via RPC: + hardfork test --genesis genesis.json --keep-running`, + }, + cfg, + func(ctx context.Context, args []string) error { + return execTest(ctx, cfg, io) + }, + ) +} + +func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar(&c.genesis, "genesis", "genesis.json", "path to the hardfork genesis.json to test") + fs.DurationVar(&c.timeout, "timeout", 30*time.Minute, "maximum time to wait for genesis replay to complete") + fs.BoolVar(&c.verbose, "verbose", false, "print each tx result during replay") + fs.BoolVar(&c.keepRunning, "keep-running", false, "keep the node running after genesis replay (for manual RPC inspection)") +} + +func execTest(ctx context.Context, cfg *testCfg, io commands.IO) error { + // ------------------------------------------------------------------------- + // Step 1: Load and parse the genesis file + // ------------------------------------------------------------------------- + io.Printf("Loading genesis: %s\n", cfg.genesis) + + data, err := os.ReadFile(cfg.genesis) + if err != nil { + return fmt.Errorf("reading genesis file: %w", err) + } + + var genDoc bft.GenesisDoc + if err := amino.UnmarshalJSON(data, &genDoc); err != nil { + return fmt.Errorf("parsing genesis: %w", err) + } + + if err := genDoc.ValidateAndComplete(); err != nil { + return fmt.Errorf("genesis validation failed: %w", err) + } + + // Extract app state for summary + appState, ok := genDoc.AppState.(gnoland.GnoGenesisState) + if !ok { + raw, err := amino.MarshalJSON(genDoc.AppState) + if err != nil { + return fmt.Errorf("marshalling app state: %w", err) + } + if err := amino.UnmarshalJSON(raw, &appState); err != nil { + return fmt.Errorf("unmarshalling app state: %w", err) + } + } + + genesisModeTxs := baseGenesisModeTxs(&appState) + historicalTxs := len(appState.Txs) - len(genesisModeTxs) + + io.Printf(" Past chain IDs: %v\n", appState.PastChainIDs) + io.Printf(" New chain ID: %s\n", genDoc.ChainID) + io.Printf(" Initial height: %d\n", genDoc.InitialHeight) + io.Printf(" Genesis-mode txs: %d\n", len(genesisModeTxs)) + io.Printf(" Historical txs: %d\n", historicalTxs) + io.Printf(" Total txs: %d\n", len(appState.Txs)) + + if len(appState.PastChainIDs) == 0 && historicalTxs > 0 { + io.Println(" WARNING: past_chain_ids is empty — historical tx signatures cannot be verified.") + } + + // ------------------------------------------------------------------------- + // Step 2: Replace validators with a local test identity + // ------------------------------------------------------------------------- + pv := bft.NewMockPV() + pk := pv.PubKey() + genDoc.Validators = []bft.GenesisValidator{ + { + Address: pk.Address(), + PubKey: pk, + Power: 10, + Name: "hardfork-test-node", + }, + } + + // ------------------------------------------------------------------------- + // Step 3: Find GNOROOT (needed for stdlibs) + // ------------------------------------------------------------------------- + gnoroot, err := gnoenv.GuessRootDir() + if err != nil { + return fmt.Errorf("cannot locate GNOROOT (set the GNOROOT env var): %w", err) + } + + stdlibDir := filepath.Join(gnoroot, "gnovm", "stdlibs") + if _, err := os.Stat(stdlibDir); err != nil { + return fmt.Errorf("stdlibs directory not found at %s (is GNOROOT correct?): %w", stdlibDir, err) + } + + // ------------------------------------------------------------------------- + // Step 4: Set up tx result tracking + // ------------------------------------------------------------------------- + var txFailures atomic.Int64 + var txProcessed atomic.Int64 + + txResultHandler := func(ctx sdk.Context, tx std.Tx, res sdk.Result) { + txProcessed.Add(1) + if res.IsErr() { + txFailures.Add(1) + if cfg.verbose { + io.Printf(" [FAIL] height=%d error=%s\n", ctx.BlockHeight(), res.Log) + } + } else if cfg.verbose { + msgs := make([]string, len(tx.Msgs)) + for i, m := range tx.Msgs { + msgs[i] = m.Type() + } + io.Printf(" [OK] height=%d msgs=%v\n", ctx.BlockHeight(), msgs) + } + } + + // ------------------------------------------------------------------------- + // Step 5: Configure in-memory node + // ------------------------------------------------------------------------- + tmConfig := tmcfg.TestConfig().SetRootDir(gnoroot) + tmConfig.Consensus.WALDisabled = true + tmConfig.Consensus.SkipTimeoutCommit = true + tmConfig.Consensus.CreateEmptyBlocks = false + tmConfig.RPC.ListenAddress = "tcp://127.0.0.1:0" // random port, avoids conflicts + tmConfig.P2P.ListenAddress = "tcp://127.0.0.1:0" + + nodeCfg := &gnoland.InMemoryNodeConfig{ + PrivValidator: pv, + Genesis: &genDoc, + TMConfig: tmConfig, + DB: memdb.NewMemDB(), + SkipGenesisSigVerification: true, + InitChainerConfig: gnoland.InitChainerConfig{ + GenesisTxResultHandler: txResultHandler, + StdlibDir: stdlibDir, + CacheStdlibLoad: false, + }, + } + + // Choose logger: quiet by default, real output when verbose + var nodeLogger *slog.Logger + if cfg.verbose { + nodeLogger = slog.Default() + } else { + nodeLogger = log.NewNoopLogger() + } + + // ------------------------------------------------------------------------- + // Step 6: Start the node + // ------------------------------------------------------------------------- + io.Println() + io.Println("Starting in-memory node for genesis replay...") + + n, err := gnoland.NewInMemoryNode(nodeLogger, nodeCfg) + if err != nil { + return fmt.Errorf("creating in-memory node: %w", err) + } + + start := time.Now() + + if err := n.Start(); err != nil { + return fmt.Errorf("starting node: %w", err) + } + + defer func() { + if stopErr := n.Stop(); stopErr != nil { + io.Printf("WARNING: error stopping node: %v\n", stopErr) + } + }() + + // ------------------------------------------------------------------------- + // Step 7: Wait for genesis replay to complete (first block produced) + // ------------------------------------------------------------------------- + io.Printf("Replaying %d txs (timeout: %s)...\n", len(appState.Txs), cfg.timeout) + + // Progress ticker: print elapsed time every 30s so the user knows it's alive + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + timeoutCtx, cancel := context.WithTimeout(ctx, cfg.timeout) + defer cancel() + + for { + select { + case <-n.Ready(): + elapsed := time.Since(start) + failures := txFailures.Load() + processed := txProcessed.Load() + + io.Println() + io.Println("=== Test Results ===") + io.Printf(" Elapsed: %s\n", elapsed.Round(time.Second)) + io.Printf(" Txs processed: %d / %d\n", processed, len(appState.Txs)) + io.Printf(" Failures: %d\n", failures) + + if failures > 0 { + io.Println() + io.Printf("FAIL: %d transaction(s) failed during genesis replay.\n", failures) + io.Println("Run with --verbose to see individual failures.") + return fmt.Errorf("genesis replay completed with %d failures", failures) + } + + io.Println() + io.Println("PASS: genesis replay completed successfully.") + + if cfg.keepRunning { + io.Println() + io.Printf("Node is running at: %s\n", tmConfig.RPC.ListenAddress) + io.Println("Press Ctrl+C to stop.") + <-ctx.Done() + } + + return nil + + case <-ticker.C: + elapsed := time.Since(start) + processed := txProcessed.Load() + io.Printf(" ... still replaying: %d/%d txs, %s elapsed\n", + processed, len(appState.Txs), elapsed.Round(time.Second)) + + case <-timeoutCtx.Done(): + processed := txProcessed.Load() + return fmt.Errorf("genesis replay timed out after %s (%d/%d txs processed)", + cfg.timeout, processed, len(appState.Txs)) + } + } +} diff --git a/contribs/gnogenesis/internal/fork/test_test.go b/contribs/gnogenesis/internal/fork/test_test.go new file mode 100644 index 00000000000..fa53c40655f --- /dev/null +++ b/contribs/gnogenesis/internal/fork/test_test.go @@ -0,0 +1,174 @@ +package fork + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/stretchr/testify/require" + + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" +) + +// writeTestGenesis writes a minimal but valid genesis.json to a temp file. +// It uses a fresh private validator so the genesis is self-contained. +func writeTestGenesis(t *testing.T, appState gnoland.GnoGenesisState) string { + t.Helper() + + pv := bft.NewMockPV() + pk := pv.PubKey() + + genDoc := bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: "test-hardfork-1", + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, + MaxDataBytes: 2_000_000, + MaxGas: 3_000_000_000, + TimeIotaMS: 100, + }, + }, + Validators: []bft.GenesisValidator{ + { + Address: pk.Address(), + PubKey: pk, + Power: 10, + Name: "test-validator", + }, + }, + AppState: appState, + } + + data, err := amino.MarshalJSONIndent(genDoc, "", " ") + require.NoError(t, err) + + dir := t.TempDir() + path := filepath.Join(dir, "genesis.json") + require.NoError(t, os.WriteFile(path, data, 0o644)) + return path +} + +func minimalAppState() gnoland.GnoGenesisState { + return gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{}, + Txs: []gnoland.TxWithMetadata{}, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vmm.DefaultGenesisState(), + } +} + +// TestExecTest_MissingGenesis verifies that a missing genesis file is caught. +func TestExecTest_MissingGenesis(t *testing.T) { + io := commands.NewTestIO() + cfg := &testCfg{ + genesis: "/nonexistent/path/genesis.json", + timeout: 5 * time.Second, + } + err := execTest(context.Background(), cfg, io) + require.ErrorContains(t, err, "reading genesis file") +} + +// TestExecTest_InvalidGenesis verifies that a malformed genesis file is caught. +func TestExecTest_InvalidGenesis(t *testing.T) { + dir := t.TempDir() + bad := filepath.Join(dir, "bad.json") + require.NoError(t, os.WriteFile(bad, []byte(`{"not_valid": "json"`), 0o644)) + + io := commands.NewTestIO() + cfg := &testCfg{ + genesis: bad, + timeout: 5 * time.Second, + } + err := execTest(context.Background(), cfg, io) + require.ErrorContains(t, err, "parsing genesis") +} + +// TestExecTest_EmptyGenesis runs a full in-process replay with an empty genesis +// (no transactions). This verifies the happy path without requiring network access. +// +// This test is skipped in short mode (-short) because loading stdlibs takes ~30s. +func TestExecTest_EmptyGenesis(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode — requires loading stdlibs (~30s)") + } + + // Ensure GNOROOT is set (required for stdlibs). + // If running from the repo root, gnoenv.GuessRootDir() will find it via go list. + path := writeTestGenesis(t, minimalAppState()) + + io := commands.NewTestIO() + cfg := &testCfg{ + genesis: path, + timeout: 3 * time.Minute, + } + + err := execTest(context.Background(), cfg, io) + require.NoError(t, err, "empty genesis replay should succeed") +} + +// TestExecTest_HardforkGenesis builds a minimal hardfork genesis (with +// PastChainIDs and InitialHeight set) and verifies it can be replayed. +// +// This test is skipped in short mode (-short) because loading stdlibs takes ~30s. +func TestExecTest_HardforkGenesis(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode — requires loading stdlibs (~30s)") + } + + appState := minimalAppState() + appState.PastChainIDs = []string{"test-hardfork-source"} + + pv := bft.NewMockPV() + pk := pv.PubKey() + + genDoc := bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: "test-hardfork-1", + InitialHeight: 100, // hardfork starts at block 100 + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, + MaxDataBytes: 2_000_000, + MaxGas: 3_000_000_000, + TimeIotaMS: 100, + }, + }, + Validators: []bft.GenesisValidator{ + { + Address: pk.Address(), + PubKey: pk, + Power: 10, + Name: "test-validator", + }, + }, + AppState: appState, + } + + data, err := amino.MarshalJSONIndent(genDoc, "", " ") + require.NoError(t, err) + + dir := t.TempDir() + path := filepath.Join(dir, "genesis.json") + require.NoError(t, os.WriteFile(path, data, 0o644)) + + io := commands.NewTestIO() + cfg := &testCfg{ + genesis: path, + timeout: 3 * time.Minute, + } + + err = execTest(context.Background(), cfg, io) + require.NoError(t, err, "hardfork genesis replay should succeed") +} + diff --git a/contribs/tx-archive/backup/backup.go b/contribs/tx-archive/backup/backup.go index 48acfe1c389..9e2cc9aa453 100644 --- a/contribs/tx-archive/backup/backup.go +++ b/contribs/tx-archive/backup/backup.go @@ -7,7 +7,8 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" - _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // amino types + _ "github.com/gnolang/gno/gnovm/stdlibs/chain" // amino types "github.com/gnolang/gno/contribs/tx-archive/backup/client" "github.com/gnolang/gno/contribs/tx-archive/backup/writer" @@ -23,18 +24,20 @@ type Service struct { writer writer.Writer logger log.Logger - batchSize uint - watchInterval time.Duration // interval for the watch routine - skipFailedTxs bool + batchSize uint + watchInterval time.Duration // interval for the watch routine + skipFailedTxs bool + populateSignerInfo bool // populate per-tx SignerInfo (default true for bounded backups) } // NewService creates a new backup service func NewService(client client.Client, writer writer.Writer, opts ...Option) *Service { s := &Service{ - client: client, - writer: writer, - logger: noop.New(), - watchInterval: 1 * time.Second, + client: client, + writer: writer, + logger: noop.New(), + watchInterval: 1 * time.Second, + populateSignerInfo: true, } for _, opt := range opts { @@ -62,12 +65,39 @@ func (s *Service) ExecuteBackup(ctx context.Context, cfg Config) error { return fmt.Errorf("unable to determine right bound, %w", boundErr) } + // Fetch source chain ID once — used to tag every tx so a hardfork replay + // (see gno.land/pkg/gnoland.GnoGenesisState.PastChainIDs) can verify the + // signature against the chain that produced it. + chainID, chainIDErr := s.client.GetChainID() + if chainIDErr != nil { + return fmt.Errorf("unable to fetch source chain id, %w", chainIDErr) + } + + // SignerInfo resolver: fills per-signer (account_num, sequence) metadata + // so hardfork replay can force-set account state before signature + // verification. Only enabled for non-watch, bounded backups — it needs + // a fixed halt height to anchor the brute-force sequence search. + // + // Because the resolver back-patches failed-tx SignerInfo retroactively + // (at the next success per signer), enabling it switches the writer + // from streaming to buffered mode: all txs are held in memory until the + // final Finalize pass, then flushed to the writer in one pass. + var ( + resolver *signerResolver + bufferedTxs []*gnoland.TxWithMetadata + ) + if s.populateSignerInfo && !cfg.Watch { + resolver = newSignerResolver(s.client, chainID, toBlock) + } + // Log info about what will be backed up s.logger.Info( "Existing blocks to backup", + "chain id", chainID, "from block", cfg.FromBlock, "to block", toBlock, "total", toBlock-cfg.FromBlock+1, + "populate signer info", resolver != nil, ) // Keep track of what has been backed up @@ -89,6 +119,13 @@ func (s *Service) ExecuteBackup(ctx context.Context, cfg Config) error { // Internal function that fetches and writes a range of blocks fetchAndWrite := func(fromBlock, toBlock uint64) error { + // Progress pacing: print one status line every ~5s, not per batch. + var ( + progressStart = time.Now() + totalRange = toBlock - fromBlock + 1 + nextProgress = progressStart.Add(5 * time.Second) + ) + // Fetch by batches for batchStart := fromBlock; batchStart <= toBlock; { // Determine batch stop block @@ -117,6 +154,33 @@ func (s *Service) ExecuteBackup(ctx context.Context, cfg Config) error { results.blocksFetched += batchSize results.blocksWithTxs += uint64(len(blocks)) + // Pace progress output at ~5s intervals + always on last batch. + if now := time.Now(); now.After(nextProgress) || batchStop == toBlock { + nextProgress = now.Add(5 * time.Second) + + elapsed := now.Sub(progressStart) + done := batchStop - fromBlock + 1 + var blocksPerSec float64 + if secs := elapsed.Seconds(); secs > 0 { + blocksPerSec = float64(done) / secs + } + eta := time.Duration(0) + if blocksPerSec > 0 && done < totalRange { + remaining := totalRange - done + eta = time.Duration(float64(remaining)/blocksPerSec) * time.Second + } + pct := float64(done) / float64(totalRange) * 100 + s.logger.Info( + "Progress", + "blocks", fmt.Sprintf("%d/%d", done, totalRange), + "pct", fmt.Sprintf("%.1f%%", pct), + "rate", fmt.Sprintf("%.0f blocks/s", blocksPerSec), + "txs", results.txsBackedUp, + "elapsed", elapsed.Round(time.Second), + "eta", eta.Round(time.Second), + ) + } + // Verbose log for blocks containing transactions s.logger.Debug( "Batch fetched successfully", @@ -142,8 +206,9 @@ func (s *Service) ExecuteBackup(ctx context.Context, cfg Config) error { for i, tx := range block.Txs { txResult := txResults[i] + failed := !txResult.IsOK() - if !txResult.IsOK() && s.skipFailedTxs { + if failed && s.skipFailedTxs { // Skip saving failed transaction s.logger.Debug( "Skipping failed tx", @@ -158,11 +223,22 @@ func (s *Service) ExecuteBackup(ctx context.Context, cfg Config) error { txData := &gnoland.TxWithMetadata{ Tx: tx, Metadata: &gnoland.GnoTxMetadata{ - Timestamp: block.Timestamp, + Timestamp: block.Timestamp, + BlockHeight: int64(block.Height), + ChainID: chainID, + Failed: failed, }, } - if writeErr := s.writer.WriteTxData(txData); writeErr != nil { + if resolver != nil { + // Buffer — SignerInfo is populated & back-patched, + // then the whole batch is flushed after Finalize. + if pErr := resolver.Populate(txData); pErr != nil { + return fmt.Errorf("populate signer info @ h=%d idx=%d: %w", + block.Height, i, pErr) + } + bufferedTxs = append(bufferedTxs, txData) + } else if writeErr := s.writer.WriteTxData(txData); writeErr != nil { return fmt.Errorf("unable to write tx data, %w", writeErr) } @@ -190,6 +266,19 @@ func (s *Service) ExecuteBackup(ctx context.Context, cfg Config) error { return fetchErr } + // Flush the resolver's buffered output (if enabled). Finalize back-patches + // any trailing failed-tx SignerInfo entries, then we stream the whole + // ordered batch to the writer. + if resolver != nil { + resolver.Finalize() + for _, txData := range bufferedTxs { + if writeErr := s.writer.WriteTxData(txData); writeErr != nil { + return fmt.Errorf("unable to write tx data, %w", writeErr) + } + } + bufferedTxs = nil + } + // Check if there needs to be a watcher setup if cfg.Watch { s.logger.Info( diff --git a/contribs/tx-archive/backup/backup_test.go b/contribs/tx-archive/backup/backup_test.go index aa86792728f..b9bfcbe1097 100644 --- a/contribs/tx-archive/backup/backup_test.go +++ b/contribs/tx-archive/backup/backup_test.go @@ -237,6 +237,9 @@ func TestBackup_ExecuteBackup_FixedRange(t *testing.T) { blockTime.Add(time.Duration(expectedBlock)*time.Minute).Local(), time.UnixMilli(txData.Metadata.Timestamp), ) + assert.Equal(t, int64(expectedBlock), txData.Metadata.BlockHeight) + assert.Equal(t, "test-chain", txData.Metadata.ChainID) + assert.False(t, txData.Metadata.Failed) } // Check for errors during scanning diff --git a/contribs/tx-archive/backup/client/client.go b/contribs/tx-archive/backup/client/client.go index a01b9529e87..3c2eaaca5dc 100644 --- a/contribs/tx-archive/backup/client/client.go +++ b/contribs/tx-archive/backup/client/client.go @@ -4,6 +4,7 @@ import ( "context" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -12,6 +13,9 @@ type Client interface { // GetLatestBlockNumber returns the latest block height from the chain GetLatestBlockNumber() (uint64, error) + // GetChainID returns the chain ID of the source chain + GetChainID() (string, error) + // GetBlocks returns a slice of Block - including the block height and its // timestamp in milliseconds - in the requested range only if they contain // transactions @@ -19,6 +23,12 @@ type Client interface { // GetTxResults returns the block transaction results (if any) GetTxResults(block uint64) ([]*abci.ResponseDeliverTx, error) + + // GetAccountAtHeight returns the (account_number, sequence) pair for + // the given address at the given block height. Used by the hardfork- + // metadata signer-info resolver to anchor brute-force sequence search. + // Returns (0, 0, nil) when the account doesn't exist yet at that height. + GetAccountAtHeight(addr crypto.Address, height uint64) (accNum, sequence uint64, err error) } type Block struct { diff --git a/contribs/tx-archive/backup/client/rpc/rpc.go b/contribs/tx-archive/backup/client/rpc/rpc.go index 6817454742e..4e0ea419178 100644 --- a/contribs/tx-archive/backup/client/rpc/rpc.go +++ b/contribs/tx-archive/backup/client/rpc/rpc.go @@ -11,6 +11,7 @@ import ( abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" rpcClient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/contribs/tx-archive/backup/client" @@ -59,6 +60,50 @@ func (c *Client) GetLatestBlockNumber() (uint64, error) { return uint64(status.SyncInfo.LatestBlockHeight), nil } +// GetChainID returns the chain ID of the source chain, fetched from /status. +func (c *Client) GetChainID() (string, error) { + status, err := c.client.Status(context.Background(), nil) + if err != nil { + return "", fmt.Errorf("unable to fetch chain ID, %w", err) + } + + return status.NodeInfo.Network, nil +} + +// GetAccountAtHeight queries auth/accounts/ at the given block height +// and returns (account_number, sequence). Returns (0, 0, nil) when the +// account does not yet exist at that height (i.e. genesis-less / pre-creation). +func (c *Client) GetAccountAtHeight(addr crypto.Address, height uint64) (uint64, uint64, error) { + path := fmt.Sprintf("auth/accounts/%s", addr) + res, err := c.client.ABCIQueryWithOptions( + context.Background(), + path, nil, + rpcClient.ABCIQueryOptions{Height: int64(height)}, + ) + if err != nil { + return 0, 0, fmt.Errorf("abci query %s at %d: %w", path, height, err) + } + if res.Response.Error != nil || len(res.Response.Data) == 0 { + // Account doesn't exist yet — not an error. + return 0, 0, nil + } + + // Response is amino JSON. Try wrapped form first, then direct. + var wrapper struct { + BaseAccount std.BaseAccount `json:"BaseAccount"` + } + if err := amino.UnmarshalJSON(res.Response.Data, &wrapper); err == nil && + wrapper.BaseAccount.Address == addr { + return wrapper.BaseAccount.AccountNumber, wrapper.BaseAccount.Sequence, nil + } + + var acc std.BaseAccount + if err := amino.UnmarshalJSON(res.Response.Data, &acc); err != nil { + return 0, 0, fmt.Errorf("decode BaseAccount for %s: %w", addr, err) + } + return acc.AccountNumber, acc.Sequence, nil +} + func (c *Client) GetBlocks(ctx context.Context, from, to uint64) ([]*client.Block, error) { // Check if the block range is valid if from > to { diff --git a/contribs/tx-archive/backup/mock_test.go b/contribs/tx-archive/backup/mock_test.go index 57946cb2ab1..51cb2b8a41a 100644 --- a/contribs/tx-archive/backup/mock_test.go +++ b/contribs/tx-archive/backup/mock_test.go @@ -5,18 +5,23 @@ import ( "github.com/gnolang/gno/contribs/tx-archive/backup/client" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/crypto" ) type ( getLatestBlockNumberDelegate func() (uint64, error) + getChainIDDelegate func() (string, error) getBlocksDelegate func(context.Context, uint64, uint64) ([]*client.Block, error) getTxResultsDelegate func(uint64) ([]*abci.ResponseDeliverTx, error) + getAccountAtHeightDelegate func(crypto.Address, uint64) (uint64, uint64, error) ) type mockClient struct { getLatestBlockNumberFn getLatestBlockNumberDelegate + getChainIDFn getChainIDDelegate getBlocksFn getBlocksDelegate getTxResultsFn getTxResultsDelegate + getAccountAtHeightFn getAccountAtHeightDelegate } func (m *mockClient) GetLatestBlockNumber() (uint64, error) { @@ -27,6 +32,14 @@ func (m *mockClient) GetLatestBlockNumber() (uint64, error) { return 0, nil } +func (m *mockClient) GetChainID() (string, error) { + if m.getChainIDFn != nil { + return m.getChainIDFn() + } + + return "test-chain", nil +} + func (m *mockClient) GetBlocks(ctx context.Context, from, to uint64) ([]*client.Block, error) { if m.getBlocksFn != nil { return m.getBlocksFn(ctx, from, to) @@ -42,3 +55,11 @@ func (m *mockClient) GetTxResults(block uint64) ([]*abci.ResponseDeliverTx, erro return nil, nil } + +func (m *mockClient) GetAccountAtHeight(addr crypto.Address, height uint64) (uint64, uint64, error) { + if m.getAccountAtHeightFn != nil { + return m.getAccountAtHeightFn(addr, height) + } + + return 0, 0, nil +} diff --git a/contribs/tx-archive/backup/options.go b/contribs/tx-archive/backup/options.go index e9ca9cda8bb..5c48c472422 100644 --- a/contribs/tx-archive/backup/options.go +++ b/contribs/tx-archive/backup/options.go @@ -24,3 +24,13 @@ func WithSkipFailedTxs(skip bool) Option { s.skipFailedTxs = skip } } + +// WithPopulateSignerInfo enables/disables per-tx SignerInfo population. +// Default is true. Disable for lightweight stream backups that don't need +// to be replay-ready (and avoids the brute-force sequence search cost). +// Ignored in watch mode (always off). +func WithPopulateSignerInfo(populate bool) Option { + return func(s *Service) { + s.populateSignerInfo = populate + } +} diff --git a/contribs/tx-archive/backup/signerinfo.go b/contribs/tx-archive/backup/signerinfo.go new file mode 100644 index 00000000000..338512414d1 --- /dev/null +++ b/contribs/tx-archive/backup/signerinfo.go @@ -0,0 +1,246 @@ +package backup + +//nolint:revive // See https://github.com/gnolang/gno/issues/1197 +import ( + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + + "github.com/gnolang/gno/contribs/tx-archive/backup/client" +) + +// signerResolver tracks per-signer account state during backup so that each +// exported tx carries a SignerInfo entry with the (account_num, sequence) +// values used to sign it on the source chain. +// +// Hardfork replay needs these values to force-set account state on the new +// chain before signature verification — see gno.land/pkg/gnoland.InitChainer +// loadAppState (PR #5511). +// +// Brute-force resolution strategy (ported from misc/hardfork/source_rpc.go): +// 1. On first sight of a signer, query auth/accounts/ at the halt +// height to learn the *final* (accNum, finalSeq). +// 2. On the signer's first *successful* tx in the stream, brute-force +// sequences in [0, finalSeq] against the tx signature to find the +// starting sequence at that point in history. Subsequent successful +// txs simply increment a counter. +// 3. Failed txs buffer until the next success — if any, re-brute-force to +// figure out how many of them actually consumed sequence (ante-fail = +// no consume, msg-fail = consume). Trailing failed txs (no later +// success to anchor) are handled in Finalize(). +// +// Failed-tx sequence values are cosmetic — replay skips failed txs — so the +// resolver's fallbacks err on the side of "roughly right" rather than +// re-fetching more RPC state. +type signerResolver struct { + client client.Client + chainID string + haltHeight uint64 + states map[crypto.Address]*signerState +} + +type signerState struct { + accNum uint64 + finalSeq uint64 // from RPC query at halt_height + seq uint64 // current pre-tx counter + initialized bool // true after first success brute-force resolves start + pendingFails []*pendingFailedTx +} + +type pendingFailedTx struct { + info *gnoland.SignerAccountInfo // direct pointer into tx.Metadata.SignerInfo + ownerSS *signerState +} + +func newSignerResolver(c client.Client, chainID string, haltHeight uint64) *signerResolver { + return &signerResolver{ + client: c, + chainID: chainID, + haltHeight: haltHeight, + states: map[crypto.Address]*signerState{}, + } +} + +// Populate fills tx.Metadata.SignerInfo for one tx. Must be called in the +// order txs were produced on the source chain (block-ascending, within-block +// index-ascending). +func (r *signerResolver) Populate(tx *gnoland.TxWithMetadata) error { + if tx.Metadata == nil { + tx.Metadata = &gnoland.GnoTxMetadata{} + } + + stdTx := tx.Tx + signers := stdTx.GetSigners() + sigs := stdTx.GetSignatures() + failed := tx.Metadata.Failed + + infos := make([]gnoland.SignerAccountInfo, len(signers)) + + for j, signer := range signers { + ss, err := r.state(signer) + if err != nil { + return err + } + infos[j] = gnoland.SignerAccountInfo{ + Address: signer, + AccountNum: ss.accNum, + // Sequence filled below. + } + } + tx.Metadata.SignerInfo = infos + + if failed { + // Buffer failed tx signer info pointers — sequences are back-patched + // at the next success (or in Finalize). + for j, signer := range signers { + ss := r.states[signer] + ss.pendingFails = append(ss.pendingFails, &pendingFailedTx{ + info: &tx.Metadata.SignerInfo[j], + ownerSS: ss, + }) + // Placeholder sequence. + tx.Metadata.SignerInfo[j].Sequence = ss.seq + } + return nil + } + + // Successful tx — resolve sequence per signer. + for j, signer := range signers { + ss := r.states[signer] + + needResolve := !ss.initialized || len(ss.pendingFails) > 0 + if needResolve { + lo := ss.seq + hi := ss.seq + uint64(len(ss.pendingFails)) + if !ss.initialized { + lo = 0 + hi = ss.finalSeq + } + + var sig std.Signature + if j < len(sigs) { + sig = sigs[j] + } + + resolved, err := bruteForceSignerSequence( + stdTx, sig, ss.accNum, lo, hi, r.chainID, + ) + if err != nil { + // Last resort: keep current counter. Subsequent txs may fail + // verification, but at least export proceeds. + resolved = ss.seq + } + + // Back-patch buffered failed txs now that we know how much + // sequence was consumed between the last success and this one. + assignFailedTxSequences(ss.pendingFails, ss.seq, resolved) + ss.pendingFails = nil + ss.seq = resolved + ss.initialized = true + } + + tx.Metadata.SignerInfo[j].Sequence = ss.seq + ss.seq++ + } + return nil +} + +// Finalize back-patches any trailing failed txs (those with no successor +// success to anchor against). Must be called once after the last Populate. +func (r *signerResolver) Finalize() { + for _, ss := range r.states { + if len(ss.pendingFails) == 0 { + continue + } + + var consumed uint64 + if ss.finalSeq > ss.seq { + consumed = ss.finalSeq - ss.seq + } + if !ss.initialized && consumed > uint64(len(ss.pendingFails)) { + // Never had a successful tx — cap consumed. + ss.seq = ss.finalSeq - uint64(len(ss.pendingFails)) + consumed = uint64(len(ss.pendingFails)) + } + assignTrailingFailedTxSequences(ss.pendingFails, ss.seq, consumed) + ss.pendingFails = nil + } +} + +// state fetches-or-creates the signerState for addr. +func (r *signerResolver) state(addr crypto.Address) (*signerState, error) { + if ss, ok := r.states[addr]; ok { + return ss, nil + } + accNum, finalSeq, err := r.client.GetAccountAtHeight(addr, r.haltHeight) + if err != nil { + return nil, fmt.Errorf("fetch account state for %s at %d: %w", + addr, r.haltHeight, err) + } + ss := &signerState{accNum: accNum, finalSeq: finalSeq} + r.states[addr] = ss + return ss, nil +} + +// bruteForceSignerSequence tries sequences in [lo, hi] to find the one that +// makes the tx signature verify. Returns the pre-tx sequence (the value that +// was used in GetSignBytes on the source chain). +func bruteForceSignerSequence( + tx std.Tx, sig std.Signature, accNum uint64, + lo, hi uint64, chainID string, +) (uint64, error) { + pubKey := sig.PubKey + if pubKey == nil { + return lo, fmt.Errorf("no pubkey in signature") + } + + for seq := lo; seq <= hi; seq++ { + signBytes, err := std.GetSignaturePayload(std.SignDoc{ + ChainID: chainID, + AccountNumber: accNum, + Sequence: seq, + Fee: tx.Fee, + Msgs: tx.Msgs, + Memo: tx.Memo, + }) + if err != nil { + continue + } + if pubKey.VerifyBytes(signBytes, sig.Signature) { + return seq, nil + } + } + return lo, fmt.Errorf("no sequence in [%d, %d] verified for account %d", + lo, hi, accNum) +} + +// assignFailedTxSequences back-patches SignerInfo.Sequence on buffered failed +// txs when we finally resolve the next successful-tx sequence. +// +// Cosmetic: failed txs are skipped on replay, so exact values don't matter +// for correctness. We approximate by assuming msg-fails (which consume +// sequence) come first in the gap, then ante-fails (which don't). +func assignFailedTxSequences(pending []*pendingFailedTx, startSeq, resolvedSeq uint64) { + consumed := resolvedSeq - startSeq + seq := startSeq + for i, pf := range pending { + pf.info.Sequence = seq + if uint64(i) < consumed { + seq++ + } + } +} + +// assignTrailingFailedTxSequences handles trailing failed txs with no later +// success to anchor against. +func assignTrailingFailedTxSequences(pending []*pendingFailedTx, startSeq, consumed uint64) { + seq := startSeq + for i, pf := range pending { + pf.info.Sequence = seq + if uint64(i) < consumed { + seq++ + } + } +} diff --git a/contribs/tx-archive/cmd/backup.go b/contribs/tx-archive/cmd/backup.go index 151213fa80e..f18540f9806 100644 --- a/contribs/tx-archive/cmd/backup.go +++ b/contribs/tx-archive/cmd/backup.go @@ -41,12 +41,13 @@ type backupCfg struct { fromBlock uint64 batchSize uint - ws bool - overwrite bool - legacy bool - watch bool - verbose bool - skipFailedTxs bool + ws bool + overwrite bool + legacy bool + watch bool + verbose bool + skipFailedTxs bool + noPopulateSigners bool } // newBackupCmd creates the backup command @@ -143,6 +144,16 @@ func (c *backupCfg) registerFlags(fs *flag.FlagSet) { false, "flag indicating if failed txs should be skipped", ) + + fs.BoolVar( + &c.noPopulateSigners, + "no-populate-signer-info", + false, + "disable per-tx SignerInfo population (account_num + sequence). "+ + "SignerInfo is required for hardfork-replay — leave off unless you "+ + "only need a plain stream backup and want to skip the brute-force "+ + "sequence resolution", + ) } // exec executes the backup command @@ -250,6 +261,7 @@ func (c *backupCfg) exec(ctx context.Context, _ []string) error { backup.WithLogger(logger), backup.WithBatchSize(c.batchSize), backup.WithSkipFailedTxs(c.skipFailedTxs), + backup.WithPopulateSignerInfo(!c.noPopulateSigners), ) // Run the backup service diff --git a/contribs/tx-archive/restore/client/http/http.go b/contribs/tx-archive/restore/client/http/http.go index 253f04acfc0..d3da3b6ac27 100644 --- a/contribs/tx-archive/restore/client/http/http.go +++ b/contribs/tx-archive/restore/client/http/http.go @@ -9,7 +9,8 @@ import ( rpcClient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/std" - _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // amino types + _ "github.com/gnolang/gno/gnovm/stdlibs/chain" // amino types ) // Client is the TM2 HTTP client diff --git a/docs/resources/gnoland-networks.md b/docs/resources/gnoland-networks.md index b67d28fe402..7ceef5f1fa0 100644 --- a/docs/resources/gnoland-networks.md +++ b/docs/resources/gnoland-networks.md @@ -2,10 +2,11 @@ ## Network configurations -| Network | RPC Endpoint | Chain ID | -|---------|------------------------------------------|-----------| -| Staging | https://rpc.gno.land:443 | `staging` | -| Test11 | https://rpc.test11.testnets.gno.land:443 | `test11` | +| Network | RPC Endpoint | Chain ID | +|-------------------|------------------------------------------|--------------| +| Betanet (current) | https://rpc.gno.land:443 | `gnoland-1` | +| Staging | https://rpc.staging.gno.land:443 | `staging` | +| Test11 | https://rpc.test11.testnets.gno.land:443 | `test11` | ### WebSocket endpoints @@ -68,8 +69,8 @@ After genesis has been replayed, the chain continues working as normal. ### Using the Staging network -The Staging network deployment can be found at [gno.land](https://gno.land), while -the exposed RPC endpoints can be found on `https://rpc.gno.land:443`. +The Staging network deployment can be found at [staging.gno.land](https://staging.gno.land), while +the exposed RPC endpoints can be found on `https://rpc.staging.gno.land:443`. #### A warning note @@ -113,7 +114,7 @@ Below you can find a breakdown of each existing testnet by these categories. ### Staging chain The Staging chain is an always up-to-date rolling testnet. It is meant to be used as -a nightly build of the Gno tech stack. The home page of [gno.land](https://gno.land) +a nightly build of the Gno tech stack. The home page of [staging.gno.land](https://staging.gno.land) is the `gnoweb` render of the Staging testnet. - **Persistence of state:** @@ -150,7 +151,7 @@ These testnets are deprecated and currently serve as archives of previous progre ### Test10 (archive) -Test9 is the testnet released on the 18th of December, 2025. +Test10 is the testnet released on the 18th of December, 2025. ### Test9 (archive) diff --git a/docs/users/interact-with-gnokey.md b/docs/users/interact-with-gnokey.md index a1efa7dfc30..1f8a156a1ea 100644 --- a/docs/users/interact-with-gnokey.md +++ b/docs/users/interact-with-gnokey.md @@ -319,6 +319,34 @@ checks the `wugnot` balance of a specific address. This is discouraged, as `maketx call` actually uses gas. To call a read-only function without spending gas, check out the `vm/qeval` query section. +### Calling a variadic function +Variadic functions are supported in Gno. To call a variadic function, pass one -args flag per variadic element. +For example, given a function with the signature: + +```go +func Add(cur realm, nums ...int) int +``` +You can call it with any number of arguments: + +```bash +# Two variadic args +gnokey maketx call \ + -pkgpath gno.land/r/demo/math \ + -func Add \ + -args 10 \ + -args 20 \ + ... # gas, broadcast, etc. + +# Zero variadic args (omit -args entirely) +gnokey maketx call \ + -pkgpath gno.land/r/demo/math \ + -func Add \ + ... # gas, broadcast, etc. +``` + +Note: Slice expansion (...) is not supported — pass each element as +a separate -args flag. + ## `Send` We can use the `Send` message type to access the TM2 [Banker](../resources/gno-stdlibs.md#banker) diff --git a/examples/gno.land/p/gnoland/boards/exts/permissions/README.md b/examples/gno.land/p/gnoland/boards/exts/permissions/README.md index 0d8a8bc1d65..c7fcbd70235 100644 --- a/examples/gno.land/p/gnoland/boards/exts/permissions/README.md +++ b/examples/gno.land/p/gnoland/boards/exts/permissions/README.md @@ -28,7 +28,7 @@ const user address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" const RoleExample boards.Role = "example" // Define a permission -const PermissionFoo boards.Permission = "foo" +const PermissionFoo boards.Permission = 42 func main() { // Define a custom foo permission validation function diff --git a/examples/gno.land/p/gnoland/boards/exts/permissions/filetests/z_readme_filetest.gno b/examples/gno.land/p/gnoland/boards/exts/permissions/filetests/z_readme_filetest.gno index 52cf7465e54..36dc8764f90 100644 --- a/examples/gno.land/p/gnoland/boards/exts/permissions/filetests/z_readme_filetest.gno +++ b/examples/gno.land/p/gnoland/boards/exts/permissions/filetests/z_readme_filetest.gno @@ -14,7 +14,7 @@ const user address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" const RoleExample boards.Role = "example" // Define a permission -const PermissionFoo boards.Permission = "foo" +const PermissionFoo boards.Permission = 42 func main() { // Define a custom foo permission validation function diff --git a/examples/gno.land/p/gnoland/boards/exts/permissions/permissions.gno b/examples/gno.land/p/gnoland/boards/exts/permissions/permissions.gno index a385e5c363c..75cd4fba90d 100644 --- a/examples/gno.land/p/gnoland/boards/exts/permissions/permissions.gno +++ b/examples/gno.land/p/gnoland/boards/exts/permissions/permissions.gno @@ -24,8 +24,8 @@ type ValidatorFunc func(boards.Permissions, boards.Args) error type Permissions struct { superRole boards.Role dao *commondao.CommonDAO - validators *avl.Tree // string(boards.Permission) -> BasicPermissionValidator - public *avl.Tree // string(boards.Permission) -> struct{}{} + public boards.PermissionSet + validators *avl.Tree // string(boards.Permission) -> ValidatorFunc singleUserRole bool } @@ -34,7 +34,6 @@ func New(options ...Option) *Permissions { s := storage.NewMemberStorage() ps := &Permissions{ validators: avl.NewTree(), - public: avl.NewTree(), dao: commondao.New(commondao.WithMemberStorage(s)), } @@ -52,20 +51,17 @@ func (ps Permissions) DAO() *commondao.CommonDAO { // ValidateFunc adds a custom permission validator function. // If an existing permission function exists it's ovewritten by the new one. func (ps *Permissions) ValidateFunc(p boards.Permission, fn ValidatorFunc) { - ps.validators.Set(string(p), fn) + ps.validators.Set(p.String(), fn) } // SetPublicPermissions assigns permissions that are available to anyone. // It removes previous public permissions and assigns the new ones. // By default there are no public permissions. func (ps *Permissions) SetPublicPermissions(permissions ...boards.Permission) { - ps.public = avl.NewTree() - for _, p := range permissions { - ps.public.Set(string(p), struct{}{}) - } + ps.public = boards.NewPermissionSet(permissions...) } -// AddRole add a role with one or more assigned permissions. +// AddRole adds a role with one or more assigned permissions. // If role exists its permissions are overwritten with the new ones. func (ps *Permissions) AddRole(r boards.Role, p boards.Permission, extra ...boards.Permission) { // If role is the super role it already has all permissions @@ -86,7 +82,8 @@ func (ps *Permissions) AddRole(r boards.Role, p boards.Permission, extra ...boar } // Save permissions within the member group overwritting any existing permissions - group.SetMeta(append([]boards.Permission{p}, extra...)) + permissions := append([]boards.Permission{p}, extra...) + group.SetMeta(boards.NewPermissionSet(permissions...)) } // RoleExists checks if a role exists. @@ -120,7 +117,7 @@ func (ps Permissions) HasRole(user address, r boards.Role) bool { // HasPermission checks if a user has a specific permission. func (ps Permissions) HasPermission(user address, perm boards.Permission) bool { - if ps.public.Has(string(perm)) { + if ps.public.Has(perm) { return true } @@ -142,10 +139,8 @@ func (ps Permissions) HasPermission(user address, perm boards.Permission) bool { } meta := group.GetMeta() - for _, p := range meta.([]boards.Permission) { - if p == perm { - return true - } + if perms, ok := meta.(boards.PermissionSet); ok && perms.Has(perm) { + return true } } return false @@ -244,11 +239,11 @@ func (ps Permissions) IterateUsers(start, count int, fn boards.UsersIterFn) (sto // If a permission validation function exists it's called before calling the callback. func (ps *Permissions) WithPermission(user address, p boards.Permission, args boards.Args, cb func()) { if !ps.HasPermission(user, p) { - panic("unauthorized") + panic("unauthorized, user " + user.String() + " doesn't have the required permission") } // Execute custom validation before calling the callback - v, found := ps.validators.Get(string(p)) + v, found := ps.validators.Get(p.String()) if found { err := v.(ValidatorFunc)(ps, args) if err != nil { diff --git a/examples/gno.land/p/gnoland/boards/exts/permissions/permissions_test.gno b/examples/gno.land/p/gnoland/boards/exts/permissions/permissions_test.gno index cd6eb766f09..3fe019bb56c 100644 --- a/examples/gno.land/p/gnoland/boards/exts/permissions/permissions_test.gno +++ b/examples/gno.land/p/gnoland/boards/exts/permissions/permissions_test.gno @@ -8,6 +8,13 @@ import ( "gno.land/p/nt/urequire/v0" ) +// Test permission constants +const ( + testPermA boards.Permission = iota + testPermB + testPermC +) + var _ boards.Permissions = (*Permissions)(nil) func TestBasicPermissionsWithPermission(t *testing.T) { @@ -23,10 +30,10 @@ func TestBasicPermissionsWithPermission(t *testing.T) { { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", + permission: testPermA, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") return perms }, @@ -35,11 +42,11 @@ func TestBasicPermissionsWithPermission(t *testing.T) { { name: "ok with arguments", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", + permission: testPermA, args: boards.Args{"a", "b"}, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") return perms }, @@ -48,23 +55,23 @@ func TestBasicPermissionsWithPermission(t *testing.T) { { name: "no permission", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", + permission: testPermA, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") return perms }, - err: "unauthorized", + err: "unauthorized, user g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 doesn't have the required permission", }, { name: "is not a DAO member", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", + permission: testPermA, setup: func() *Permissions { return New() }, - err: "unauthorized", + err: "unauthorized, user g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 doesn't have the required permission", }, } @@ -96,22 +103,22 @@ func TestBasicPermissionsSetPublicPermissions(t *testing.T) { perms := New() // Add a new role with permissions - perms.AddRole("adminRole", "fooPerm", "barPerm", "bazPerm") - urequire.False(t, perms.HasPermission(user, "fooPerm")) - urequire.False(t, perms.HasPermission(user, "barPerm")) - urequire.False(t, perms.HasPermission(user, "bazPerm")) + perms.AddRole("adminRole", testPermA, testPermB, testPermC) + urequire.False(t, perms.HasPermission(user, testPermA)) + urequire.False(t, perms.HasPermission(user, testPermB)) + urequire.False(t, perms.HasPermission(user, testPermC)) // Assign a couple of public permissions - perms.SetPublicPermissions("fooPerm", "bazPerm") - urequire.True(t, perms.HasPermission(user, "fooPerm")) - urequire.False(t, perms.HasPermission(user, "barPerm")) - urequire.True(t, perms.HasPermission(user, "bazPerm")) + perms.SetPublicPermissions(testPermA, testPermC) + urequire.True(t, perms.HasPermission(user, testPermA)) + urequire.False(t, perms.HasPermission(user, testPermB)) + urequire.True(t, perms.HasPermission(user, testPermC)) // Clear all public permissions perms.SetPublicPermissions() - urequire.False(t, perms.HasPermission(user, "fooPerm")) - urequire.False(t, perms.HasPermission(user, "barPerm")) - urequire.False(t, perms.HasPermission(user, "bazPerm")) + urequire.False(t, perms.HasPermission(user, testPermA)) + urequire.False(t, perms.HasPermission(user, testPermB)) + urequire.False(t, perms.HasPermission(user, testPermC)) } func TestBasicPermissionsGetUserRoles(t *testing.T) { @@ -127,7 +134,7 @@ func TestBasicPermissionsGetUserRoles(t *testing.T) { roles: []string{"admin"}, setup: func() *Permissions { perms := New() - perms.AddRole("admin", "x") + perms.AddRole("admin", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin") return perms }, @@ -138,9 +145,9 @@ func TestBasicPermissionsGetUserRoles(t *testing.T) { roles: []string{"admin", "bar", "foo"}, setup: func() *Permissions { perms := New() - perms.AddRole("admin", "x") - perms.AddRole("foo", "x") - perms.AddRole("bar", "x") + perms.AddRole("admin", testPermA) + perms.AddRole("foo", testPermA) + perms.AddRole("bar", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar") return perms }, @@ -167,8 +174,8 @@ func TestBasicPermissionsGetUserRoles(t *testing.T) { roles: []string{"admin"}, setup: func() *Permissions { perms := New() - perms.AddRole("admin", "x") - perms.AddRole("bar", "x") + perms.AddRole("admin", testPermA) + perms.AddRole("bar", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin") perms.SetUserRoles("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin") perms.SetUserRoles("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar") @@ -204,7 +211,7 @@ func TestBasicPermissionsHasRole(t *testing.T) { role: "admin", setup: func() *Permissions { perms := New() - perms.AddRole("admin", "x") + perms.AddRole("admin", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin") return perms }, @@ -216,8 +223,8 @@ func TestBasicPermissionsHasRole(t *testing.T) { role: "foo", setup: func() *Permissions { perms := New() - perms.AddRole("admin", "x") - perms.AddRole("foo", "x") + perms.AddRole("admin", testPermA) + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo") return perms }, @@ -238,7 +245,7 @@ func TestBasicPermissionsHasRole(t *testing.T) { role: "bar", setup: func() *Permissions { perms := New() - perms.AddRole("foo", "x") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") return perms }, @@ -265,10 +272,10 @@ func TestBasicPermissionsHasPermission(t *testing.T) { { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", + permission: testPermA, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") return perms }, @@ -277,10 +284,10 @@ func TestBasicPermissionsHasPermission(t *testing.T) { { name: "ok with multiple users", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", + permission: testPermA, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") perms.SetUserRoles("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo") return perms @@ -290,11 +297,11 @@ func TestBasicPermissionsHasPermission(t *testing.T) { { name: "ok with multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "other", + permission: testPermB, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") - perms.AddRole("baz", "other") + perms.AddRole("foo", testPermA) + perms.AddRole("baz", testPermB) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz") return perms }, @@ -303,10 +310,10 @@ func TestBasicPermissionsHasPermission(t *testing.T) { { name: "no permission", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "other", + permission: testPermB, setup: func() *Permissions { perms := New() - perms.AddRole("foo", "bar") + perms.AddRole("foo", testPermA) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") return perms }, @@ -336,7 +343,7 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a"}, setup: func() *Permissions { perms := New() - perms.AddRole("a", "permission1") + perms.AddRole("a", testPermA) return perms }, }, @@ -346,8 +353,8 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a", "b"}, setup: func() *Permissions { perms := New() - perms.AddRole("a", "permission1") - perms.AddRole("b", "permission2") + perms.AddRole("a", testPermA) + perms.AddRole("b", testPermB) return perms }, }, @@ -357,7 +364,7 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a"}, setup: func() *Permissions { perms := New() - perms.AddRole("a", "permission1") + perms.AddRole("a", testPermA) perms.SetUserRoles("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "a") perms.SetUserRoles("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc") return perms @@ -369,7 +376,7 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a"}, setup: func() *Permissions { perms := New(UseSingleUserRole()) - perms.AddRole("a", "permission1") + perms.AddRole("a", testPermA) return perms }, }, @@ -379,9 +386,9 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a", "b"}, setup: func() *Permissions { perms := New() - perms.AddRole("a", "permission1") - perms.AddRole("b", "permission2") - perms.AddRole("c", "permission2") + perms.AddRole("a", testPermA) + perms.AddRole("b", testPermB) + perms.AddRole("c", testPermB) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "c") return perms }, @@ -392,8 +399,8 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"b"}, setup: func() *Permissions { perms := New(UseSingleUserRole()) - perms.AddRole("a", "permission1") - perms.AddRole("b", "permission2") + perms.AddRole("a", testPermA) + perms.AddRole("b", testPermB) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a") return perms }, @@ -404,8 +411,8 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{}, setup: func() *Permissions { perms := New() - perms.AddRole("a", "permission1") - perms.AddRole("b", "permission2") + perms.AddRole("a", testPermA) + perms.AddRole("b", testPermB) perms.SetUserRoles("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "b") return perms }, @@ -416,7 +423,7 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a", "foo"}, setup: func() *Permissions { perms := New() - perms.AddRole("a", "permission1") + perms.AddRole("a", testPermA) return perms }, err: "invalid role: foo", @@ -427,8 +434,8 @@ func TestBasicPermissionsSetUserRoles(t *testing.T) { expectedRoles: []boards.Role{"a", "b"}, setup: func() *Permissions { perms := New(UseSingleUserRole()) - perms.AddRole("a", "permission1") - perms.AddRole("b", "permission2") + perms.AddRole("a", testPermA) + perms.AddRole("b", testPermB) return perms }, err: "user can only have one role", @@ -511,8 +518,8 @@ func TestBasicPermissionsIterateUsers(t *testing.T) { } perms := New() - perms.AddRole("foo", "perm1") - perms.AddRole("bar", "perm2") + perms.AddRole("foo", testPermA) + perms.AddRole("bar", testPermB) for _, u := range users { perms.SetUserRoles(u.Address, u.Roles...) } diff --git a/examples/gno.land/p/gnoland/boards/permission_set.gno b/examples/gno.land/p/gnoland/boards/permission_set.gno new file mode 100644 index 00000000000..a08e4b9e1ef --- /dev/null +++ b/examples/gno.land/p/gnoland/boards/permission_set.gno @@ -0,0 +1,54 @@ +package boards + +// PermissionSet defines a type to store any number of permissions. +type PermissionSet []uint64 + +// NewPermissionSet creates a new PermissionSet containing the given permissions. +func NewPermissionSet(perms ...Permission) PermissionSet { + if len(perms) == 0 { + return nil + } + + // Find max permission value to calculate slice size. + // This allows any number of permissions to be assigned in any order. + var max Permission + for _, p := range perms { + if p > max { + max = p + } + } + + s := make(PermissionSet, int(max)/64+1) + for _, p := range perms { + // Calculate the index within the set where the permission should be defined. + // Each item in the set can contain 64 permissions, for example: + // - Item 0: permissions 0 to 63 + // - Item 1: permissions 64 to 127 + idx := int(p) / 64 + + // Turn on the bit that matches the permission, ranging from bit 0 to 63 + s[idx] |= 1 << (uint(p) % 64) + } + return s +} + +// Has checks if a permission is in the set. +func (s PermissionSet) Has(p Permission) bool { + idx := int(p) / 64 + if idx >= len(s) { + return false + } + + // Check if the bit for the current permission is on + return s[idx]&(1<<(uint(p)%64)) != 0 +} + +// IsEmpty reports whether the set contains no permissions. +func (s PermissionSet) IsEmpty() bool { + for _, v := range s { + if v != 0 { + return false + } + } + return true +} diff --git a/examples/gno.land/p/gnoland/boards/permission_set_test.gno b/examples/gno.land/p/gnoland/boards/permission_set_test.gno new file mode 100644 index 00000000000..8b784694b45 --- /dev/null +++ b/examples/gno.land/p/gnoland/boards/permission_set_test.gno @@ -0,0 +1,124 @@ +package boards + +import ( + "testing" + + "gno.land/p/nt/uassert/v0" +) + +func TestNewPermissionSet(t *testing.T) { + cases := []struct { + name string + perms []Permission + check []Permission + want bool + }{ + { + name: "empty", + check: []Permission{0}, + want: false, + }, + { + name: "single permission", + perms: []Permission{0}, + check: []Permission{0}, + want: true, + }, + { + name: "multiple permissions", + perms: []Permission{1, 3, 5}, + check: []Permission{1, 3, 5}, + want: true, + }, + { + name: "high permission value", + perms: []Permission{100}, + check: []Permission{100}, + want: true, + }, + { + name: "missing permission", + perms: []Permission{100}, + check: []Permission{0}, + want: false, + }, + { + name: "multiple missing permissions", + perms: []Permission{1, 3, 5}, + check: []Permission{0, 2, 4}, + want: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := NewPermissionSet(tc.perms...) + + for _, p := range tc.check { + uassert.Equal(t, tc.want, s.Has(p)) + } + }) + } +} + +func TestPermissionSetHas(t *testing.T) { + cases := []struct { + name string + set PermissionSet + check Permission + want bool + }{ + { + name: "out of range", + set: NewPermissionSet(0), + check: 100, + want: false, + }, + { + name: "nil set", + check: 0, + want: false, + }, + { + name: "permission present", + set: NewPermissionSet(5), + check: 5, + want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + uassert.Equal(t, tc.want, tc.set.Has(tc.check)) + }) + } +} + +func TestPermissionSetIsEmpty(t *testing.T) { + cases := []struct { + name string + set PermissionSet + want bool + }{ + { + name: "nil set", + want: true, + }, + { + name: "non-empty set", + set: NewPermissionSet(0), + want: false, + }, + { + name: "empty allocated set", + set: make(PermissionSet, 1), + want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + uassert.Equal(t, tc.want, tc.set.IsEmpty()) + }) + } +} diff --git a/examples/gno.land/p/gnoland/boards/permissions.gno b/examples/gno.land/p/gnoland/boards/permissions.gno index 380e8956122..8186f65fea4 100644 --- a/examples/gno.land/p/gnoland/boards/permissions.gno +++ b/examples/gno.land/p/gnoland/boards/permissions.gno @@ -1,9 +1,8 @@ package boards -type ( - // Permission defines the type for permissions. - Permission string +import "strconv" +type ( // Role defines the type for user roles. Role string @@ -59,3 +58,11 @@ type ( IterateUsers(start, count int, fn UsersIterFn) bool } ) + +// Permission defines the type for permissions. +type Permission uint16 + +// String returns the string representation of a permission value. +func (p Permission) String() string { + return strconv.FormatUint(uint64(p), 10) +} diff --git a/examples/gno.land/p/moul/helplink/helplink.gno b/examples/gno.land/p/moul/helplink/helplink.gno index 087e4d2e1c8..60aad45dfd3 100644 --- a/examples/gno.land/p/moul/helplink/helplink.gno +++ b/examples/gno.land/p/moul/helplink/helplink.gno @@ -47,7 +47,8 @@ type Realm string func (r Realm) prefix() string { // relative if r == "" { - return "" + curPath := runtime.CurrentRealm().PkgPath() + return strings.TrimPrefix(curPath, chainDomain) } // local realm -> /realm diff --git a/examples/gno.land/p/moul/helplink/helplink_test.gno b/examples/gno.land/p/moul/helplink/helplink_test.gno index 27e3697bb00..de961a9b34d 100644 --- a/examples/gno.land/p/moul/helplink/helplink_test.gno +++ b/examples/gno.land/p/moul/helplink/helplink_test.gno @@ -85,12 +85,13 @@ func TestFuncURL(t *testing.T) { } func TestHome(t *testing.T) { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/test/helplink")) cd := runtime.ChainDomain() tests := []struct { realm_XXX Realm want string }{ - {"", "$help"}, + {"", "/r/test/helplink$help"}, {Realm(cd + "/r/lorem/ipsum"), "/r/lorem/ipsum$help"}, {"gno.world/r/lorem/ipsum", "https://gno.world/r/lorem/ipsum$help"}, } diff --git a/examples/gno.land/p/moul/md/md.gno b/examples/gno.land/p/moul/md/md.gno index d7b1d6d7d6f..854dab7baec 100644 --- a/examples/gno.land/p/moul/md/md.gno +++ b/examples/gno.land/p/moul/md/md.gno @@ -183,7 +183,7 @@ func HorizontalRule() string { // Link returns a hyperlink for markdown. // Example: Link("foo", "http://example.com") => "[foo](http://example.com)" func Link(text, url string) string { - return "[" + EscapeText(text) + "](" + url + ")" + return "[" + EscapeText(text) + "](" + EscapeURL(url) + ")" } // UserLink returns a user profile link for markdown. @@ -192,21 +192,21 @@ func Link(text, url string) string { // Example: UserLink("g1blah") => "[g1blah](/u/g1blah)" func UserLink(user string) string { if strings.HasPrefix(user, "g1") { - return "[" + EscapeText(user) + "](/u/" + user + ")" + return "[" + EscapeText(user) + "](/u/" + EscapeURL(user) + ")" } - return "[@" + EscapeText(user) + "](/u/" + user + ")" + return "[@" + EscapeText(user) + "](/u/" + EscapeURL(user) + ")" } // InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown. // Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[![alt text](image-url)](link-url)" func InlineImageWithLink(altText, imageUrl, linkUrl string) string { - return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")" + return "[" + Image(altText, imageUrl) + "](" + EscapeURL(linkUrl) + ")" } // Image returns an image for markdown. // Example: Image("foo", "http://example.com") => "![foo](http://example.com)" func Image(altText, url string) string { - return "![" + EscapeText(altText) + "](" + url + ")" + return "![" + EscapeText(altText) + "](" + EscapeURL(url) + ")" } // Footnote returns a footnote for markdown. @@ -234,6 +234,15 @@ func CollapsibleSection(title, content string) string { return "
" + EscapeText(title) + "\n\n" + content + "\n
\n" } +// EscapeURL escapes characters in a URL that would break markdown link syntax. +func EscapeURL(url string) string { + r := strings.NewReplacer( + "(", `%28`, + ")", `%29`, + ) + return r.Replace(url) +} + // EscapeText escapes special Markdown characters in regular text where needed. func EscapeText(text string) string { replacer := strings.NewReplacer( diff --git a/examples/gno.land/p/nt/fqname/v0/fqname.gno b/examples/gno.land/p/nt/fqname/v0/fqname.gno index 66be07d8887..3cbb6dd5a4b 100644 --- a/examples/gno.land/p/nt/fqname/v0/fqname.gno +++ b/examples/gno.land/p/nt/fqname/v0/fqname.gno @@ -63,15 +63,28 @@ func RenderLink(pkgPath, slug string) string { if strings.HasPrefix(pkgPath, "gno.land") { pkgLink := strings.TrimPrefix(pkgPath, "gno.land") if slug != "" { - return "[" + pkgPath + "](" + pkgLink + ")." + slug + safeSlug := escapeMarkdown(slug) + return "[" + pkgPath + "](" + pkgLink + ")." + safeSlug } return "[" + pkgPath + "](" + pkgLink + ")" } if slug != "" { - return pkgPath + "." + slug + safeSlug := escapeMarkdown(slug) + return pkgPath + "." + safeSlug } return pkgPath } + +// escapeMarkdown escapes characters that could break markdown link syntax. +func escapeMarkdown(s string) string { + r := strings.NewReplacer( + "[", `\[`, + "]", `\]`, + "(", `\(`, + ")", `\)`, + ) + return r.Replace(s) +} diff --git a/examples/gno.land/r/demo/defi/grc20reg/grc20reg.gno b/examples/gno.land/r/demo/defi/grc20reg/grc20reg.gno index b9cbdf260d9..1f7f6268c27 100644 --- a/examples/gno.land/r/demo/defi/grc20reg/grc20reg.gno +++ b/examples/gno.land/r/demo/defi/grc20reg/grc20reg.gno @@ -5,6 +5,7 @@ import ( "chain/runtime" "gno.land/p/demo/tokens/grc20" + "gno.land/p/moul/md" "gno.land/p/nt/avl/v0" "gno.land/p/nt/avl/v0/rotree" "gno.land/p/nt/fqname/v0" @@ -13,6 +14,9 @@ import ( var registry = avl.NewTree() // rlmPath[.slug] -> *Token (slug is optional) func Register(cur realm, token *grc20.Token, slug string) { + if slug != "" { + validateSlug(slug) + } rlmPath := runtime.PreviousRealm().PkgPath() key := fqname.Construct(rlmPath, slug) registry.Set(key, token) @@ -51,7 +55,7 @@ func Render(path string) string { rlmPath, slug := fqname.Parse(key) rlmLink := fqname.RenderLink(rlmPath, slug) infoLink := "/r/demo/grc20reg:" + key - s += ufmt.Sprintf("- **%s** - %s - [info](%s)\n", token.GetName(), rlmLink, infoLink) + s += "- " + md.Bold(md.EscapeText(token.GetName())) + " - " + rlmLink + " - " + md.Link("info", infoLink) + "\n" return false }) if count == 0 { @@ -63,8 +67,8 @@ func Render(path string) string { token := MustGet(key) rlmPath, slug := fqname.Parse(key) rlmLink := fqname.RenderLink(rlmPath, slug) - s := ufmt.Sprintf("# %s\n", token.GetName()) - s += ufmt.Sprintf("- symbol: **%s**\n", token.GetSymbol()) + s := ufmt.Sprintf("# %s\n", md.EscapeText(token.GetName())) + s += "- symbol: " + md.Bold(md.EscapeText(token.GetSymbol())) + "\n" s += ufmt.Sprintf("- realm: %s\n", rlmLink) s += ufmt.Sprintf("- decimals: %d\n", token.GetDecimals()) s += ufmt.Sprintf("- total supply: %d\n", token.TotalSupply()) @@ -77,3 +81,17 @@ const registerEvent = "register" func GetRegistry() *rotree.ReadOnlyTree { return rotree.Wrap(registry, nil) } + +// validateSlug panics if the slug contains non-alphanumeric characters. +// Only letters, digits, dashes, and underscores are allowed. +func validateSlug(slug string) { + for _, c := range slug { + if !isAlphanumeric(c) && c != '_' && c != '-' { + panic("grc20reg: invalid slug character: " + string(c)) + } + } +} + +func isAlphanumeric(c rune) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') +} diff --git a/examples/gno.land/r/demo/defi/grc20reg/grc20reg_test.gno b/examples/gno.land/r/demo/defi/grc20reg/grc20reg_test.gno index d5d83f7f59f..625486aae4f 100644 --- a/examples/gno.land/r/demo/defi/grc20reg/grc20reg_test.gno +++ b/examples/gno.land/r/demo/defi/grc20reg/grc20reg_test.gno @@ -53,3 +53,47 @@ func TestRegistry(t *testing.T) { got = Render("gno.land/r/demo/foo.mySlug") urequire.Equal(t, expected, got) } + +func TestValidateSlug(t *testing.T) { + // Valid slugs — should not panic + valid := []string{"mytoken", "my-token", "my_token", "Token123", "a", "A-B_c"} + for _, slug := range valid { + validateSlug(slug) // no panic = pass + } +} + +func TestValidateSlugPanicsOnSpace(t *testing.T) { + defer func() { recover() }() + validateSlug("has space") + t.Errorf("should have panicked") +} + +func TestValidateSlugPanicsOnDot(t *testing.T) { + defer func() { recover() }() + validateSlug("has.dot") + t.Errorf("should have panicked") +} + +func TestValidateSlugPanicsOnSlash(t *testing.T) { + defer func() { recover() }() + validateSlug("has/slash") + t.Errorf("should have panicked") +} + +func TestValidateSlugPanicsOnBrackets(t *testing.T) { + defer func() { recover() }() + validateSlug("[brackets]") + t.Errorf("should have panicked") +} + +func TestValidateSlugPanicsOnParens(t *testing.T) { + defer func() { recover() }() + validateSlug("(parens)") + t.Errorf("should have panicked") +} + +func TestValidateSlugPanicsOnInjection(t *testing.T) { + defer func() { recover() }() + validateSlug(`) [Claim](https://evil.com`) + t.Errorf("should have panicked") +} diff --git a/examples/gno.land/r/devrels/events/render.gno b/examples/gno.land/r/devrels/events/render.gno index 08d85cca81c..d5a0c650700 100644 --- a/examples/gno.land/r/devrels/events/render.gno +++ b/examples/gno.land/r/devrels/events/render.gno @@ -111,8 +111,8 @@ func (e Event) Render(admin bool) string { buf.WriteString(ufmt.Sprintf("**Ends:** %s UTC%s%d\n\n", e.endTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) if admin { - buf.WriteString(ufmt.Sprintf("[EDIT](/r/gnoland/events$help&func=EditEvent&id=%s)\n\n", e.id)) - buf.WriteString(ufmt.Sprintf("[DELETE](/r/gnoland/events$help&func=DeleteEvent&id=%s)\n\n", e.id)) + buf.WriteString(ufmt.Sprintf("[EDIT](/r/devrels/events$help&func=EditEvent&id=%s)\n\n", e.id)) + buf.WriteString(ufmt.Sprintf("[DELETE](/r/devrels/events$help&func=DeleteEvent&id=%s)\n\n", e.id)) } if e.link != "" { diff --git a/examples/gno.land/r/gnoland/boards2/v1/boards.gno b/examples/gno.land/r/gnoland/boards2/v1/boards.gno index 4c33d86ab83..49be99b0e66 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/boards.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/boards.gno @@ -11,11 +11,25 @@ import ( "gno.land/p/nt/avl/v0" ) +var ( + // RealmLink contains Boards2 realm link. + // It can be used to generate board TX links from other realms. + RealmLink = txlink.Realm(runtime.CurrentRealm().PkgPath()) + + // RequiredAccountAmount contains the required account amount for open board interactions. + // The amount requirement is not applied to members that were invited to an open board. + // Amount is defined as ugnot. + RequiredAccountAmount = int64(3_000_000_000) + + // Notice contains an optional message that is displayed globally within the realm. + Notice string + + // Help contains optional Markdown with Boards2 realm help. + Help string +) + // TODO: Refactor globals in favor of a cleaner pattern var ( - gRealmLink txlink.Realm - gNotice string - gHelp string gListedBoardsByID avl.Tree // string(id) -> *boards.Board gInviteRequests avl.Tree // string(board id) -> *avl.Tree(address -> time.Time) gBannedUsers avl.Tree // string(board id) -> *avl.Tree(address -> time.Time) @@ -29,20 +43,9 @@ var ( gBoards = boards.NewStorage() gBoardsSequence = boards.NewIdentifierGenerator() gRealmPath = strings.TrimPrefix(runtime.CurrentRealm().PkgPath(), "gno.land") - gPerms = initRealmPermissions( - "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq", - "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh", // govdao t1 multisig - ) - - // TODO: Allow updating open account amount though a proposal (GovDAO, CommonDAO?) - gOpenAccountAmount = int64(3_000_000_000) // ugnot required for open board actions + gPerms = initRealmPermissions("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh") // GovDAO T1 multisig ) -func init() { - // Save current realm path so it's available during render calls - gRealmLink = txlink.Realm(runtime.CurrentRealm().PkgPath()) -} - // initRealmPermissions returns the default realm permissions. func initRealmPermissions(owners ...address) boards.Permissions { perms := permissions.New( @@ -142,6 +145,6 @@ func mustGetPermissions(bid boards.ID) boards.Permissions { func parseRealmPath(path string) *realmpath.Request { // Make sure request is using current realm path so paths can be parsed during Render r := realmpath.Parse(path) - r.Realm = string(gRealmLink) + r.Realm = string(RealmLink) return r } diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_00_filetest.gno index 3b9204e2162..cdf48564681 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_01_filetest.gno index d22b1c20d3c..fe83f9e9f63 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_02_filetest.gno index 0dae4c0b508..e64ad2aef50 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_03_filetest.gno index b4c2b1bc2a1..85e668638aa 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_accept_invite_03_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) @@ -31,4 +31,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_00_filetest.gno index 1b52697c729..f29d9056fc2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_01_filetest.gno index 37d31a588b5..235ba33c4fd 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_02_filetest.gno index 54943c979c4..dcff2b5596b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ban_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) @@ -28,4 +28,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_00_filetest.gno index 8cb66e1b106..486d47184e7 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 newRole = boards2.RoleOwner bid = boards.ID(0) // Operate on realm DAO instead of individual boards diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_01_filetest.gno index 4a5bdc66776..5d9b03ac398 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 newRole = boards2.RoleAdmin ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_02_filetest.gno index e95ae981abd..23aa66300a0 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" owner2 address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 admin address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_03_filetest.gno index 0bd9d7971b3..c649445dbf4 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_03_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" admin address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 admin2 address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_04_filetest.gno index 84cf2170708..1a899d6d5e6 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_04_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 bid = boards.ID(0) // Operate on realm DAO members instead of individual boards newRole = boards2.RoleOwner diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_05_filetest.gno index 76f8a88d8ce..267ce755bc2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" admin address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_06_filetest.gno index 928e3280bf0..fd05705f78f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" admin address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_07_filetest.gno index 90f9f063cdb..d6f3274d605 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_change_member_role_07_filetest.gno @@ -15,4 +15,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_00_filetest.gno index be1b2941385..eba58267925 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_00_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_01_filetest.gno index 0c597d70174..a7b8e6e794d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_02_filetest.gno index a0400b711c3..268bec1f265 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_02_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_03_filetest.gno index 6696c925927..f5514c70fd3 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_03_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_04_filetest.gno index 42333f0ee08..25f700c3757 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_04_filetest.gno @@ -7,7 +7,7 @@ import ( uinit "gno.land/r/sys/users/init" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { uinit.RegisterUser(cross, "gnoland", address("g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7")) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_05_filetest.gno index 06b856e48b9..5b1d8a83851 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_05_filetest.gno @@ -7,7 +7,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var name = strings.Repeat("X", boards2.MaxBoardNameLength+1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_06_filetest.gno index dad945cb63a..56dc17bffc8 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 name = "test123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_07_filetest.gno index fd81445f760..237626d66ba 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_07_filetest.gno @@ -15,4 +15,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_08_filetest.gno index 6ad94b1a952..5f7adcd9d51 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_08_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_08_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "TestBoard" ) @@ -34,7 +34,7 @@ func main() { // // ================== // # [Boards](/r/gnoland/boards2/v1) › TestBoard -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help&func=RequestInvite&boardID=1) • [Manage Board](?menu=manageBoard) // // --- diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_09_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_09_filetest.gno index 209ce12a2c3..2243581f255 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_09_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_09_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_10_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_10_filetest.gno index 90a2a28788f..c07680d4496 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_10_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_board_10_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_00_filetest.gno index 9ef788166bf..42ff1cd854d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_00_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" path = "test-board/1/2" comment = "Test comment" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_01_filetest.gno index e42407a2422..adfd1a51879 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_02_filetest.gno index 149551d4468..54a859cb1d1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_03_filetest.gno index 6827d0a83bc..0de15570ce3 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_03_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_04_filetest.gno index e3011edc280..31c06c74a24 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_05_filetest.gno index b50d8e7149c..f62ec309088 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_05_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_06_filetest.gno index e71a965f44c..456f3de54cc 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_06_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_07_filetest.gno index 17e7ce30c07..27095620dae 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_07_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -31,4 +31,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_08_filetest.gno index 6ff87bd9b21..f1a2d104a2c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_08_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_08_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_09_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_09_filetest.gno index f9354605078..5d9928896f7 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_09_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_09_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" path = "test-board/1/2" comment = "Second comment" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_10_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_10_filetest.gno index 8db2eac4965..93de2fe599a 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_10_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_10_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" comment = "Second comment" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_11_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_11_filetest.gno index 393ce78cf7c..96cc81d4b9c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_11_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_11_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_12_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_12_filetest.gno index d51f12de484..a48de50acc1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_12_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_12_filetest.gno @@ -9,7 +9,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_13_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_13_filetest.gno index 8732692c57f..1f0e9f7ee71 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_13_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_13_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_14_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_14_filetest.gno index 17418543ec3..fec47360783 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_14_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_14_filetest.gno @@ -14,7 +14,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 comment = "Test Comment" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_15_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_15_filetest.gno index 6ff5b44de6a..196c65accb2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_15_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_15_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 comment = "Test Comment" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_16_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_16_filetest.gno new file mode 100644 index 00000000000..59da0754a01 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_reply_16_filetest.gno @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + +var bid, tid boards.ID + +func init() { + testing.SetRealm(testing.NewUserRealm(owner)) + bid = boards2.CreateBoard(cross, "test-board", false, false) + tid = boards2.CreateThread(cross, bid, "Foo", "bar") +} + +func main() { + testing.SetRealm(testing.NewUserRealm(owner)) + + boards2.CreateReply(cross, bid, tid, 0, "") +} + +// Error: +// Gno-Flavored Markdown forms are not allowed in replies diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_00_filetest.gno index 08709ed553c..b4e8f99f250 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( srcBID boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_01_filetest.gno index c5a7cb5743f..b3d3c4ccabe 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_01_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( srcBID boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_02_filetest.gno index ce72f62572b..dcd467b71d4 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -34,4 +34,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_03_filetest.gno index 33799b30cd9..311a8ad5856 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_03_filetest.gno @@ -10,7 +10,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( srcBID boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_04_filetest.gno index d599f3b8eb7..7f8f3959858 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_repost_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( srcBID boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_00_filetest.gno index 8cdd203ab59..8a897d28a04 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" title = "Test Thread" body = "Test body" path = "test-board/1" @@ -43,7 +43,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Test Thread // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Test body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_01_filetest.gno index 5effbf04a4c..d0a5d8d2c4a 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_02_filetest.gno index ca77ee4f58d..e5d87beae77 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -27,4 +27,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_03_filetest.gno index b5bc717c039..3489626c314 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_03_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_04_filetest.gno index 9ce703f842f..b3b908fbaa4 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_05_filetest.gno index cc6c9fa4321..7f4d215429f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_05_filetest.gno @@ -13,7 +13,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 title = "Test Thread" body = "Test body" diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_06_filetest.gno index 1bf33cb644e..1c10e5a4a15 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_create_thread_06_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_00_filetest.gno index 97d7dd3dee2..f579bcf0c84 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_01_filetest.gno index b215d6f092c..08e91c83e3a 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_02_filetest.gno index ac5371685ff..e29341e8b90 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_03_filetest.gno index ffa6355462a..fe4e765f388 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_03_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_04_filetest.gno index e9baaf9ffa4..85dd9e72a08 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID @@ -36,20 +36,20 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // bar // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > ⚠ This comment has been deleted // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=2&threadID=1) // > // > > -// > > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) +// > > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) // > > Child reply // > > // > > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=3&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_05_filetest.gno index 1f4e4489cfb..8fda5a51caf 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -33,4 +33,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_06_filetest.gno index 715e9063411..79f029c2e46 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_07_filetest.gno index f47ce3bd203..503c2fb6c0d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_07_filetest.gno @@ -13,7 +13,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_08_filetest.gno index 51d04a7009d..6dd46971388 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_08_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_reply_08_filetest.gno @@ -13,7 +13,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 user2 address = "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt" ) @@ -44,4 +44,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_00_filetest.gno index 4dbf43eb0ed..857be549d8a 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" title = "Test Thread" body = "Test body" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_01_filetest.gno index 4cc1662176e..04ac3355093 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_02_filetest.gno index fae0da01d0c..aaf4ca4fd30 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_03_filetest.gno index a59109e9679..23a36501b92 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_03_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -32,4 +32,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_04_filetest.gno index 5a43919735c..80f28c81b37 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_04_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_05_filetest.gno index 578aff287d1..c6f62e768a0 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_05_filetest.gno @@ -13,7 +13,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_06_filetest.gno index e7b94a1b149..c65b993a165 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_delete_thread_06_filetest.gno @@ -13,7 +13,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 user2 address = "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt" ) @@ -43,4 +43,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_00_filetest.gno index 81074b53fbe..af05ec66909 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_00_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" body = "Test reply" path = "test-board/1/2" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_01_filetest.gno index f7efe696969..8f65300f6c3 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_01_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" body = "Test reply" path = "test-board/1/2" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_02_filetest.gno index d94992d15ee..61b06d9b2b0 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_02_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_03_filetest.gno index f2e2602f940..7133f6ed617 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_03_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_04_filetest.gno index c4aff9c4a21..12de958a99b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_05_filetest.gno index 4a94453b773..d28b16633cf 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_06_filetest.gno index f6bb07cf932..480c06eb01c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_07_filetest.gno index d9179544bf9..dc34a508279 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_07_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_08_filetest.gno new file mode 100644 index 00000000000..eddac49dab5 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_reply_08_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + +var bid, tid, rid boards.ID + +func init() { + testing.SetRealm(testing.NewUserRealm(owner)) + bid = boards2.CreateBoard(cross, "test-board", false, false) + tid = boards2.CreateThread(cross, bid, "Foo", "bar") + rid = boards2.CreateReply(cross, bid, tid, 0, "body") +} + +func main() { + testing.SetRealm(testing.NewUserRealm(owner)) + + boards2.EditReply(cross, bid, tid, rid, "") +} + +// Error: +// Gno-Flavored Markdown forms are not allowed in replies diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_00_filetest.gno index aedfdd478e2..d9572ae9568 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" title = "Test Thread" body = "Test body" path = "test-board/1" @@ -38,7 +38,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Test Thread // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Test body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_01_filetest.gno index ba84cf7dbcc..e7bb31f14b8 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_01_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_02_filetest.gno index 7d3bf3a164e..b372062f819 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_03_filetest.gno index 91f73ad2c0c..2d79046c8e3 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_03_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_04_filetest.gno index 7d3bf3a164e..b372062f819 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_05_filetest.gno index ca388111df8..a9d54da0916 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -31,4 +31,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_06_filetest.gno index addd93f7cba..4d46bfab7d5 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" admin address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 title = "Test Thread" body = "Test body" @@ -42,7 +42,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Test Thread // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Test body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_07_filetest.gno index 4b6314fa796..f09a5c3776d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_edit_thread_07_filetest.gno @@ -9,7 +9,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_00_filetest.gno index 0c23f377239..4bb553b7844 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID @@ -34,12 +34,12 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // bar // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_01_filetest.gno index a72ed778884..98a99107d97 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_02_filetest.gno index b5e6c86aac5..611e4991de2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_03_filetest.gno index 6c58ee3ac03..3f871db8afd 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_03_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_04_filetest.gno index 3285d308ecd..cb640314b46 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_05_filetest.gno index 21daa9b11e2..fbf9be42964 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -32,4 +32,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_06_filetest.gno index ae243f92dc1..04dcc64a64c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" moderator address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -40,12 +40,12 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // bar // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_07_filetest.gno index c99a409063a..b5b674de98e 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_07_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" moderator address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -43,12 +43,12 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // bar // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_08_filetest.gno index 0c8b5eb8b39..ead95e4767c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_08_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_reply_08_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_00_filetest.gno index 80e42dc29f5..9c8c4d17578 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" moderator address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -41,7 +41,7 @@ func main() { // Output: // # [Boards](/r/gnoland/boards2/v1) › test-board -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // ↳ [Create Thread](/r/gnoland/boards2/v1:test-board/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help&func=RequestInvite&boardID=1) • [Manage Board](?menu=manageBoard) // // --- diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_01_filetest.gno index 5b8cfc4ce2d..a2095a6a266 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_02_filetest.gno index 4ed9b7e0478..70522f22ac9 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) @@ -32,4 +32,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_03_filetest.gno index 928dc4572ad..e4a59d06cf5 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_03_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_04_filetest.gno index 5ddb68a6294..ceee43bfcef 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_04_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_05_filetest.gno index 5c76ba45612..629175e1556 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" moderator address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_06_filetest.gno index aa8fe10e3ac..1a88a58bf97 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_06_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" moderator address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_07_filetest.gno index 1a2fdbb4583..65b1f31b80a 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_07_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_08_filetest.gno index 3b9c4557385..89cb60b146f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_08_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_flag_thread_08_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_00_filetest.gno index 1b67af00256..c0ac340417e 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_01_filetest.gno index 789f4c282cd..94865f61282 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_board_01_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_00_filetest.gno index 848ef1d01cd..12d53544d0b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_01_filetest.gno index 574711d7332..3d24fc1b895 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_01_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_02_filetest.gno index 84a94b23ff9..8ff985661c1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_freeze_thread_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_00_filetest.gno index 4f05fdf5ef7..ca249423ab9 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID @@ -18,11 +18,11 @@ func init() { } func main() { - testing.SetRealm(testing.NewUserRealm(owner)) + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) - // Calling as an owner should succeed - board := boards2.GetBoard(bid) - if board == nil { + // Calling as a Boards2 sub realm should succeed + board, found := boards2.GetBoard(bid) + if !found { return } diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_01_filetest.gno index 5a44600c035..5189a7b5551 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_01_filetest.gno @@ -8,26 +8,21 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" - admin address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" -) +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID func init() { testing.SetRealm(testing.NewUserRealm(owner)) - bid = boards2.CreateBoard(cross, "test123", false, false) - boards2.InviteMember(cross, bid, admin, boards2.RoleAdmin) } func main() { - testing.SetRealm(testing.NewUserRealm(admin)) + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/test")) - // Calling as a non owner member should fail + // Calling as a non Boards2 sub realm should fail boards2.GetBoard(bid) } // Error: -// forbidden +// forbidden, caller should live within "gno.land/r/gnoland/boards2/" namespace diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_02_filetest.gno deleted file mode 100644 index aae6febbb68..00000000000 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_02_filetest.gno +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "testing" - - "gno.land/p/gnoland/boards" - - boards2 "gno.land/r/gnoland/boards2/v1" -) - -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" - -var bid boards.ID - -func init() { - testing.SetRealm(testing.NewUserRealm(owner)) - bid = boards2.CreateBoard(cross, "test123", false, false) -} - -func main() { - testing.SetRealm(testing.NewUserRealm("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5")) - - // Calling as a non member should fail - boards2.GetBoard(bid) -} - -// Error: -// forbidden diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_00_filetest.gno index 3309756f495..04c93e83297 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "test123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_01_filetest.gno index d20d0de7b33..183408ae980 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_board_id_from_name_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_realm_permissions_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_realm_permissions_00_filetest.gno new file mode 100644 index 00000000000..d7465b222dd --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_realm_permissions_00_filetest.gno @@ -0,0 +1,16 @@ +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + perms := boards2.GetRealmPermissions() + println(perms != nil) +} + +// Output: +// true diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_realm_permissions_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_realm_permissions_01_filetest.gno new file mode 100644 index 00000000000..56a6c0e3d3c --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_get_realm_permissions_01_filetest.gno @@ -0,0 +1,17 @@ +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/test")) + + // Calling as a non Boards2 sub realm should fail + _ = boards2.GetRealmPermissions() +} + +// Error: +// forbidden, caller should live within "gno.land/r/gnoland/boards2/" namespace diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_00_filetest.gno index 29aea22562a..d24d2418498 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 bid = boards.ID(0) // Operate on realm DAO instead of individual boards role = boards2.RoleOwner diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_01_filetest.gno index 721871cf145..f74f0d56b9f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" admin address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 user address = "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_02_filetest.gno index 411e76690ac..527d07c8135 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" admin address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 user address = "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn" role = boards2.RoleAdmin diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_03_filetest.gno index 12c585b6313..19ddcec374d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_03_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_04_filetest.gno index 1cac68a1d2f..aa79f1d52d1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_04_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 role = boards2.RoleOwner ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_05_filetest.gno index b12c899e06e..d78e1a42a59 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_05_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 bid = boards.ID(0) // Operate on realm DAO instead of individual boards role = boards2.RoleOwner diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_06_filetest.gno index d6cf2a5c54a..6f994791b42 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_invite_member_06_filetest.gno @@ -15,4 +15,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_00_filetest.gno index 80eb2671c0c..a8da05f4249 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_01_filetest.gno index 2c6927272f7..8a627a9635b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_banned_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_00_filetest.gno index 4335fd47246..39d40827aad 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 role = boards2.RoleGuest ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_01_filetest.gno index b250bcb94dc..16c928cd1b1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_is_member_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 bid = boards.ID(0) // Operate on realm DAO instead of individual boards role = boards2.RoleGuest diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_iterate_realm_members_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_iterate_realm_members_00_filetest.gno index 6d7297e5dc4..d36db1db159 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_iterate_realm_members_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_iterate_realm_members_00_filetest.gno @@ -9,27 +9,26 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" bid = boards.ID(0) // Operate on realm DAO instead of individual boards ) func init() { testing.SetRealm(testing.NewUserRealm(owner)) - boards2.InviteMember(cross, bid, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", boards2.RoleOwner) - boards2.InviteMember(cross, bid, "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5", boards2.RoleAdmin) + boards2.InviteMember(cross, bid, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", boards2.RoleAdmin) } func main() { - testing.SetRealm(testing.NewUserRealm(owner)) + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) - boards2.IterateRealmMembers(0, func(u boards.User) bool { + // Calling as a Boards2 sub realm should succeed + perms := boards2.GetRealmPermissions() + perms.IterateUsers(0, perms.UsersCount(), func(u boards.User) bool { println(u.Address, string(u.Roles[0])) return false }) } // Output: -// g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq owner // g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh owner -// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj owner -// g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 admin +// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj admin diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_00_filetest.gno index cbe85bf3bc3..1196a414bfb 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_00_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_01_filetest.gno index 51394d7f4e9..1bbe9eb7c94 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_01_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_02_filetest.gno index f41628d95a5..fedac8b5c26 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_02_filetest.gno @@ -16,4 +16,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_03_filetest.gno index 6585fe59439..a3346edea15 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_03_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_04_filetest.gno index 5c1adf1ea46..ee5e56be2f1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_04_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_05_filetest.gno index 11379570f04..85c136070ae 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_lock_realm_05_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { // Lock the realm without locking realm members diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_00_filetest.gno index 537cfc6dadf..420c67776ad 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_01_filetest.gno index 1a76eb29529..816fa230793 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_01_filetest.gno @@ -15,4 +15,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_02_filetest.gno index 60e4f36f17f..603a20ea580 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_02_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_03_filetest.gno index 7dbaa38a1dc..2b638fa340f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_remove_member_03_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_00_filetest.gno index 8a218a35c00..911ae719dae 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "foo123" newName = "bar123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_01_filetest.gno index 79734453fbf..0dcc8445e7c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_01_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "foo123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_02_filetest.gno index e4ebc65838f..0b0e0f051ff 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_02_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "foo123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_03_filetest.gno index d2696d57572..427e9a0703b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_03_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_04_filetest.gno index 246cb83a608..9931782b72c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_04_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "foo123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_05_filetest.gno index 7e97a4da32d..dbce525bc70 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_05_filetest.gno @@ -11,7 +11,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 name = "foo123" newName = "barbaz123" diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_06_filetest.gno index 4cb7c2d281a..849a760ceab 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_06_filetest.gno @@ -12,7 +12,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 member2 address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" name = "foo123" diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_07_filetest.gno index d0d99480a61..58f9e89f418 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_07_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_07_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" name = "foo123" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_08_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_08_filetest.gno index 4cb7c2d281a..849a760ceab 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_08_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_08_filetest.gno @@ -12,7 +12,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" member address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 member2 address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" name = "foo123" diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_09_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_09_filetest.gno index e0010174423..e3dbbbff83d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_09_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_rename_board_09_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 name = "foo123" ) @@ -24,4 +24,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_00_filetest.gno index 3d7f6c6a967..e7190c6b57b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_01_filetest.gno index fefc7cc4642..c4f67dcc3da 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_01_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_02_filetest.gno index 1fbc1d384ed..feae0e9169e 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_02_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_03_filetest.gno index 2c04bac6fd6..90fb4b95bd1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_03_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_04_filetest.gno index 7289430468f..337a3506bcf 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_request_invite_04_filetest.gno @@ -7,7 +7,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_00_filetest.gno index 3ecf3a0650b..f863665947d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_01_filetest.gno index 1f698a57839..4e37247f88c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_02_filetest.gno index 9626c11ae21..e4e53d7e9d8 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_revoke_invite_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) @@ -31,4 +31,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_00_filetest.gno index 52e594e4cce..b31f94151a9 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_01_filetest.gno index 5bbe2a3fc87..95a7169ca64 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_01_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" bid boards.ID = 404 ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_02_filetest.gno index 8f4917e5894..fbfe2fa0618 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_flagging_threshold_02_filetest.gno @@ -6,7 +6,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_00_filetest.gno index ee6a0b93daa..613459222b1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_00_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" bid = boards.ID(0) // Operate on realm instead of individual boards ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_01_filetest.gno index 9307b0b477a..0a13eacf668 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_01_filetest.gno @@ -28,4 +28,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_02_filetest.gno index fe9c0f1b0b1..e3f8a70b4e7 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_permissions_02_filetest.gno @@ -9,7 +9,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var ( perms boards.Permissions diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_00_filetest.gno index 408c1142c6c..b28544bcf7f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_00_filetest.gno @@ -6,23 +6,15 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func main() { testing.SetRealm(testing.NewUserRealm(owner)) boards2.SetRealmNotice(cross, "This is a test realm message") - // Render content must contain the message - println(boards2.Render("")) + println(boards2.Notice) } // Output: -// > [!INFO] Notice -// > This is a test realm message -// # Boards -// [Create Board](/r/gnoland/boards2/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help) -// -// --- -// ### Currently there are no boards -// Be the first to [create a new board](/r/gnoland/boards2/v1:create-board)! +// This is a test realm message diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_01_filetest.gno index 25937199a52..12241827df2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_01_filetest.gno @@ -1,13 +1,12 @@ package main import ( - "strings" "testing" boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { // Set an initial message so it can be cleared @@ -20,12 +19,8 @@ func main() { boards2.SetRealmNotice(cross, "") - // Render content must contain the message - content := boards2.Render("") - println(strings.HasPrefix(content, "> This is a test realm message\n\n")) - println(strings.HasPrefix(content, "# Boards")) + println(boards2.Notice == "") } // Output: -// false // true diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_02_filetest.gno index 5adf5c56f74..08874c545cc 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_02_filetest.gno @@ -16,4 +16,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_03_filetest.gno new file mode 100644 index 00000000000..8af16f45ad1 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_realm_notice_03_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + +func main() { + testing.SetRealm(testing.NewUserRealm(owner)) + + boards2.SetRealmNotice(cross, "This is a test realm message") + + println(boards2.Render("")) +} + +// Output: +// > [!INFO] Notice +// > This is a test realm message +// # Boards +// [Create Board](/r/gnoland/boards2/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help) +// +// --- +// ### Currently there are no boards +// Be the first to [create a new board](/r/gnoland/boards2/v1:create-board)! diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_00_filetest.gno new file mode 100644 index 00000000000..1fa86eee505 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_00_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + +func main() { + testing.SetRealm(testing.NewUserRealm(owner)) + + boards2.SetRequiredAccountAmount(cross, 1_000_000) + + println(boards2.RequiredAccountAmount) +} + +// Output: +// 1000000 diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_01_filetest.gno new file mode 100644 index 00000000000..1f40a6af15d --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_01_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + +func main() { + testing.SetRealm(testing.NewUserRealm(owner)) + + boards2.SetRequiredAccountAmount(cross, 0) // Disable + + println(boards2.RequiredAccountAmount) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_02_filetest.gno new file mode 100644 index 00000000000..159b90f4ae3 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_set_required_account_amount_02_filetest.gno @@ -0,0 +1,18 @@ +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const owner address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" // @test2 + +func main() { + testing.SetRealm(testing.NewUserRealm(owner)) + + boards2.SetRequiredAccountAmount(cross, 1_000_000) +} + +// Error: +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_admin_users_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_admin_users_00_filetest.gno index 93ca6ad1639..60b39f8dc08 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_admin_users_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_admin_users_00_filetest.gno @@ -14,5 +14,4 @@ func main() { // ### These are the admin users of the realm // | Member | Role | Actions | // | --- | --- | --- | -// | [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) | owner | [remove](/r/gnoland/boards2/v1$help&func=RemoveMember&boardID=0&member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) • [change role](/r/gnoland/boards2/v1$help&func=ChangeMemberRole&boardID=0&member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq&role=) | // | [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) | owner | [remove](/r/gnoland/boards2/v1$help&func=RemoveMember&boardID=0&member=g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) • [change role](/r/gnoland/boards2/v1$help&func=ChangeMemberRole&boardID=0&member=g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh&role=) | diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_00_filetest.gno index a36f36fb978..96be8ac4f8b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_00_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "TestBoard" ) @@ -37,20 +37,20 @@ func main() { // Output: // # [Boards](/r/gnoland/boards2/v1) › TestBoard -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help&func=RequestInvite&boardID=1) • [Manage Board](?menu=manageBoard) // // --- // Sort by: [oldest first](/r/gnoland/boards2/v1:TestBoard?order=desc) // // ###### [A](/r/gnoland/boards2/v1:TestBoard/1) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **1 replies • 0 reposts** // // ###### [B](/r/gnoland/boards2/v1:TestBoard/3) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 0 reposts** // // ###### [C](/r/gnoland/boards2/v1:TestBoard/4) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 1 reposts** diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_01_filetest.gno index d446eae711f..3bde2733f79 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_01_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "TestBoard" ) @@ -30,20 +30,20 @@ func main() { // Output: // # [Boards](/r/gnoland/boards2/v1) › TestBoard -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help&func=RequestInvite&boardID=1) • [Manage Board](?menu=manageBoard) // // --- // Sort by: [newest first](/r/gnoland/boards2/v1:TestBoard?order=asc) // // ###### [C](/r/gnoland/boards2/v1:TestBoard/3) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 0 reposts** // // ###### [B](/r/gnoland/boards2/v1:TestBoard/2) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 0 reposts** // // ###### [A](/r/gnoland/boards2/v1:TestBoard/1) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 0 reposts** diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_02_filetest.gno index 0b1eadc9084..765a1783a36 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_02_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "TestBoard" ) @@ -27,7 +27,7 @@ func main() { // Output: // # [Boards](/r/gnoland/boards2/v1) › TestBoard -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help&func=RequestInvite&boardID=1) • **Manage Board** // └─ [Invite Member](/r/gnoland/boards2/v1:TestBoard/invite-member) • [List Invite Requests](/r/gnoland/boards2/v1:TestBoard/invites) • [List Members](/r/gnoland/boards2/v1:TestBoard/members) • [List Banned Users](/r/gnoland/boards2/v1:TestBoard/banned-users) • [Freeze Board](/r/gnoland/boards2/v1$help&func=FreezeBoard&boardID=1) // @@ -35,5 +35,5 @@ func main() { // Sort by: [oldest first](/r/gnoland/boards2/v1:TestBoard?menu=manageBoard&order=desc) // // ###### [A](/r/gnoland/boards2/v1:TestBoard/1) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 0 reposts** diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_03_filetest.gno index f93784c67ae..b225b2c10c3 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_03_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "TestBoard" ) @@ -30,12 +30,12 @@ func main() { // > [!WARNING] Info // > Creating new threads and commenting are disabled within this board // -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // [List Members](/r/gnoland/boards2/v1:TestBoard/members) • [Unfreeze Board](/r/gnoland/boards2/v1$help&func=UnfreezeBoard&boardID=1&replyID=&threadID=) // // --- // Sort by: [oldest first](/r/gnoland/boards2/v1:TestBoard?order=desc) // // ###### [A](/r/gnoland/boards2/v1:TestBoard/1) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) `owner` on 2009-02-13 11:31pm UTC // **0 replies • 0 reposts** diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_04_filetest.gno index e5ad0b83a0f..0a01b7a0ad9 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_04_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "TestBoard" ) @@ -24,7 +24,7 @@ func main() { // Output: // # [Boards](/r/gnoland/boards2/v1) › TestBoard -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help&func=RequestInvite&boardID=1) • [Manage Board](?menu=manageBoard) // // --- diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_members_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_members_00_filetest.gno index 067f5403cb7..80946e1901d 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_members_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_board_members_00_filetest.gno @@ -8,7 +8,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "BoardName" ) @@ -28,4 +28,4 @@ func main() { // ### These are the board members // | Member | Role | Actions | // | --- | --- | --- | -// | [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) | owner | [remove](/r/gnoland/boards2/v1$help&func=RemoveMember&boardID=1&member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) • [change role](/r/gnoland/boards2/v1$help&func=ChangeMemberRole&boardID=1&member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq&role=) | +// | [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) | owner | [remove](/r/gnoland/boards2/v1$help&func=RemoveMember&boardID=1&member=g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) • [change role](/r/gnoland/boards2/v1$help&func=ChangeMemberRole&boardID=1&member=g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh&role=) | diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_00_filetest.gno index 525c8c43725..aabf0f29a4b 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_00_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { testing.SetRealm(testing.NewUserRealm(owner)) @@ -35,13 +35,13 @@ func main() { // Sort by: [oldest first](/r/gnoland/boards2/v1:?order=desc) // // ###### [AAA](/r/gnoland/boards2/v1:AAA) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // **1 threads** // // ###### [BBB](/r/gnoland/boards2/v1:BBB) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #2 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #2 // **0 threads** // // ###### [CCC](/r/gnoland/boards2/v1:CCC) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #3 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #3 // **0 threads** diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_01_filetest.gno index 4a43d0172f5..b4227a35b0f 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_home_01_filetest.gno @@ -7,7 +7,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" func init() { testing.SetRealm(testing.NewUserRealm(owner)) @@ -29,13 +29,13 @@ func main() { // Sort by: [newest first](/r/gnoland/boards2/v1:?order=asc) // // ###### [CCC](/r/gnoland/boards2/v1:CCC) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #3 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #3 // **0 threads** // // ###### [BBB](/r/gnoland/boards2/v1:BBB) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #2 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #2 // **0 threads** // // ###### [AAA](/r/gnoland/boards2/v1:AAA) -// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1 +// Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC, #1 // **0 threads** diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_00_filetest.gno index 6d517db2a3a..3f859710c7c 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_00_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test-board" ) @@ -41,20 +41,20 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) // > Second comment // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=3&threadID=1) // > // > > -// > > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4) +// > > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4) // > > Third comment // > > // > > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=4&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_01_filetest.gno index b469ac9fd6e..c3e88993821 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_01_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test-board" ) @@ -42,20 +42,20 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > ⚠ This comment has been deleted // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=2&threadID=1) // > // > > -// > > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) +// > > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) // > > Third comment // > > // > > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=3&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_02_filetest.gno index 3e94bee1ee0..a7bdb261f13 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_reply_02_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test-board" ) @@ -42,18 +42,18 @@ func main() { // Output: // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1) // // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons) // > // > > -// > > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) +// > > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) // > > Third comment // > > // > > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=3&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_00_filetest.gno index 59b8c0aa99c..31e71028772 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_00_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test-board" ) @@ -39,7 +39,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) @@ -48,19 +48,19 @@ func main() { // Sort by: [oldest first](/r/gnoland/boards2/v1:test-board/1?order=desc) // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > First comment // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=2&threadID=1) // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) // > Second comment // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=3&threadID=1) // > // > > -// > > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4) +// > > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4) // > > Third comment // > > // > > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=4&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_01_filetest.gno index fe99c156fa3..b10fba810d2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_01_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test-board" ) @@ -39,7 +39,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) @@ -48,19 +48,19 @@ func main() { // Sort by: [newest first](/r/gnoland/boards2/v1:test-board/1?order=asc) // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3) // > Second comment // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=3&threadID=1) // > // > > -// > > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4) +// > > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4) // > > Third comment // > > // > > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=4&threadID=1) // // > -// > **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) +// > **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2) // > First comment // > // > ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteReply&boardID=1&replyID=2&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_02_filetest.gno index d9e810eb307..2c6907a0ee1 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_02_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" srcBoardName = "test-board" dstBoardName = "test-board-2" ) @@ -46,7 +46,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) [1] • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) @@ -56,10 +56,10 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board\-2](/r/gnoland/boards2/v1:test-board-2) // ## Bar // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // > [!INFO]- Thread Repost // > Original thread is [Foo](/r/gnoland/boards2/v1:test-board/1) -// > Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC +// > Created by [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) on 2009-02-13 11:31pm UTC // // Body2 // diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_03_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_03_filetest.gno index d67b04bcda6..2390f15ae6a 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_03_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_03_filetest.gno @@ -11,7 +11,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "test-board" ) @@ -35,7 +35,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) // ## Foo // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body // // ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_04_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_04_filetest.gno index f55cdaceb5d..50a55830394 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_04_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_04_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" srcBoardName = "test-board" dstBoardName = "test-board-2" ) @@ -41,7 +41,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board\-2](/r/gnoland/boards2/v1:test-board-2) // ## Bar // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body2 // // > ⚠ Source post has been deleted diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_05_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_05_filetest.gno index 01aead7d797..b137ef848be 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_05_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_05_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" boardName = "BoardName" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_06_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_06_filetest.gno index f8070cb84a5..d348d48f597 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_06_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_06_filetest.gno @@ -10,7 +10,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" srcBoardName = "test-board" dstBoardName = "test-board-2" ) @@ -42,7 +42,7 @@ func main() { // # [Boards](/r/gnoland/boards2/v1) › [test\-board\-2](/r/gnoland/boards2/v1:test-board-2) // ## Bar // -// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC // Body2 // // > ⚠ Source post has been flagged as inappropriate diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_07_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_07_filetest.gno new file mode 100644 index 00000000000..622cc1c77ed --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_ui_thread_07_filetest.gno @@ -0,0 +1,39 @@ +// Render thread with a title that contains Markdown +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +const ( + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + boardName = "test-board" +) + +var threadID boards.ID + +func init() { + testing.SetRealm(testing.NewUserRealm(owner)) + + // Create a board and a thread + boardID := boards2.CreateBoard(cross, boardName, false, false) + threadID = boards2.CreateThread(cross, boardID, "[Foo](https://foo.com)", "Body") +} + +func main() { + path := boardName + "/" + threadID.String() + println(boards2.Render(path)) +} + +// Output: +// # [Boards](/r/gnoland/boards2/v1) › [test\-board](/r/gnoland/boards2/v1:test-board) +// ## \[Foo\]\(https://foo\.com\) +// +// **[g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh)** `owner` · 2009-02-13 11:31pm UTC +// Body +// +// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help&func=DeleteThread&boardID=1&threadID=1) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_00_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_00_filetest.gno index fbab4519aa7..420ac006d36 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_00_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_00_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_01_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_01_filetest.gno index 67229098b53..6e58a478ef8 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_01_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_01_filetest.gno @@ -8,7 +8,7 @@ import ( boards2 "gno.land/r/gnoland/boards2/v1" ) -const owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" +const owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" var bid boards.ID diff --git a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_02_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_02_filetest.gno index 29552951d08..d793336d5bc 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_02_filetest.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/filetests/z_unban_02_filetest.gno @@ -9,7 +9,7 @@ import ( ) const ( - owner address = "g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq" + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" user address = "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5" ) @@ -29,4 +29,4 @@ func main() { } // Error: -// unauthorized +// unauthorized, user g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj doesn't have the required permission diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/board.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/board.gno new file mode 100644 index 00000000000..736a008f1fa --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/board.gno @@ -0,0 +1,83 @@ +package hub + +import ( + "gno.land/p/gnoland/boards" +) + +// Member defines a type for board members. +type Member struct { + Address address + Roles []string +} + +// Board defines a safe type for boards. +type Board struct { + ref *boards.Board + + // ID is the unique identifier of the board. + ID uint64 + + // Name is the current name of the board. + Name string + + // Aliases contains a list of alternative names for the board. + Aliases []string + + // Readonly indicates that the board is readonly. + Readonly bool + + // ThreadsCount contains the number of threads within the board. + ThreadCount int + + // MemberCount contains the number of members of the board. + MemberCount int + + // Creator is the account address that created the board. + Creator address + + // CreatedAt is the board's creation time as Unix time. + CreatedAt int64 + + // UpdatedAt is the board's update time as Unix time. + UpdatedAt int64 +} + +// IterateThreads iterates board threads by creation time. +// To reverse iterate use a negative count. +func (b Board) IterateThreads(start, count int, fn func(Thread) bool) bool { + return b.ref.Threads.Iterate(start, count, func(thread *boards.Post) bool { + return fn(NewSafeThread(thread)) + }) +} + +// IterateMembers iterates board members. +// To reverse iterate use a negative count. +func (b Board) IterateMembers(start, count int, fn func(boards.User) bool) bool { + return b.ref.Permissions.IterateUsers(start, count, fn) +} + +// NewSafeBoard creates a safe board. +func NewSafeBoard(ref *boards.Board) Board { + var usersCount int + if ref.Permissions != nil { + usersCount = ref.Permissions.UsersCount() + } + + var threadCount int + if ref.Threads != nil { + threadCount = ref.Threads.Size() + } + + return Board{ + ref: ref, + ID: uint64(ref.ID), + Name: ref.Name, + Aliases: append([]string(nil), ref.Aliases...), + Readonly: ref.Readonly, + ThreadCount: threadCount, + MemberCount: usersCount, + Creator: ref.Creator, + CreatedAt: timeToUnix(ref.CreatedAt), + UpdatedAt: timeToUnix(ref.UpdatedAt), + } +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/board_test.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/board_test.gno new file mode 100644 index 00000000000..0ef60e79faa --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/board_test.gno @@ -0,0 +1,111 @@ +package hub_test + +import ( + "testing" + + "gno.land/p/gnoland/boards" + "gno.land/p/gnoland/boards/exts/permissions" + "gno.land/p/nt/urequire/v0" + + "gno.land/r/gnoland/boards2/v1/hub" +) + +func TestBoardIterateThreads(t *testing.T) { + tests := []struct { + name string + setup func(*boards.Board) + }{ + { + name: "no threads", + }, + { + name: "one thread", + setup: func(b *boards.Board) { + t := boards.MustNewThread(b, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Title 1", "Body 1") + b.Threads.Add(t) + }, + }, + { + name: "multiple threads", + setup: func(b *boards.Board) { + threads := []*boards.Post{ + boards.MustNewThread(b, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Title 1", "Body 1"), + boards.MustNewThread(b, "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh", "Title 2", "Body 2"), + boards.MustNewThread(b, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", "Title 3", "Body 3"), + } + for _, t := range threads { + b.Threads.Add(t) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref := boards.New(1) + if tt.setup != nil { + tt.setup(ref) + } + + board := hub.NewSafeBoard(ref) + + urequire.Equal(t, ref.Threads.Size(), board.ThreadCount, "expect number of threads to match") + board.IterateThreads(0, board.ThreadCount, func(thread hub.Thread) bool { + id := boards.ID(thread.ID) + expected, found := ref.Threads.Get(id) + + urequire.True(t, found, "expect thread to be found") + urequire.Equal(t, expected.Creator, thread.Creator, "expect creator to match") + urequire.Equal(t, expected.Title, thread.Title, "expect title to match") + urequire.Equal(t, expected.Body, thread.Body, "expect body to match") + return false + }) + }) + } +} + +func TestBoardIterateMembers(t *testing.T) { + tests := []struct { + name string + users []boards.User + }{ + { + name: "no members", + }, + { + name: "one member", + users: []boards.User{ + {Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, + }, + }, + { + name: "multiple members", + users: []boards.User{ + {Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, + {Address: "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh"}, + {Address: "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var i int + perms := permissions.New() + for _, u := range tt.users { + perms.SetUserRoles(u.Address) + } + + ref := boards.New(1) + ref.Permissions = perms + board := hub.NewSafeBoard(ref) + + urequire.Equal(t, len(tt.users), board.MemberCount, "expect number of members to match") + board.IterateMembers(0, board.MemberCount, func(user boards.User) bool { + urequire.Equal(t, tt.users[i].Address, user.Address, "expect address to match") + i++ + return false + }) + }) + } +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/comment.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/comment.gno new file mode 100644 index 00000000000..4f7a5e2eb69 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/comment.gno @@ -0,0 +1,90 @@ +package hub + +import ( + "gno.land/p/gnoland/boards" +) + +// Comment defines a type for threads comment/replies. +type Comment struct { + ref *boards.Post + + // ID is the unique identifier of the comment. + ID uint64 + + // BoardID is the board ID where comment is created. + BoardID uint64 + + // ThreadID contains is the ID of the thread where comment is created. + ThreadID uint64 + + // ParentID is the ID of the parent comment or reply. + ParentID uint64 + + // Body contains the comment's content. + Body string + + // Hidden indicates that comment is hidden. + Hidden bool + + // ReplyCount contains the number of comments replies. + // Count only includes top level replies, sub-replies are not included. + ReplyCount int + + // FlagCount contains the number of flags that comment has. + FlagCount int + + // Creator is the account address that created the comment or reply. + Creator address + + // CreatedAt is thread's creation time as Unix time. + CreatedAt int64 + + // UpdatedAt is thread's update time as Unix time. + UpdatedAt int64 +} + +// IterateFlags iterates comment moderation flags. +// To reverse iterate use a negative count. +func (c Comment) IterateFlags(start, count int, fn func(boards.Flag) bool) bool { + return c.ref.Flags.Iterate(start, count, fn) +} + +// IterateReplies iterates comment replies (sub-comments). +// To reverse iterate use a negative count. +func (t Comment) IterateReplies(start, count int, fn func(Comment) bool) bool { + return t.ref.Replies.Iterate(start, count, func(comment *boards.Post) bool { + return fn(NewSafeComment(comment)) + }) +} + +// NewSafeComment creates a safe comment. +func NewSafeComment(ref *boards.Post) Comment { + if boards.IsThread(ref) { + panic("post is not a comment or reply") + } + + var replyCount int + if ref.Replies != nil { + replyCount = ref.Replies.Size() + } + + var flagCount int + if ref.Flags != nil { + flagCount = ref.Flags.Size() + } + + return Comment{ + ref: ref, + ID: uint64(ref.ID), + BoardID: uint64(ref.Board.ID), + ThreadID: uint64(ref.ThreadID), + ParentID: uint64(ref.ParentID), + Body: ref.Body, + Hidden: ref.Hidden, + ReplyCount: replyCount, + FlagCount: flagCount, + Creator: ref.Creator, + CreatedAt: timeToUnix(ref.CreatedAt), + UpdatedAt: timeToUnix(ref.UpdatedAt), + } +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/comment_test.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/comment_test.gno new file mode 100644 index 00000000000..f9070ae6afc --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/comment_test.gno @@ -0,0 +1,115 @@ +package hub_test + +import ( + "testing" + + "gno.land/p/gnoland/boards" + "gno.land/p/nt/urequire/v0" + + "gno.land/r/gnoland/boards2/v1/hub" +) + +func TestCommentIterateFlags(t *testing.T) { + tests := []struct { + name string + flags []boards.Flag + }{ + { + name: "no flags", + }, + { + name: "one flag", + flags: []boards.Flag{ + {User: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Reason: "Reason 1"}, + }, + }, + { + name: "multiple flags", + flags: []boards.Flag{ + {User: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Reason: "Reason 1"}, + {User: "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh", Reason: "Reason 2"}, + {User: "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", Reason: "Reason 3"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var i int + ref := createComment(t, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Comment") + for _, f := range tt.flags { + ref.Flags.Add(f) + } + + comment := hub.NewSafeComment(ref) + + urequire.Equal(t, len(tt.flags), comment.FlagCount, "expect number of flags to match") + comment.IterateFlags(0, comment.FlagCount, func(f boards.Flag) bool { + urequire.Equal(t, tt.flags[i].User, f.User, "expect user to match") + urequire.Equal(t, tt.flags[i].Reason, f.Reason, "expect reason to match") + i++ + return false + }) + }) + } +} + +func TestCommentIterateReplies(t *testing.T) { + tests := []struct { + name string + setup func(*boards.Post) + }{ + { + name: "no replies", + }, + { + name: "one reply", + setup: func(comment *boards.Post) { + r := boards.MustNewReply(comment, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Body 1") + comment.Replies.Add(r) + }, + }, + { + name: "multiple replies", + setup: func(comment *boards.Post) { + replies := []*boards.Post{ + boards.MustNewReply(comment, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Body 1"), + boards.MustNewReply(comment, "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh", "Body 2"), + boards.MustNewReply(comment, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", "Body 3"), + } + for _, t := range replies { + comment.Replies.Add(t) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref := createComment(t, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Comment") + if tt.setup != nil { + tt.setup(ref) + } + + comment := hub.NewSafeComment(ref) + + urequire.Equal(t, ref.Replies.Size(), comment.ReplyCount, "expect number of replies to match") + comment.IterateReplies(0, comment.ReplyCount, func(reply hub.Comment) bool { + id := boards.ID(reply.ID) + expected, found := ref.Replies.Get(id) + + urequire.True(t, found, "expect reply to be found") + urequire.Equal(t, expected.Creator, reply.Creator, "expect creator to match") + urequire.Equal(t, expected.Body, reply.Body, "expect body to match") + return false + }) + }) + } +} + +func createComment(t *testing.T, user address, body string) *boards.Post { + t.Helper() + + thread := createThread(t, user, "Title", "Body") + return boards.MustNewReply(thread, user, body) +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_0_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_0_a_filetest.gno new file mode 100644 index 00000000000..8af252a17c1 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_0_a_filetest.gno @@ -0,0 +1,47 @@ +// Test default board values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var boardID boards.ID + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "test123", false, false) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + board, found := hub.GetBoard(uint64(boardID)) + if !found { + return + } + + println(board.ID) + println(board.Name) + println(board.Aliases) + println(board.Readonly) + println(board.ThreadCount) + println(board.MemberCount) + println(board.Creator) + println(board.CreatedAt) + println(board.UpdatedAt) +} + +// Output: +// 1 +// test123 +// (nil []string) +// false +// 0 +// 1 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_0_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_0_b_filetest.gno new file mode 100644 index 00000000000..060372aab6b --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_0_b_filetest.gno @@ -0,0 +1,59 @@ +// Test non default board values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var boardID boards.ID + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "test123", false, false) + + // Invite member + boards2.InviteMember(cross, boardID, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", "admin") + + // Rename board + boards2.RenameBoard(cross, "test123", "foo123") + + // Create a thread + boards2.CreateThread(cross, boardID, "Title", "Body") + + // Freeze board + boards2.FreezeBoard(cross, boardID) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + board, found := hub.GetBoard(uint64(boardID)) + if !found { + return + } + + println(board.ID) + println(board.Name) + println(board.Aliases) + println(board.Readonly) + println(board.ThreadCount) + println(board.MemberCount) + println(board.Creator) + println(board.CreatedAt) + println(board.UpdatedAt) +} + +// Output: +// 1 +// foo123 +// slice[("test123" string)] +// true +// 1 +// 2 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 1234567890 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_a_filetest.gno new file mode 100644 index 00000000000..d9913e8d998 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_a_filetest.gno @@ -0,0 +1,61 @@ +// Test default thread values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "origin123", false, false) + threadID = boards2.CreateThread(cross, boardID, "Title", "Body") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, found := hub.GetThread(uint64(boardID), uint64(threadID)) + if !found { + return + } + + println(thread.ID) + println(thread.OriginalBoardID) + println(thread.OriginalThreadID) + println(thread.BoardID) + println(thread.Title) + println(thread.Body) + println(thread.Hidden) + println(thread.Readonly) + println(thread.CommentCount) + println(thread.RepostCount) + println(thread.FlagCount) + println(thread.Creator) + println(thread.CreatedAt) + println(thread.UpdatedAt) +} + +// Output: +// 1 +// 0 +// 0 +// 1 +// Title +// Body +// false +// false +// 0 +// 0 +// 0 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_b_filetest.gno new file mode 100644 index 00000000000..9620d1371ec --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_b_filetest.gno @@ -0,0 +1,77 @@ +// Test non default thread values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "test123", false, false) + threadID = boards2.CreateThread(cross, boardID, "Title", "Body") + + // Create another board and repost thread + dstBoardID := boards2.CreateBoard(cross, "destination123", false, false) + boards2.CreateRepost(cross, boardID, threadID, dstBoardID, "Title", "Body") + + // Edit thread + boards2.EditThread(cross, boardID, threadID, "Foo", "Bar") + + // Add a comment to the thread + boards2.CreateReply(cross, boardID, threadID, 0, "Comment") + + // Freeze thread + boards2.FreezeThread(cross, boardID, threadID) + + // Flag thread + boards2.FlagThread(cross, boardID, threadID, "Reason") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, found := hub.GetThread(uint64(boardID), uint64(threadID)) + if !found { + return + } + + println(thread.ID) + println(thread.OriginalBoardID) // Only reposts have an original board ID + println(thread.OriginalThreadID) // Only reposts have an original thread ID + println(thread.BoardID) + println(thread.Title) + println(thread.Body) + println(thread.Hidden) + println(thread.Readonly) + println(thread.CommentCount) + println(thread.RepostCount) + println(thread.FlagCount) + println(thread.Creator) + println(thread.CreatedAt) + println(thread.UpdatedAt) +} + +// Output: +// 1 +// 0 +// 0 +// 1 +// Foo +// Bar +// true +// true +// 1 +// 1 +// 1 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 1234567890 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_c_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_c_filetest.gno new file mode 100644 index 00000000000..c27cf5a7709 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_1_c_filetest.gno @@ -0,0 +1,80 @@ +// Test non default reposted thread values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + srcBoardID := boards2.CreateBoard(cross, "origin123", false, false) + + // Create two threads where the second is the one to repost + boards2.CreateThread(cross, srcBoardID, "Title1", "Body1") + srcThreadID := boards2.CreateThread(cross, srcBoardID, "Title2", "Body2") // ID = 2 + + // Create repost + boardID = boards2.CreateBoard(cross, "test123", false, false) + threadID = boards2.CreateRepost(cross, srcBoardID, srcThreadID, boardID, "Title", "Body") + + // Edit repost + boards2.EditThread(cross, boardID, threadID, "Foo", "Bar") + + // Add a comment to the repost + boards2.CreateReply(cross, boardID, threadID, 0, "Comment") + + // Freeze repost + boards2.FreezeThread(cross, boardID, threadID) + + // Flag repost + boards2.FlagThread(cross, boardID, threadID, "Reason") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, found := hub.GetThread(uint64(boardID), uint64(threadID)) + if !found { + return + } + + println(thread.ID) + println(thread.OriginalBoardID) + println(thread.OriginalThreadID) + println(thread.BoardID) + println(thread.Title) + println(thread.Body) + println(thread.Hidden) + println(thread.Readonly) + println(thread.CommentCount) + println(thread.RepostCount) // Reposts can't be reposted, so count must be 0 + println(thread.FlagCount) + println(thread.Creator) + println(thread.CreatedAt) + println(thread.UpdatedAt) +} + +// Output: +// 1 +// 1 +// 2 +// 2 +// Foo +// Bar +// true +// true +// 1 +// 0 +// 1 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 1234567890 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_2_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_2_a_filetest.gno new file mode 100644 index 00000000000..1fe2eae2ae1 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_2_a_filetest.gno @@ -0,0 +1,18 @@ +// Test getting boards when no boards exist +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + boards := hub.GetBoards(0, boards2.BoardCount()) + println(len(boards)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_2_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_2_b_filetest.gno new file mode 100644 index 00000000000..9337b4bfb4e --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_2_b_filetest.gno @@ -0,0 +1,30 @@ +// Test getting boards +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boards2.CreateBoard(cross, "aaa123", false, false) + boards2.CreateBoard(cross, "bbb123", false, false) + boards2.CreateBoard(cross, "ccc123", false, false) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + boards := hub.GetBoards(0, boards2.BoardCount()) + + for _, b := range boards { + println(b.ID, b.Name) + } +} + +// Output: +// 1 aaa123 +// 2 bbb123 +// 3 ccc123 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_3_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_3_a_filetest.gno new file mode 100644 index 00000000000..d2968f4252f --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_3_a_filetest.gno @@ -0,0 +1,27 @@ +// Test getting threads from an empty board +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var boardID boards.ID + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "test123", false, false) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + boards := hub.GetThreads(uint64(boardID), 0, boards2.BoardCount()) + println(len(boards)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_3_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_3_b_filetest.gno new file mode 100644 index 00000000000..66e19e6127e --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_3_b_filetest.gno @@ -0,0 +1,39 @@ +// Test getting threads from a board +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var board hub.Board + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "test123", false, false) + + // Create a couple of board threads + boards2.CreateThread(cross, boardID, "First", "Body") + boards2.CreateThread(cross, boardID, "Second", "Body") + boards2.CreateThread(cross, boardID, "Third", "Body") + + // Get readonly board + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + board, _ = hub.GetBoard(uint64(boardID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + threads := hub.GetThreads(board.ID, 0, board.ThreadCount) + + for _, t := range threads { + println(t.ID, t.Title) + } +} + +// Output: +// 1 First +// 2 Second +// 3 Third diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_4_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_4_a_filetest.gno new file mode 100644 index 00000000000..db207204c36 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_4_a_filetest.gno @@ -0,0 +1,38 @@ +// Test getting board members +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var board hub.Board + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "test123", false, false) + + // Invite board members + boards2.InviteMember(cross, boardID, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "owner") + boards2.InviteMember(cross, boardID, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", "admin") + + // Get readonly board + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + board, _ = hub.GetBoard(uint64(boardID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + members := hub.GetMembers(board.ID, 0, board.MemberCount) + + for _, m := range members { + println(m.Address, m.Roles) + } +} + +// Output: +// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 slice[("owner" gno.land/p/gnoland/boards.Role)] +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh slice[("owner" gno.land/p/gnoland/boards.Role)] +// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj slice[("admin" gno.land/p/gnoland/boards.Role)] diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_5_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_5_a_filetest.gno new file mode 100644 index 00000000000..3a37af96213 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_5_a_filetest.gno @@ -0,0 +1,30 @@ +// Test getting reposts from a thread without reposts +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var thread hub.Thread + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "aaa123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + + // Get readonly thread + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, _ = hub.GetThread(uint64(boardID), uint64(threadID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + reposts := hub.GetReposts(thread.BoardID, thread.ID, 0, thread.RepostCount) + println(len(reposts)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_5_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_5_b_filetest.gno new file mode 100644 index 00000000000..803a209fccf --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_5_b_filetest.gno @@ -0,0 +1,42 @@ +// Test getting reposts of a thread +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var thread hub.Thread + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "aaa123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + + // Create first repost + dstBoardID := boards2.CreateBoard(cross, "bbb123", false, false) + boards2.CreateRepost(cross, boardID, threadID, dstBoardID, "First", "Body") + + // Create second repost + dstBoardID = boards2.CreateBoard(cross, "ccc123", false, false) + boards2.CreateRepost(cross, boardID, threadID, dstBoardID, "Second", "Body") + + // Get readonly thread + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, _ = hub.GetThread(uint64(boardID), uint64(threadID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + reposts := hub.GetReposts(thread.BoardID, thread.ID, 0, thread.RepostCount) + + for _, t := range reposts { + println(t.ID, t.Title) + } +} + +// Output: +// 1 First +// 1 Second diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_a_filetest.gno new file mode 100644 index 00000000000..ce67e7708b3 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_a_filetest.gno @@ -0,0 +1,31 @@ +// Test getting flags from an unflagged thread +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "test123", false, false) + threadID = boards2.CreateThread(cross, boardID, "Title", "Body") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + flags := hub.GetFlags(uint64(boardID), uint64(threadID), 0, 0, 1) + println(len(flags)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_b_filetest.gno new file mode 100644 index 00000000000..4d1477b78f4 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_b_filetest.gno @@ -0,0 +1,33 @@ +// Test getting flags from an unflagged comment +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID + commentID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "test123", false, false) + threadID = boards2.CreateThread(cross, boardID, "Title", "Body") + commentID = boards2.CreateReply(cross, boardID, threadID, 0, "Comment") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + flags := hub.GetFlags(uint64(boardID), uint64(threadID), uint64(commentID), 0, 1) + println(len(flags)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_c_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_c_filetest.gno new file mode 100644 index 00000000000..6d6367f6855 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_c_filetest.gno @@ -0,0 +1,55 @@ +// Test getting flags from a thread +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +const ( + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + admin = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + moderator = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" +) + +var thread hub.Thread + +func init() { + testing.SetRealm(testing.NewUserRealm(owner)) + boardID := boards2.CreateBoard(cross, "test123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + + // Invite members + boards2.InviteMember(cross, boardID, admin, "admin") + boards2.InviteMember(cross, boardID, moderator, "moderator") + + // Update flagging threshold to two flags + boards2.SetFlaggingThreshold(cross, boardID, 2) + + // Add first flag + testing.SetRealm(testing.NewUserRealm(admin)) + boards2.FlagThread(cross, boardID, threadID, "Reason 1") + + // Add second flag + testing.SetRealm(testing.NewUserRealm(moderator)) + boards2.FlagThread(cross, boardID, threadID, "Reason 2") + + // Get readonly thread + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, _ = hub.GetThread(uint64(boardID), uint64(threadID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + flags := hub.GetFlags(thread.BoardID, thread.ID, 0, 0, thread.FlagCount) + + for _, f := range flags { + println(f.User, f.Reason) + } +} + +// Output: +// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 Reason 1 +// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj Reason 2 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_d_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_d_filetest.gno new file mode 100644 index 00000000000..1933c5f9e28 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_6_d_filetest.gno @@ -0,0 +1,56 @@ +// Test getting flags from a comment +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +const ( + owner address = "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" + admin = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + moderator = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" +) + +var comment hub.Comment + +func init() { + testing.SetRealm(testing.NewUserRealm(owner)) + boardID := boards2.CreateBoard(cross, "test123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + commentID := boards2.CreateReply(cross, boardID, threadID, 0, "Comment") + + // Invite member + boards2.InviteMember(cross, boardID, admin, "admin") + boards2.InviteMember(cross, boardID, moderator, "moderator") + + // Update flagging threshold to two flags + boards2.SetFlaggingThreshold(cross, boardID, 2) + + // Add first flag + testing.SetRealm(testing.NewUserRealm(admin)) + boards2.FlagReply(cross, boardID, threadID, commentID, "Reason 1") + + // Add second flag + testing.SetRealm(testing.NewUserRealm(moderator)) + boards2.FlagReply(cross, boardID, threadID, commentID, "Reason 2") + + // Get readonly comment + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comment, _ = hub.GetComment(uint64(boardID), uint64(threadID), uint64(commentID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + flags := hub.GetFlags(comment.BoardID, comment.ThreadID, comment.ID, 0, comment.FlagCount) + + for _, f := range flags { + println(f.User, f.Reason) + } +} + +// Output: +// g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 Reason 1 +// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj Reason 2 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_7_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_7_a_filetest.gno new file mode 100644 index 00000000000..a7f75abe930 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_7_a_filetest.gno @@ -0,0 +1,30 @@ +// Test getting comments from a thread without comments +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var thread hub.Thread + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "test123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + + // Get readonly thread + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, _ = hub.GetThread(uint64(boardID), uint64(threadID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comments := hub.GetComments(thread.BoardID, thread.ID, 0, thread.CommentCount) + println(len(comments)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_7_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_7_b_filetest.gno new file mode 100644 index 00000000000..b0b4cde29ce --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_7_b_filetest.gno @@ -0,0 +1,41 @@ +// Test getting comments from a thread +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var thread hub.Thread + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "test123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + commentID := boards2.CreateReply(cross, boardID, threadID, 0, "Comment 1") + + // Create another comment + boards2.CreateReply(cross, boardID, threadID, 0, "Comment 2") + + // Add a reply (it should not be included in the output) + boards2.CreateReply(cross, boardID, threadID, commentID, "Reply") + + // Get readonly thread + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + thread, _ = hub.GetThread(uint64(boardID), uint64(threadID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comments := hub.GetComments(thread.BoardID, thread.ID, 0, thread.CommentCount) + + for _, c := range comments { + println(c.ID, c.Body) + } +} + +// Output: +// 2 Comment 1 +// 3 Comment 2 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_8_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_8_a_filetest.gno new file mode 100644 index 00000000000..af415c7468a --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_8_a_filetest.gno @@ -0,0 +1,31 @@ +// Test getting replies from a comment without replies +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var comment hub.Comment + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "test123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + commentID := boards2.CreateReply(cross, boardID, threadID, 0, "Comment") + + // Get readonly comment + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comment, _ = hub.GetComment(uint64(boardID), uint64(threadID), uint64(commentID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + replies := hub.GetReplies(comment.BoardID, comment.ThreadID, comment.ID, 0, comment.ReplyCount) + println(len(replies)) +} + +// Output: +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_8_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_8_b_filetest.gno new file mode 100644 index 00000000000..962037cf9dc --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_8_b_filetest.gno @@ -0,0 +1,42 @@ +// Test getting replies from a comment +package main + +import ( + "testing" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var comment hub.Comment + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID := boards2.CreateBoard(cross, "test123", false, false) + threadID := boards2.CreateThread(cross, boardID, "Title", "Body") + commentID := boards2.CreateReply(cross, boardID, threadID, 0, "Comment") + + // Create replies + boards2.CreateReply(cross, boardID, threadID, commentID, "Reply 1") + subCommentID := boards2.CreateReply(cross, boardID, threadID, commentID, "Reply 2") + + // Add a sub-reply (it should not be included in the output) + boards2.CreateReply(cross, boardID, threadID, subCommentID, "Reply 3") + + // Get readonly comment + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comment, _ = hub.GetComment(uint64(boardID), uint64(threadID), uint64(commentID)) +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + replies := hub.GetReplies(comment.BoardID, comment.ThreadID, comment.ID, 0, comment.ReplyCount) + + for _, r := range replies { + println(r.ID, r.Body) + } +} + +// Output: +// 3 Reply 1 +// 4 Reply 2 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_9_a_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_9_a_filetest.gno new file mode 100644 index 00000000000..e2ecee6e510 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_9_a_filetest.gno @@ -0,0 +1,57 @@ +// Test default thread comment values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID + commentID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "origin123", false, false) + threadID = boards2.CreateThread(cross, boardID, "Title", "Body") + commentID = boards2.CreateReply(cross, boardID, threadID, 0, "Comment") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comment, found := hub.GetComment(uint64(boardID), uint64(threadID), uint64(commentID)) + if !found { + return + } + + println(comment.ID) + println(comment.BoardID) + println(comment.ThreadID) + println(comment.ParentID) + println(comment.Body) + println(comment.Hidden) + println(comment.ReplyCount) + println(comment.FlagCount) + println(comment.Creator) + println(comment.CreatedAt) + println(comment.UpdatedAt) +} + +// Output: +// 2 +// 1 +// 1 +// 1 +// Comment +// false +// 0 +// 0 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 0 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_9_b_filetest.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_9_b_filetest.gno new file mode 100644 index 00000000000..d60dc4c8ccd --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/filetests/z_9_b_filetest.gno @@ -0,0 +1,66 @@ +// Test non default thread comment values +package main + +import ( + "testing" + + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" + "gno.land/r/gnoland/boards2/v1/hub" +) + +var ( + boardID boards.ID + threadID boards.ID + commentID boards.ID +) + +func init() { + testing.SetRealm(testing.NewUserRealm("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")) + boardID = boards2.CreateBoard(cross, "origin123", false, false) + threadID = boards2.CreateThread(cross, boardID, "Title", "Body") + commentID = boards2.CreateReply(cross, boardID, threadID, 0, "Comment") + + // Edit comment + boards2.EditReply(cross, boardID, threadID, commentID, "Test comment") + + // Create a comment reply + boards2.CreateReply(cross, boardID, threadID, commentID, "Reply") + + // Flag comment + boards2.FlagReply(cross, boardID, threadID, commentID, "Reason") +} + +func main() { + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/boards2/test")) + comment, found := hub.GetComment(uint64(boardID), uint64(threadID), uint64(commentID)) + if !found { + return + } + + println(comment.ID) + println(comment.BoardID) + println(comment.ThreadID) + println(comment.ParentID) + println(comment.Body) + println(comment.Hidden) + println(comment.ReplyCount) + println(comment.FlagCount) + println(comment.Creator) + println(comment.CreatedAt) + println(comment.UpdatedAt) +} + +// Output: +// 2 +// 1 +// 1 +// 1 +// Test comment +// true +// 1 +// 1 +// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh +// 1234567890 +// 1234567890 diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/format.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/format.gno new file mode 100644 index 00000000000..5a079b1980d --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/format.gno @@ -0,0 +1,11 @@ +package hub + +import "time" + +// timeToUnix converts time to Unix epoch. +func timeToUnix(t time.Time) int64 { + if t.IsZero() { + return 0 + } + return t.Unix() +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/gnomod.toml b/examples/gno.land/r/gnoland/boards2/v1/hub/gnomod.toml new file mode 100644 index 00000000000..68de66e6e3e --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoland/boards2/v1/hub" +gno = "0.9" diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/hub.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/hub.gno new file mode 100644 index 00000000000..2a3e4f0aea8 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/hub.gno @@ -0,0 +1,195 @@ +package hub + +import ( + "gno.land/p/gnoland/boards" + + boards2 "gno.land/r/gnoland/boards2/v1" +) + +// GetBoard returns a safe board. +func GetBoard(id uint64) (Board, bool) { + b, found := getBoard(id) + if !found { + return Board{}, false + } + return NewSafeBoard(b), true +} + +// GetThread returns a safe board thread. +func GetThread(boardID, threadID uint64) (Thread, bool) { + t, found := getThread(boardID, threadID) + if !found { + return Thread{}, false + } + return NewSafeThread(t), true +} + +// GetComment returns a safe thread comment. +func GetComment(boardID, threadID, commentID uint64) (Comment, bool) { + c, found := getComment(boardID, threadID, commentID) + if !found { + return Comment{}, false + } + return NewSafeComment(c), true +} + +// GetBoards returns a list with all boards. +// To reverse iterate use a negative count. +func GetBoards(start, count int) []Board { + var boards_ []Board + + // Cross into current hub realm to be able to call protected `Iterate()` fucntion + func(realm) { + boards2.Iterate(start, count, func(b *boards.Board) bool { + boards_ = append(boards_, NewSafeBoard(b)) + return false + }) + }(cross) + + return boards_ +} + +// GetThreads returns a list with threads of a board. +// To reverse iterate use a negative count. +func GetThreads(boardID uint64, start, count int) []Thread { + b, found := getBoard(boardID) + if !found { + return nil + } + + var threads []Thread + b.Threads.Iterate(start, count, func(thread *boards.Post) bool { + threads = append(threads, NewSafeThread(thread)) + return false + }) + return threads +} + +// GetMembers returns a list with the members of a board. +// To reverse iterate use a negative count. +func GetMembers(boardID uint64, start, count int) []boards.User { + b, found := getBoard(boardID) + if !found { + return nil + } + + var members []boards.User + b.Permissions.IterateUsers(start, count, func(u boards.User) bool { + members = append(members, u) + return false + }) + return members +} + +// GetReposts returns a list with repost of a board thread. +// To reverse iterate use a negative count. +func GetReposts(boardID, threadID uint64, start, count int) []Thread { + t, found := getThread(boardID, threadID) + if !found { + return nil + } + + var reposts []Thread + t.Reposts.Iterate(start, count, func(boardID, repostID boards.ID) bool { + r, found := GetThread(uint64(boardID), uint64(repostID)) + if found { + reposts = append(reposts, r) + } + return false + }) + return reposts +} + +// GetFlag returns a list with thread or comment moderation flags. +// To reverse iterate use a negative count. +// Thread flags are returned when `commentID` is zero, or comment flags are returned otherwise. +func GetFlags(boardID, threadID, commentID uint64, start, count int) []boards.Flag { + var storage boards.FlagStorage + if commentID == 0 { + t, found := getThread(boardID, threadID) + if !found { + return nil + } + + storage = t.Flags + } else { + c, found := getComment(boardID, threadID, commentID) + if !found { + return nil + } + + storage = c.Flags + } + + var flags []boards.Flag + storage.Iterate(start, count, func(f boards.Flag) bool { + flags = append(flags, f) + return false + }) + return flags +} + +// GetComments returns a list with all thread comments and replies. +// To reverse iterate use a negative count. +// Top level comments can be filtered by checking `Comment.ParentID`, replies +// always have a parent comment or reply, while comments have no parent. +func GetComments(boardID, threadID uint64, start, count int) []Comment { + t, found := getThread(boardID, threadID) + if !found { + return nil + } + + var comments []Comment + t.Replies.Iterate(start, count, func(comment *boards.Post) bool { + comments = append(comments, NewSafeComment(comment)) + return false + }) + return comments +} + +// GetReplies returns a list with top level comment replies. +// To reverse iterate use a negative count. +func GetReplies(boardID, threadID, commentID uint64, start, count int) []Comment { + c, found := getComment(boardID, threadID, commentID) + if !found { + return nil + } + + var replies []Comment + c.Replies.Iterate(start, count, func(comment *boards.Post) bool { + replies = append(replies, NewSafeComment(comment)) + return false + }) + return replies +} + +func getBoard(boardID uint64) (*boards.Board, bool) { + // Cross into current hub realm to be able to call protected `GetBoard()` fucntion + return func(realm) (*boards.Board, bool) { + return boards2.GetBoard(boards.ID(boardID)) + }(cross) +} + +func getThread(boardID, threadID uint64) (*boards.Post, bool) { + b, found := getBoard(boardID) + if !found { + return nil, false + } + + t, found := b.Threads.Get(boards.ID(threadID)) + if !found { + // When thread is not found search it within hidden threads + meta := b.Meta.(*boards2.BoardMeta) + t, found = meta.HiddenThreads.Get(boards.ID(threadID)) + } + return t, found +} + +func getComment(boardID, threadID, commentID uint64) (*boards.Post, bool) { + t, found := getThread(boardID, threadID) + if !found { + return nil, false + } + + return t.Replies.Get(boards.ID(commentID)) +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/thread.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/thread.gno new file mode 100644 index 00000000000..7a7949d70a8 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/thread.gno @@ -0,0 +1,109 @@ +package hub + +import ( + "gno.land/p/gnoland/boards" +) + +// Flag defines a type for thread and comment flags. +type Flag struct { + // User is the user that flagged. + User address + + // Reason is the reason for flagging. + Reason string +} + +// Thread defines a type for board threads. +type Thread struct { + ref *boards.Post + + // ID is the unique identifier of the thread. + ID uint64 + + // OriginalBoardID contains the board ID of the original thread when current is a repost. + OriginalBoardID uint64 + + // OriginalThreadID contains the ID of the original thread when current is a repost. + OriginalThreadID uint64 + + // BoardID is the board ID where thread is created. + BoardID uint64 + + // Title contains thread's title. + Title string + + // Body contains content of the thread. + Body string + + // Hidden indicates that thread is hidden. + Hidden bool + + // Readonly indicates that thread is readonly. + Readonly bool + + // CommentCount contains the number of thread comments. + // Count only includes top level comment, replies are not included. + CommentCount int + + // RepostCount contains the number of times thread has been reposted. + RepostCount int + + // FlagCount contains the number of flags that thread has. + FlagCount int + + // Creator is the account address that created the thread. + Creator address + + // CreatedAt is thread's creation time as Unix time. + CreatedAt int64 + + // UpdatedAt is thread's update time as unix time. + UpdatedAt int64 +} + +// IterateFlags iterates thread moderation flags. +// To reverse iterate use a negative count. +func (t Thread) IterateFlags(start, count int, fn func(boards.Flag) bool) bool { + return t.ref.Flags.Iterate(start, count, fn) +} + +// IterateReposts iterates thread reposts. +// To reverse iterate use a negative count. +func (t Thread) IterateReposts(start, count int, fn func(boardID, repostThreadID uint64) bool) bool { + return t.ref.Reposts.Iterate(start, count, func(boardID, repostThreadID boards.ID) bool { + return fn(uint64(boardID), uint64(repostThreadID)) + }) +} + +// IterateComments iterates thread comments. +// To reverse iterate use a negative count. +func (t Thread) IterateComments(start, count int, fn func(Comment) bool) bool { + return t.ref.Replies.Iterate(start, count, func(comment *boards.Post) bool { + return fn(NewSafeComment(comment)) + }) +} + +// NewSafeThread creates a safe thread. +func NewSafeThread(ref *boards.Post) Thread { + if !boards.IsThread(ref) { + panic("post is not a thread") + } + + return Thread{ + ref: ref, + ID: uint64(ref.ID), + OriginalBoardID: uint64(ref.OriginalBoardID), + OriginalThreadID: uint64(ref.ParentID), + BoardID: uint64(ref.Board.ID), + Title: ref.Title, + Body: ref.Body, + Hidden: ref.Hidden, + Readonly: ref.Readonly, + CommentCount: ref.Replies.Size(), + RepostCount: ref.Reposts.Size(), + FlagCount: ref.Flags.Size(), + Creator: ref.Creator, + CreatedAt: timeToUnix(ref.CreatedAt), + UpdatedAt: timeToUnix(ref.UpdatedAt), + } +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/hub/thread_test.gno b/examples/gno.land/r/gnoland/boards2/v1/hub/thread_test.gno new file mode 100644 index 00000000000..ff96acd2fd5 --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/hub/thread_test.gno @@ -0,0 +1,175 @@ +package hub_test + +import ( + "testing" + + "gno.land/p/gnoland/boards" + "gno.land/p/nt/urequire/v0" + + "gno.land/r/gnoland/boards2/v1/hub" +) + +func TestThreadIterateFlags(t *testing.T) { + tests := []struct { + name string + flags []boards.Flag + }{ + { + name: "no flags", + }, + { + name: "one flag", + flags: []boards.Flag{ + {User: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Reason: "Reason 1"}, + }, + }, + { + name: "multiple flags", + flags: []boards.Flag{ + {User: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Reason: "Reason 1"}, + {User: "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh", Reason: "Reason 2"}, + {User: "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", Reason: "Reason 3"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var i int + ref := createComment(t, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Comment") + for _, f := range tt.flags { + ref.Flags.Add(f) + } + + comment := hub.NewSafeComment(ref) + + urequire.Equal(t, len(tt.flags), comment.FlagCount, "expect number of flags to match") + comment.IterateFlags(0, comment.FlagCount, func(f boards.Flag) bool { + urequire.Equal(t, tt.flags[i].User, f.User, "expect user to match") + urequire.Equal(t, tt.flags[i].Reason, f.Reason, "expect reason to match") + i++ + return false + }) + }) + } +} + +func TestThreadIterateReposts(t *testing.T) { + tests := []struct { + name string + setup func(*boards.Post) + expected [][2]uint64 // {boardID, repostThreadID} + }{ + { + name: "no reposts", + }, + { + name: "one repost", + setup: func(thread *boards.Post) { + r := boards.MustNewRepost(thread, boards.New(1), "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + thread.Reposts.Add(r) + }, + expected: [][2]uint64{ + {1, 1}, + }, + }, + { + name: "multiple reposts", + setup: func(thread *boards.Post) { + reposts := []*boards.Post{ + boards.MustNewRepost(thread, boards.New(1), "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + boards.MustNewRepost(thread, boards.New(2), "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh"), + boards.MustNewRepost(thread, boards.New(5), "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"), + } + for _, r := range reposts { + thread.Reposts.Add(r) + } + }, + expected: [][2]uint64{ + {1, 1}, + {2, 1}, + {5, 1}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var i int + ref := createThread(t, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Title", "Body") + if tt.setup != nil { + tt.setup(ref) + } + + thread := hub.NewSafeThread(ref) + + urequire.Equal(t, ref.Reposts.Size(), thread.RepostCount, "expect number of reposts to match") + thread.IterateReposts(0, thread.RepostCount, func(boardID, repostThreadID uint64) bool { + urequire.Equal(t, tt.expected[i][0], boardID, "expect repost board ID to match") + urequire.Equal(t, tt.expected[i][1], repostThreadID, "expect repost thread ID to match") + i++ + return false + }) + }) + } +} + +func TestThreadIterateComments(t *testing.T) { + tests := []struct { + name string + setup func(*boards.Post) + }{ + { + name: "no comments", + }, + { + name: "one comment", + setup: func(thread *boards.Post) { + r := boards.MustNewReply(thread, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Body 1") + thread.Replies.Add(r) + }, + }, + { + name: "multiple comments", + setup: func(thread *boards.Post) { + replies := []*boards.Post{ + boards.MustNewReply(thread, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Body 1"), + boards.MustNewReply(thread, "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh", "Body 2"), + boards.MustNewReply(thread, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", "Body 3"), + } + for _, t := range replies { + thread.Replies.Add(t) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref := createThread(t, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "Title", "Body") + if tt.setup != nil { + tt.setup(ref) + } + + thread := hub.NewSafeThread(ref) + + urequire.Equal(t, ref.Replies.Size(), thread.CommentCount, "expect number of replies to match") + thread.IterateComments(0, thread.CommentCount, func(comment hub.Comment) bool { + id := boards.ID(comment.ID) + expected, found := ref.Replies.Get(id) + + urequire.True(t, found, "expect comment to be found") + urequire.Equal(t, expected.Creator, comment.Creator, "expect creator to match") + urequire.Equal(t, expected.Body, comment.Body, "expect body to match") + return false + }) + }) + } +} + +func createThread(t *testing.T, user address, title, body string) *boards.Post { + t.Helper() + + board := boards.New(1) + return boards.MustNewThread(board, user, title, body) +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/permissions.gno b/examples/gno.land/r/gnoland/boards2/v1/permissions.gno index f1a63e9935f..c258c6df2ec 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/permissions.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/permissions.gno @@ -5,6 +5,7 @@ import ( "gno.land/p/gnoland/boards/exts/permissions" ) +// List of Boards2 member roles. const ( RoleOwner boards.Role = "owner" RoleAdmin = "admin" @@ -12,30 +13,45 @@ const ( RoleGuest = "guest" ) +// PermissionCustom defines an initial value for custom board permissions. +// When a board defines custom permissions it must starts from a value +// greater or equal than PermissionCustom. +// +// Custom permissions definition example: +// +// const ( +// PermissionCustom1 boards.Permission = iota + boards2.PermissionCustom +// PermissionCustom2 +// PermissionCustom3 +// ) +const PermissionCustom boards.Permission = 200 + +// List of Boards2 permissions. const ( - PermissionBoardCreate boards.Permission = "board:create" - PermissionBoardFlaggingUpdate = "board:flagging-update" - PermissionBoardFreeze = "board:freeze" - PermissionBoardRename = "board:rename" - PermissionMemberInvite = "member:invite" - PermissionMemberInviteRevoke = "member:invite-remove" - PermissionMemberRemove = "member:remove" - PermissionPermissionsUpdate = "permissions:update" - PermissionRealmHelp = "realm:help" - PermissionRealmLock = "realm:lock" - PermissionRealmNotice = "realm:notice" - PermissionReplyCreate = "reply:create" - PermissionReplyDelete = "reply:delete" - PermissionReplyFlag = "reply:flag" - PermissionRoleChange = "role:change" - PermissionThreadCreate = "thread:create" - PermissionThreadDelete = "thread:delete" - PermissionThreadEdit = "thread:edit" - PermissionThreadFlag = "thread:flag" - PermissionThreadFreeze = "thread:freeze" - PermissionThreadRepost = "thread:repost" - PermissionUserBan = "user:ban" - PermissionUserUnban = "user:unban" + PermissionBoardCreate boards.Permission = iota + PermissionBoardFlaggingUpdate + PermissionBoardFreeze + PermissionBoardRename + PermissionMemberInvite + PermissionMemberInviteRevoke + PermissionMemberRemove + PermissionPermissionsUpdate + PermissionRealmHelpChange + PermissionRealmLock + PermissionRealmNotice + PermissionAccountRequiredAmountChange + PermissionReplyCreate + PermissionReplyDelete + PermissionReplyFlag + PermissionRoleChange + PermissionThreadCreate + PermissionThreadDelete + PermissionThreadEdit + PermissionThreadFlag + PermissionThreadFreeze + PermissionThreadRepost + PermissionUserBan + PermissionUserUnban ) func createBasicBoardPermissions(owner address) *permissions.Permissions { @@ -94,6 +110,7 @@ func createOpenBoardPermissions(owner address) *permissions.Permissions { ) perms.SetPublicPermissions( PermissionThreadCreate, + PermissionThreadRepost, PermissionReplyCreate, ) perms.AddRole( diff --git a/examples/gno.land/r/gnoland/boards2/v1/permissions_validators_open.gno b/examples/gno.land/r/gnoland/boards2/v1/permissions_validators_open.gno index f9d1fe16c4e..03da6e64627 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/permissions_validators_open.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/permissions_validators_open.gno @@ -120,7 +120,7 @@ func validateOpenThreadCreate(perms boards.Permissions, args boards.Args) error } // Require non members to have some GNOT in their accounts - if err := checkAccountHasAmount(caller, gOpenAccountAmount); err != nil { + if err := checkAccountHasAmount(caller, RequiredAccountAmount); err != nil { return ufmt.Errorf("caller is not allowed to create threads: %s", err) } return nil @@ -147,7 +147,7 @@ func validateOpenReplyCreate(perms boards.Permissions, args boards.Args) error { } // Require non members to have some GNOT in their accounts - if err := checkAccountHasAmount(caller, gOpenAccountAmount); err != nil { + if err := checkAccountHasAmount(caller, RequiredAccountAmount); err != nil { return ufmt.Errorf("caller is not allowed to comment: %s", err) } return nil @@ -156,7 +156,7 @@ func validateOpenReplyCreate(perms boards.Permissions, args boards.Args) error { func checkAccountHasAmount(addr address, amount int64) error { bnk := banker.NewBanker(banker.BankerTypeReadonly) coins := bnk.GetCoins(addr) - if coins.AmountOf("ugnot") < gOpenAccountAmount { + if coins.AmountOf("ugnot") < RequiredAccountAmount { amount = amount / 1_000_000 // ugnot -> GNOT return ufmt.Errorf("account amount is lower than %d GNOT", amount) } diff --git a/examples/gno.land/r/gnoland/boards2/v1/protected.gno b/examples/gno.land/r/gnoland/boards2/v1/protected.gno new file mode 100644 index 00000000000..1689b714edf --- /dev/null +++ b/examples/gno.land/r/gnoland/boards2/v1/protected.gno @@ -0,0 +1,56 @@ +// The `protected.gno` file contains public realm functions that must only be called +// from realms that live within the Boards2 package namespace. This allows sub realms +// to access boards data to be able to migrate content from one version to another or +// to implement specific features in separate sub realms. +package boards2 + +import ( + "chain/runtime" + "path" + "strings" + + "gno.land/p/gnoland/boards" +) + +// TODO: Authorize sub realms to be able to call protected functions (use DAOs) + +// Keeps the package path to Boards2 realm. +// It ignores the last realm path element which is the current version. +var boardsNS = path.Dir(runtime.CurrentRealm().PkgPath()) + "/" + +// GetRealmPermissions returns Boards2 realm permissions. +// This is a protected function only callable by Boards2 sub realms. +func GetRealmPermissions() boards.Permissions { + assertRealmHasBoardsNS(runtime.CurrentRealm()) + return gPerms +} + +// GetBoard returns a board. +// This is a protected function only callable by Boards2 sub realms. +func GetBoard(boardID boards.ID) (_ *boards.Board, found bool) { + assertRealmHasBoardsNS(runtime.CurrentRealm()) + return gBoards.Get(boardID) +} + +// MustGetBoard returns a board or panics on error. +// This is a protected function only callable by Boards2 sub realms. +func MustGetBoard(boardID boards.ID) *boards.Board { + assertRealmHasBoardsNS(runtime.CurrentRealm()) + return mustGetBoard(boardID) +} + +// Iterate iterates boards. +// Iteration is done for all boards, including the ones that are not listed. +// To reverse iterate boards use a negative count. +// If the callback returns true, iteration is stopped. +func Iterate(start, count int, fn boards.BoardIterFn) bool { + assertRealmHasBoardsNS(runtime.CurrentRealm()) + return gBoards.Iterate(start, count, fn) +} + +// assertRealmHasBoardsNS asserts that a realm lives within Boards2 namespace. +func assertRealmHasBoardsNS(r runtime.Realm) { + if !strings.HasPrefix(r.PkgPath(), boardsNS) { + panic("forbidden, caller should live within \"" + boardsNS + "\" namespace") + } +} diff --git a/examples/gno.land/r/gnoland/boards2/v1/public.gno b/examples/gno.land/r/gnoland/boards2/v1/public.gno index 7fdae0984d4..e1da793f7cb 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/public.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/public.gno @@ -36,8 +36,29 @@ func SetHelp(_ realm, content string) { content = strings.TrimSpace(content) caller := runtime.PreviousRealm().Address() args := boards.Args{content} - gPerms.WithPermission(caller, PermissionRealmHelp, args, crossingFn(func() { - gHelp = content + gPerms.WithPermission(caller, PermissionRealmHelpChange, args, crossingFn(func() { + Help = content + })) +} + +// SetRequiredAccountAmount sets the required account amount to interact as a non member with open boards. +// Amount must be given as ugnot. +// The amount requirement is not applied to members that were invited to an open board. +func SetRequiredAccountAmount(_ realm, amount int64) { + if amount < 0 { + panic("invalid amount") + } + + caller := runtime.PreviousRealm().Address() + args := boards.Args{amount} + gPerms.WithPermission(caller, PermissionAccountRequiredAmountChange, args, crossingFn(func() { + RequiredAccountAmount = amount + + chain.Emit( + "RequiredAccountAmountChanged", + "caller", caller.String(), + "amount", strconv.FormatInt(amount, 10), + ) })) } @@ -60,7 +81,7 @@ func SetPermissions(_ realm, boardID boards.ID, p boards.Permissions) { gPerms = p chain.Emit( - "RealmPermissionsUpdated", + "RealmPermissionsChanged", "caller", caller.String(), ) return @@ -71,21 +92,21 @@ func SetPermissions(_ realm, boardID boards.ID, p boards.Permissions) { board.Permissions = p chain.Emit( - "BoardPermissionsUpdated", + "BoardPermissionsChanged", "caller", caller.String(), "boardID", board.ID.String(), ) })) } -// SetRealmNotice sets a notice to be displayed globally by the realm. +// SetRealmNotice sets a notice to be displayed globally within the realm. // An empty message removes the realm notice. func SetRealmNotice(_ realm, message string) { message = strings.TrimSpace(message) caller := runtime.PreviousRealm().Address() args := boards.Args{message} gPerms.WithPermission(caller, PermissionRealmNotice, args, crossingFn(func() { - gNotice = message + Notice = message chain.Emit( "RealmNoticeChanged", @@ -104,6 +125,11 @@ func GetBoardIDFromName(_ realm, name string) (_ boards.ID, found bool) { return board.ID, true } +// BoardCount returns the total number of boards. +func BoardCount() int { + return gBoards.Size() +} + // CreateBoard creates a new board. // // Listed boards are included in the realm's list of boards. @@ -176,6 +202,7 @@ func RenameBoard(_ realm, name, newName string) { board.Aliases = append(board.Aliases, board.Name) board.Name = newName + board.UpdatedAt = time.Now() // Index board for the new name keeping previous indexes for older names gBoards.Add(board) @@ -309,6 +336,9 @@ func CreateRepost(_ realm, boardID, threadID, destinationBoardID boards.ID, titl repost.Title = title repost.Body = strings.TrimSpace(body) + repost.Meta = &ThreadMeta{ + AllReplies: boards.NewPostStorage(), + } if err := dst.Threads.Add(repost); err != nil { panic(err) @@ -624,22 +654,6 @@ func ChangeMemberRole(_ realm, boardID boards.ID, member address, role boards.Ro })) } -// IterateRealmMembers iterates boards realm members. -// The iteration is done only for realm members, board members are not iterated. -func IterateRealmMembers(offset int, fn boards.UsersIterFn) (halted bool) { - count := gPerms.UsersCount() - offset - return gPerms.IterateUsers(offset, count, fn) -} - -// GetBoard returns a single board. -func GetBoard(boardID boards.ID) *boards.Board { - board := mustGetBoard(boardID) - if !board.Permissions.HasRole(runtime.OriginCaller(), RoleOwner) { - panic("forbidden") - } - return board -} - // Wraps a function to cross back to Boards2 realm. func crossingFn(fn func()) func() { return func() { @@ -659,12 +673,6 @@ func assertUserAddressIsValid(user address) { } } -func assertHasPermission(perms boards.Permissions, user address, p boards.Permission) { - if !perms.HasPermission(user, p) { - panic("unauthorized") - } -} - func assertBoardExists(id boards.ID) { if id == 0 { // ID zero is used to refer to the realm return @@ -772,6 +780,10 @@ func assertReplyBodyIsValid(body string) { if reDeniedReplyLinePrefixes.MatchString(body) { panic("using Markdown headings, blockquotes or horizontal lines is not allowed in replies") } + + if strings.Contains(body, "gno-form") { + panic("Gno-Flavored Markdown forms are not allowed in replies") + } } func assertMembersUpdateIsEnabled(boardID boards.ID) { diff --git a/examples/gno.land/r/gnoland/boards2/v1/render.gno b/examples/gno.land/r/gnoland/boards2/v1/render.gno index 2a78a56c549..0c323a17cfd 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/render.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/render.gno @@ -61,8 +61,8 @@ func Render(path string) string { } // Render common realm header before resolving render path - if gNotice != "" { - b.WriteString(infoAlert("Notice", gNotice)) + if Notice != "" { + b.WriteString(infoAlert("Notice", Notice)) } // Render view for current path @@ -73,12 +73,12 @@ func Render(path string) string { func renderHelp(res *mux.ResponseWriter, _ *mux.Request) { res.Write(md.H1("Boards Help")) - if gHelp != "" { - res.Write(gHelp) + if Help != "" { + res.Write(Help) return } - link := gRealmLink.Call("SetHelp", "content", "") + link := RealmLink.Call("SetHelp", "content", "") res.Write(md.H3("Help content has not been uploaded")) res.Write("Do you want to " + md.Link("upload boards help", link) + "?") } @@ -224,12 +224,12 @@ func renderMembers(res *mux.ResponseWriter, req *mux.Request) { perms.IterateUsers(p.Offset(), p.PageSize(), func(u boards.User) bool { actions := []string{ - md.Link("remove", gRealmLink.Call( + md.Link("remove", RealmLink.Call( "RemoveMember", "boardID", boardID.String(), "member", u.Address.String(), )), - md.Link("change role", gRealmLink.Call( + md.Link("change role", RealmLink.Call( "ChangeMemberRole", "boardID", boardID.String(), "member", u.Address.String(), @@ -280,12 +280,12 @@ func renderInvites(res *mux.ResponseWriter, req *mux.Request) { res.Write(md.H3("These users have requested to be invited to the board")) requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool { actions := []string{ - md.Link("accept", gRealmLink.Call( + md.Link("accept", RealmLink.Call( "AcceptInvite", "boardID", board.ID.String(), "user", addr, )), - md.Link("revoke", gRealmLink.Call( + md.Link("revoke", RealmLink.Call( "RevokeInvite", "boardID", board.ID.String(), "user", addr, @@ -338,7 +338,7 @@ func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) { table.Append([]string{ userLink(address(addr)), v.(time.Time).Format(dateFormat), - md.Link("unban", gRealmLink.Call( + md.Link("unban", RealmLink.Call( "Unban", "boardID", board.ID.String(), "user", addr, diff --git a/examples/gno.land/r/gnoland/boards2/v1/render_post.gno b/examples/gno.land/r/gnoland/boards2/v1/render_post.gno index 942a1e826fc..b4870ed17c2 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/render_post.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/render_post.gno @@ -28,7 +28,7 @@ func renderPost(post *boards.Post, path, indent string, levels int) string { } if title != "" { // Replies don't have a title - b.WriteString(md.H2(title)) + b.WriteString(md.H2(md.EscapeText(title))) } b.WriteString(indent + "\n") diff --git a/examples/gno.land/r/gnoland/boards2/v1/uris_board.gno b/examples/gno.land/r/gnoland/boards2/v1/uris_board.gno index ba66f5c3292..7fb8c2f6c0e 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/uris_board.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/uris_board.gno @@ -8,19 +8,19 @@ import ( ) func makeBoardURI(b *boards.Board) string { - path := strings.TrimPrefix(string(gRealmLink), "gno.land") + path := strings.TrimPrefix(string(RealmLink), "gno.land") return path + ":" + url.PathEscape(b.Name) } func makeFreezeBoardURI(b *boards.Board) string { - return gRealmLink.Call( + return RealmLink.Call( "FreezeBoard", "boardID", b.ID.String(), ) } func makeUnfreezeBoardURI(b *boards.Board) string { - return gRealmLink.Call( + return RealmLink.Call( "UnfreezeBoard", "boardID", b.ID.String(), "threadID", "", @@ -37,7 +37,7 @@ func makeCreateThreadURI(b *boards.Board) string { } func makeRequestInviteURI(b *boards.Board) string { - return gRealmLink.Call( + return RealmLink.Call( "RequestInvite", "boardID", b.ID.String(), ) diff --git a/examples/gno.land/r/gnoland/boards2/v1/uris_post.gno b/examples/gno.land/r/gnoland/boards2/v1/uris_post.gno index ac1ad5e8958..ed011e77b77 100644 --- a/examples/gno.land/r/gnoland/boards2/v1/uris_post.gno +++ b/examples/gno.land/r/gnoland/boards2/v1/uris_post.gno @@ -30,13 +30,13 @@ func makeCreateRepostURI(p *boards.Post) string { func makeDeletePostURI(p *boards.Post) string { if boards.IsThread(p) { - return gRealmLink.Call( + return RealmLink.Call( "DeleteThread", "boardID", p.Board.ID.String(), "threadID", p.ThreadID.String(), ) } - return gRealmLink.Call( + return RealmLink.Call( "DeleteReply", "boardID", p.Board.ID.String(), "threadID", p.ThreadID.String(), diff --git a/examples/gno.land/r/gnoland/home/filetests/home_filetest.gno b/examples/gno.land/r/gnoland/home/filetests/home_filetest.gno index fec7ea768cd..8f91653e1bc 100644 --- a/examples/gno.land/r/gnoland/home/filetests/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/filetests/home_filetest.gno @@ -9,18 +9,29 @@ func main() { // Output: // # Welcome to Gno.land // -// We’re building Gno.land, set to become the leading open-source smart contract +// We're building Gno.land, set to become the leading open-source smart contract // platform, using Gno, an interpreted and fully deterministic variation of the // Go programming language for succinct and composable smart contracts. // // With transparent and timeless code, Gno.land is the next generation of smart -// contract platforms, serving as the “GitHub” of the ecosystem, with realms built +// contract platforms, serving as the "GitHub" of the ecosystem, with realms built // using fully transparent, auditable code that anyone can inspect and reuse. // // Intuitive and easy to use, Gno.land lowers the barrier to web3 and makes // censorship-resistant platforms accessible to everyone. If you want to help lay // the foundations of a fairer and freer world, join us today. // +// --- +// +// ## [Boards](/r/gnoland/boards2/v1) - On-chain forum for the Gno.land community +// +// **Post, discuss, and create your content community**: Boards is a fully on-chain social forum to create Boards topics, post threads, comment and reply. A plug-and-deploy DAO lets communities manage content, permissions and moderation their way. +// +// Explore this ready-to-use Gno dApp, and experience decentralized social media in action. +// +// **[Open Boards](/r/gnoland/boards2/v1)** +// +// --- // // // ## Learn about Gno.land @@ -40,7 +51,6 @@ func main() { // - [Write Gno in the browser](https://play.gno.land) // - [Read about the Gno Language](/gnolang) // - [Visit the official documentation](https://docs.gno.land) -// - [Gno by Example](/r/docs/home) // - [Efficient local development for Gno](https://docs.gno.land/builders/local-dev-with-gnodev) // - [Get testnet GNOTs](https://faucet.gno.land) // @@ -50,10 +60,10 @@ func main() { // // - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) // - [Gnoscan](https://gnoscan.io) -// - [Staging chain](https://docs.gno.land/resources/gnoland-networks/#staging-environments-portal-loops) -// - [Testnet 11](https://test11.testnets.gno.land/) +// - [Gno networks documentation](https://docs.gno.land/resources/gnoland-networks/) +// - [Staging](https://staging.gno.land/) +// - [Testnet 12](https://test12.testnets.gno.land/) // - [Faucet Hub](https://faucet.gno.land) -// - [Boards](https://gno.land/r/gnoland/boards2/v1:OpenDiscussions): community forum // // // @@ -65,18 +75,10 @@ func main() { // // ||| // -// ## [Latest Events](/r/devrels/events) +// ## [Latest Events](/events) // // No events. // -// ||| -// -// ## [Hall of Realms](/r/leon/hor) -// -// No items in the Hall of Realms. -// -// -// // // // --- diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index 586dfb31da3..d8c9693f0b0 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -4,13 +4,11 @@ import ( "chain/runtime" "strconv" + "gno.land/p/leon/svgbtn" "gno.land/p/moul/dynreplacer" "gno.land/p/nt/ownable/v0" - blog "gno.land/r/gnoland/blog" - - "gno.land/p/leon/svgbtn" "gno.land/r/devrels/events" - "gno.land/r/leon/hor" + blog "gno.land/r/gnoland/blog" ) var ( @@ -27,9 +25,6 @@ func Render(_ string) string { out, _ := events.RenderEventWidget(events.MaxWidgetSize) return out }) - r.RegisterCallback(":latest-hor:", func() string { - return hor.RenderExhibWidget(5) - }) r.RegisterCallback(":qotb:", quoteOfTheBlock) r.RegisterCallback(":newsletter-button:", newsletterButton) r.RegisterCallback(":chain-height:", func() string { @@ -38,18 +33,29 @@ func Render(_ string) string { template := `# Welcome to Gno.land -We’re building Gno.land, set to become the leading open-source smart contract +We're building Gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts. With transparent and timeless code, Gno.land is the next generation of smart -contract platforms, serving as the “GitHub” of the ecosystem, with realms built +contract platforms, serving as the "GitHub" of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse. Intuitive and easy to use, Gno.land lowers the barrier to web3 and makes censorship-resistant platforms accessible to everyone. If you want to help lay the foundations of a fairer and freer world, join us today. +--- + +## [Boards](/r/gnoland/boards2/v1) - On-chain forum for the Gno.land community + +**Post, discuss, and create your content community**: Boards is a fully on-chain social forum to create Boards topics, post threads, comment and reply. A plug-and-deploy DAO lets communities manage content, permissions and moderation their way. + +Explore this ready-to-use Gno dApp, and experience decentralized social media in action. + +**[Open Boards](/r/gnoland/boards2/v1)** + +--- ## Learn about Gno.land @@ -69,7 +75,6 @@ the foundations of a fairer and freer world, join us today. - [Write Gno in the browser](https://play.gno.land) - [Read about the Gno Language](/gnolang) - [Visit the official documentation](https://docs.gno.land) -- [Gno by Example](/r/docs/home) - [Efficient local development for Gno](https://docs.gno.land/builders/local-dev-with-gnodev) - [Get testnet GNOTs](https://faucet.gno.land) @@ -79,10 +84,10 @@ the foundations of a fairer and freer world, join us today. - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) - [Gnoscan](https://gnoscan.io) -- [Staging chain](https://docs.gno.land/resources/gnoland-networks/#staging-environments-portal-loops) -- [Testnet 11](https://test11.testnets.gno.land/) +- [Gno networks documentation](https://docs.gno.land/resources/gnoland-networks/) +- [Staging](https://staging.gno.land/) +- [Testnet 12](https://test12.testnets.gno.land/) - [Faucet Hub](https://faucet.gno.land) -- [Boards](https://gno.land/r/gnoland/boards2/v1:OpenDiscussions): community forum @@ -94,16 +99,10 @@ the foundations of a fairer and freer world, join us today. ||| -## [Latest Events](/r/devrels/events) +## [Latest Events](/events) :upcoming-events: -||| - -## [Hall of Realms](/r/leon/hor) - -:latest-hor: - --- diff --git a/examples/gno.land/r/gnops/valopers/proposal/filetests/z_1_filetest.gno b/examples/gno.land/r/gnops/valopers/proposal/filetests/z_1_filetest.gno index 24bfbb33525..9c08f835515 100644 --- a/examples/gno.land/r/gnops/valopers/proposal/filetests/z_1_filetest.gno +++ b/examples/gno.land/r/gnops/valopers/proposal/filetests/z_1_filetest.gno @@ -50,7 +50,7 @@ func main() { // ## Members // [> Go to Memberstore <](/r/gov/dao/v3/memberstore) // ## Proposals -// ### [Prop #0 - Add valoper test-1 to the valset](/r/gov/dao:0) +// ### [Prop #0 - Add valoper test\-1 to the valset](/r/gov/dao:0) // Author: g1vuch2um9wf047h6lta047h6lta047h6l2ewm6w // // Status: ACTIVE diff --git a/examples/gno.land/r/gov/dao/proxy.gno b/examples/gno.land/r/gov/dao/proxy.gno index 85055000baf..0b44ce80f82 100644 --- a/examples/gno.land/r/gov/dao/proxy.gno +++ b/examples/gno.land/r/gov/dao/proxy.gno @@ -49,25 +49,19 @@ func MustCreateProposal(cur realm, r ProposalRequest) ProposalID { // If the proposal was denied, it will return false. If the proposal is correctly // executed, it will return true. If something happens this function will panic. func ExecuteProposal(cur realm, pid ProposalID) bool { - if dao == nil { - return false - } - execute, err := dao.PreExecuteProposal(pid) - if err != nil { - panic(err.Error()) - } + return executeProposal(cur, pid, false) +} - if !execute { - return false - } - prop, err := GetProposal(cur, pid) - if err != nil { - panic(err.Error()) - } - if err := prop.executor.Execute(cross); err != nil { - panic(err.Error()) - } - return true +// ExecuteOrRejectProposal executes the proposal with the provided ProposalID or rejects +// it when there is an execution error. +// If the proposal was denied, it will return false. If the proposal is correctly +// executed, it will return true, unless execution fails with an error, in which case +// proposal is rejected with the error as the reason. +// This function allows to finish proposals by rejecting them when there is a state +// change or an error in the proposal parameters that makes execution fail, potentially +// leaving the proposal active forever because it can't be successfully executed. +func ExecuteOrRejectProposal(cur realm, pid ProposalID) bool { + return executeProposal(cur, pid, true) } // CreateProposal will try to create a new proposal, that will be validated by the actual @@ -193,3 +187,31 @@ func InAllowedDAOs(pkg string) bool { } return false } + +func executeProposal(cur realm, pid ProposalID, execErrorRejects bool) bool { + if dao == nil { + return false + } + execute, err := dao.PreExecuteProposal(pid) + if err != nil { + panic(err.Error()) + } + + if !execute { + return false + } + prop, err := GetProposal(cur, pid) + if err != nil { + panic(err.Error()) + } + + err = dao.ExecuteProposal(pid, prop.executor) + if err != nil { + if execErrorRejects { + return false + } + + panic(err.Error()) + } + return true +} diff --git a/examples/gno.land/r/gov/dao/proxy_test.gno b/examples/gno.land/r/gov/dao/proxy_test.gno index bbdcad88479..db4609fd3a1 100644 --- a/examples/gno.land/r/gov/dao/proxy_test.gno +++ b/examples/gno.land/r/gov/dao/proxy_test.gno @@ -128,6 +128,10 @@ func (dd *dummyDao) PreExecuteProposal(pid ProposalID) (bool, error) { return true, nil } +func (dd *dummyDao) ExecuteProposal(pid ProposalID, e Executor) error { + return nil +} + func (dd *dummyDao) Render(pkgpath string, path string) string { return "Render: " + pkgpath + "/" + path } diff --git a/examples/gno.land/r/gov/dao/types.gno b/examples/gno.land/r/gov/dao/types.gno index ba4aa571c11..5f201a5c993 100644 --- a/examples/gno.land/r/gov/dao/types.gno +++ b/examples/gno.land/r/gov/dao/types.gno @@ -241,6 +241,11 @@ type DAO interface { // Is intended to be used to validate who can trigger the proposal execution. PreExecuteProposal(pid ProposalID) (bool, error) + // ExecuteProposal executes the proposal executor and on error changes proposal + // status to denied with the error message being the denial reason. + // It returns the executor error when it fails. + ExecuteProposal(pid ProposalID, e Executor) error + // Render will return a human-readable string in markdown format that // will be used to show new data through the dao proxy entrypoint. Render(pkgpath string, path string) string diff --git a/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_00_filetest.gno b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_00_filetest.gno new file mode 100644 index 00000000000..25464bdc883 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_00_filetest.gno @@ -0,0 +1,82 @@ +// PKGPATH: gno.land/r/test/govdao +package govdao + +import ( + "errors" + "strconv" + "testing" + + "gno.land/r/gov/dao" + "gno.land/r/gov/dao/v3/impl" + "gno.land/r/gov/dao/v3/memberstore" +) + +const user address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + +var ( + executor dao.Executor + proposalID dao.ProposalID + renderPath string + govdao = impl.NewGovDAO() +) + +func init() { + // Initialize GovDAO members + memberstore.Get().DeleteAll() + memberstore.Get().SetTier(memberstore.T1) + memberstore.Get().SetMember(memberstore.T1, user, &memberstore.Member{InvitationPoints: 3}) + dao.UpdateImpl(cross, dao.UpdateRequest{DAO: impl.NewGovDAO()}) + + // Create an executor that always fails + cb := func(realm) error { return errors.New("Boom!") } + executor = dao.NewSimpleExecutor(cb, "") + + // Create a proposal request that fails on execution + request := dao.NewProposalRequest("Test", "This proposal always fails on execution", executor) + + // Create the proposal from a realm so GovDAO instance is able to render the proposal + testing.SetRealm(testing.NewUserRealm(user)) + proposalID = dao.MustCreateProposal(cross, request) + renderPath = strconv.FormatUint(uint64(proposalID), 10) + + // Register proposal with the local GovDAO instance + govdao.PostCreateProposal(request, proposalID) +} + +func main() { + // Execute proposal, status should be REJECTED + err := govdao.ExecuteProposal(proposalID, executor) + + println(err.Error()) + println() + println(govdao.Render("gno.land/r/gov/dao/v3/impl", renderPath)) +} + +// Output: +// Boom! +// +// ## Prop #0 - Test +// Author: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 +// +// This proposal always fails on execution +// +// +// +// --- +// +// ### Stats +// - **PROPOSAL HAS BEEN DENIED** +// REASON: execution failed: Boom! +// - Tiers eligible to vote: T1, T2, T3 +// - YES PERCENT: 0% +// - NO PERCENT: 0% +// - ABSTAIN PERCENT: 0% +// +// [Detailed voting list](/r/gov/dao/v3/impl:0/votes) +// +// --- +// +// ### Actions +// [Vote YES](/r/gov/dao$help&func=MustVoteOnProposalSimple&option=YES&pid=0) | [Vote NO](/r/gov/dao$help&func=MustVoteOnProposalSimple&option=NO&pid=0) | [Vote ABSTAIN](/r/gov/dao$help&func=MustVoteOnProposalSimple&option=ABSTAIN&pid=0) +// +// WARNING: Please double check transaction data before voting. diff --git a/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_01_filetest.gno b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_01_filetest.gno new file mode 100644 index 00000000000..74d088da687 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_01_filetest.gno @@ -0,0 +1,80 @@ +// PKGPATH: gno.land/r/test/govdao +package govdao + +import ( + "strconv" + "testing" + + "gno.land/r/gov/dao" + "gno.land/r/gov/dao/v3/impl" + "gno.land/r/gov/dao/v3/memberstore" +) + +const user address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + +var ( + executor dao.Executor + proposalID dao.ProposalID + renderPath string + govdao = impl.NewGovDAO() +) + +func init() { + // Initialize GovDAO members + memberstore.Get().DeleteAll() + memberstore.Get().SetTier(memberstore.T1) + memberstore.Get().SetMember(memberstore.T1, user, &memberstore.Member{InvitationPoints: 3}) + dao.UpdateImpl(cross, dao.UpdateRequest{DAO: impl.NewGovDAO()}) + + // Create a dummy executor + cb := func(realm) error { return nil } + executor = dao.NewSimpleExecutor(cb, "") + + // Create a proposal request that pass on execution + request := dao.NewProposalRequest("Test", "This proposal always pass on execution", executor) + + // Create the proposal from a realm so GovDAO instance is able to render the proposal + testing.SetRealm(testing.NewUserRealm(user)) + proposalID = dao.MustCreateProposal(cross, request) + renderPath = strconv.FormatUint(uint64(proposalID), 10) + + // Register proposal with the local GovDAO instance + govdao.PostCreateProposal(request, proposalID) +} + +func main() { + // Execute proposal, status should be ACTIVE + err := govdao.ExecuteProposal(proposalID, executor) + + println(err == nil) + println() + println(govdao.Render("gno.land/r/gov/dao/v3/impl", renderPath)) +} + +// Output: +// true +// +// ## Prop #0 - Test +// Author: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 +// +// This proposal always pass on execution +// +// +// +// --- +// +// ### Stats +// - **Proposal is open for votes** +// - Tiers eligible to vote: T1, T2, T3 +// - YES PERCENT: 0% +// - NO PERCENT: 0% +// - ABSTAIN PERCENT: 0% +// +// [Detailed voting list](/r/gov/dao/v3/impl:0/votes) +// +// --- +// +// ### Actions +// [Vote YES](/r/gov/dao$help&func=MustVoteOnProposalSimple&option=YES&pid=0) | [Vote NO](/r/gov/dao$help&func=MustVoteOnProposalSimple&option=NO&pid=0) | [Vote ABSTAIN](/r/gov/dao$help&func=MustVoteOnProposalSimple&option=ABSTAIN&pid=0) +// +// WARNING: Please double check transaction data before voting. diff --git a/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_02_filetest.gno b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_02_filetest.gno new file mode 100644 index 00000000000..b2abe5d912b --- /dev/null +++ b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_02_filetest.gno @@ -0,0 +1,13 @@ +package main + +import "gno.land/r/gov/dao/v3/impl" + +var govdao = impl.NewGovDAO() + +func main() { + // Try to execute a proposal using a nil executor + govdao.ExecuteProposal(0, nil) +} + +// Error: +// an executor is required to execute the proposal diff --git a/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_03_filetest.gno b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_03_filetest.gno new file mode 100644 index 00000000000..f19bf2f7693 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v3/impl/filetests/govdao_execute_proposal_03_filetest.gno @@ -0,0 +1,26 @@ +// PKGPATH: gno.land/r/test/govdao +package govdao + +import ( + "gno.land/r/gov/dao" + "gno.land/r/gov/dao/v3/impl" +) + +var ( + executor dao.Executor + govdao = impl.NewGovDAO() +) + +func init() { + // Create a dummy executor + cb := func(realm) error { return nil } + executor = dao.NewSimpleExecutor(cb, "") +} + +func main() { + // Try to execute a proposal that doesn't exist + govdao.ExecuteProposal(404, executor) +} + +// Error: +// proposal not found diff --git a/examples/gno.land/r/gov/dao/v3/impl/z_stringify_proposal_filetest.gno b/examples/gno.land/r/gov/dao/v3/impl/filetests/stringify_proposal_00_filetest.gno similarity index 100% rename from examples/gno.land/r/gov/dao/v3/impl/z_stringify_proposal_filetest.gno rename to examples/gno.land/r/gov/dao/v3/impl/filetests/stringify_proposal_00_filetest.gno diff --git a/examples/gno.land/r/gov/dao/v3/impl/govdao.gno b/examples/gno.land/r/gov/dao/v3/impl/govdao.gno index 7db946b447a..22155891260 100644 --- a/examples/gno.land/r/gov/dao/v3/impl/govdao.gno +++ b/examples/gno.land/r/gov/dao/v3/impl/govdao.gno @@ -126,8 +126,11 @@ func (g *GovDAO) VoteOnProposal(r dao.VoteRequest) error { case dao.NoVote: status.AllVotes.SetMember(tie, caller, mem) status.NoVotes.SetMember(tie, caller, mem) + case dao.AbstainVote: + status.AllVotes.SetMember(tie, caller, mem) + status.AbstainVotes.SetMember(tie, caller, mem) default: - return errors.New("voting can only be YES or NO") + return errors.New("voting can only be YES, NO, or ABSTAIN") } return nil @@ -163,6 +166,25 @@ func (g *GovDAO) PreExecuteProposal(pid dao.ProposalID) (bool, error) { return false, errors.New(ufmt.Sprintf("proposal didn't reach supermajority yet: %v", law.Supermajority)) } +func (g *GovDAO) ExecuteProposal(pid dao.ProposalID, e dao.Executor) error { + if e == nil { + panic("an executor is required to execute the proposal") + } + + status := g.pss.GetStatus(pid) + if status == nil { + panic("proposal not found") + } + + err := e.Execute(cross) + if err != nil { + status.Accepted = false + status.Denied = true + status.DeniedReason = "execution failed: " + err.Error() + } + return err +} + func (g *GovDAO) Render(pkgPath string, path string) string { return g.render.Render(pkgPath, path) } diff --git a/examples/gno.land/r/gov/dao/v3/impl/govdao_test.gno b/examples/gno.land/r/gov/dao/v3/impl/govdao_test.gno index bc8962881f4..15548190a88 100644 --- a/examples/gno.land/r/gov/dao/v3/impl/govdao_test.gno +++ b/examples/gno.land/r/gov/dao/v3/impl/govdao_test.gno @@ -146,7 +146,7 @@ func TestCreateProposalAndVote(cur realm, t *testing.T) { urequire.Equal(t, false, accepted) urequire.Equal(t, true, contains(dao.Render("0"), "**PROPOSAL HAS BEEN DENIED**")) - urequire.Equal(t, true, contains(dao.Render("0"), "NO PERCENT: 68.42105263157895%")) + urequire.Equal(t, true, contains(dao.Render("0"), "NO PERCENT: 81.25%")) } func TestExecutorCreationRealm(cur realm, t *testing.T) { @@ -306,6 +306,69 @@ func TestUpgradeDaoImplementation(t *testing.T) { urequire.Equal(t, true, contains(dao.Render("8"), "YES PERCENT: 68.42105263157895%")) } +func TestAbstainVote(cur realm, t *testing.T) { + loadMembers() + + portfolio := "# This is my portfolio:\n\n- THINGS" + + testing.SetOriginCaller(m1) + testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl")) + + nm1 := testutils.TestAddress("nm1") + proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio) + + testing.SetOriginCaller(m1) + testing.SetRealm(testing.NewUserRealm(m1)) + pid := dao.MustCreateProposal(cross, proposalRequest) + + // m1 votes abstain + dao.MustVoteOnProposalSimple(cross, int64(pid), "ABSTAIN") + + // Other members vote YES to reach supermajority + testing.SetOriginCaller(m11) + dao.MustVoteOnProposal(cross, dao.VoteRequest{ + Option: dao.YesVote, + ProposalID: pid, + }) + + testing.SetOriginCaller(m111) + dao.MustVoteOnProposal(cross, dao.VoteRequest{ + Option: dao.YesVote, + ProposalID: pid, + }) + + testing.SetOriginCaller(m1111) + dao.MustVoteOnProposal(cross, dao.VoteRequest{ + Option: dao.YesVote, + ProposalID: pid, + }) + + testing.SetOriginCaller(m2) + dao.MustVoteOnProposal(cross, dao.VoteRequest{ + Option: dao.YesVote, + ProposalID: pid, + }) + + testing.SetOriginCaller(m3) + dao.MustVoteOnProposal(cross, dao.VoteRequest{ + Option: dao.YesVote, + ProposalID: pid, + }) + + // Verify render shows correct percentages + urequire.Equal(t, true, contains(dao.Render(pid.String()), "YES PERCENT: 81.25%")) + urequire.Equal(t, true, contains(dao.Render(pid.String()), "NO PERCENT: 0%")) + urequire.Equal(t, true, contains(dao.Render(pid.String()), "ABSTAIN PERCENT: 18.75%")) + + // Supermajority reached despite one abstain voter + accepted := dao.ExecuteProposal(cross, pid) + urequire.Equal(t, true, accepted) + urequire.Equal(t, true, contains(dao.Render(pid.String()), "**PROPOSAL HAS BEEN ACCEPTED**")) + + // Abstain voter appears in vote list + urequire.Equal(t, true, contains(dao.Render(fmt.Sprintf("%v/votes", int64(pid))), "ABSTAIN")) +} + func contains(s, substr string) bool { return strings.Index(s, substr) >= 0 } diff --git a/examples/gno.land/r/gov/dao/v3/impl/render.gno b/examples/gno.land/r/gov/dao/v3/impl/render.gno index cdc97e20b07..461a1ddd925 100644 --- a/examples/gno.land/r/gov/dao/v3/impl/render.gno +++ b/examples/gno.land/r/gov/dao/v3/impl/render.gno @@ -6,6 +6,7 @@ import ( "strings" "gno.land/p/moul/helplink" + "gno.land/p/moul/md" "gno.land/p/nt/avl/v0/pager" "gno.land/p/nt/mux/v0" "gno.land/p/nt/seqid/v0" @@ -92,7 +93,7 @@ func (ren *render) renderProposalPage(sPid string, d *GovDAO) string { } ps := d.pss.GetStatus(dao.ProposalID(pid)) - out := ufmt.Sprintf("## Prop #%v - %v\n", pid, p.Title()) + out := ufmt.Sprintf("## Prop #%v - %v\n", pid, md.EscapeText(p.Title())) out += "Author: " + tryResolveAddr(p.Author()) + "\n\n" out += p.Description() @@ -132,7 +133,7 @@ func (ren *render) renderProposalListItem(sPid string, d *GovDAO) string { } ps := d.pss.GetStatus(dao.ProposalID(pid)) - out := ufmt.Sprintf("### [Prop #%v - %v](%v:%v)\n", pid, p.Title(), ren.relativeRealmPath, pid) + out := ufmt.Sprintf("### [Prop #%v - %v](%v:%v)\n", pid, md.EscapeText(p.Title()), ren.relativeRealmPath, pid) out += ufmt.Sprintf("Author: %s\n\n", tryResolveAddr(p.Author())) out += "Status: " + getPropStatus(ps) diff --git a/examples/gno.land/r/gov/dao/v3/impl/types.gno b/examples/gno.land/r/gov/dao/v3/impl/types.gno index 88ca0130fdb..4590d9f9ade 100644 --- a/examples/gno.land/r/gov/dao/v3/impl/types.gno +++ b/examples/gno.land/r/gov/dao/v3/impl/types.gno @@ -46,9 +46,10 @@ func (pss ProposalsStatuses) GetStatus(id dao.ProposalID) *proposalStatus { } type proposalStatus struct { - YesVotes memberstore.MembersByTier - NoVotes memberstore.MembersByTier - AllVotes memberstore.MembersByTier + YesVotes memberstore.MembersByTier + NoVotes memberstore.MembersByTier + AbstainVotes memberstore.MembersByTier + AllVotes memberstore.MembersByTier Accepted bool Denied bool @@ -56,68 +57,62 @@ type proposalStatus struct { DeniedReason string TiersAllowedToVote []string - - TotalPower float64 // TotalPower is the power of all the members existing when this proposal was created. } func getMembers(cur realm) memberstore.MembersByTier { return memberstore.Get() } -func newProposalStatus(allowedToVote []string) *proposalStatus { - yv := memberstore.NewMembersByTier() - yv.SetTier(memberstore.T1) - yv.SetTier(memberstore.T2) - yv.SetTier(memberstore.T3) - nv := memberstore.NewMembersByTier() - nv.SetTier(memberstore.T1) - nv.SetTier(memberstore.T2) - nv.SetTier(memberstore.T3) - av := memberstore.NewMembersByTier() - av.SetTier(memberstore.T1) - av.SetTier(memberstore.T2) - av.SetTier(memberstore.T3) +func newEmptyVoteStore() memberstore.MembersByTier { + mbt := memberstore.NewMembersByTier() + mbt.SetTier(memberstore.T1) + mbt.SetTier(memberstore.T2) + mbt.SetTier(memberstore.T3) + return mbt +} +func newProposalStatus(allowedToVote []string) *proposalStatus { return &proposalStatus{ - YesVotes: yv, - NoVotes: nv, - AllVotes: av, - + YesVotes: newEmptyVoteStore(), + NoVotes: newEmptyVoteStore(), + AbstainVotes: newEmptyVoteStore(), + AllVotes: newEmptyVoteStore(), TiersAllowedToVote: allowedToVote, - - TotalPower: getMembers(cross).GetTotalPower(), } } -func (ps *proposalStatus) YesPercent() float64 { - var yp float64 - - memberstore.IterateTiers(func(tn string, tier memberstore.Tier) bool { - power := memberstore.GetTierPower(tn, getMembers(cross)) - ts := ps.YesVotes.GetTierSize(tn) - - yp = yp + (power * float64(ts)) - - return false - }) - - return (yp / ps.TotalPower) * 100 +// totalPower computes the total voting power dynamically from current members +// rather than using a snapshot. See https://github.com/gnolang/gno/pull/5271#discussion_r2952523023 +func (ps *proposalStatus) totalPower() float64 { + members := getMembers(cross) + var tp float64 + for _, tn := range ps.TiersAllowedToVote { + power := memberstore.GetTierPower(tn, members) + tp += power * float64(members.GetTierSize(tn)) + } + return tp } -func (ps *proposalStatus) NoPercent() float64 { - var np float64 - +func (ps *proposalStatus) votePowerPercent(votes memberstore.MembersByTier) float64 { + members := getMembers(cross) + var vp float64 memberstore.IterateTiers(func(tn string, tier memberstore.Tier) bool { - power := memberstore.GetTierPower(tn, getMembers(cross)) - ts := ps.NoVotes.GetTierSize(tn) - np = np + (power * float64(ts)) - + power := memberstore.GetTierPower(tn, members) + ts := votes.GetTierSize(tn) + vp = vp + (power * float64(ts)) return false }) - - return (np / ps.TotalPower) * 100 + tp := ps.totalPower() + if tp == 0 { + return 0 + } + return (vp / tp) * 100 } +func (ps *proposalStatus) YesPercent() float64 { return ps.votePowerPercent(ps.YesVotes) } +func (ps *proposalStatus) NoPercent() float64 { return ps.votePowerPercent(ps.NoVotes) } +func (ps *proposalStatus) AbstainPercent() float64 { return ps.votePowerPercent(ps.AbstainVotes) } + func (ps *proposalStatus) IsAllowed(tier string) bool { for _, ta := range ps.TiersAllowedToVote { if ta == tier { @@ -128,6 +123,7 @@ func (ps *proposalStatus) IsAllowed(tier string) bool { return false } +// XXX: can be optimized by passing down total power to Yes/No/Abstain percent fn to avoid re-computing func (ps *proposalStatus) String() string { var sb strings.Builder sb.WriteString("### Stats\n") @@ -151,6 +147,7 @@ func (ps *proposalStatus) String() string { sb.WriteString(ufmt.Sprintf("- YES PERCENT: %v%%\n", ps.YesPercent())) sb.WriteString(ufmt.Sprintf("- NO PERCENT: %v%%\n", ps.NoPercent())) + sb.WriteString(ufmt.Sprintf("- ABSTAIN PERCENT: %v%%\n", ps.AbstainPercent())) return sb.String() } @@ -160,6 +157,7 @@ func StringifyVotes(ps *proposalStatus) string { writeVotes(&sb, ps.YesVotes, "YES") writeVotes(&sb, ps.NoVotes, "NO") + writeVotes(&sb, ps.AbstainVotes, "ABSTAIN") if sb.String() == "" { return "No one voted yet." @@ -172,13 +170,14 @@ func writeVotes(sb *strings.Builder, t memberstore.MembersByTier, title string) if t.Size() == 0 { return } + members := getMembers(cross) t.Iterate("", "", func(tn string, value interface{}) bool { _, ok := memberstore.GetTier(tn) if !ok { panic("tier not found") } - power := memberstore.GetTierPower(tn, getMembers(cross)) + power := memberstore.GetTierPower(tn, members) sb.WriteString(ufmt.Sprintf("%v from %v (VPPM %v):\n\n", title, tn, power)) ms, _ := value.(*avl.Tree) diff --git a/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub.gno b/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub.gno index d71a7f98acd..ce520b0c3e6 100644 --- a/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub.gno +++ b/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub.gno @@ -32,18 +32,11 @@ func init() { } // RegisterToken is a function that uses gno.land/r/demo/defi/grc20reg to register a token -// It uses the slug to construct a key and then registers the token in the registry -// The logic is the same as in grc20reg, but it's done here so the key path is callers pkgpath and not of this realm -// After doing so, the token hub realm uses grc20reg's registry as a read-only avl.Tree -// -// Note: register token returns the key path that can be used to retrieve the token +// RegisterToken registers a token in grc20reg with the given slug. +// Returns the registry key that can be used to retrieve the token. func RegisterToken(cur realm, token *grc20.Token, slug string) string { - rlmPath := runtime.PreviousRealm().PkgPath() - key := fqname.Construct(rlmPath, slug) - - grc20reg.Register(cross, token, key) - - return fqname.Construct(runtime.CurrentRealm().PkgPath(), key) + grc20reg.Register(cross, token, slug) + return fqname.Construct(runtime.CurrentRealm().PkgPath(), slug) } // RegisterNFT is a function that registers an NFT in an avl.Tree diff --git a/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub_test.gno b/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub_test.gno index 58b1f7b7b27..c59d096c7e9 100644 --- a/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub_test.gno +++ b/examples/gno.land/r/matijamarjanovic/tokenhub/tokenhub_test.gno @@ -22,7 +22,7 @@ func TestTokenRegistration(t *testing.T) { token, _ := grc20.NewToken("Test Token", "TEST", 6) RegisterToken(cross, token, "test_token") - retrievedToken := GetToken("gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/testrealm.test_token") + retrievedToken := GetToken("gno.land/r/matijamarjanovic/tokenhub.test_token") urequire.True(t, retrievedToken != nil, "Should retrieve registered token") uassert.Equal(t, "Test Token", retrievedToken.GetName(), "Token name should match") @@ -76,7 +76,7 @@ func TestBalanceRetrieval(t *testing.T) { balances := GetUserTokenBalances(runtime.CurrentRealm().Address().String()) uassert.True(t, strings.Contains(balances, - "Token:gno.land/r/matijamarjanovic/tokenhub."+testRealmPkgPath+".test_token:1000"), "Should show correct GRC20 balance") + "Token:gno.land/r/matijamarjanovic/tokenhub.test_token:1000"), "Should show correct GRC20 balance") nft := grc721.NewBasicNFT("Test NFT", "TNFT") nft.Mint(runtime.CurrentRealm().Address(), grc721.TokenID("1")) @@ -94,7 +94,7 @@ func TestBalanceRetrieval(t *testing.T) { nonZeroBalances := GetUserTokenBalancesNonZero(runtime.CurrentRealm().Address().String()) uassert.True(t, strings.Contains(nonZeroBalances, - "Token:gno.land/r/matijamarjanovic/tokenhub."+testRealmPkgPath+".test_token:1000"), "Should show non-zero GRC20 balance") + "Token:gno.land/r/matijamarjanovic/tokenhub.test_token:1000"), "Should show non-zero GRC20 balance") } func TestErrorCases(t *testing.T) { @@ -122,7 +122,7 @@ func TestTokenListingFunctions(t *testing.T) { RegisterToken(cross, grc20Token, "listing_token") grc20List := GetAllTokens() - uassert.True(t, strings.Contains(grc20List, "Token:gno.land/r/matijamarjanovic/tokenhub."+testRealmPkgPath+".listing_token"), + uassert.True(t, strings.Contains(grc20List, "Token:gno.land/r/matijamarjanovic/tokenhub.listing_token"), "GetAllGRC20Tokens should list registered token") nftToken := grc721.NewBasicNFT("Listing NFT", "LNFT") @@ -143,7 +143,7 @@ func TestTokenListingFunctions(t *testing.T) { completeList := GetAllRegistered() uassert.True(t, strings.Contains(completeList, "NFT:"+testRealmPkgPath+".listing_nft.1"), "GetAllTokens should list NFTs") - uassert.True(t, strings.Contains(completeList, "Token:gno.land/r/matijamarjanovic/tokenhub."+testRealmPkgPath+".listing_token"), + uassert.True(t, strings.Contains(completeList, "Token:gno.land/r/matijamarjanovic/tokenhub.listing_token"), "GetAllTokens should list GRC20 tokens") uassert.True(t, strings.Contains(completeList, "MultiToken:"+testRealmPkgPath+".listing_mt"), "GetAllTokens should list multi-tokens") @@ -155,7 +155,7 @@ func TestMustGetFunctions(t *testing.T) { token, _ := grc20.NewToken("Must Token", "MUST", 6) RegisterToken(cross, token, "must_token") - retrievedToken := MustGetToken("gno.land/r/matijamarjanovic/tokenhub." + testRealmPkgPath + ".must_token") + retrievedToken := MustGetToken("gno.land/r/matijamarjanovic/tokenhub.must_token") uassert.Equal(t, "Must Token", retrievedToken.GetName(), "Token name should match") defer func() { diff --git a/examples/gno.land/r/nt/commondao/v0/render.gno b/examples/gno.land/r/nt/commondao/v0/render.gno index 12ed44ab856..ee38781610f 100644 --- a/examples/gno.land/r/nt/commondao/v0/render.gno +++ b/examples/gno.land/r/nt/commondao/v0/render.gno @@ -346,7 +346,7 @@ func renderProposalsListItem(res *mux.ResponseWriter, dao *commondao.CommonDAO, o := getOptions(dao.ID()) // Render title - res.Write(ufmt.Sprintf("**[#%d %s](%s)** \n", p.ID(), def.Title(), proposalURL(dao.ID(), p.ID()))) + res.Write(ufmt.Sprintf("**[#%d %s](%s)** \n", p.ID(), md.EscapeText(def.Title()), proposalURL(dao.ID(), p.ID()))) // Render details res.Write(ufmt.Sprintf("Created by %s \n", userLink(p.Creator()))) @@ -389,7 +389,7 @@ func renderProposal(res *mux.ResponseWriter, req *mux.Request) { def := p.Definition() // Render header - res.Write(md.H1("#" + strconv.FormatUint(p.ID(), 10) + " " + def.Title())) + res.Write(md.H1("#" + strconv.FormatUint(p.ID(), 10) + " " + md.EscapeText(def.Title()))) // Render main menu items := []string{goToDAOLink(dao.ID())} diff --git a/examples/gno.land/r/sys/cla/render.gno b/examples/gno.land/r/sys/cla/render.gno index f59d867c69e..68477fcdcbe 100644 --- a/examples/gno.land/r/sys/cla/render.gno +++ b/examples/gno.land/r/sys/cla/render.gno @@ -1,18 +1,44 @@ package cla -import "gno.land/p/moul/helplink" +import ( + "gno.land/p/moul/helplink" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/nt/ufmt/v0" +) func Render(path string) string { + out := md.H1("Contributor License Agreement (CLA)") + + out += md.Paragraph("A Contributor License Agreement (CLA) must be signed before deploying packages.") + out += md.Paragraph( + "The Agreement governs Contributions uploaded, published, or made available " + + "for execution on the Gno.land blockchain network, and the " + + "related software and repositories used to publish such Contributions.", + ) + if requiredHash == "" { - return "# Gno CLA Registry\n\n**Status:** CLA enforcement is DISABLED\n" + out += md.HorizontalRule() + out += md.H2("Status") + out += md.Paragraph(md.Bold("CLA enforcement is currently DISABLED.")) + out += md.Paragraph("All package deployments are allowed.") + return out } - output := "# Gno CLA Registry\n\n**Status:** CLA enforcement is ENABLED\n\n" - output += "**Required Hash:** " + requiredHash + "\n" + out += md.HorizontalRule() + out += md.H2("Status") + out += md.Paragraph(md.Bold("CLA enforcement is ENABLED")) + if claURL != "" { - output += "**CLA Document:** " + claURL + "\n" + out += md.Paragraph("You can read the full agreement here: " + md.Link(claURL, claURL)) } - output += "\n### Actions\n\n" - output += helplink.Func("Sign CLA", "Sign", "hash", requiredHash) + "\n" - return output + + table := mdtable.Table{Headers: []string{"", ""}} + table.Append([]string{md.Bold("Required Hash"), md.InlineCode(requiredHash)}) + table.Append([]string{md.Bold("Signers"), ufmt.Sprintf("%d contributor(s)", signatures.Size())}) + out += table.String() + + out += md.H3("Actions") + out += md.Paragraph(helplink.Func("Sign CLA", "Sign", "hash", requiredHash)) + return out } diff --git a/examples/gno.land/r/sys/names/verifier.gno b/examples/gno.land/r/sys/names/verifier.gno index 0f075b7a0b5..4f4e5a3cf14 100644 --- a/examples/gno.land/r/sys/names/verifier.gno +++ b/examples/gno.land/r/sys/names/verifier.gno @@ -2,10 +2,10 @@ // Only address-prefix (PA) namespaces are allowed. package names -import "gno.land/p/nt/ownable/v0" +import "chain/runtime" var ( - Ownable = ownable.NewWithAddressByPrevious("g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p") // genesis deployer — dropped in genesis via Enable. + admin = address("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh") // govdao t1 multisig enabled = false ) @@ -15,12 +15,12 @@ func IsAuthorizedAddressForNamespace(address_XXX address, namespace string) bool return verifier(enabled, address_XXX, namespace) } -// Enable enables the namespace check and drops centralized ownership of this realm. +// Enable enables the namespace check for this realm. // The namespace check is disabled initially to ease txtar and other testing contexts, // but this function is meant to be called in the genesis of a chain. func Enable(cur realm) { - if err := Ownable.DropOwnership(); err != nil { - panic(err) + if runtime.PreviousRealm().Address() != admin { + panic("caller is not admin") } enabled = true } diff --git a/examples/gno.land/r/sys/names/verifier_test.gno b/examples/gno.land/r/sys/names/verifier_test.gno index 8b55bb53e8c..644b26a8906 100644 --- a/examples/gno.land/r/sys/names/verifier_test.gno +++ b/examples/gno.land/r/sys/names/verifier_test.gno @@ -3,7 +3,6 @@ package names import ( "testing" - "gno.land/p/nt/ownable/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) @@ -30,15 +29,15 @@ func TestDefaultVerifier(t *testing.T) { } func TestEnable(t *testing.T) { - testing.SetRealm(testing.NewUserRealm("g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p")) - - uassert.NotPanics(t, func() { + testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("random"))) + uassert.AbortsWithMessage(t, "caller is not admin", func() { Enable(cross) }) + uassert.False(t, IsEnabled()) - // Confirm enable drops ownership - uassert.Equal(t, Ownable.Owner().String(), "") - uassert.AbortsWithMessage(t, ownable.ErrUnauthorized.Error(), func() { + testing.SetRealm(testing.NewUserRealm(admin)) + uassert.NotPanics(t, func() { Enable(cross) }) + uassert.True(t, IsEnabled()) } diff --git a/examples/gno.land/r/sys/params/halt.gno b/examples/gno.land/r/sys/params/halt.gno new file mode 100644 index 00000000000..f9dccaac0ea --- /dev/null +++ b/examples/gno.land/r/sys/params/halt.gno @@ -0,0 +1,51 @@ +package params + +import ( + "strconv" + + "chain" + prms "sys/params" + + "gno.land/r/gov/dao" +) + +const ( + nodeModulePrefix = "node" + haltHeightKey = "halt_height" + haltMinVersionKey = "halt_min_version" +) + +// NewSetHaltRequest creates a GovDAO proposal to halt all chain nodes at the given block height. +// Once approved and executed, nodes will gracefully stop after committing the specified block, +// enabling coordinated chain upgrades. +// +// minVersion, if non-empty, sets the minimum binary version required to resume after the halt. +// Nodes will refuse to restart unless their version satisfies the minimum requirement, +// preventing old binaries from accidentally resuming a chain halted for an upgrade. +// Example: minVersion="chain/gnoland1.1" prevents gnoland1.0 from resuming. +// +// Use height=0 to cancel a previously scheduled halt. +func NewSetHaltRequest(height int64, minVersion string) dao.ProposalRequest { + callback := func(cur realm) error { + prms.SetSysParamInt64(nodeModulePrefix, "p", haltHeightKey, height) + prms.SetSysParamString(nodeModulePrefix, "p", haltMinVersionKey, minVersion) + chain.Emit("set_halt", + "height", strconv.FormatInt(height, 10), + "min_version", minVersion, + ) + return nil + } + + var desc string + if height == 0 { + desc = "Cancel the scheduled chain halt and clear the minimum version requirement." + } else { + desc = "Halt the chain at block " + strconv.FormatInt(height, 10) + "." + if minVersion != "" { + desc += " Requires binary version >= " + minVersion + " to resume." + } + } + + e := dao.NewSimpleExecutor(callback, "") + return dao.NewProposalRequest("Set node halt height", desc, e) +} diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno index e0e901e2dbb..f82b2451a19 100644 --- a/examples/gno.land/r/sys/params/params_test.gno +++ b/examples/gno.land/r/sys/params/params_test.gno @@ -15,3 +15,24 @@ func TestNewStringPropRequest(t *testing.T) { t.Errorf("executor shouldn't be nil") } } + +func TestNewSetHaltRequest(t *testing.T) { + pr := NewSetHaltRequest(100_000, "chain/gnoland1.1") + if pr.Title() == "" { + t.Errorf("proposal title shouldn't be empty") + } +} + +func TestNewSetHaltRequestNoVersion(t *testing.T) { + pr := NewSetHaltRequest(100_000, "") + if pr.Title() == "" { + t.Errorf("proposal title shouldn't be empty") + } +} + +func TestNewSetHaltRequestCancel(t *testing.T) { + pr := NewSetHaltRequest(0, "") + if pr.Title() == "" { + t.Errorf("proposal title shouldn't be empty") + } +} diff --git a/examples/gno.land/r/sys/users/admin.gno b/examples/gno.land/r/sys/users/admin.gno index f0d159ee6eb..b1e55e51b6e 100644 --- a/examples/gno.land/r/sys/users/admin.gno +++ b/examples/gno.land/r/sys/users/admin.gno @@ -87,6 +87,64 @@ func ProposeControllerAdditionAndRemoval(toAdd, toRemove address) dao.ProposalRe return dao.NewProposalRequest("Add and Remove Whitelisted Callers From \"sys/users\" Realm", desc, dao.NewSimpleExecutor(cb, "")) } +// ProposeRegisterUser allows GovDAO to register a name without checking controllers +func ProposeRegisterUser(name string, addr address) dao.ProposalRequest { + // Validate the name and address now, even though registerUser will validate again + if err := validateName(name); err != nil { + panic(err.Error()) + } + if !addr.IsValid() { + panic(ErrInvalidAddress) + } + + cb := func(cur realm) error { + return registerUser(cur, name, addr) + } + + desc := "This proposal registers " + name + " with address " + addr.String() + " in `sys/users`." + return dao.NewProposalRequest("Register User to \"sys/users\" Realm", desc, dao.NewSimpleExecutor(cb, "")) +} + +// ProposeUpdateName allows GovDAO to update a name with an alias without checking controllers +func ProposeUpdateName(addr address, newName string) dao.ProposalRequest { + if !addr.IsValid() { + panic(ErrInvalidAddress) + } + if err := validateName(newName); err != nil { + panic(err.Error()) + } + + cb := func(cur realm) error { + data := ResolveAddress(addr) + if data == nil { + return ErrUserNotExistOrDeleted + } + return data.updateName(newName) + } + + desc := "This proposal updates address " + addr.String() + " with alias " + newName + " in `sys/users`." + return dao.NewProposalRequest("Update Name Alias in \"sys/users\" Realm", desc, dao.NewSimpleExecutor(cb, "")) +} + +// ProposeDeleteUser allows GovDAO to delete a user without checking controllers +func ProposeDeleteUser(addr address) dao.ProposalRequest { + if !addr.IsValid() { + panic(ErrInvalidAddress) + } + + cb := func(cur realm) error { + data := ResolveAddress(addr) + if data == nil { + return ErrUserNotExistOrDeleted + } + return data.delete() + } + + desc := "This proposal deletes the user with address " + addr.String() + " in `sys/users`." + return dao.NewProposalRequest("Delete User in \"sys/users\" Realm", desc, dao.NewSimpleExecutor(cb, "")) +} + + // Helpers func deleteFromWhitelist(addr address) error { diff --git a/examples/gno.land/r/sys/users/store.gno b/examples/gno.land/r/sys/users/store.gno index e18293ef857..89ed1e9902a 100644 --- a/examples/gno.land/r/sys/users/store.gno +++ b/examples/gno.land/r/sys/users/store.gno @@ -51,14 +51,8 @@ func (u UserData) RenderLink(linkText string) string { return ufmt.Sprintf("[%s](/u/%s)", linkText, u.username) } -// RegisterUser adds a new user to the system. -func RegisterUser(cur realm, name string, address_XXX address) error { - // At genesis (height 0), allow any caller to register users. - // After genesis, only whitelisted controllers can register. - if runtime.ChainHeight() > 0 && !controllers.Has(runtime.PreviousRealm().Address()) { - return NewErrNotWhitelisted() - } - +// registerUser adds a new user to the system without checking controllers +func registerUser(cur realm, name string, address_XXX address) error { // Validate name if err := validateName(name); err != nil { return err @@ -103,19 +97,25 @@ func RegisterUser(cur realm, name string, address_XXX address) error { return nil } -// UpdateName adds a name that is associated with a specific address +// RegisterUser adds a new user to the system. +func RegisterUser(cur realm, name string, address_XXX address) error { + // At genesis (height 0), allow any caller to register users. + // After genesis, only whitelisted controllers can register. + if runtime.ChainHeight() > 0 && !controllers.Has(runtime.PreviousRealm().Address()) { + return NewErrNotWhitelisted() + } + + return registerUser(cur, name, address_XXX) +} + +// updateName adds a name that is associated with a specific address without checking controllers // All previous names are preserved and resolvable. // The new name is the default value returned for address lookups. -func (u *UserData) UpdateName(newName string) error { - if u == nil { // either doesnt exists or was deleted +func (u *UserData) updateName(newName string) error { + if u == nil { // either doesn't exist or was deleted return ErrUserNotExistOrDeleted } - // Validate caller - if !controllers.Has(runtime.CurrentRealm().Address()) { - return NewErrNotWhitelisted() - } - // Validate name if err := validateName(newName); err != nil { return err @@ -136,8 +136,10 @@ func (u *UserData) UpdateName(newName string) error { return nil } -// Delete marks a user and all their aliases as deleted. -func (u *UserData) Delete() error { +// UpdateName adds a name that is associated with a specific address +// All previous names are preserved and resolvable. +// The new name is the default value returned for address lookups. +func (u *UserData) UpdateName(newName string) error { if u == nil { return ErrUserNotExistOrDeleted } @@ -147,12 +149,35 @@ func (u *UserData) Delete() error { return NewErrNotWhitelisted() } + return u.updateName(newName) +} + +// delete marks a user and all their aliases as deleted without checking controllers. +func (u *UserData) delete() error { + if u == nil { + return ErrUserNotExistOrDeleted + } + u.deleted = true chain.Emit(DeleteUserEvent, "address", u.addr.String()) return nil } +// Delete marks a user and all their aliases as deleted. +func (u *UserData) Delete() error { + if u == nil { + return ErrUserNotExistOrDeleted + } + + // Validate caller + if !controllers.Has(runtime.CurrentRealm().Address()) { + return NewErrNotWhitelisted() + } + + return u.delete() +} + // Validate validates username and address passed in // Most of the validation is done in the controllers // This provides more flexibility down the line diff --git a/examples/gno.land/r/sys/users/store_test.gno b/examples/gno.land/r/sys/users/store_test.gno index cb2c147b4ef..fa634c4b022 100644 --- a/examples/gno.land/r/sys/users/store_test.gno +++ b/examples/gno.land/r/sys/users/store_test.gno @@ -216,6 +216,12 @@ func TestDelete(t *testing.T) { }) } +func TestRegisterNotWhitelisted(t *testing.T) { + t.Run("register_not_whitelisted", func(t *testing.T) { + uassert.ErrorContains(t, RegisterUser(cross, alice, aliceAddr), "does not exist in whitelist") + }) +} + // cleanStore should not be needed, as vm store should be reset after each test. // Reference: https://github.com/gnolang/gno/issues/1982 func cleanStore(t *testing.T) { diff --git a/examples/gno.land/r/sys/users/users_test.gno b/examples/gno.land/r/sys/users/users_test.gno index 857a8fb20a1..91ac762b49b 100644 --- a/examples/gno.land/r/sys/users/users_test.gno +++ b/examples/gno.land/r/sys/users/users_test.gno @@ -201,6 +201,40 @@ func TestResolveAny(t *testing.T) { }) } +func TestProposeErrors(t *testing.T) { + t.Run("propose_register_user_errors", func(t *testing.T) { + urequire.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() { + ProposeRegisterUser("bad name", aliceAddr) + }) + urequire.PanicsWithMessage(t, ErrInvalidAddress.Error(), func() { + ProposeRegisterUser(alice, "badaddress") + }) + }) + + t.Run("propose_update_name_errors", func(t *testing.T) { + cleanStore(t) + + urequire.PanicsWithMessage(t, ErrInvalidAddress.Error(), func() { + ProposeUpdateName("badaddress", "alice1") + }) + urequire.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() { + ProposeUpdateName(aliceAddr, "bad name") + }) + // Note: unregistered user is not checked at proposal creation time. + // The callback handles it at execution time. + }) + + t.Run("propose_delete_user_errors", func(t *testing.T) { + cleanStore(t) + + urequire.PanicsWithMessage(t, ErrInvalidAddress.Error(), func() { + ProposeDeleteUser("badaddress") + }) + // Note: unregistered user is not checked at proposal creation time. + // The callback handles it at execution time. + }) +} + // TODO Uncomment after gnoweb /u/ page. //func TestUserRenderLink(t *testing.T) { // testing.SetOriginCaller(whitelistedCallerAddr) diff --git a/examples/gno.land/r/sys/validators/v2/gnosdk.gno b/examples/gno.land/r/sys/validators/v2/gnosdk.gno index 60b8f84966c..2e705a3bfba 100644 --- a/examples/gno.land/r/sys/validators/v2/gnosdk.gno +++ b/examples/gno.land/r/sys/validators/v2/gnosdk.gno @@ -1,16 +1,29 @@ package validators import ( + "math" + "gno.land/p/sys/validators" ) -// GetChanges returns the validator changes stored on the realm, since the given block number. -// This function is intended to be called by gno.land through the GnoSDK -func GetChanges(from int64) []validators.Validator { +// GetChanges returns the validator changes stored on the realm, +// for blocks in the [from, to] range (inclusive on both ends). +// If to >= math.MaxInt64, it is clamped to math.MaxInt64-1 to avoid overflow. +// Panics if from > to (after clamping). +// This function is intended to be called by gno.land through the GnoSDK. +func GetChanges(from, to int64) []validators.Validator { + if to > math.MaxInt64-1 { + to = math.MaxInt64 - 1 + } + if to < from { + panic("invalid range: from must be <= to") + } + valsetChanges := make([]validators.Validator, 0) - // Gather the changes from the specified block - changes.Iterate(getBlockID(from), "", func(_ string, value any) bool { + // Gather the changes in the [from, to] block range. + // AVL Iterate uses an exclusive end, so we pass to+1. + changes.Iterate(getBlockID(from), getBlockID(to+1), func(_ string, value any) bool { chs := value.([]change) for _, ch := range chs { diff --git a/examples/gno.land/r/sys/validators/v2/validators_test.gno b/examples/gno.land/r/sys/validators/v2/validators_test.gno index 763b17bdd65..497e09ede24 100644 --- a/examples/gno.land/r/sys/validators/v2/validators_test.gno +++ b/examples/gno.land/r/sys/validators/v2/validators_test.gno @@ -2,9 +2,11 @@ package validators import ( "chain/runtime" + "math" "testing" "gno.land/p/nt/avl/v0" + "gno.land/p/nt/poa/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/p/nt/ufmt/v0" @@ -49,7 +51,7 @@ func TestValidators_AddRemove(t *testing.T) { for i := initialHeight; i < initialHeight+int64(len(vals)); i++ { // Make sure the changes are saved - chs := GetChanges(i) + chs := GetChanges(i, initialHeight+int64(len(vals))) // We use the funky index calculation to make sure // changes are properly handled for each block span @@ -83,7 +85,7 @@ func TestValidators_AddRemove(t *testing.T) { for i := initialRemoveHeight; i < initialRemoveHeight+int64(len(vals)); i++ { // Make sure the changes are saved - chs := GetChanges(i) + chs := GetChanges(i, initialRemoveHeight+int64(len(vals))) // We use the funky index calculation to make sure // changes are properly handled for each block span @@ -99,3 +101,72 @@ func TestValidators_AddRemove(t *testing.T) { } } } + +// TestGetChanges_BoundedRange verifies that GetChanges(from, to) correctly +// returns only changes within the [from, to] block range. +func TestGetChanges_BoundedRange(t *testing.T) { + changes = avl.NewTree() + vp = poa.NewPoA() + + vals := generateTestValidators(3) + + // Store additions at block h1 + h1 := runtime.ChainHeight() + for _, val := range vals { + addValidator(val) + } + testing.SkipHeights(1) + + // Store removals at block h2 + h2 := runtime.ChainHeight() + for _, val := range vals { + removeValidator(val.Address) + } + testing.SkipHeights(1) + + // Query spanning both blocks returns all changes + all := GetChanges(h1, h2) + uassert.Equal(t, 6, len(all)) + + // Query for h1 only returns additions + atH1 := GetChanges(h1, h1) + uassert.Equal(t, 3, len(atH1)) + for i, ch := range atH1 { + uassert.Equal(t, vals[i].Address, ch.Address) + uassert.True(t, ch.VotingPower > 0) + } + + // Query for h2 only returns removals + atH2 := GetChanges(h2, h2) + uassert.Equal(t, 3, len(atH2)) + for i, ch := range atH2 { + uassert.Equal(t, vals[i].Address, ch.Address) + uassert.Equal(t, uint64(0), ch.VotingPower) + } + + // Query beyond stored range returns empty + uassert.Equal(t, 0, len(GetChanges(h2+1, h2+1))) +} + +func TestGetChanges_PanicsOnInvalidRange(t *testing.T) { + uassert.PanicsWithMessage(t, "invalid range: from must be <= to", func() { + GetChanges(10, 5) + }) +} + +func TestGetChanges_ClampsMaxInt64(t *testing.T) { + changes = avl.NewTree() + + vals := generateTestValidators(1) + + // Simulate a validator change at block math.MaxInt64-1 (the boundary value). + changes.Set(getBlockID(math.MaxInt64-1), []change{ + {blockNum: math.MaxInt64 - 1, validator: vals[0]}, + }) + + // Passing math.MaxInt64 as "to" means "get all updates from here onwards". + // The clamp (to = MaxInt64-1) must still include the boundary block. + result := GetChanges(math.MaxInt64-1, math.MaxInt64) + uassert.Equal(t, 1, len(result)) + uassert.Equal(t, vals[0].Address, result[0].Address) +} diff --git a/examples/gno.land/r/sys/validators/v3/doc.gno b/examples/gno.land/r/sys/validators/v3/doc.gno new file mode 100644 index 00000000000..a50eb8f688a --- /dev/null +++ b/examples/gno.land/r/sys/validators/v3/doc.gno @@ -0,0 +1,4 @@ +// Package validators implements on-chain validator set management through Proof of Authority. +// The realm exposes a public executor for GovDAO proposals to suggest validator set changes. +// Validator set changes are propagated out through the VM params keeper (v3 approach). +package validators diff --git a/examples/gno.land/r/sys/validators/v3/gnomod.toml b/examples/gno.land/r/sys/validators/v3/gnomod.toml new file mode 100644 index 00000000000..8a53ec14b8d --- /dev/null +++ b/examples/gno.land/r/sys/validators/v3/gnomod.toml @@ -0,0 +1,5 @@ +module = "gno.land/r/sys/validators/v3" +gno = "0.9" + +[addpkg] + creator = "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l" diff --git a/examples/gno.land/r/sys/validators/v3/init.gno b/examples/gno.land/r/sys/validators/v3/init.gno new file mode 100644 index 00000000000..ff294647b53 --- /dev/null +++ b/examples/gno.land/r/sys/validators/v3/init.gno @@ -0,0 +1,15 @@ +package validators + +import ( + "chain/params" + + "gno.land/p/nt/poa/v0" +) + +func init() { + // The default valset protocol is PoA. + vp = poa.NewPoA() + + // Explicitly initialize the previous valset as empty. + params.SetStrings(valsetPrevKey, []string{}) +} diff --git a/examples/gno.land/r/sys/validators/v3/poc.gno b/examples/gno.land/r/sys/validators/v3/poc.gno new file mode 100644 index 00000000000..9b366ad2715 --- /dev/null +++ b/examples/gno.land/r/sys/validators/v3/poc.gno @@ -0,0 +1,131 @@ +package validators + +import ( + "chain/params" + "chain/runtime" + "strings" + + "gno.land/p/nt/ufmt/v0" + "gno.land/p/sys/validators" + "gno.land/r/gov/dao" +) + +// Keep in sync with gno.land/pkg/gnoland/app.go +const ( + // newUpdatesAvailableKey is a flag indicating the chain valset should be updated. + // Set by the contract, but reset by the chain (EndBlocker). + newUpdatesAvailableKey = "new_updates_available" + + // valsetNewKey is the param that holds the new proposed valset. Set by the contract, + // and read (but never modified) by the chain. + valsetNewKey = "valset_new" + + // valsetPrevKey is the param that holds the latest applied valset. Initially set by + // the contract (init), but later only written by the chain (EndBlocker). + valsetPrevKey = "valset_prev" +) + +const errNoChangesProposed = "no set changes proposed" + +// NewValsetChangeExecutor creates a new GovDAO executor for proposing valset changes. +// changesFn is evaluated eagerly; the resulting slice is captured by the returned +// Executor. Evaluating lazily would close over the caller's ephemeral realm and +// fail to persist via dao.MustCreateProposal ("cannot persist function from the +// private realm"). +func NewValsetChangeExecutor(changesFn func() []validators.Validator) dao.Executor { + if changesFn == nil { + panic(errNoChangesProposed) + } + + changes := changesFn() + if len(changes) == 0 { + panic(errNoChangesProposed) + } + + callback := func(cur realm) error { + // Apply each change to the on-chain valset. + for _, change := range changes { + if change.VotingPower == 0 { + // Voting power == 0 means remove the validator. + removeValidator(change.Address) + continue + } + + // Otherwise, add or update the validator. + addValidator(change) + } + + // Propagate the new valset through the VM params keeper. + params.SetBool(newUpdatesAvailableKey, true) + params.SetStrings(valsetNewKey, serializeValset(vp.GetValidators())) + + return nil + } + + return dao.NewSimpleExecutor(callback, "") +} + +// serializeValset serializes the validator set for storage in the params keeper. +// Format: "
::" +func serializeValset(valset []validators.Validator) []string { + serialized := make([]string, 0, len(valset)) + + for _, v := range valset { + serialized = append(serialized, ufmt.Sprintf( + "%s:%s:%d", + v.Address.String(), + v.PubKey, + v.VotingPower, + )) + } + + return serialized +} + +// IsValidator returns true if the given address is part of the validator set. +func IsValidator(addr address) bool { + return vp.IsValidator(addr) +} + +// GetValidator returns the validator with the given address. +// Panics if the validator is not found. +func GetValidator(addr address) validators.Validator { + v, err := vp.GetValidator(addr) + if err != nil { + panic("validator not found") + } + + return v +} + +// GetValidators returns the current validator set. +func GetValidators() []validators.Validator { + return vp.GetValidators() +} + +// Render displays the current validator set. +func Render(string) string { + var ( + sb strings.Builder + h = runtime.ChainHeight() + set = GetValidators() + ) + + sb.WriteString(ufmt.Sprintf("## Valset at height %d\n\n", h)) + + if len(set) == 0 { + sb.WriteString("Valset is empty.\n") + return sb.String() + } + + for i, v := range set { + sb.WriteString(ufmt.Sprintf( + "- #%d: %s (%d)\n", + i, + v.Address.String(), + v.VotingPower, + )) + } + + return sb.String() +} diff --git a/examples/gno.land/r/sys/validators/v3/validators.gno b/examples/gno.land/r/sys/validators/v3/validators.gno new file mode 100644 index 00000000000..14f3a9e6b09 --- /dev/null +++ b/examples/gno.land/r/sys/validators/v3/validators.gno @@ -0,0 +1,23 @@ +package validators + +import ( + "gno.land/p/sys/validators" +) + +var vp validators.ValsetProtocol // vp is the underlying validator set protocol + +// addValidator adds a new validator to the validator set. +// If the validator is already present, the method panics. +func addValidator(validator validators.Validator) { + if _, err := vp.AddValidator(validator.Address, validator.PubKey, validator.VotingPower); err != nil { + panic(err) + } +} + +// removeValidator removes the given validator from the set. +// If the validator is not present in the set, the method panics. +func removeValidator(addr address) { + if _, err := vp.RemoveValidator(addr); err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/tests/vm/crossrealm_d/crossrealm.gno b/examples/gno.land/r/tests/vm/crossrealm_d/crossrealm.gno new file mode 100644 index 00000000000..d57af4f4e45 --- /dev/null +++ b/examples/gno.land/r/tests/vm/crossrealm_d/crossrealm.gno @@ -0,0 +1,38 @@ +package crossrealm_d + +// Simple stateful realm for cross-realm consistency tests. +// Separated from crossrealm_b to avoid perturbing its object IDs. +// +// Contains both crossing and non-crossing setters so tests can +// demonstrate that non-crossing calls from another realm cannot +// silently mutate state via assign+recover. + +var counter int + +func init() { + counter = 100 +} + +// SetCounter: non-crossing. Calling this cross-realm triggers +// the readonly check because it directly assigns a package var. +func SetCounter(n int) { + counter = n +} + +// SetCounterCrossing: crossing version. This works correctly +// cross-realm because the caller enters this realm's context. +func SetCounterCrossing(cur realm, n int) { + counter = n +} + +func GetCounter(cur realm) int { + return counter +} + +// DoubleCounter reads counter and doubles it. Used to show that +// if counter were silently corrupted in memory, subsequent crossing +// calls would act on the wrong value. +func DoubleCounter(cur realm) int { + counter = counter * 2 + return counter +} diff --git a/examples/gno.land/r/tests/vm/crossrealm_d/gnomod.toml b/examples/gno.land/r/tests/vm/crossrealm_d/gnomod.toml new file mode 100644 index 00000000000..13232d96d28 --- /dev/null +++ b/examples/gno.land/r/tests/vm/crossrealm_d/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/tests/vm/crossrealm_d" +gno = "0.9" diff --git a/examples/gno.land/r/tests/vm/crossrealm_e/crossrealm.gno b/examples/gno.land/r/tests/vm/crossrealm_e/crossrealm.gno new file mode 100644 index 00000000000..9edf92ec72b --- /dev/null +++ b/examples/gno.land/r/tests/vm/crossrealm_e/crossrealm.gno @@ -0,0 +1,41 @@ +package crossrealm_e + +import "chain/runtime" + +var ( + balance int64 + owner address +) + +func init() { + balance = 1000 + SetOwner(address("g1dao_address_here")) +} + +// SetOwner is an internal helper that was exported by mistake +// (should be setOwner). Without the pre-mutation readonly check, +// a cross-realm caller could call SetOwner + recover to silently +// hijack ownership in memory, then call TransferToken to steal funds. +func SetOwner(o address) { + owner = o +} + +func GetOwner() address { + return owner +} + +func TransferOwnership(cur realm, o address) { + if runtime.PreviousRealm().Address() != owner { + panic("unauthorized") + } + owner = o +} + +func TransferToken(cur realm) { + caller := runtime.PreviousRealm().Address() + if caller != owner { + panic("unauthorized") + } + balance -= 500 + println("===send token to: ", caller) +} diff --git a/examples/gno.land/r/tests/vm/crossrealm_e/gnomod.toml b/examples/gno.land/r/tests/vm/crossrealm_e/gnomod.toml new file mode 100644 index 00000000000..4f9b03a854c --- /dev/null +++ b/examples/gno.land/r/tests/vm/crossrealm_e/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/tests/vm/crossrealm_e" +gno = "0.9" diff --git a/examples/gno.land/r/tests/vm/subtests/subtests.gno b/examples/gno.land/r/tests/vm/subtests/subtests.gno index 48fa5060478..55961aae1a5 100644 --- a/examples/gno.land/r/tests/vm/subtests/subtests.gno +++ b/examples/gno.land/r/tests/vm/subtests/subtests.gno @@ -1,6 +1,9 @@ package subtests -import "chain/runtime" +import ( + "chain/banker" + "chain/runtime" +) func GetCurrentRealm(cur realm) runtime.Realm { return runtime.CurrentRealm() @@ -21,3 +24,11 @@ func CallAssertOriginCall(cur realm) { func CallIsOriginCall(cur realm) bool { return runtime.PreviousRealm().IsUser() } + +func RealmSentCoins(cur realm) string { + return cur.SentCoins().String() +} + +func BankerOriginSend(cur realm) string { + return banker.OriginSend().String() +} diff --git a/examples/gno.land/r/tests/vm/tests.gno b/examples/gno.land/r/tests/vm/tests.gno index f249a26b8d1..796eb17a94f 100644 --- a/examples/gno.land/r/tests/vm/tests.gno +++ b/examples/gno.land/r/tests/vm/tests.gno @@ -1,6 +1,7 @@ package tests import ( + "chain/banker" "chain/runtime" "gno.land/p/demo/nestedpkg" @@ -120,3 +121,19 @@ func IsCallerParentPath(cur realm) bool { func HasCallerSameNamespace(cur realm) bool { return nestedpkg.IsSameNamespace() } + +func RealmSentCoins(cur realm) string { + return cur.SentCoins().String() +} + +func BankerOriginSend(cur realm) string { + return banker.OriginSend().String() +} + +func RTestsRealmSentCoins(cur realm) string { + return rsubtests.RealmSentCoins(cross) +} + +func RTestsOriginSend(cur realm) string { + return rsubtests.BankerOriginSend(cross) +} diff --git a/examples/gno.land/r/tests/vm/variadic/gnomod.toml b/examples/gno.land/r/tests/vm/variadic/gnomod.toml new file mode 100644 index 00000000000..218b22e4de7 --- /dev/null +++ b/examples/gno.land/r/tests/vm/variadic/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/tests/vm/variadic" +gno = "0.9" diff --git a/examples/gno.land/r/tests/vm/variadic/main.gno b/examples/gno.land/r/tests/vm/variadic/main.gno new file mode 100644 index 00000000000..4f0f1c4df9b --- /dev/null +++ b/examples/gno.land/r/tests/vm/variadic/main.gno @@ -0,0 +1,28 @@ +package variadic + +import "strings" + +func Echo(cur realm, vals ...string) string { + return strings.Join(vals, " ") +} + +func Add(cur realm, nums ...int) int { + res := 0 + + for _, num := range nums { + res += num + } + + return res +} + +func And(cur realm, booleans ...bool) bool { + + for _, boolean := range booleans { + if !boolean { + return false + } + } + + return true +} diff --git a/examples/gno.land/r/tests/vm/z4_filetest.gno b/examples/gno.land/r/tests/vm/z4_filetest.gno new file mode 100644 index 00000000000..fb3f9eb23a5 --- /dev/null +++ b/examples/gno.land/r/tests/vm/z4_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "chain" + "testing" + + tests "gno.land/r/tests/vm" +) + +func main() { + testing.SetOriginSend(chain.Coins{{Denom: "foo", Amount: 42}, {Denom: "ugnot", Amount: 100}}) + println("tests.RealmSentCoins: " + tests.RealmSentCoins(cross)) + println("tests.BankerOriginSend: " + tests.BankerOriginSend(cross)) + + // subtests is cross-called from tests, so cur.SentCoins() returns empty there. + println("tests.RTestsRealmSentCoins: " + tests.RTestsRealmSentCoins(cross)) + // banker.OriginSend() always returns the origin send regardless of call + // depth. + println("tests.RTestsOriginSend: " + tests.RTestsOriginSend(cross)) +} + +// Output: +// tests.RealmSentCoins: 42foo,100ugnot +// tests.BankerOriginSend: 42foo,100ugnot +// tests.RTestsRealmSentCoins: +// tests.RTestsOriginSend: 42foo,100ugnot diff --git a/gno.land/adr/pr5511_chain_upgrade_genesis_replay.md b/gno.land/adr/pr5511_chain_upgrade_genesis_replay.md new file mode 100644 index 00000000000..e7e8d1ce093 --- /dev/null +++ b/gno.land/adr/pr5511_chain_upgrade_genesis_replay.md @@ -0,0 +1,243 @@ +# PR5511: Chain upgrade genesis replay + +## Context + +gno.land needs to support in-place chain hardforks: halt the source +chain at some height `H`, export its full state + transaction history, +and start a new chain whose genesis includes all that history so the +new chain can reach the same state by replaying it from scratch. After +replay, the new chain starts producing fresh blocks at height `H + 1`. + +Historical transactions were signed against the source chain's +`chain_id`, `account_number`, and `sequence`. For signatures to verify +during replay, all three must be available in their original form — we +can't re-sign because we don't have the private keys. + +The tm2-level consensus / state / app changes that enable +`InitialHeight > 1` live in +[tm2/adr/pr5511_initial_height.md](../../tm2/adr/pr5511_initial_height.md). + +Earlier design iterations used a single `OriginalChainID` field +(simpler, but fragile across multi-hop upgrades). This ADR describes +the final design with `PastChainIDs` + per-tx `ChainID`. + +## Decision + +### `GnoTxMetadata` — per-tx replay metadata + +Populated by the hardfork export tool (`gnogenesis fork generate`): + +- **`Timestamp`** (`int64`) — Unix timestamp of the original block. + When non-zero, overrides the block header time during replay. +- **`BlockHeight`** (`int64`) — original block height. When `> 0`, the + ctx's block header height is set to this value during replay, which + makes the ante handler treat the tx as non-genesis (full sig + verification, real account numbers, sequences). +- **`ChainID`** (`string`) — originating chain ID. Used for per-tx + chain-ID override during replay if `ChainID ∈ GnoGenesisState.PastChainIDs`. +- **`Failed`** (`bool`) — true if the tx had a non-zero return code on + the source chain. Failed txs are included in the genesis for + sequence-tracking purposes but are NOT re-executed during replay + (re-executing could double-spend or succeed unexpectedly if a VM fix + makes them now pass). The replay emits a non-empty `ResponseDeliverTx` + with an error marker so indexers don't mistake the skip for success. +- **`SignerInfo`** (`[]SignerAccountInfo`) — per-signer `(Address, + AccountNum, Sequence)`. Before each historical tx is delivered, the + replay loop force-sets each signer's account number and pre-tx + sequence from this. If the account doesn't exist yet, + `auth.NewAccountWithNumber` creates it with the specified number, + bypassing the auto-increment counter. +- **`GasUsed`**, **`GasWanted`** (`int64`) — source-chain gas; used by + `GasReplayMode="source"` and the replay report. + +### `GnoGenesisState` — genesis-level replay configuration + +- **`PastChainIDs`** (`[]string`) — allowlist of chain IDs from which + historical transactions originated. Only chain IDs in this slice can + override the context chain ID during replay. `PastChainIDs[0]` is + also used for sig verification of genesis-mode txs (no metadata or + `BlockHeight == 0`) when a hardfork is in progress, since those txs + were signed against the source chain. Empty = no overrides. + `PastChainIDs` MAY contain the current chain ID — this is valid for + same-chain-ID hardforks (e.g. minor fork with no external identity + change). Do NOT add validation that rejects this. +- **`InitialHeight`** (`int64`) — new chain's starting block height. + Cross-checked against `GenesisDoc.InitialHeight` via + `RequestInitChain.InitialHeight`; `loadAppState` rejects the genesis + on divergence. +- **`GasReplayMode`** (`string`) — historical-tx gas metering: + - `""` or `"strict"` (default) — new VM's gas meter is authoritative. + Historical txs may fail if gas requirements changed between chains. + - `"source"` — historical txs (`BlockHeight > 0`) bypass the new VM's + gas meter via `auth.SkipGasMeteringKey`, preserving source-chain + outcomes even when gas metering changed. Response records + `metadata.GasUsed` for audit. + +### Sequence recovery algorithm (`gnogenesis fork generate`) + +Account numbers come from one RPC call per address at halt height — +they're stable, never change once assigned. + +Sequences are harder: they advance through both genesis-mode txs and +successful historical txs, but failed txs sometimes consume a sequence +(msg-fail) and sometimes don't (ante-fail). We can't tell the two apart +without re-verifying the signature, and the tx bytes don't carry a +"sequence used" field. + +The tool uses a single-pass algorithm with buffered brute-force: + +1. **Initialisation**: for each signer, query their current sequence + at halt height (`finalSeq`). Brute-force their first successful + historical tx's signature against `[0, finalSeq]` — the matching + value is the signer's starting counter (typically `0`, or `N` if + they had `N` genesis-mode txs). + +2. **Forward pass**, block-ordered: + - **Successful tx, no pending failures**: assign current counter as + pre-tx sequence, increment counter. + - **Failed tx**: buffer it. + - **Successful tx after failed txs from same signer**: brute-force + the successful tx's signature against `[counter, counter + len(buffer)]`. + The matching value is this tx's pre-tx sequence; work backwards to + assign sequences to each buffered failed tx (ante-fails keep the + counter, msg-fails consume one). + +3. **Trailing failures** (no subsequent success): query sequence at + halt height and diff against the last known counter. + +Signature verification is offline — `GetSignBytes` only needs +`chain_id`, `account_number`, `sequence`, fee, msgs, memo, all of +which are available from the tx bytes + metadata. + +### Genesis replay flow + +1. `InitChain` → `loadAppState` validates + `state.InitialHeight == req.InitialHeight` (if set) and that + `GasReplayMode` is recognised. +2. Genesis-mode txs (no metadata or `BlockHeight == 0`) → current + genesis behaviour: package deploys, infinite gas, auto-account + creation. Sig-verify against `PastChainIDs[0]` when a hardfork is + in progress (these txs were signed with the source chain ID). +3. Historical txs (`BlockHeight > 0`) → full ante handler. For each: + a. Ctx's block header height is overridden to `metadata.BlockHeight`. + b. If `metadata.ChainID ∈ PastChainIDs`, ctx's chain ID is + overridden for sig verification. + c. If `metadata.Timestamp != 0`, ctx's time is overridden. + d. If `SignerInfo` is present, each signer's account number and + pre-tx sequence are force-set. + e. If `GasReplayMode == "source"`, ctx carries + `auth.SkipGasMeteringKey` so `auth.SetGasMeter` installs an + infinite gas meter for this tx. + f. If `Failed`, skip `Deliver` and emit an error-marker response. + Otherwise `Deliver` normally. +4. At end of loop, the replay report is emitted via logger: summary + counts (`ok` / `ok_gas_differs` / `failed` / `skipped_failed`) and + per-failure detail. +5. Consensus advances `state.LastBlockHeight` to + `GenesisDoc.InitialHeight - 1` so the next block is produced at + `InitialHeight`. + +### Replay report + +`replayReport` accumulates per-tx outcomes and emits via the `slog` +logger at the end of `InitChain`. Categories: +- `ok` — succeeded, gas matched source (or no source gas recorded). +- `ok_gas_differs` — succeeded but gas consumption differs from source. +- `failed` — delivery failed (detail logged per-failure). +- `skipped_failed` — marked `Failed` in metadata, correctly skipped. + +Summary counts at info level; each failure gets its own warn line with +source height, gas delta, and error. `replayReport.Outcomes()` exposes +the data for tooling that wants to write a structured +`replay-report.json`. + +### Tooling — `gnogenesis fork` + +Integrated into the existing `gnogenesis` CLI as a subcommand group +(`contribs/gnogenesis/internal/fork/`): + +- **`gnogenesis fork generate`** — reads the source (RPC URL / local + dir / tarball), runs sequence recovery, emits a ready-to-replay + genesis with `PastChainIDs`, `InitialHeight`, and per-tx metadata. +- **`--patch-realm PKGPATH=SRCDIR`** (repeatable, on `generate`) — + rewrites the genesis-mode `addpkg` tx for `PKGPATH` in-place with + files from `SRCDIR` before writing. The source genesis on disk is + untouched. This is the only way to land a realm code change as part + of a fork (you can't re-`addpkg` post-deploy). +- **`gnogenesis fork test`** — in-process `InitChain` smoke-test + against a genesis.json. + +A chain-specific wrapper (`misc/deployments/gnoland-1/generate-genesis.sh`) +hardcodes the gnoland1→gnoland-1 chain IDs and delegates to the CLI. + +## Alternatives considered + +1. **Re-sign all transactions** — requires access to all private keys. + Not feasible. +2. **Skip sig verification entirely** — reduces security guarantees. +3. **Single `OriginalChainID string`** — simpler but fragile; assumes + all historical txs come from one chain, breaks for multi-hop + upgrades (chain A → B → C). `PastChainIDs` + per-tx `ChainID` + handles the multi-hop case cleanly. +4. **Absorb `hardfork` tool as a standalone CLI in `misc/`** — + original design, but it's really a genesis-manipulation tool, so + it lives with its siblings under `gnogenesis`. + +## Consequences + +- Genesis files for chain upgrades are large (all historical txs with + metadata). ~192 MB for gnoland1 → gnoland-1 at halt height 704052. +- `GnoGenesisState.InitialHeight`, `GnoGenesisState.GasReplayMode`, + and all the `GnoTxMetadata` fields use `omitempty`; existing genesis + files are unaffected. +- Future chain A → B → C upgrades can set + `PastChainIDs: ["A", "B"]` to replay both predecessors' histories. + +## Bugs found and fixed during review + +### App layer + +- **Failed-tx `ResponseDeliverTx` was empty (looked like success)** — + now carries an explicit error marker so indexers can distinguish. +- **`state.InitialHeight` wasn't cross-checked against + `GenesisDoc.InitialHeight`** — `RequestInitChain.InitialHeight` plumbed + through, `loadAppState` validates match. + +### Tooling (`gnogenesis fork`) + +- **`applyOverlay` silent no-op** — listed scripts but didn't execute + them, returned success. Entire overlay mechanism removed (dead code). +- **JSONL export used `encoding/json` instead of amino** — lost + interface types (`std.Msg`) on round-trip. Both writer and reader + now use amino. +- **`verifyGenesisFile` failure returned success** — now aborts + (opt out with `--no-verify`). +- **Zero unit tests for `bruteForceSignerSequence`** — added 10 + table-driven tests. + +### Docs linter (side fix to unblock CI) + +- Added `staging.gno.land` + `archive.org` to skip list, added retry + with backoff and HTTP timeout so transient external-host failures + don't block unrelated PRs. + +## Known unfixed (follow-up PRs) + +1. **RPC source has no retry/resume.** A single transient error aborts + the entire multi-block fetch. Needs exponential backoff + + checkpointing. +2. **All txs accumulated in memory.** Full tx history is held in a + single slice — will OOM on large chains. Needs streaming writer. +3. **`NewAccountWithNumber` has no duplicate check.** Pre-flight + validation in `loadAppState` is the recommended approach (see PR + discussion). +4. **`queryAccountAtHeight` silent nil.** All error paths return nil + with no indication; flaky RPC → wrong sequence metadata. + +## Validation + +End-to-end test via the hf-glue testbed +([#5486](https://github.com/gnolang/gno/pull/5486)): production-sized +hardfork genesis (~192 MB, 2715 historical txs, +`InitialHeight = 704053`) replays with **0 tx failures** and boots a +live `gnoland-1` node producing fresh blocks. diff --git a/gno.land/adr/pr5589_test13_hardfork_launch.md b/gno.land/adr/pr5589_test13_hardfork_launch.md new file mode 100644 index 00000000000..f6d15c0704b --- /dev/null +++ b/gno.land/adr/pr5589_test13_hardfork_launch.md @@ -0,0 +1,190 @@ +# ADR: test-13 hardfork launch + +## Context + +[PR #5511](https://github.com/gnolang/gno/pull/5511) shipped the +replay-engine primitives (`PastChainIDs`, `GnoTxMetadata`, +`InitialHeight`, the hardfork-aware ante handler). [PR +#5486](https://github.com/gnolang/gno/pull/5486) shipped `hf-glue`, the +testbed that drives those primitives end-to-end. [PR +#5485](https://github.com/gnolang/gno/pull/5485) shipped +`r/sys/validators/v3`, the params-keeper-based valset flow that +replaces v2's event-collector path. + +This ADR is the launch playbook for **test-13**, the first concrete +chain to exercise all three: a hardfork of `gnoland1` that halts it at +a chosen height, rebuilds the genesis with its history preserved, and +resumes as `test-13` — with the v3 valset flow live from the first +post-fork block. + +Unlike the referenced ADRs, this one is not proposing a gno primitive. +It records the set of operational decisions we took _as a consumer_ of +those primitives so that: + +- a reviewer of the commit series understands why each rc exists; +- a future hardfork (test-14, gnoland-2.0) can reuse the same pattern; +- a later operator reading state reports can tell which post-fork + divergence from source was intentional vs what would be a bug. + +## Decision + +### 1. rc-stacking branch model + +Work ships as a sequence of focused rc branches rather than a single +growing one: + +``` +chain/gnoland1 → test13-base → test13-rc1 → rc2 → rc3 → rc4 → rc5 → rc6 +``` + +Each rc is a strict superset of the previous and introduces one +coherent concern (see `misc/deployments/test13.gno.land/PR-DESCRIPTION.md` +for the per-rc delta list). Older rcs stay reachable on the `aeddi` +remote — a reviewer bisecting a regression between rc4 and rc5 can +check out rc4 and reproduce against it, unchanged. + +Cost: the rc number becomes a load-bearing identifier in commit +messages and in external communication. That's cheap; in exchange we +get a cleaner review surface and a real history of what the launch +prep actually looked like. + +### 2. Migration sequence 01 → 08 + +Post-history migration txs, applied to the replayed genesis in order, +each signed by the current sole-T1 member under +`--skip-genesis-sig-verification`: + +| Step | Purpose | Why it exists | +| ------ | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **01** | `r/sys/validators/v2.NewPropRequest` — remove pre-fork validators, add the post-fork validator(s) | Keeps v2's in-realm valset view in sync with the new `GenesisDoc.Validators`. v2 is vestigial post-#5485 (EndBlocker no longer reads it), but tooling that still queries v2 should not return a ghost of the pre-fork world. | +| **02** | `r/gov/dao/v3/impl.NewAddMemberRequest(NEW_T1_ADDR, T1, …)` signed by manfred | Manfred is the single T1 at halt height. Before removing him we must add the post-fork T1 so the DAO never has zero T1 members (would brick governance). | +| **03** | `r/gov/dao/v3/impl.NewWithdrawMemberRequest(manfred, …)` — manfred proposes his own removal, votes YES | With two T1 members (manfred + NEW_T1_ADDR), one YES vote is 50% — not enough to execute. The proposal sits in `ACTIVE` state waiting for the second vote. | +| **04** | NEW_T1_ADDR votes YES on the open withdraw proposal + executes | Second YES brings approval to 100%. Proposal executes, manfred is removed, NEW_T1_ADDR becomes the sole T1. | +| **05** | `r/sys/params.NewSysParamStringPropRequest("vm","p","sysnames_pkgpath","")` — govDAO proposal | v3 needs to be addpkg'd under `r/sys/*`. `r/sys/names` is enabled at halt height and rejects any addpkg whose namespace doesn't equal the caller's address (no real address satisfies `sys`). Clearing the VM param lets `checkNamespacePermission` short-circuit at `sysNamesPkg == ""`. | +| **06** | `MsgAddPackage` for `r/sys/validators/v3`, creator=manfred | Deploys v3 itself. Runs while the namespace check is disabled. | +| **07** | Restore `vm:p:sysnames_pkgpath` to `gno.land/r/sys/names` | Re-enables the namespace check so subsequent `r/sys/*` addpkgs on the chain go through the same authorisation path they did on mainnet. | +| **08** | Set `vm:p:valset_realm_path` to `gno.land/r/sys/validators/v3` | Without this, EndBlocker reads the param as the empty string (mainnet state predates the field), picks an invalid realm path, and silently drops every v3 valset update. Can't be folded into v3's `init()` because the VM param lives outside the realm's scope. | + +The ordering is constrained: step 01 requires v2 to still exist, steps +02–04 require a two-member DAO window, step 06 requires the namespace +check to be off and is bracketed by 05 / 07 to keep that window +minimal. Step 08 must run after step 06 (the realm has to exist before +we point the param at it). Beyond those constraints the current order +is the simplest linearisation. + +### 3. Replay posture + +- `--skip-genesis-sig-verification` is **required** on every validator + at launch. Without it the first gnoland1 tx whose signature can't be + reconstructed (e.g. because its `SignerInfo` wasn't exported) panics + the chain at InitChain. +- `--skip-failing-genesis-txs` is **required** on every validator at + launch. With `PanicOnFailingTxResultHandler` (the default), the + ~2580 expected `InsufficientFundsError` failures would take the + chain down; with `NoopGenesisTxResultHandler` they are counted and + skipped. +- `app_state.gas_replay_mode` stays empty/`strict`. `source` mode + (bypass the new VM gas meter for historical txs) would not recover + the InsufficientFunds failures — those fire at + `auth.DeductFees` _before_ the gas meter is consulted. See + `scripts/compare-gas-modes.sh` for the A/B result; delta is zero. + +### 4. Intentional divergences from source + +Every post-fork state that differs from the gnoland1 state at halt +height is either recorded as intentional here or is a bug. + +- **Balance top-ups** via `hf_topup_balance` in `scripts/migrate.sh`. + Currently one entry: `g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l` + +1 Gugnot, for storage-deposit headroom on its 7 `r/sys/*` genesis + addpkgs. Every new top-up is logged to + `out/TOPUP-REPORT.md`; the auditable rule is "any balance in that + report is expected to diverge, any balance not in that report and + not zero-on-source must match source". +- **`r/sys/validators/v2` is frozen cosmetic state.** EndBlocker + ignores it (PR #5485). Migration 01 aims to bring v2 in sync, but it + is known to partially apply when the removal batch hits an address + already removed by governance history (v2's batched prop panics the + whole batch on the first miss). Consensus is unaffected; valset + authority is tm2 + v3. +- **Proposal IDs post-fork are contiguous with mainnet's series.** + Mainnet at halt height had proposals up to id N; test-13 continues + at id N+1. Our migrations create several of those ids during + replay. +- **govDAO membership is rotated to a single post-fork T1** + (`NEW_T1_ADDR`). Known fragility — a single-EOA T1 is the minimum + viable and should be expanded before real production. Not a launch + blocker for test-13 (testnet); would be for a gnoland-2.0. + +### 5. Launch verification + +Before calling a genesis shippable, run on the final build: + +1. `make verify-reproducibility` — two independent builds produce + identical SHA256 on the same host. +2. `make verify-txs-jsonl` — the cached historical tx set matches + source-chain `total_txs` at halt height, plus random spot-checks. +3. `make smoketest` — `gnogenesis fork test` completes; failure count + matches the documented baseline (2605 for the gnoland1 → test-13 + case). +4. Boot the genesis on a multi-node cluster, then: + - `make assert-migrations` — every migration's intended effect + landed. + - `make state-diff` — rendered state diff against mainnet at halt + height has no unexpected divergences. + - `make audit-balances` — diverged accounts match the ones + documented in `out/TOPUP-REPORT.md` (or the policy has been + updated to accept new divergences). + - `make audit-realm-imports` — every import in the deployed + realms resolves against the running stdlib. + +All steps are non-interactive and exit non-zero on failure, so a +simple Makefile chain (or CI job) can gate the launch build. + +## Consequences + +- **Silent failures are bounded.** Every divergence surface has a + check; an operator who runs all of §5 on the build they're about to + ship sees every documented drift line up with the check output, and + nothing else. +- **Recovery from mid-replay kill is bootable.** The `defaultStore` + `AddMemPackage` path is not crash-consistent (index in pebble + body + in IAVL, two separate writes). The rc5 nil-skip in + `PreprocessAllFilesAndSaveBlockNodes` makes a partially-persisted + store still boot; any missed realms surface as VM errors when + first called, not as boot-time crash loops. The proper atomic-write + fix belongs upstream. +- **`halt_height` is one-shot-per-param.** After the halt fires the + in-memory `BaseApp.haltHeight` is set but is not re-applied on + process restart; operators restarting the same chain post-halt will + resume past the halt. Intentional: after a coordinated halt the next + action is to rebuild genesis on the new chain id, not to resume the + old chain. +- **`/genesis` RPC serves the full ~200 MB genesis in one response.** + Large-response handling is a known tm2 weakness; clients that + disconnect mid-write trigger a recoverable panic on the server. + Validators should not expose `/genesis` publicly; distribute the + file out-of-band (GitHub release, IPFS) and point clients at that. + +## Alternatives considered + +- **`source` gas-replay mode** would skip the new VM's gas meter for + historical txs. On the test-13 genesis it eliminates zero failures + (measured with `scripts/compare-gas-modes.sh`) because the failures + fire in the ante handler. Rejected for this launch; would add audit + complexity with no payoff. +- **In-place `AddValidator` power update** would simplify + `change-power.sh`. PoA's `AddValidator` panics on duplicate address + ("validator must not be in the set already"). Rejected (upstream + semantic change). `change-power.sh` uses an atomic remove+add in one + proposal so the EndBlocker diff covers the transition without an + intermediate absence. +- **Single un-stacked rc branch** would remove the rc-number + bookkeeping. Rejected — losing the ability to bisect against older + rcs is a real cost when debugging a late-breaking regression. +- **Fold v3 addpkg into the pre-history genesis-mode txs** (instead of + migration 06) would avoid the sysnames disable/restore dance. + Requires modifying the source genesis set before replay starts, and + that set is deterministically derived from the source chain — editing + it would desynchronise our SHA from independent rebuilds. Rejected; + the 05/07 wrap is cheaper. diff --git a/gno.land/adr/prxxxx_valset_params.md b/gno.land/adr/prxxxx_valset_params.md new file mode 100644 index 00000000000..72bc617e2dc --- /dev/null +++ b/gno.land/adr/prxxxx_valset_params.md @@ -0,0 +1,84 @@ +# ADR: Valset Updates via VM Params Keeper (v3) + +## Context + +This PR introduces the third iteration of on-chain validator set management +(`r/sys/validators/v3`). Previous iterations: + +- **v1**: Valset changes emitted as events, caught by `EndBlocker`. +- **v2**: Events triggered a VM query in `EndBlocker` to scrape on-chain state + via `GetChanges(from, to)` — still event-driven. + +Both v1 and v2 required the `EndBlocker` to: +1. Listen to on-chain events via an event collector (`collector[validatorUpdate]`). +2. Call back into the GnoVM to fetch the actual changes (v2), or parse event + payloads (v1). + +Problems with those approaches: +- **Coupling**: The `EndBlocker` needed a `VMKeeperI` reference solely to query + the valset realm. +- **Fragility**: Regex-based parsing of typed GnoVM response strings. +- **Indirection**: An event triggers a VM query, which returns data that was + already computed on-chain. + +## Decision + +Replace the event-based approach with a **params-keeper-based** approach: + +1. The valset realm (`r/sys/validators/v3`) writes changes directly into the + VM params keeper under realm-scoped keys. +2. `EndBlocker` reads those keys from the params keeper, computes the diff + between `valset_prev` and `valset_new`, and propagates the changes to + consensus. + +### Params keys (prefix: `vm:gno.land/r/sys/validators/v3:`) + +| Key | Written by | Read by | Description | +|------------------------|-------------|-------------|--------------------------------------------| +| `new_updates_available`| realm | EndBlocker | Flag: set true when valset changed | +| `valset_new` | realm | EndBlocker | Serialized proposed valset | +| `valset_prev` | EndBlocker | EndBlocker | Serialized previously applied valset | + +Serialization format: `::` + +### Valset diff + +A new `ValidatorUpdates.UpdatesFrom(v2)` method on `tm2/pkg/bft/abci/types` +computes the minimal diff between two validator sets: +- Additions: in v2 but not prev. +- Removals: in prev but not v2 (emitted with `Power=0`). +- Power changes: in both but with different power. + +### Validation + +`WillSetParam` in `VMKeeper` validates `valset_new` updates at write time, +ensuring each entry is well-formed (address/pubkey match, valid power). + +The `EndBlocker` still filters out updates with disallowed pubkey types. + +### Active valset realm path + +The realm path is configurable via `vm:p:valset_realm_path` (default: +`gno.land/r/sys/validators/v3`). This allows future upgrades without changing +the `EndBlocker` code. + +## Alternatives Considered + +- **Keep v2 approach**: Simpler for the realm (no params awareness) but + requires `EndBlocker` to call back into the VM. Rejected because of the + coupling and fragility. +- **ABCI events with typed payloads**: Would require extending the GnoVM's + event system with typed values. More invasive; params keeper already exists. + +## Consequences + +**Positive**: +- `EndBlocker` no longer needs a `VMKeeperI` reference. +- No regex parsing of VM responses. +- Validation happens at write time (fail fast). +- Realm path is configurable. + +**Negative / Tradeoffs**: +- The realm must know about the params keeper API. +- The param keys must be kept in sync between the realm and `app.go`. +- Existing v2 realm/chain state is not migrated (v3 is a fresh start). diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 55e23fa76e3..738d6fe343d 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -148,7 +148,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { &c.logLevel, "log-level", zapcore.DebugLevel.String(), - "log level for the gnoland node,", + "log level for the gnoland node (debug, info, warn, error)", ) fs.StringVar( @@ -264,6 +264,7 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { cfg.Application, evsw, logger, + cfg.BaseConfig.SkipUpgradeHeight, ) if err != nil { return fmt.Errorf("unable to create the Gnoland app, %w", err) diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 41c0020c8b1..10ea4ebbe15 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoweb" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/tm2/pkg/commands" "go.uber.org/zap" @@ -77,7 +78,11 @@ func main() { Name: "gnoweb", ShortUsage: "gnoweb [flags] [path ...]", ShortHelp: "runs gno.land web interface", - LongHelp: `gnoweb web interface`, + LongHelp: `gnoweb web interface + +Environment variables: + GNOWEB_BANNER_TEXT Banner content (supports inline markdown). Max 400 chars. + GNOWEB_BANNER_URL Optional link for the banner (requires GNOWEB_BANNER_TEXT).`, }, &cfg, func(ctx context.Context, args []string) error { @@ -235,6 +240,19 @@ func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) { appcfg.UnsafeHTML = cfg.html appcfg.FaucetURL = cfg.faucetURL + // Parse banner from env + if text := os.Getenv("GNOWEB_BANNER_TEXT"); text != "" { + bannerURL := os.Getenv("GNOWEB_BANNER_URL") + banner, err := components.NewBannerData(text, bannerURL) + if err != nil { + logger.Warn("invalid banner markdown, banner disabled", "error", err) + } else { + appcfg.Banner = banner + } + } else if os.Getenv("GNOWEB_BANNER_URL") != "" { + logger.Warn("GNOWEB_BANNER_URL is set but GNOWEB_BANNER_TEXT is empty; banner will not be shown") + } + if cfg.noDefaultAliases { appcfg.Aliases = map[string]gnoweb.AliasTarget{} } diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index af8b35e4179..03b0cf0c73e 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -8,6 +8,7 @@ import ( "path/filepath" "slices" "strconv" + "strings" "time" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -41,6 +42,7 @@ type AppOptions struct { EventSwitch events.EventSwitch // required VMOutput io.Writer // optional SkipGenesisSigVerification bool // default to verify genesis transactions + SkipUpgradeHeight int64 // if set, skip the halt_min_version check at this height InitChainerConfig // options related to InitChainer MinGasPrices string // optional PruneStrategy types.PruneStrategy @@ -114,6 +116,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { prmk.Register(auth.ModuleName, acck) prmk.Register(bank.ModuleName, bankk) prmk.Register(vm.ModuleName, vmk) + prmk.Register("node", nodeParamsKeeper{}) // Set InitChainer icc := cfg.InitChainerConfig @@ -175,19 +178,12 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { } }) - // Set up the event collector - c := newCollector[validatorUpdate]( - cfg.EventSwitch, // global event switch filled by the node - validatorEventFilter, // filter fn that keeps the collector valid - ) - // Set EndBlocker baseApp.SetEndBlocker( EndBlocker( - c, + prmk, acck, gpk, - vmk, baseApp, ), ) @@ -208,6 +204,11 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { vmk.Initialize(cfg.Logger, ms) ms.MultiWrite() // XXX why was't this needed? + // Verify node startup constraints set by governance halt proposals. + if err := checkNodeStartupParams(prmk, baseApp.GetCacheMultiStore(), baseApp.LastBlockHeight(), cfg.SkipUpgradeHeight); err != nil { + return nil, err + } + return baseApp, nil } @@ -233,6 +234,7 @@ func NewApp( appCfg *sdkCfg.AppConfig, evsw events.EventSwitch, logger *slog.Logger, + skipUpgradeHeight int64, ) (abci.Application, error) { var err error @@ -245,6 +247,7 @@ func NewApp( }, MinGasPrices: appCfg.MinGasPrices, SkipGenesisSigVerification: genesisCfg.SkipSigVerification, + SkipUpgradeHeight: skipUpgradeHeight, PruneStrategy: appCfg.PruneStrategy, } if genesisCfg.SkipFailingTxs { @@ -313,7 +316,7 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh // load app state. AppState may be nil mostly in some minimal testing setups; // so log a warning when that happens. - txResponses, err := cfg.loadAppState(ctx, req.AppState) + txResponses, err := cfg.loadAppState(ctx, req.AppState, req.InitialHeight) if err != nil { return abci.ResponseInitChain{ ResponseBase: abci.ResponseBase{ @@ -350,12 +353,34 @@ func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { msCache.MultiWrite() } -func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { +func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any, reqInitialHeight int64) ([]abci.ResponseDeliverTx, error) { state, ok := appState.(GnoGenesisState) if !ok { return nil, fmt.Errorf("invalid AppState of type %T", appState) } + // If GnoGenesisState.InitialHeight is set, it must match the authoritative + // GenesisDoc.InitialHeight (which comes in via req.InitialHeight). These + // fields are duplicated so tooling can read the app-level one; if they + // diverge, the genesis file is malformed. + if state.InitialHeight != 0 && state.InitialHeight != reqInitialHeight { + return nil, fmt.Errorf( + "InitialHeight mismatch: GnoGenesisState.InitialHeight=%d, GenesisDoc.InitialHeight=%d", + state.InitialHeight, reqInitialHeight, + ) + } + + if err := validateGasReplayMode(state.GasReplayMode); err != nil { + return nil, err + } + + if len(state.PastChainIDs) > 0 { + ctx.Logger().Info("Chain upgrade genesis replay", + "past_chain_ids", state.PastChainIDs, + "initial_height", reqInitialHeight, + ) + } + cfg.bankk.InitGenesis(ctx, state.Bank) // Apply genesis balances. for _, bal := range state.Balances { @@ -392,9 +417,10 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci // Replay genesis txs. txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) + report := newReplayReport(state.GasReplayMode) // Run genesis txs - for _, tx := range state.Txs { + for txIdx, tx := range state.Txs { var ( stdTx = tx.Tx metadata = tx.Metadata @@ -404,18 +430,88 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci // Check if there is metadata associated with the tx if metadata != nil { - // Create a custom context modifier ctxFn = func(ctx sdk.Context) sdk.Context { - // Create a copy of the header, in - // which only the timestamp information is modified header := ctx.BlockHeader().(*bft.Header).Copy() - header.Time = time.Unix(metadata.Timestamp, 0) + if metadata.Timestamp != 0 { + header.Time = time.Unix(metadata.Timestamp, 0) + } + if metadata.BlockHeight > 0 { + header.Height = metadata.BlockHeight + } + + ctx = ctx.WithBlockHeader(header) + + // For historical txs (BlockHeight > 0), override the chain ID + // for signature verification using the per-tx ChainID, provided + // it is in the genesis allowlist. This allows replaying txs from + // multiple past chains during a hard fork. + if metadata.BlockHeight > 0 && metadata.ChainID != "" && isPastChainID(state.PastChainIDs, metadata.ChainID) { + ctx = ctx.WithChainID(metadata.ChainID) + } + + // GasReplayMode="source": bypass the new VM's gas meter for + // historical txs so outcomes match the source chain even when + // gas metering changed. + if state.GasReplayMode == "source" && metadata.BlockHeight > 0 { + ctx = ctx.WithValue(auth.SkipGasMeteringKey{}, true) + } - // Save the modified header - return ctx.WithBlockHeader(header) + return ctx } } + // Genesis-mode txs (no metadata or BlockHeight == 0) were signed with + // the original chain ID. During a hardfork (PastChainIDs is set), we + // need to verify their signatures against the original chain ID, not + // the new one. Use the first PastChainID as the signing context. + if (metadata == nil || metadata.BlockHeight == 0) && len(state.PastChainIDs) > 0 { + originalChainID := state.PastChainIDs[0] + ctxFn = func(ctx sdk.Context) sdk.Context { + return ctx.WithChainID(originalChainID) + } + } + + // For historical txs with signer metadata, force-set account state + // so signature verification succeeds even if prior txs diverged. + // Uses pre-tx sequence — the value the signature was signed with. + // + // Invariant: SignerInfo is only populated by the export tool for historical + // txs (BlockHeight > 0). Genesis-mode txs (BlockHeight == 0) must never + // carry SignerInfo — if they did, the force-set would corrupt fresh account + // state. The BlockHeight > 0 guard enforces this. + if metadata != nil && metadata.BlockHeight > 0 && len(metadata.SignerInfo) > 0 { + for _, si := range metadata.SignerInfo { + acc := cfg.acck.GetAccount(ctx, si.Address) + if acc == nil { + // Account doesn't exist yet — create with specific account + // number, bypassing the auto-increment counter. + acc = cfg.acck.NewAccountWithNumber(ctx, si.Address, si.AccountNum) + } else { + acc.SetAccountNumber(si.AccountNum) + } + acc.SetSequence(si.Sequence) + cfg.acck.SetAccount(ctx, acc) + } + } + + // Failed txs: pre-tx sequence already set above. Skip execution — + // re-executing failed txs could cause double spends or unexpected + // behavior if the VM fix makes them succeed. The next tx's force-set + // will handle the correct sequence state. + // Response carries an explicit error so downstream consumers + // (indexers, explorers) don't mistake a skipped failed tx for a + // successful one. + if metadata != nil && metadata.Failed { + txResponses = append(txResponses, abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Error: abci.StringError("replay skipped: tx failed on source chain"), + Log: "genesis replay: skipped failed tx from source chain", + }, + }) + report.record(txIdx, metadata, 0, 0, replayCategorySkippedFailed, nil) + continue + } + res := cfg.baseApp.Deliver(stdTx, ctxFn) if res.IsErr() { ctx.Logger().Error( @@ -431,9 +527,19 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci GasWanted: res.GasWanted, GasUsed: res.GasUsed, }) + report.recordDeliverResult(txIdx, metadata, res) cfg.GenesisTxResultHandler(ctx, stdTx, res) } + + if reqInitialHeight > 1 { + ctx.Logger().Info("Genesis replay complete, chain will start from initial height", + "initial_height", reqInitialHeight, + ) + } + + report.emit(ctx.Logger()) + return txResponses, nil } @@ -444,23 +550,54 @@ type endBlockerApp interface { // Logger returns the logger reference Logger() *slog.Logger + + // SetHaltHeight sets the block height at which the node will halt. + SetHaltHeight(uint64) +} + +// isPastChainID reports whether chainID is present in the pastChainIDs allowlist. +func isPastChainID(pastChainIDs []string, chainID string) bool { + return slices.Contains(pastChainIDs, chainID) +} + +// Keep in sync with examples/gno.land/r/sys/validators/v3/poc.gno +const ( + vmModulePrefix = "vm" + + // newUpdatesAvailableKey is a flag indicating the chain valset should be updated. + // Set by the contract, but reset by the chain (EndBlocker). + newUpdatesAvailableKey = "new_updates_available" + + // valsetNewKey is the param that holds the new proposed valset. Set by the contract, + // and read (but never modified) by the chain. + valsetNewKey = "valset_new" + + // valsetPrevKey is the param that holds the latest applied valset. Initially set by + // the contract (init), but later only written by the chain (EndBlocker). + valsetPrevKey = "valset_prev" +) + +// valsetParamPath constructs the full param key for a valset-realm-scoped param: +// +// vm:: +func valsetParamPath(valsetRealm, key string) string { + return fmt.Sprintf("%s:%s:%s", vmModulePrefix, valsetRealm, key) } // EndBlocker defines the logic executed after every block. -// Currently, it parses events that happened during execution to calculate -// validator set changes +// It reads valset changes from the VM params keeper, checks for a +// governance-requested chain halt, and propagates updates to consensus. func EndBlocker( - collector *collector[validatorUpdate], + prmk params.ParamsKeeperI, acck auth.AccountKeeperI, gpk auth.GasPriceKeeperI, - vmk vm.VMKeeperI, app endBlockerApp, ) func( ctx sdk.Context, req abci.RequestEndBlock, ) abci.ResponseEndBlock { - return func(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock { - // set the auth params value in the ctx. The EndBlocker will use InitialGasPrice in + return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + // Set the auth params value in the ctx. The EndBlocker will use InitialGasPrice in // the params to calculate the updated gas price. if acck != nil { ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acck.GetParams(ctx)) @@ -469,64 +606,104 @@ func EndBlocker( auth.EndBlocker(ctx, gpk) } - // Check if there was a valset change - if len(collector.getEvents()) == 0 { - // No valset updates + // Check if GovDAO has requested a halt at this height. + // Use == (not >=) so we only trigger once: at the exact halt height. + // SetHaltHeight causes BeginBlock of the *next* block to panic, ensuring + // this block is fully committed before the node stops. + // On restart, req.Height > halt_height, so == never re-fires — no infinite loop. + if prmk != nil { + var haltHeight int64 + prmk.GetInt64(ctx, nodeParamHaltHeight, &haltHeight) + if haltHeight > 0 && req.Height == haltHeight { + app.Logger().Info( + "GovDAO halt height reached, will halt after this block", + "height", req.Height, + "halt_height", haltHeight, + ) + app.SetHaltHeight(uint64(haltHeight)) + } + } + + // Determine which realm is responsible for valset management. + valsetRealm := vm.ValsetRealmDefault + prmk.GetString(ctx, vm.ValsetRealmParamPath, &valsetRealm) + + // Check if there are any pending valset changes. + updatesAvailable := false + prmk.GetBool(ctx, valsetParamPath(valsetRealm, newUpdatesAvailableKey), &updatesAvailable) + + if !updatesAvailable { return abci.ResponseEndBlock{} } - // Run the VM to get the updates from the chain - response, err := vmk.QueryEval( - ctx, - valRealm, - fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), + var ( + prevValset []string + proposedValset []string + + prevValsetPath = valsetParamPath(valsetRealm, valsetPrevKey) + proposedValsetPath = valsetParamPath(valsetRealm, valsetNewKey) ) + + prmk.GetStrings(ctx, prevValsetPath, &prevValset) + prmk.GetStrings(ctx, proposedValsetPath, &proposedValset) + + // Parse the previous set. + prevSet, err := extractUpdatesFromParams(prevValset) if err != nil { - app.Logger().Error("unable to call VM during EndBlocker", "err", err) + app.Logger().Error( + "unable to parse prev valset in EndBlocker", + "err", err, + ) return abci.ResponseEndBlock{} } - // Extract the updates from the VM response - updates, err := extractUpdatesFromResponse(response) + // Parse the proposed set. + proposedSet, err := extractUpdatesFromParams(proposedValset) if err != nil { - app.Logger().Error("unable to extract updates from response", "err", err) + app.Logger().Error( + "unable to parse proposed valset in EndBlocker", + "err", err, + ) return abci.ResponseEndBlock{} } + // Compute the diff between prev and proposed. + updates := prevSet.UpdatesFrom(proposedSet) + + app.Logger().Info( + "valset changes to be applied", + "count", len(updates), + ) + + // Advance prevValset to match proposedValset. + prmk.SetStrings(ctx, prevValsetPath, proposedValset) + + // Clear the pending-updates flag. + prmk.SetBool(ctx, valsetParamPath(valsetRealm, newUpdatesAvailableKey), false) + allowedKeyTypes := ctx.ConsensusParams().Validator.PubKeyTypeURLs - // Filter out the updates that are not valid + // Filter out updates that fail consensus-level validation. updates = slices.DeleteFunc(updates, func(u abci.ValidatorUpdate) bool { - // Make sure the power is valid - if u.Power < 0 { - app.Logger().Error( - "valset update invalid; voting power < 0", - "address", u.Address.String(), - "power", u.Power, - ) - - return true // delete it + // Power == 0 means removal; skip further validation for removals. + if u.Power == 0 { + return false } - // Make sure the public key matches the address - if u.PubKey.Address().Compare(u.Address) != 0 { + // Make sure the public key is an allowed consensus key type. + if !slices.Contains(allowedKeyTypes, amino.GetTypeURL(u.PubKey)) { app.Logger().Error( - "valset update invalid; pubkey + address mismatch", + "valset update invalid; unsupported pubkey type", "address", u.Address.String(), - "pubkey", u.PubKey.String(), + "pubkey_type", amino.GetTypeURL(u.PubKey), ) return true // delete it } - // Make sure the public key is an allowed consensus key type - if !slices.Contains(allowedKeyTypes, amino.GetTypeURL(u.PubKey)) { - return true // delete it - } - - return false // keep it, update is valid + return false // keep it }) return abci.ResponseEndBlock{ @@ -535,52 +712,44 @@ func EndBlocker( } } -// extractUpdatesFromResponse extracts the validator set updates -// from the VM response. +// extractUpdatesFromParams parses serialized validator updates from the params keeper. +// Each entry is expected to be in the form: // -// This method is not ideal, but currently there is no mechanism -// in place to parse typed VM responses -func extractUpdatesFromResponse(response string) ([]abci.ValidatorUpdate, error) { - // Find the submatches - matches := valRegexp.FindAllStringSubmatch(response, -1) - if len(matches) == 0 { - // No changes to extract - return nil, nil - } - - updates := make([]abci.ValidatorUpdate, 0, len(matches)) - for _, match := range matches { - var ( - addressRaw = match[1] - pubKeyRaw = match[2] - powerRaw = match[3] - ) +//
:: +// +// A voting power of 0 indicates a validator removal. +func extractUpdatesFromParams(changes []string) (abci.ValidatorUpdates, error) { + updates := make(abci.ValidatorUpdates, 0, len(changes)) + + for _, change := range changes { + parts := strings.Split(change, ":") + if len(parts) != 3 { + return nil, fmt.Errorf( + "valset update is not in the format
::, but %q", + change, + ) + } - // Parse the address - address, err := crypto.AddressFromBech32(addressRaw) + address, err := crypto.AddressFromBech32(parts[0]) if err != nil { - return nil, fmt.Errorf("unable to parse address, %w", err) + return nil, fmt.Errorf("invalid validator address: %w", err) } - // Parse the public key - pubKey, err := crypto.PubKeyFromBech32(pubKeyRaw) + pubKey, err := crypto.PubKeyFromBech32(parts[1]) if err != nil { - return nil, fmt.Errorf("unable to parse public key, %w", err) + return nil, fmt.Errorf("invalid validator pubkey: %w", err) } - // Parse the voting power - power, err := strconv.ParseInt(powerRaw, 10, 64) + votingPower, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { - return nil, fmt.Errorf("unable to parse voting power, %w", err) + return nil, fmt.Errorf("invalid voting power: %w", err) } - update := abci.ValidatorUpdate{ + updates = append(updates, abci.ValidatorUpdate{ Address: address, PubKey: pubKey, - Power: power, - } - - updates = append(updates, update) + Power: votingPower, + }) } return updates, nil diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 8ccfb0de6c3..6ba80b0aef6 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -2,10 +2,9 @@ package gnoland import ( "context" - "errors" "fmt" "path/filepath" - "strings" + "sort" "testing" "time" @@ -14,7 +13,6 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/stdlibs/chain" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" bftCfg "github.com/gnolang/gno/tm2/pkg/bft/config" @@ -143,11 +141,29 @@ func TestNewAppWithOptions_ErrNoDB(t *testing.T) { assert.ErrorContains(t, err, "no db provided") } +func TestNewAppWithOptions_ErrNoLogger(t *testing.T) { + t.Parallel() + + opts := TestAppOptions(memdb.NewMemDB()) + opts.Logger = nil + _, err := NewAppWithOptions(opts) + assert.ErrorContains(t, err, "no logger provided") +} + +func TestNewAppWithOptions_ErrNoEventSwitch(t *testing.T) { + t.Parallel() + + opts := TestAppOptions(memdb.NewMemDB()) + opts.EventSwitch = nil + _, err := NewAppWithOptions(opts) + assert.ErrorContains(t, err, "no event switch provided") +} + func TestNewApp(t *testing.T) { // NewApp should have good defaults and manage to run InitChain. td := t.TempDir() - app, err := NewApp(td, NewTestGenesisAppConfig(), config.DefaultAppConfig(), events.NewEventSwitch(), log.NewNoopLogger()) + app, err := NewApp(td, NewTestGenesisAppConfig(), config.DefaultAppConfig(), events.NewEventSwitch(), log.NewNoopLogger(), 0) require.NoError(t, err, "NewApp should be successful") resp := app.InitChain(abci.RequestInitChain{ @@ -296,6 +312,18 @@ func createAndSignTx( ) std.Tx { t.Helper() + return createAndSignTxWithAccSeq(t, msgs, chainID, key, 0, 0) +} + +func createAndSignTxWithAccSeq( + t *testing.T, + msgs []std.Msg, + chainID string, + key crypto.PrivKey, + accNum, seq uint64, +) std.Tx { + t.Helper() + tx := std.Tx{ Msgs: msgs, Fee: std.Fee{ @@ -304,7 +332,7 @@ func createAndSignTx( }, } - signBytes, err := tx.GetSignBytes(chainID, 0, 0) + signBytes, err := tx.GetSignBytes(chainID, accNum, seq) require.NoError(t, err) // Sign the tx @@ -358,6 +386,24 @@ func TestInitChainer_MetadataTxs(t *testing.T) { VM: vm.DefaultGenesisState(), } } + + getZeroTimestampMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState { + return GnoGenesisState{ + // Metadata present but Timestamp=0 — genesis block time should be preserved + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: 0, // zero — must not override to Unix epoch + }, + }, + }, + Balances: balances, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + } + } ) testTable := []struct { @@ -378,6 +424,12 @@ func TestInitChainer_MetadataTxs(t *testing.T) { laterTimestamp, getMetadataState, }, + { + "metadata transaction with zero timestamp uses genesis block time", + currentTimestamp, + currentTimestamp, // zero Timestamp → falls back to genesis block time + getZeroTimestampMetadataState, + }, } for _, testCase := range testTable { @@ -484,421 +536,196 @@ func TestInitChainer_MetadataTxs(t *testing.T) { func TestEndBlocker(t *testing.T) { t.Parallel() - constructVMResponse := func(updates []abci.ValidatorUpdate) string { - var builder strings.Builder - - builder.WriteString("(slice[") - - for i, update := range updates { - builder.WriteString( - fmt.Sprintf( - "(struct{(%q std.Address),(%q string),(%d uint64)} gno.land/p/sys/validators.Validator)", - update.Address, - update.PubKey, - update.Power, - ), - ) - - if i < len(updates)-1 { - builder.WriteString(",") - } - } - - builder.WriteString("] []gno.land/p/sys/validators.Validator)") - - return builder.String() - } - - newCommonEvSwitch := func() *mockEventSwitch { - var cb events.EventCallback - - return &mockEventSwitch{ - addListenerFn: func(_ string, callback events.EventCallback) { - cb = callback - }, - fireEventFn: func(event events.Event) { - cb(event) - }, - } - } - - t.Run("no collector events", func(t *testing.T) { - t.Parallel() - - noFilter := func(_ events.Event) []validatorUpdate { - return []validatorUpdate{} - } - - // Create the collector - c := newCollector[validatorUpdate](&mockEventSwitch{}, noFilter) - - // Create the EndBlocker - eb := EndBlocker(c, nil, nil, nil, &mockEndBlockerApp{}) - - // Run the EndBlocker - res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ - Validator: &abci.ValidatorParams{ - PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, - }, - }), abci.RequestEndBlock{}) - - // Verify the response was empty - assert.Equal(t, abci.ResponseEndBlock{}, res) - }) - - t.Run("invalid VM call", func(t *testing.T) { + t.Run("no valset changes", func(t *testing.T) { t.Parallel() var ( - noFilter = func(_ events.Event) []validatorUpdate { - return make([]validatorUpdate, 1) // 1 update - } - - vmCalled bool - - mockEventSwitch = newCommonEvSwitch() - - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { - vmCalled = true - - require.Equal(t, valRealm, pkgPath) - require.NotEmpty(t, expr) - - return "", errors.New("random call error") + mockParamsKeeper = &mockParamsKeeper{ + getStringFn: func(_ sdk.Context, key string, ptr *string) { + // valset realm lookup - return default + }, + getBoolFn: func(_ sdk.Context, key string, ptr *bool) { + // updatesAvailable stays false (default) }, } - ) - // Create the collector - c := newCollector[validatorUpdate](mockEventSwitch, noFilter) - - // Fire a GnoVM event - mockEventSwitch.FireEvent(chain.Event{}) + mockApp = &mockEndBlockerApp{} + ) - // Create the EndBlocker - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(mockParamsKeeper, nil, nil, mockApp) - // Run the EndBlocker res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, }, }), abci.RequestEndBlock{}) - // Verify the response was empty assert.Equal(t, abci.ResponseEndBlock{}, res) - - // Make sure the VM was called - assert.True(t, vmCalled) }) - t.Run("empty VM response", func(t *testing.T) { + t.Run("invalid valset changes in prev", func(t *testing.T) { t.Parallel() var ( - noFilter = func(_ events.Event) []validatorUpdate { - return make([]validatorUpdate, 1) // 1 update - } - - vmCalled bool - - mockEventSwitch = newCommonEvSwitch() - - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { - vmCalled = true - - require.Equal(t, valRealm, pkgPath) - require.NotEmpty(t, expr) - - return constructVMResponse([]abci.ValidatorUpdate{}), nil + updateFlag = true + paramUpdates []string + + mockParamsKeeper = &mockParamsKeeper{ + getStringFn: func(_ sdk.Context, key string, ptr *string) {}, + getStringsFn: func(_ sdk.Context, key string, ptr *[]string) { + switch key { + case valsetParamPath(vm.ValsetRealmDefault, valsetPrevKey): + *ptr = []string{"totally invalid format"} + case valsetParamPath(vm.ValsetRealmDefault, valsetNewKey): + // empty proposed set + } + }, + getBoolFn: func(_ sdk.Context, key string, ptr *bool) { + if key == valsetParamPath(vm.ValsetRealmDefault, newUpdatesAvailableKey) { + *ptr = updateFlag + } + }, + setBoolFn: func(_ sdk.Context, key string, value bool) { + updateFlag = value + }, + setStringsFn: func(_ sdk.Context, key string, value []string) { + paramUpdates = value }, } - ) - - // Create the collector - c := newCollector[validatorUpdate](mockEventSwitch, noFilter) - // Fire a GnoVM event - mockEventSwitch.FireEvent(chain.Event{}) + mockApp = &mockEndBlockerApp{} + ) - // Create the EndBlocker - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(mockParamsKeeper, nil, nil, mockApp) - // Run the EndBlocker res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, }, }), abci.RequestEndBlock{}) - // Verify the response was empty assert.Equal(t, abci.ResponseEndBlock{}, res) - - // Make sure the VM was called - assert.True(t, vmCalled) + // Flag was not cleared, updates were not saved + assert.True(t, updateFlag) + assert.Empty(t, paramUpdates) }) - t.Run("multiple valset updates", func(t *testing.T) { + t.Run("valid valset changes", func(t *testing.T) { t.Parallel() - var ( - changes = generateValidatorUpdates(t, 100) - - mockEventSwitch = newCommonEvSwitch() - - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { - require.Equal(t, valRealm, pkgPath) - require.NotEmpty(t, expr) - - return constructVMResponse(changes), nil - }, - } - ) - - // Create the collector - c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) - - // Construct the GnoVM events - vmEvents := make([]abci.Event, 0, len(changes)) - for index := range changes { - event := chain.Event{ - Type: validatorAddedEvent, - PkgPath: valRealm, - } - - // Make half the changes validator removes - if index%2 == 0 { - changes[index].Power = 0 - - event = chain.Event{ - Type: validatorRemovedEvent, - PkgPath: valRealm, - } - } - - vmEvents = append(vmEvents, event) - } - - // Fire the tx result event - txEvent := bft.EventTx{ - Result: bft.TxResult{ - Response: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Events: vmEvents, - }, - }, - }, - } - - mockEventSwitch.FireEvent(txEvent) + updates := generateValidatorUpdates(t, 10) - // Create the EndBlocker - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) - - // Run the EndBlocker - res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ - Validator: &abci.ValidatorParams{ - PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, - }, - }), abci.RequestEndBlock{}) - - // Verify the response was not empty - require.Len(t, res.ValidatorUpdates, len(changes)) - - for index, update := range res.ValidatorUpdates { - assert.Equal(t, changes[index].Address, update.Address) - assert.True(t, changes[index].PubKey.Equals(update.PubKey)) - assert.Equal(t, changes[index].Power, update.Power) + serializeUpdate := func(u abci.ValidatorUpdate) string { + return fmt.Sprintf("%s:%s:%d", u.Address.String(), u.PubKey, u.Power) } - }) - - t.Run("negative power filtered out", func(t *testing.T) { - t.Parallel() var ( - keys = generateDummyKeys(t, 2) - - validUpdate = abci.ValidatorUpdate{ - Address: keys[0].PubKey().Address(), - PubKey: keys[0].PubKey(), - Power: 1, - } - - invalidUpdate = abci.ValidatorUpdate{ - Address: keys[1].PubKey().Address(), - PubKey: keys[1].PubKey(), - Power: -1, // Invalid negative power - } - - updates = []abci.ValidatorUpdate{validUpdate, invalidUpdate} - - mockEventSwitch = newCommonEvSwitch() - - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { - require.Equal(t, valRealm, pkgPath) - require.NotEmpty(t, expr) - - return constructVMResponse(updates), nil + updateFlag = true + paramUpdates []string + + mockParamsKeeper = &mockParamsKeeper{ + getStringFn: func(_ sdk.Context, key string, ptr *string) {}, + getStringsFn: func(_ sdk.Context, key string, ptr *[]string) { + switch key { + case valsetParamPath(vm.ValsetRealmDefault, valsetPrevKey): + *ptr = []string{} // empty prev set + case valsetParamPath(vm.ValsetRealmDefault, valsetNewKey): + serialized := make([]string, 0, len(updates)) + for _, u := range updates { + serialized = append(serialized, serializeUpdate(u)) + } + *ptr = serialized + } }, - } - - vmEvents = []abci.Event{ - chain.Event{ - Type: validatorAddedEvent, - PkgPath: valRealm, + getBoolFn: func(_ sdk.Context, key string, ptr *bool) { + if key == valsetParamPath(vm.ValsetRealmDefault, newUpdatesAvailableKey) { + *ptr = updateFlag + } }, - chain.Event{ - Type: validatorAddedEvent, - PkgPath: valRealm, + setBoolFn: func(_ sdk.Context, key string, value bool) { + updateFlag = value }, - } - txEvent = bft.EventTx{ - Result: bft.TxResult{ - Response: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Events: vmEvents, - }, - }, + setStringsFn: func(_ sdk.Context, key string, value []string) { + paramUpdates = value }, } + + mockApp = &mockEndBlockerApp{} ) - c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) - mockEventSwitch.FireEvent(txEvent) + eb := EndBlocker(mockParamsKeeper, nil, nil, mockApp) - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, }, }), abci.RequestEndBlock{}) - require.Len(t, res.ValidatorUpdates, 1) - assert.Equal(t, validUpdate.Address, res.ValidatorUpdates[0].Address) - assert.Equal(t, validUpdate.Power, res.ValidatorUpdates[0].Power) - }) - - t.Run("pubkey address mismatch filtered out", func(t *testing.T) { - t.Parallel() - - var ( - keys = generateDummyKeys(t, 3) - - validUpdate = abci.ValidatorUpdate{ - Address: keys[0].PubKey().Address(), - PubKey: keys[0].PubKey(), - Power: 1, - } - invalidUpdate = abci.ValidatorUpdate{ - Address: keys[1].PubKey().Address(), // Address from key1 - PubKey: keys[2].PubKey(), // PubKey from key2 (mismatch) - Power: 1, - } - - updates = []abci.ValidatorUpdate{validUpdate, invalidUpdate} - - mockEventSwitch = newCommonEvSwitch() - - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { - require.Equal(t, valRealm, pkgPath) - require.NotEmpty(t, expr) + require.Len(t, res.ValidatorUpdates, len(updates)) - return constructVMResponse(updates), nil - }, - } - - vmEvents = []abci.Event{ - chain.Event{ - Type: validatorAddedEvent, - PkgPath: valRealm, - }, - chain.Event{ - Type: validatorAddedEvent, - PkgPath: valRealm, - }, - } - txEvent = bft.EventTx{ - Result: bft.TxResult{ - Response: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Events: vmEvents, - }, - }, - }, - } - ) + // Sort both for comparison + sort.Slice(updates, func(i, j int) bool { + return updates[i].Address.Compare(updates[j].Address) < 0 + }) + sort.Slice(res.ValidatorUpdates, func(i, j int) bool { + return res.ValidatorUpdates[i].Address.Compare(res.ValidatorUpdates[j].Address) < 0 + }) - c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) - mockEventSwitch.FireEvent(txEvent) - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) - res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ - Validator: &abci.ValidatorParams{ - PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, - }, - }), abci.RequestEndBlock{}) + for i, u := range updates { + assert.Equal(t, u.Address.String(), res.ValidatorUpdates[i].Address.String()) + assert.True(t, u.PubKey.Equals(res.ValidatorUpdates[i].PubKey)) + assert.Equal(t, u.Power, res.ValidatorUpdates[i].Power) + } - // Verify only the valid update is returned - require.Len(t, res.ValidatorUpdates, 1) - assert.Equal(t, validUpdate.Address, res.ValidatorUpdates[0].Address) - assert.True(t, validUpdate.PubKey.Equals(res.ValidatorUpdates[0].PubKey)) + // Flag cleared, prev updated + assert.False(t, updateFlag) + assert.NotEmpty(t, paramUpdates) }) - t.Run("wrong pubkey type", func(t *testing.T) { + t.Run("wrong pubkey type filtered out", func(t *testing.T) { t.Parallel() - var ( - key1 = getDummyKey(t) - - updates = []abci.ValidatorUpdate{ - { - Address: key1.PubKey().Address(), - PubKey: key1.PubKey(), - Power: 1, - }, - } - - mockEventSwitch = newCommonEvSwitch() + updates := generateValidatorUpdates(t, 1) - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { - require.Equal(t, valRealm, pkgPath) - require.NotEmpty(t, expr) + serializeUpdate := func(u abci.ValidatorUpdate) string { + return fmt.Sprintf("%s:%s:%d", u.Address.String(), u.PubKey, u.Power) + } - return constructVMResponse(updates), nil + var ( + updateFlag = true + + mockParamsKeeper = &mockParamsKeeper{ + getStringFn: func(_ sdk.Context, key string, ptr *string) {}, + getStringsFn: func(_ sdk.Context, key string, ptr *[]string) { + switch key { + case valsetParamPath(vm.ValsetRealmDefault, valsetPrevKey): + *ptr = []string{} + case valsetParamPath(vm.ValsetRealmDefault, valsetNewKey): + *ptr = []string{serializeUpdate(updates[0])} + } }, - } - txEvent = bft.EventTx{ - Result: bft.TxResult{ - Response: abci.ResponseDeliverTx{ - ResponseBase: abci.ResponseBase{ - Events: []abci.Event{ - chain.Event{ - Type: validatorAddedEvent, - PkgPath: valRealm, - }, - }, - }, - }, + getBoolFn: func(_ sdk.Context, key string, ptr *bool) { + if key == valsetParamPath(vm.ValsetRealmDefault, newUpdatesAvailableKey) { + *ptr = updateFlag + } }, + setBoolFn: func(_ sdk.Context, _ string, value bool) { updateFlag = value }, + setStringsFn: func(_ sdk.Context, _ string, _ []string) {}, } + + mockApp = &mockEndBlockerApp{} ) - c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) - mockEventSwitch.FireEvent(txEvent) - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(mockParamsKeeper, nil, nil, mockApp) + res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ - PubKeyTypeURLs: []string{"/tm.PubKeyEd25519"}, + PubKeyTypeURLs: []string{"/tm.PubKeyEd25519"}, // wrong type }, }), abci.RequestEndBlock{}) - // Verify only the valid update is returned - require.Len(t, res.ValidatorUpdates, 0) + // The update is filtered out due to wrong pubkey type + assert.Empty(t, res.ValidatorUpdates) }) } @@ -1107,19 +934,12 @@ func newGasPriceTestApp(t *testing.T) abci.Application { }, ) - // Set up the event collector - c := newCollector[validatorUpdate]( - cfg.EventSwitch, // global event switch filled by the node - validatorEventFilter, // filter fn that keeps the collector valid - ) - // Set EndBlocker baseApp.SetEndBlocker( EndBlocker( - c, + prmk, acck, gpk, - nil, baseApp, ), ) @@ -1261,6 +1081,7 @@ func TestPruneStrategyNothing(t *testing.T) { appCfg, events.NewEventSwitch(), log.NewNoopLogger(), + 0, ) require.NoError(t, err) @@ -1315,3 +1136,1217 @@ func TestPruneStrategyNothing(t *testing.T) { err = db.Close() require.NoError(t, err) } + +func TestChainUpgradeGenesisReplay(t *testing.T) { + t.Parallel() + + t.Run("fields serialize correctly", func(t *testing.T) { + t.Parallel() + + state := GnoGenesisState{ + Balances: []Balance{}, + Txs: []TxWithMetadata{}, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"old-chain-1", "old-chain-2"}, + InitialHeight: 100, + } + + // Serialize and deserialize + data, err := amino.MarshalJSON(state) + require.NoError(t, err) + + var decoded GnoGenesisState + require.NoError(t, amino.UnmarshalJSON(data, &decoded)) + + assert.Equal(t, []string{"old-chain-1", "old-chain-2"}, decoded.PastChainIDs) + assert.Equal(t, int64(100), decoded.InitialHeight) + }) + + t.Run("historical tx replays with correct block height", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path = "gno.land/r/demo/upgradetest" + body = `package upgradetest + +import "chain/runtime" + +var height int64 = runtime.ChainHeight() + +func GetHeight(cur realm) int64 { return height } +` + ) + + // Create a fresh app instance + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + // Prepare the deploy transaction + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "upgradetest", + Path: path, + Files: []*std.MemFile{ + { + Name: "file.gno", + Body: body, + }, + { + Name: "gnomod.toml", + Body: gnolang.GenGnoModLatest(path), + }, + }, + }, + MaxDeposit: nil, + } + + // Sign with the old chain ID — metadata.BlockHeight > 0 and metadata.ChainID + // in PastChainIDs will cause the ctxFn to override the chain ID for sig verification. + // Account number=0 and sequence=0 because the account is created from balances + // but hasn't processed any transactions yet. + tx := createAndSignTx(t, []std.Msg{msg}, "old-chain", key) + + // Run InitChain with PastChainIDs and InitialHeight set, + // and the deploy tx using metadata with BlockHeight=42 and ChainID="old-chain" + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + InitialHeight: 100, + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 42, + ChainID: "old-chain", // must be in PastChainIDs for override + }, + }, + }, + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"old-chain"}, + InitialHeight: 100, + }, + }) + + // Call GetHeight to verify the realm captured height=42 + callMsg := vm.MsgCall{ + Caller: key.PubKey().Address(), + PkgPath: path, + Func: "GetHeight", + } + + callTx := createAndSignTx(t, []std.Msg{callMsg}, chainID, key) + + marshalledTx, err := amino.Marshal(callTx) + require.NoError(t, err) + + resp := app.DeliverTx(abci.RequestDeliverTx{ + Tx: marshalledTx, + }) + + require.True(t, resp.IsOK(), "DeliverTx failed: %s", resp.Log) + + // The realm should have captured block height 42 + assert.Contains(t, string(resp.Data), "(42 int64)") + }) + + t.Run("metadata block height in GnoTxMetadata serializes correctly", func(t *testing.T) { + t.Parallel() + + txm := TxWithMetadata{ + Tx: std.Tx{}, + Metadata: &GnoTxMetadata{ + Timestamp: 1234567890, + BlockHeight: 42, + ChainID: "gnoland1", + }, + } + + data, err := amino.MarshalJSON(txm) + require.NoError(t, err) + + var decoded TxWithMetadata + require.NoError(t, amino.UnmarshalJSON(data, &decoded)) + + require.NotNil(t, decoded.Metadata) + assert.Equal(t, int64(1234567890), decoded.Metadata.Timestamp) + assert.Equal(t, int64(42), decoded.Metadata.BlockHeight) + assert.Equal(t, "gnoland1", decoded.Metadata.ChainID) + }) + + t.Run("chain ID not overridden when BlockHeight is zero in metadata", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path = "gno.land/r/demo/chainidtest" + body = `package chainidtest + +var Deployed = true + +func IsDeployed(cur realm) bool { return Deployed } +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "chainidtest", + Path: path, + Files: []*std.MemFile{ + {Name: "file.gno", Body: body}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path)}, + }, + }, + } + + // When metadata.BlockHeight == 0, the chain ID override must NOT happen. + // So the tx must be signed with the current chain ID (chainID), not any past chain ID. + tx := createAndSignTx(t, []std.Msg{msg}, chainID, key) + + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 0, // zero — no chain ID override + ChainID: "old-chain", // present but ignored since BlockHeight == 0 + }, + }, + }, + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"old-chain"}, // set, but should NOT be used since BlockHeight == 0 + }, + }) + }) + + t.Run("no chain ID override when metadata.ChainID not in PastChainIDs", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path = "gno.land/r/demo/nooverride" + body = `package nooverride + +var Deployed = true +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "nooverride", + Path: path, + Files: []*std.MemFile{ + {Name: "file.gno", Body: body}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path)}, + }, + }, + } + + // BlockHeight > 0 and metadata.ChainID is set, but the chain ID is NOT in + // PastChainIDs — no chain ID override should happen. The tx is signed with + // chainID so it verifies correctly without the override. + tx := createAndSignTx(t, []std.Msg{msg}, chainID, key) + + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 10, + ChainID: "unknown-chain", // not in PastChainIDs — no override + }, + }, + }, + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + // PastChainIDs intentionally empty — no chain ID override allowed + }, + }) + }) + + t.Run("txs from multiple past chains replay correctly", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path1 = "gno.land/r/demo/multichain1" + path2 = "gno.land/r/demo/multichain2" + body = `package %s +var Deployed = true +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + // Both txs come from the same account (accNum=0) but different past chains. + // tx1: seq=0, chain-a; tx2: seq=1, chain-b (sequence incremented by tx1). + msg1 := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "multichain1", + Path: path1, + Files: []*std.MemFile{ + {Name: "file.gno", Body: fmt.Sprintf(body, "multichain1")}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path1)}, + }, + }, + } + msg2 := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "multichain2", + Path: path2, + Files: []*std.MemFile{ + {Name: "file.gno", Body: fmt.Sprintf(body, "multichain2")}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path2)}, + }, + }, + } + + tx1 := createAndSignTx(t, []std.Msg{msg1}, "chain-a", key) // accNum=0, seq=0 + + // tx2 must use seq=1 because tx1 already incremented the sequence. + tx2Raw := std.Tx{ + Msgs: []std.Msg{msg2}, + Fee: std.Fee{GasFee: std.NewCoin("ugnot", 2_000_000), GasWanted: 10_000_000}, + } + signBytes2, err := tx2Raw.GetSignBytes("chain-b", 0, 1) // accNum=0, seq=1 + require.NoError(t, err) + sig2, err := key.Sign(signBytes2) + require.NoError(t, err) + tx2Raw.Signatures = []std.Signature{{PubKey: key.PubKey(), Signature: sig2}} + + // Both chain IDs in the allowlist; each tx carries its own ChainID + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx1, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 10, + ChainID: "chain-a", + }, + }, + { + Tx: tx2Raw, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 20, + ChainID: "chain-b", + }, + }, + }, + Balances: []Balance{ + {Address: key.PubKey().Address(), Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000))}, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"chain-a", "chain-b"}, + }, + }) + }) +} + +func TestNodeParamsKeeperWillSetParam(t *testing.T) { + t.Parallel() + + npk := nodeParamsKeeper{} + + t.Run("valid halt_height (no block context)", func(t *testing.T) { + t.Parallel() + // Without a block header, safeBlockHeight returns 0, so no future check. + assert.NotPanics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_height", int64(100)) + }) + }) + + t.Run("halt_height zero is allowed (cancel sentinel)", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_height", int64(0)) + }) + }) + + t.Run("halt_height in the future is valid when block height is known", func(t *testing.T) { + t.Parallel() + ctx := sdk.Context{}.WithBlockHeader(&bft.Header{Height: 50}) + assert.NotPanics(t, func() { + npk.WillSetParam(ctx, "p:halt_height", int64(100)) + }) + }) + + t.Run("halt_height equal to current block height panics", func(t *testing.T) { + t.Parallel() + ctx := sdk.Context{}.WithBlockHeader(&bft.Header{Height: 100}) + assert.Panics(t, func() { + npk.WillSetParam(ctx, "p:halt_height", int64(100)) + }) + }) + + t.Run("halt_height in the past panics", func(t *testing.T) { + t.Parallel() + ctx := sdk.Context{}.WithBlockHeader(&bft.Header{Height: 200}) + assert.Panics(t, func() { + npk.WillSetParam(ctx, "p:halt_height", int64(100)) + }) + }) + + t.Run("negative halt_height panics", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_height", int64(-1)) + }) + }) + + t.Run("halt_height wrong type panics", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_height", "not-an-int64") + }) + }) + + t.Run("valid halt_min_version", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_min_version", "chain/gnoland1.1") + }) + }) + + t.Run("empty halt_min_version is allowed", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_min_version", "") + }) + }) + + t.Run("halt_min_version wrong type panics", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:halt_min_version", int64(1)) + }) + }) + + t.Run("unknown p: key panics", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + npk.WillSetParam(sdk.Context{}, "p:unknown_key", int64(0)) + }) + }) + + t.Run("non-p: key is allowed", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + npk.WillSetParam(sdk.Context{}, "other:key", "value") + }) + }) +} + +// TestInitChainer_InitialHeightMismatch verifies that loadAppState rejects +// a genesis where GnoGenesisState.InitialHeight diverges from the +// GenesisDoc.InitialHeight passed in via RequestInitChain. +func TestInitChainer_InitialHeightMismatch(t *testing.T) { + t.Parallel() + + t.Run("mismatch is rejected", func(t *testing.T) { + t.Parallel() + + app, err := NewAppWithOptions(TestAppOptions(memdb.NewMemDB())) + require.NoError(t, err) + resp := app.InitChain(abci.RequestInitChain{ + ChainID: "test-chain", + Time: time.Now(), + InitialHeight: 100, + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{PubKeyTypeURLs: []string{}}, + }, + AppState: GnoGenesisState{ + Balances: []Balance{}, + Txs: []TxWithMetadata{}, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + InitialHeight: 200, // diverges from RequestInitChain.InitialHeight + }, + }) + require.NotNil(t, resp.Error, "InitChainer should reject InitialHeight mismatch") + assert.Contains(t, resp.Error.Error(), "InitialHeight mismatch") + }) + + t.Run("match is accepted", func(t *testing.T) { + t.Parallel() + + app, err := NewAppWithOptions(TestAppOptions(memdb.NewMemDB())) + require.NoError(t, err) + resp := app.InitChain(abci.RequestInitChain{ + ChainID: "test-chain", + Time: time.Now(), + InitialHeight: 100, + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{PubKeyTypeURLs: []string{}}, + }, + AppState: GnoGenesisState{ + Balances: []Balance{}, + Txs: []TxWithMetadata{}, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + InitialHeight: 100, + }, + }) + require.Nil(t, resp.Error, "matching InitialHeight should be accepted: %v", resp.Error) + }) + + t.Run("zero app-level InitialHeight is accepted", func(t *testing.T) { + t.Parallel() + + // GnoGenesisState.InitialHeight = 0 means "not set"; no check needed. + app, err := NewAppWithOptions(TestAppOptions(memdb.NewMemDB())) + require.NoError(t, err) + resp := app.InitChain(abci.RequestInitChain{ + ChainID: "test-chain", + Time: time.Now(), + InitialHeight: 100, + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{PubKeyTypeURLs: []string{}}, + }, + AppState: GnoGenesisState{ + Balances: []Balance{}, + Txs: []TxWithMetadata{}, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + // InitialHeight not set + }, + }) + require.Nil(t, resp.Error, "zero app-level InitialHeight should pass validation: %v", resp.Error) + }) +} + +func TestIsPastChainID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + pastChainIDs []string + chainID string + expected bool + }{ + {"empty allowlist", []string{}, "chain-a", false}, + {"nil allowlist", nil, "chain-a", false}, + {"single match", []string{"chain-a"}, "chain-a", true}, + {"no match in list", []string{"chain-a", "chain-b"}, "chain-c", false}, + {"match second element", []string{"chain-a", "chain-b"}, "chain-b", true}, + {"empty chain ID", []string{"chain-a"}, "", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, isPastChainID(tc.pastChainIDs, tc.chainID)) + }) + } +} + +func TestMeetsMinVersion(t *testing.T) { + t.Parallel() + + cases := []struct { + binary string + minVer string + want bool + }{ + // Empty minVersion always passes + {"chain/gnoland1.0", "", true}, + {"develop", "", true}, + + // Same version passes + {"chain/gnoland1.0", "chain/gnoland1.0", true}, + {"chain/gnoland1.1", "chain/gnoland1.1", true}, + + // Newer binary passes + {"chain/gnoland1.1", "chain/gnoland1.0", true}, + {"chain/gnoland2.0", "chain/gnoland1.0", true}, + {"chain/gnoland1.2", "chain/gnoland1.1", true}, + + // Older binary fails + {"chain/gnoland1.0", "chain/gnoland1.1", false}, + {"chain/gnoland1.0", "chain/gnoland2.0", false}, + + // Non-gnoland format: requires exact match + {"develop", "chain/gnoland1.1", false}, + {"v1.0.0", "v1.0.0", true}, + {"v1.0.0", "v1.1.0", false}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.binary+">="+tc.minVer, func(t *testing.T) { + t.Parallel() + got := meetsMinVersion(tc.binary, tc.minVer) + assert.Equal(t, tc.want, got, + "meetsMinVersion(%q, %q)", tc.binary, tc.minVer) + }) + } +} + +func TestSignerInfoForceSetAccountState(t *testing.T) { + t.Parallel() + + t.Run("force-sets existing account sequence and number", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path = "gno.land/r/demo/signertest" + body = `package signertest + +var Deployed = true + +func IsDeployed(cur realm) bool { return Deployed } +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "signertest", + Path: path, + Files: []*std.MemFile{ + {Name: "file.gno", Body: body}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path)}, + }, + }, + } + + // Sign with old chain, accNum=5, seq=10 — the SignerInfo will force-set + // the account to these values before signature verification. + tx := createAndSignTxWithAccSeq(t, []std.Msg{msg}, "old-chain", key, 5, 10) + + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 42, + ChainID: "old-chain", + SignerInfo: []SignerAccountInfo{ + { + Address: key.PubKey().Address(), + AccountNum: 5, + Sequence: 10, + }, + }, + }, + }, + }, + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"old-chain"}, + }, + }) + + // If SignerInfo was correctly applied, the tx would have been + // delivered successfully (sig verification passed). + // Verify by calling the deployed realm. + callMsg := vm.MsgCall{ + Caller: key.PubKey().Address(), + PkgPath: path, + Func: "IsDeployed", + } + + callTx := createAndSignTxWithAccSeq(t, []std.Msg{callMsg}, chainID, key, 5, 11) + + marshalledTx, err := amino.Marshal(callTx) + require.NoError(t, err) + + resp := app.DeliverTx(abci.RequestDeliverTx{Tx: marshalledTx}) + require.True(t, resp.IsOK(), "DeliverTx failed: %s", resp.Log) + assert.Contains(t, string(resp.Data), "true") + }) + + t.Run("creates new account via SignerInfo when account does not exist", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path = "gno.land/r/demo/newacctest" + body = `package newacctest + +var Deployed = true + +func IsDeployed(cur realm) bool { return Deployed } +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "newacctest", + Path: path, + Files: []*std.MemFile{ + {Name: "file.gno", Body: body}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path)}, + }, + }, + } + + // Sign with accNum=7 — account won't exist from balances, + // so NewAccountWithNumber must be called. + tx := createAndSignTxWithAccSeq(t, []std.Msg{msg}, "old-chain", key, 7, 0) + + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 10, + ChainID: "old-chain", + SignerInfo: []SignerAccountInfo{ + { + Address: key.PubKey().Address(), + AccountNum: 7, + Sequence: 0, + }, + }, + }, + }, + }, + // No balances — account doesn't exist before SignerInfo creates it. + // But the account needs funds for gas, so we must provide balances. + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"old-chain"}, + }, + }) + + // Verify deployment succeeded + callMsg := vm.MsgCall{ + Caller: key.PubKey().Address(), + PkgPath: path, + Func: "IsDeployed", + } + + callTx := createAndSignTxWithAccSeq(t, []std.Msg{callMsg}, chainID, key, 7, 1) + + marshalledTx, err := amino.Marshal(callTx) + require.NoError(t, err) + + resp := app.DeliverTx(abci.RequestDeliverTx{Tx: marshalledTx}) + require.True(t, resp.IsOK(), "DeliverTx failed: %s", resp.Log) + assert.Contains(t, string(resp.Data), "true") + }) + + t.Run("failed tx is skipped and does not execute", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "new-chain" + + path = "gno.land/r/demo/failedtest" + body = `package failedtest + +var Deployed = true + +func IsDeployed(cur realm) bool { return Deployed } +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "failedtest", + Path: path, + Files: []*std.MemFile{ + {Name: "file.gno", Body: body}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path)}, + }, + }, + } + + // This tx is marked as Failed — it should be skipped entirely. + tx := createAndSignTxWithAccSeq(t, []std.Msg{msg}, "old-chain", key, 0, 0) + + initResp := app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 5, + ChainID: "old-chain", + Failed: true, + SignerInfo: []SignerAccountInfo{ + { + Address: key.PubKey().Address(), + AccountNum: 0, + Sequence: 0, + }, + }, + }, + }, + }, + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + PastChainIDs: []string{"old-chain"}, + }, + }) + + // The skipped failed tx should produce a non-success response so + // downstream consumers (indexers, explorers) don't mistake it for + // success. + require.Len(t, initResp.TxResponses, 1) + skippedResp := initResp.TxResponses[0] + require.NotNil(t, skippedResp.Error, "skipped failed tx response should carry an error marker") + assert.Contains(t, skippedResp.Error.Error(), "replay skipped") + + // The package should NOT be deployed since the tx was marked as failed. + // Trying to call it should fail. + callMsg := vm.MsgCall{ + Caller: key.PubKey().Address(), + PkgPath: path, + Func: "IsDeployed", + } + + callTx := createAndSignTxWithAccSeq(t, []std.Msg{callMsg}, chainID, key, 0, 1) + + marshalledTx, err := amino.Marshal(callTx) + require.NoError(t, err) + + resp := app.DeliverTx(abci.RequestDeliverTx{Tx: marshalledTx}) + // Should fail because the package was never deployed + require.False(t, resp.IsOK(), "DeliverTx should have failed — failed tx should not deploy package") + }) + + t.Run("SignerInfo is ignored when BlockHeight is zero", func(t *testing.T) { + t.Parallel() + + var ( + db = memdb.NewMemDB() + key = getDummyKey(t) + chainID = "test-chain" + + path = "gno.land/r/demo/genesismode" + body = `package genesismode + +var Deployed = true + +func IsDeployed(cur realm) bool { return Deployed } +` + ) + + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &std.MemPackage{ + Name: "genesismode", + Path: path, + Files: []*std.MemFile{ + {Name: "file.gno", Body: body}, + {Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path)}, + }, + }, + } + + // Sign with the current chain ID (genesis-mode tx). + // BlockHeight=0 means SignerInfo should be ignored entirely. + tx := createAndSignTx(t, []std.Msg{msg}, chainID, key) + + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: time.Now(), + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + AppState: GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: time.Now().Unix(), + BlockHeight: 0, // genesis-mode — SignerInfo must be ignored + SignerInfo: []SignerAccountInfo{ + { + Address: key.PubKey().Address(), + AccountNum: 999, // would corrupt state if applied + Sequence: 999, + }, + }, + }, + }, + }, + Balances: []Balance{ + { + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), + }, + }) + + // If SignerInfo was correctly ignored, the deployment should succeed + // with the normal account state (accNum=0, seq=0). + callMsg := vm.MsgCall{ + Caller: key.PubKey().Address(), + PkgPath: path, + Func: "IsDeployed", + } + + callTx := createAndSignTx(t, []std.Msg{callMsg}, chainID, key) + + marshalledTx, err := amino.Marshal(callTx) + require.NoError(t, err) + + resp := app.DeliverTx(abci.RequestDeliverTx{Tx: marshalledTx}) + require.True(t, resp.IsOK(), "DeliverTx failed: %s", resp.Log) + assert.Contains(t, string(resp.Data), "true") + }) +} + +func TestParseGnolandVersion(t *testing.T) { + t.Parallel() + + cases := []struct { + input string + major int + minor int + ok bool + }{ + {"chain/gnoland1.0", 1, 0, true}, + {"chain/gnoland1.1", 1, 1, true}, + {"chain/gnoland2.3", 2, 3, true}, + {"develop", 0, 0, false}, + {"v1.0.0", 0, 0, false}, + {"chain/gnoland", 0, 0, false}, + {"chain/gnolandX.Y", 0, 0, false}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.input, func(t *testing.T) { + t.Parallel() + major, minor, ok := parseGnolandVersion(tc.input) + assert.Equal(t, tc.ok, ok) + if tc.ok { + assert.Equal(t, tc.major, major) + assert.Equal(t, tc.minor, minor) + } + }) + } +} + +// newTestParamsKeeper creates a minimal ParamsKeeper with an in-memory store +// and pre-seeds it with the given halt params. +func newTestParamsKeeper(t *testing.T, haltHeight int64, minVersion string) (params.ParamsKeeper, store.MultiStore) { + t.Helper() + + db := memdb.NewMemDB() + mainKey := store.NewStoreKey("main") + + cms := store.NewCommitMultiStore(db) + cms.MountStoreWithDB(mainKey, iavl.StoreConstructor, db) + require.NoError(t, cms.LoadLatestVersion()) + + prmk := params.NewParamsKeeper(mainKey) + prmk.Register("node", nodeParamsKeeper{}) + + ms := cms.MultiCacheWrap() + ctx := sdk.Context{}.WithMultiStore(ms).WithChainID("_") + + prmk.SetInt64(ctx, nodeParamHaltHeight, haltHeight) + prmk.SetString(ctx, nodeParamHaltMinVersion, minVersion) + ms.MultiWrite() + cms.Commit() + + return prmk, cms.MultiCacheWrap() +} + +func TestCheckNodeStartupParams(t *testing.T) { + t.Parallel() + + t.Run("no halt configured", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 0, "") + require.NoError(t, checkNodeStartupParams(prmk, ms, 50, 0)) + }) + + t.Run("halt with no version passes", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 100, "") + require.NoError(t, checkNodeStartupParams(prmk, ms, 100, 0)) + }) + + t.Run("binary meets version after halt", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 100, "develop") + // binary "develop" == "develop" -> meetsMinVersion (exact match), lastBlock >= haltHeight + require.NoError(t, checkNodeStartupParams(prmk, ms, 100, 0)) + }) + + t.Run("old binary rejected after halt", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 100, "chain/gnoland9.9") + // binary "develop" doesn't meet "chain/gnoland9.9" -> rejected + err := checkNodeStartupParams(prmk, ms, 100, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "does not meet the minimum version") + }) + + t.Run("new binary rejected before halt height", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 100, "develop") + // binary "develop" == "develop" -> meetsMinVersion, but chain hasn't halted yet + err := checkNodeStartupParams(prmk, ms, 50, 0) + require.Error(t, err) + assert.Contains(t, err.Error(), "upgrade intended for halt height") + }) + + t.Run("old binary allowed before halt height", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 100, "chain/gnoland9.9") + // binary "develop" doesn't meet "chain/gnoland9.9", chain hasn't halted -> old binary, OK + require.NoError(t, checkNodeStartupParams(prmk, ms, 50, 0)) + }) + + t.Run("skip_upgrade_height bypasses check", func(t *testing.T) { + t.Parallel() + prmk, ms := newTestParamsKeeper(t, 100, "develop") + // Even though binary meets version before halt, skip_upgrade_height=100 bypasses + require.NoError(t, checkNodeStartupParams(prmk, ms, 50, 100)) + }) +} + +func TestEndBlockerHalt(t *testing.T) { + t.Parallel() + + t.Run("halts at exact height", func(t *testing.T) { + t.Parallel() + + var haltSet uint64 + mockApp := &mockEndBlockerApp{ + setHaltHeightFn: func(h uint64) { haltSet = h }, + } + mockPrmk := &mockConfigurableParamsKeeper{ + int64s: map[string]int64{nodeParamHaltHeight: 100}, + } + + eb := EndBlocker(mockPrmk, nil, nil, mockApp) + eb(sdk.Context{}, abci.RequestEndBlock{Height: 100}) + + assert.Equal(t, uint64(100), haltSet, "SetHaltHeight should be called with halt_height") + }) + + t.Run("does not halt before halt height", func(t *testing.T) { + t.Parallel() + + var haltSet uint64 + mockApp := &mockEndBlockerApp{ + setHaltHeightFn: func(h uint64) { haltSet = h }, + } + mockPrmk := &mockConfigurableParamsKeeper{ + int64s: map[string]int64{nodeParamHaltHeight: 100}, + } + + eb := EndBlocker(mockPrmk, nil, nil, mockApp) + eb(sdk.Context{}, abci.RequestEndBlock{Height: 99}) + + assert.Equal(t, uint64(0), haltSet, "SetHaltHeight should NOT be called before halt height") + }) + + t.Run("does not re-halt after halt height (no infinite loop)", func(t *testing.T) { + t.Parallel() + + var haltSet uint64 + mockApp := &mockEndBlockerApp{ + setHaltHeightFn: func(h uint64) { haltSet = h }, + } + mockPrmk := &mockConfigurableParamsKeeper{ + int64s: map[string]int64{nodeParamHaltHeight: 100}, + } + + eb := EndBlocker(mockPrmk, nil, nil, mockApp) + // After restart at height 101, halt_height=100 still in params but == doesn't re-fire + eb(sdk.Context{}, abci.RequestEndBlock{Height: 101}) + + assert.Equal(t, uint64(0), haltSet, "SetHaltHeight must NOT be called after halt height (prevents infinite loop)") + }) + + t.Run("cancel: halt_height zero never halts", func(t *testing.T) { + t.Parallel() + + var haltSet uint64 + mockApp := &mockEndBlockerApp{ + setHaltHeightFn: func(h uint64) { haltSet = h }, + } + mockPrmk := &mockConfigurableParamsKeeper{ + int64s: map[string]int64{nodeParamHaltHeight: 0}, + } + + eb := EndBlocker(mockPrmk, nil, nil, mockApp) + eb(sdk.Context{}, abci.RequestEndBlock{Height: 100}) + + assert.Equal(t, uint64(0), haltSet, "SetHaltHeight should NOT be called when halt_height=0 (cancelled)") + }) +} diff --git a/gno.land/pkg/gnoland/events.go b/gno.land/pkg/gnoland/events.go deleted file mode 100644 index ba78c40979e..00000000000 --- a/gno.land/pkg/gnoland/events.go +++ /dev/null @@ -1,51 +0,0 @@ -package gnoland - -import ( - "github.com/gnolang/gno/tm2/pkg/events" - "github.com/rs/xid" -) - -// filterFn is the filter method for incoming events -type filterFn[T any] func(events.Event) []T - -// collector is the generic in-memory event collector -type collector[T any] struct { - events []T // temporary event storage - filter filterFn[T] // method used for filtering events -} - -// newCollector creates a new event collector -func newCollector[T any]( - evsw events.EventSwitch, - filter filterFn[T], -) *collector[T] { - c := &collector[T]{ - events: make([]T, 0), - filter: filter, - } - - // Register the listener - evsw.AddListener(xid.New().String(), func(e events.Event) { - c.updateWith(e) - }) - - return c -} - -// updateWith updates the collector with the given event -func (c *collector[T]) updateWith(event events.Event) { - if extracted := c.filter(event); extracted != nil { - c.events = append(c.events, extracted...) - } -} - -// getEvents returns the filtered events, -// and resets the collector store -func (c *collector[T]) getEvents() []T { - capturedEvents := make([]T, len(c.events)) - copy(capturedEvents, c.events) - - c.events = c.events[:0] - - return capturedEvents -} diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 9732d2a2f56..bde81e1075c 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -158,6 +158,14 @@ type mockAuthKeeper struct{} func (m *mockAuthKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) std.Account { return nil } + +// NewAccountWithNumber returns nil. This mock is only safe in tests where no +// TxWithMetadata carries SignerInfo — if SignerInfo is present and an account +// doesn't exist, the replay loop calls this and then calls acc.SetSequence, +// which will panic on a nil return. Use a real AccountKeeper for those tests. +func (m *mockAuthKeeper) NewAccountWithNumber(ctx sdk.Context, addr crypto.Address, accNum uint64) std.Account { + return nil +} func (m *mockAuthKeeper) GetAccount(ctx sdk.Context, addr crypto.Address) std.Account { return nil } func (m *mockAuthKeeper) GetAllAccounts(ctx sdk.Context) []std.Account { return nil } func (m *mockAuthKeeper) SetAccount(ctx sdk.Context, acc std.Account) {} @@ -165,21 +173,93 @@ func (m *mockAuthKeeper) IterateAccounts(ctx sdk.Context, process func(std.Accou func (m *mockAuthKeeper) InitGenesis(ctx sdk.Context, data auth.GenesisState) {} func (m *mockAuthKeeper) GetParams(ctx sdk.Context) auth.Params { return auth.Params{} } -type mockParamsKeeper struct{} +type mockParamsKeeper struct { + getStringFn func(sdk.Context, string, *string) + getInt64Fn func(sdk.Context, string, *int64) + getUint64Fn func(sdk.Context, string, *uint64) + getBoolFn func(sdk.Context, string, *bool) + getBytesFn func(sdk.Context, string, *[]byte) + getStringsFn func(sdk.Context, string, *[]string) + + setStringFn func(sdk.Context, string, string) + setInt64Fn func(sdk.Context, string, int64) + setUint64Fn func(sdk.Context, string, uint64) + setBoolFn func(sdk.Context, string, bool) + setBytesFn func(sdk.Context, string, []byte) + setStringsFn func(sdk.Context, string, []string) +} + +func (m *mockParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) { + if m.getStringFn != nil { + m.getStringFn(ctx, key, ptr) + } +} + +func (m *mockParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) { + if m.getInt64Fn != nil { + m.getInt64Fn(ctx, key, ptr) + } +} + +func (m *mockParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) { + if m.getUint64Fn != nil { + m.getUint64Fn(ctx, key, ptr) + } +} + +func (m *mockParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) { + if m.getBoolFn != nil { + m.getBoolFn(ctx, key, ptr) + } +} + +func (m *mockParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) { + if m.getBytesFn != nil { + m.getBytesFn(ctx, key, ptr) + } +} + +func (m *mockParamsKeeper) GetStrings(ctx sdk.Context, key string, ptr *[]string) { + if m.getStringsFn != nil { + m.getStringsFn(ctx, key, ptr) + } +} + +func (m *mockParamsKeeper) SetString(ctx sdk.Context, key string, value string) { + if m.setStringFn != nil { + m.setStringFn(ctx, key, value) + } +} + +func (m *mockParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) { + if m.setInt64Fn != nil { + m.setInt64Fn(ctx, key, value) + } +} -func (m *mockParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) {} -func (m *mockParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) {} -func (m *mockParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) {} -func (m *mockParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) {} -func (m *mockParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) {} -func (m *mockParamsKeeper) GetStrings(ctx sdk.Context, key string, ptr *[]string) {} +func (m *mockParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) { + if m.setUint64Fn != nil { + m.setUint64Fn(ctx, key, value) + } +} -func (m *mockParamsKeeper) SetString(ctx sdk.Context, key string, value string) {} -func (m *mockParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) {} -func (m *mockParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) {} -func (m *mockParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) {} -func (m *mockParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) {} -func (m *mockParamsKeeper) SetStrings(ctx sdk.Context, key string, value []string) {} +func (m *mockParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) { + if m.setBoolFn != nil { + m.setBoolFn(ctx, key, value) + } +} + +func (m *mockParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) { + if m.setBytesFn != nil { + m.setBytesFn(ctx, key, value) + } +} + +func (m *mockParamsKeeper) SetStrings(ctx sdk.Context, key string, value []string) { + if m.setStringsFn != nil { + m.setStringsFn(ctx, key, value) + } +} func (m *mockParamsKeeper) Has(ctx sdk.Context, key string) bool { return false } func (m *mockParamsKeeper) GetStruct(ctx sdk.Context, key string, strctPtr any) {} @@ -197,11 +277,13 @@ func (m *mockGasPriceKeeper) UpdateGasPrice(ctx sdk.Context) {} type ( lastBlockHeightDelegate func() int64 loggerDelegate func() *slog.Logger + setHaltHeightDelegate func(uint64) ) type mockEndBlockerApp struct { lastBlockHeightFn lastBlockHeightDelegate loggerFn loggerDelegate + setHaltHeightFn setHaltHeightDelegate } func (m *mockEndBlockerApp) LastBlockHeight() int64 { @@ -219,3 +301,43 @@ func (m *mockEndBlockerApp) Logger() *slog.Logger { return log.NewNoopLogger() } + +func (m *mockEndBlockerApp) SetHaltHeight(height uint64) { + if m.setHaltHeightFn != nil { + m.setHaltHeightFn(height) + } +} + +// mockConfigurableParamsKeeper is a ParamsKeeperI that returns values from pre-seeded maps. +type mockConfigurableParamsKeeper struct { + int64s map[string]int64 + strings map[string]string +} + +func (m *mockConfigurableParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) { + if v, ok := m.int64s[key]; ok { + *ptr = v + } +} +func (m *mockConfigurableParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) { + if v, ok := m.strings[key]; ok { + *ptr = v + } +} +func (m *mockConfigurableParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) {} +func (m *mockConfigurableParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) {} +func (m *mockConfigurableParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) {} +func (m *mockConfigurableParamsKeeper) GetStrings(ctx sdk.Context, key string, ptr *[]string) { +} +func (m *mockConfigurableParamsKeeper) SetString(ctx sdk.Context, key, value string) {} +func (m *mockConfigurableParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) {} +func (m *mockConfigurableParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) {} +func (m *mockConfigurableParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) {} +func (m *mockConfigurableParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) {} +func (m *mockConfigurableParamsKeeper) SetStrings(ctx sdk.Context, key string, value []string) { +} +func (m *mockConfigurableParamsKeeper) Has(ctx sdk.Context, key string) bool { return false } +func (m *mockConfigurableParamsKeeper) GetStruct(ctx sdk.Context, key string, strctPtr any) {} +func (m *mockConfigurableParamsKeeper) SetStruct(ctx sdk.Context, key string, strct any) {} +func (m *mockConfigurableParamsKeeper) GetAny(ctx sdk.Context, key string) any { return nil } +func (m *mockConfigurableParamsKeeper) SetAny(ctx sdk.Context, key string, value any) {} diff --git a/gno.land/pkg/gnoland/node_initial_height_test.go b/gno.land/pkg/gnoland/node_initial_height_test.go new file mode 100644 index 00000000000..8512ff71481 --- /dev/null +++ b/gno.land/pkg/gnoland/node_initial_height_test.go @@ -0,0 +1,80 @@ +package gnoland + +import ( + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/log" + + "github.com/gnolang/gno/gnovm/pkg/gnoenv" +) + +// TestNodeBootWithInitialHeight boots a full in-memory node whose genesis doc +// has InitialHeight = 100. It verifies that: +// +// - The node starts without panicking (exercises all the InitialHeight paths +// through Handshaker → ConsensusState.reconstructLastCommit → +// BlockchainReactor.NewBlockchainReactor). +// - The first committed block is at height 100, not 1. +func TestNodeBootWithInitialHeight(t *testing.T) { + const initialHeight = int64(100) + + td := t.TempDir() + tmcfg := NewDefaultTMConfig(td) + + pv := bft.NewMockPV() + pk := pv.PubKey() + + genesis := &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: tmcfg.ChainID(), + InitialHeight: initialHeight, + ConsensusParams: abci.ConsensusParams{ + Block: defaultBlockParams(), + }, + Validators: []bft.GenesisValidator{ + { + Address: pk.Address(), + PubKey: pk, + Power: 10, + Name: "self", + }, + }, + AppState: DefaultGenState(), + } + + cfg := &InMemoryNodeConfig{ + PrivValidator: pv, + Genesis: genesis, + TMConfig: tmcfg, + DB: memdb.NewMemDB(), + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + CacheStdlibLoad: true, + }, + } + + n, err := NewInMemoryNode(log.NewTestingLogger(t), cfg) + require.NoError(t, err) + + require.NoError(t, n.Start()) + t.Cleanup(func() { require.NoError(t, n.Stop()) }) + + select { + case <-n.Ready(): + // first block committed + case <-time.After(30 * time.Second): + t.Fatal("timeout waiting for node to produce first block") + } + + height := n.BlockStore().Height() + require.Equal(t, initialHeight, height, + "first committed block should be at InitialHeight (%d), got %d", initialHeight, height) +} diff --git a/gno.land/pkg/gnoland/node_params.go b/gno.land/pkg/gnoland/node_params.go new file mode 100644 index 00000000000..7efe785942c --- /dev/null +++ b/gno.land/pkg/gnoland/node_params.go @@ -0,0 +1,152 @@ +package gnoland + +import ( + "fmt" + "strconv" + "strings" + + "github.com/gnolang/gno/tm2/pkg/sdk" + sdkparams "github.com/gnolang/gno/tm2/pkg/sdk/params" + "github.com/gnolang/gno/tm2/pkg/store" + tmver "github.com/gnolang/gno/tm2/pkg/version" +) + +const ( + nodeParamHaltHeight = "node:p:halt_height" + nodeParamHaltMinVersion = "node:p:halt_min_version" +) + +// nodeParamsKeeper implements a minimal ParamfulKeeper for the "node" module. +// It validates node-level parameters set through governance proposals. +type nodeParamsKeeper struct{} + +// WillSetParam validates node parameters before they are written to the params store. +func (nodeParamsKeeper) WillSetParam(ctx sdk.Context, key string, value any) { + switch key { + case "p:halt_height": + h, ok := value.(int64) + if !ok { + panic(fmt.Sprintf("halt_height must be an int64, got %T", value)) + } + if h < 0 { + panic(fmt.Sprintf("halt_height must be non-negative, got %d", h)) + } + // Reject halt heights that are in the past or present. + // h == 0 is the cancel sentinel and is always allowed. + // safeBlockHeight handles genesis/test contexts where the block header may not be set. + if curHeight := safeBlockHeight(ctx); h > 0 && curHeight > 0 && h <= curHeight { + panic(fmt.Sprintf("halt_height %d must be greater than the current block height %d", h, curHeight)) + } + case "p:halt_min_version": + _, ok := value.(string) + if !ok { + panic(fmt.Sprintf("halt_min_version must be a string, got %T", value)) + } + default: + if strings.HasPrefix(key, "p:") { + panic(fmt.Sprintf("unknown node param key: %q", key)) + } + } +} + +// checkNodeStartupParams reads halt-related params from the committed state and verifies: +// 1. The running binary meets the minimum version requirement set by governance. +// 2. A new (upgraded) binary is not started before the chain has actually halted. +// +// skipUpgradeHeight, if non-zero, skips all upgrade checks at that specific height. +func checkNodeStartupParams(prmk sdkparams.ParamsKeeperI, ms store.MultiStore, lastBlockHeight, skipUpgradeHeight int64) error { + // Build a minimal read-only context with just the multistore and a placeholder chain ID. + // We only need store access to read params; no block execution context is required. + ctx := sdk.Context{}.WithMultiStore(ms).WithChainID("_") + + var haltHeight int64 + prmk.GetInt64(ctx, nodeParamHaltHeight, &haltHeight) + + var minVersion string + prmk.GetString(ctx, nodeParamHaltMinVersion, &minVersion) + + // Nothing to check if no governance halt is configured. + if haltHeight == 0 || minVersion == "" { + return nil + } + + // Allow skipping upgrade checks at a specific height (e.g., validator already migrated). + if skipUpgradeHeight > 0 && skipUpgradeHeight == haltHeight { + return nil + } + + binaryVersion := tmver.Version + + // Check 1: Prevent old binaries from resuming after a halt. + if lastBlockHeight >= haltHeight { + if !meetsMinVersion(binaryVersion, minVersion) { + return fmt.Errorf( + "binary version %q does not meet the minimum version %q required by governance; "+ + "please upgrade to a compatible binary before restarting", + binaryVersion, minVersion, + ) + } + return nil + } + + // Check 2: Prevent new (upgraded) binaries from running before the halt height. + // Any binary that meets the minimum version is rejected until the halt occurs. + if meetsMinVersion(binaryVersion, minVersion) { + return fmt.Errorf( + "binary version %q is an upgrade intended for halt height %d, "+ + "but the chain is at height %d; please use the previous binary until the halt, "+ + "or set skip_upgrade_height = %d in config.toml if you have already migrated", + binaryVersion, haltHeight, lastBlockHeight, haltHeight, + ) + } + + return nil +} + +// safeBlockHeight returns ctx.BlockHeight() or 0 if the context has no block header. +// This handles genesis and test contexts where the header may not be initialized. +func safeBlockHeight(ctx sdk.Context) (h int64) { + defer func() { recover() }() //nolint:errcheck + return ctx.BlockHeight() +} + +// meetsMinVersion reports whether binaryVersion satisfies the minVersion requirement. +// Versions are expected to follow the "chain/gnolandX.Y" format used for gno.land chain releases. +// If either version cannot be parsed in that format, an exact string match is required. +func meetsMinVersion(binaryVersion, minVersion string) bool { + if minVersion == "" { + return true + } + + bMajor, bMinor, bOK := parseGnolandVersion(binaryVersion) + mMajor, mMinor, mOK := parseGnolandVersion(minVersion) + + if bOK && mOK { + if bMajor != mMajor { + return bMajor > mMajor + } + return bMinor >= mMinor + } + + // Fall back to exact match if versions are not in the recognized format. + return binaryVersion == minVersion +} + +// parseGnolandVersion parses a version string like "chain/gnoland1.2" into its major and minor parts. +func parseGnolandVersion(v string) (major, minor int, ok bool) { + const prefix = "chain/gnoland" + if !strings.HasPrefix(v, prefix) { + return 0, 0, false + } + rest := v[len(prefix):] + dot := strings.IndexByte(rest, '.') + if dot < 0 { + return 0, 0, false + } + maj, err1 := strconv.Atoi(rest[:dot]) + minor, err2 := strconv.Atoi(rest[dot+1:]) + if err1 != nil || err2 != nil { + return 0, 0, false + } + return maj, minor, true +} diff --git a/gno.land/pkg/gnoland/package.go b/gno.land/pkg/gnoland/package.go index e4b2449c972..4cae4d99cd9 100644 --- a/gno.land/pkg/gnoland/package.go +++ b/gno.land/pkg/gnoland/package.go @@ -13,4 +13,5 @@ var Package = amino.RegisterPackage(amino.NewPackage( GnoGenesisState{}, "GenesisState", TxWithMetadata{}, "TxWithMetadata", GnoTxMetadata{}, "GnoTxMetadata", + SignerAccountInfo{}, "SignerAccountInfo", )) diff --git a/gno.land/pkg/gnoland/replay_report.go b/gno.land/pkg/gnoland/replay_report.go new file mode 100644 index 00000000000..fb3cf2d4864 --- /dev/null +++ b/gno.land/pkg/gnoland/replay_report.go @@ -0,0 +1,150 @@ +package gnoland + +import ( + "fmt" + "log/slog" + + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// ReplayCategory classifies the outcome of a genesis tx replay. +type ReplayCategory string + +const ( + // ReplayCategoryOK: tx replayed successfully (gas matched source within tolerance, if source gas was recorded). + ReplayCategoryOK ReplayCategory = "ok" + // ReplayCategoryOKGasDiffers: tx succeeded but gas consumption differs from source chain. + ReplayCategoryOKGasDiffers ReplayCategory = "ok_gas_differs" + // ReplayCategoryFailed: tx failed during replay (any reason not covered by specific categories). + ReplayCategoryFailed ReplayCategory = "failed" + // ReplayCategorySkippedFailed: tx was marked Failed in source metadata, correctly skipped. + ReplayCategorySkippedFailed ReplayCategory = "skipped_failed" + + // aliases for callers (lowercase internal): + replayCategorySkippedFailed = ReplayCategorySkippedFailed +) + +// replayOutcome is a single tx outcome during genesis replay. +type replayOutcome struct { + TxIndex int `json:"tx_index"` + SourceHeight int64 `json:"source_height,omitempty"` // metadata.BlockHeight + SourceChainID string `json:"source_chain_id,omitempty"` + Category ReplayCategory `json:"category"` + GasSource int64 `json:"gas_source,omitempty"` // metadata.GasUsed (from tx-archive) + GasReplay int64 `json:"gas_replay,omitempty"` // actual gas consumed during replay + Error string `json:"error,omitempty"` // brief error if failed +} + +// replayReport accumulates per-tx outcomes and emits a summary. +type replayReport struct { + mode string // GasReplayMode from GnoGenesisState + outcomes []replayOutcome +} + +func newReplayReport(mode string) *replayReport { + return &replayReport{mode: mode} +} + +// record appends an outcome with fully explicit values (used for skipped txs). +func (r *replayReport) record(txIdx int, metadata *GnoTxMetadata, gasReplay int64, gasSource int64, cat ReplayCategory, err error) { + o := replayOutcome{ + TxIndex: txIdx, + Category: cat, + GasReplay: gasReplay, + GasSource: gasSource, + } + if metadata != nil { + o.SourceHeight = metadata.BlockHeight + o.SourceChainID = metadata.ChainID + if o.GasSource == 0 { + o.GasSource = metadata.GasUsed + } + } + if err != nil { + o.Error = err.Error() + } + r.outcomes = append(r.outcomes, o) +} + +// recordDeliverResult derives the outcome from a Deliver result and metadata. +func (r *replayReport) recordDeliverResult(txIdx int, metadata *GnoTxMetadata, res sdk.Result) { + o := replayOutcome{ + TxIndex: txIdx, + GasReplay: res.GasUsed, + } + if metadata != nil { + o.SourceHeight = metadata.BlockHeight + o.SourceChainID = metadata.ChainID + o.GasSource = metadata.GasUsed + } + if res.IsErr() { + o.Category = ReplayCategoryFailed + if res.Error != nil { + o.Error = res.Error.Error() + } else if res.Log != "" { + o.Error = res.Log + } + } else if o.GasSource > 0 && o.GasReplay != o.GasSource { + o.Category = ReplayCategoryOKGasDiffers + } else { + o.Category = ReplayCategoryOK + } + r.outcomes = append(r.outcomes, o) +} + +// emit writes a summary to the logger. +func (r *replayReport) emit(logger *slog.Logger) { + if logger == nil || len(r.outcomes) == 0 { + return + } + counts := map[ReplayCategory]int{} + for _, o := range r.outcomes { + counts[o.Category]++ + } + logger.Info( + "Genesis replay report", + "mode", modeOrDefault(r.mode), + "total", len(r.outcomes), + "ok", counts[ReplayCategoryOK], + "ok_gas_differs", counts[ReplayCategoryOKGasDiffers], + "failed", counts[ReplayCategoryFailed], + "skipped_failed", counts[ReplayCategorySkippedFailed], + ) + // For failures, log each one so operators can review. + for _, o := range r.outcomes { + if o.Category == ReplayCategoryFailed { + logger.Warn("Genesis replay failure", + "tx_index", o.TxIndex, + "source_height", o.SourceHeight, + "gas_source", o.GasSource, + "gas_replay", o.GasReplay, + "error", o.Error, + ) + } + } +} + +// Outcomes returns a copy of recorded outcomes. Exposed for tests and tooling +// that wants to write its own replay-report.json. +func (r *replayReport) Outcomes() []replayOutcome { + out := make([]replayOutcome, len(r.outcomes)) + copy(out, r.outcomes) + return out +} + +func modeOrDefault(mode string) string { + if mode == "" { + return "strict" + } + return mode +} + +// validateGasReplayMode returns an error if the given mode is not recognised. +func validateGasReplayMode(mode string) error { + switch mode { + case "", "strict", "source": + return nil + default: + return fmt.Errorf("unknown GasReplayMode %q (valid: \"\", \"strict\", \"source\")", mode) + } +} diff --git a/gno.land/pkg/gnoland/replay_report_test.go b/gno.land/pkg/gnoland/replay_report_test.go new file mode 100644 index 00000000000..8e3a2172484 --- /dev/null +++ b/gno.land/pkg/gnoland/replay_report_test.go @@ -0,0 +1,89 @@ +package gnoland + +import ( + "errors" + "testing" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateGasReplayMode(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + mode string + wantErr bool + }{ + {"", false}, + {"strict", false}, + {"source", false}, + {"max", true}, // not implemented yet + {"skip", true}, // not implemented yet + {"STRICT", true}, // case-sensitive + {"garbage", true}, + } { + tc := tc + t.Run(tc.mode, func(t *testing.T) { + t.Parallel() + err := validateGasReplayMode(tc.mode) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestReplayReport_Categorization(t *testing.T) { + t.Parallel() + + r := newReplayReport("source") + + // Tx 0: success, gas differs from source + r.recordDeliverResult(0, &GnoTxMetadata{BlockHeight: 10, GasUsed: 50_000}, sdk.Result{ + GasUsed: 75_000, + }) + // Tx 1: success, gas matches source + r.recordDeliverResult(1, &GnoTxMetadata{BlockHeight: 11, GasUsed: 30_000}, sdk.Result{ + GasUsed: 30_000, + }) + // Tx 2: success, no source gas recorded + r.recordDeliverResult(2, &GnoTxMetadata{BlockHeight: 12}, sdk.Result{ + GasUsed: 10_000, + }) + // Tx 3: delivery failed + r.recordDeliverResult(3, &GnoTxMetadata{BlockHeight: 13, GasUsed: 20_000}, sdk.Result{ + ResponseBase: abci.ResponseBase{ + Error: abci.StringError("out of gas"), + }, + GasUsed: 20_000, + }) + // Tx 4: skipped (source failed) + r.record(4, &GnoTxMetadata{BlockHeight: 14, Failed: true}, 0, 0, ReplayCategorySkippedFailed, nil) + + outcomes := r.Outcomes() + require.Len(t, outcomes, 5) + assert.Equal(t, ReplayCategoryOKGasDiffers, outcomes[0].Category) + assert.Equal(t, ReplayCategoryOK, outcomes[1].Category) + assert.Equal(t, ReplayCategoryOK, outcomes[2].Category) + assert.Equal(t, ReplayCategoryFailed, outcomes[3].Category) + assert.Contains(t, outcomes[3].Error, "out of gas") + assert.Equal(t, ReplayCategorySkippedFailed, outcomes[4].Category) + + // Explicit record with error + r.record(5, &GnoTxMetadata{BlockHeight: 15}, 0, 0, ReplayCategoryFailed, errors.New("boom")) + outcomes = r.Outcomes() + require.Len(t, outcomes, 6) + assert.Equal(t, "boom", outcomes[5].Error) +} + +func TestReplayReport_ModeDefault(t *testing.T) { + t.Parallel() + + assert.Equal(t, "strict", modeOrDefault("")) + assert.Equal(t, "source", modeOrDefault("source")) +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 050eda60c92..94b17985eb6 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -126,6 +126,18 @@ type GnoGenesisState struct { Auth auth.GenesisState `json:"auth"` Bank bank.GenesisState `json:"bank"` VM vm.GenesisState `json:"vm"` + // Chain upgrade fields + PastChainIDs []string `json:"past_chain_ids,omitempty"` // Allowlist of chain IDs valid for historical tx signature verification + InitialHeight int64 `json:"initial_height,omitempty"` // Block height to start from after genesis replay + // GasReplayMode controls how historical txs (metadata.BlockHeight > 0) are + // metered during replay. Valid values: + // "" or "strict" — use the new VM's gas meter (default; may fail txs + // that worked on the source chain if gas requirements changed) + // "source" — bypass the new gas meter for historical txs; they + // execute with unlimited gas and the response records + // metadata.GasUsed from the source chain. This preserves the + // historical outcome even if the VM's gas metering changed. + GasReplayMode string `json:"gas_replay_mode,omitempty"` } type TxWithMetadata struct { @@ -134,7 +146,22 @@ type TxWithMetadata struct { } type GnoTxMetadata struct { - Timestamp int64 `json:"timestamp"` + Timestamp int64 `json:"timestamp"` + BlockHeight int64 `json:"block_height,omitempty"` // Original block height for historical tx replay + ChainID string `json:"chain_id,omitempty"` // Originating chain ID, populated by tx-archive export + Failed bool `json:"failed,omitempty"` // True if tx had non-zero return code on source chain + SignerInfo []SignerAccountInfo `json:"signer_info,omitempty"` // Per-signer account metadata for signature verification + GasUsed int64 `json:"gas_used,omitempty"` // Gas consumed on source chain (used when GasReplayMode="source") + GasWanted int64 `json:"gas_wanted,omitempty"` // Gas requested on source chain (informational / report) +} + +// SignerAccountInfo records a signer's account number and sequence at the time +// a historical tx was executed on the source chain. Used during hardfork replay +// to force-set account state so signatures verify correctly. +type SignerAccountInfo struct { + Address crypto.Address `json:"address"` + AccountNum uint64 `json:"account_num"` // Stable, never changes once assigned + Sequence uint64 `json:"sequence"` // Pre-tx sequence (value used in GetSignBytes) } // ReadGenesisTxs reads the genesis txs from the given file path diff --git a/gno.land/pkg/gnoland/validators.go b/gno.land/pkg/gnoland/validators.go deleted file mode 100644 index 8097215a559..00000000000 --- a/gno.land/pkg/gnoland/validators.go +++ /dev/null @@ -1,61 +0,0 @@ -package gnoland - -import ( - "regexp" - - "github.com/gnolang/gno/gnovm/stdlibs/chain" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/events" -) - -const ( - valRealm = "gno.land/r/sys/validators/v2" // XXX: make it configurable from GovDAO - valChangesFn = "GetChanges" - - validatorAddedEvent = "ValidatorAdded" - validatorRemovedEvent = "ValidatorRemoved" -) - -// XXX: replace with amino-based clean approach -var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) - -// validatorUpdate is a type being used for "notifying" -// that a validator change happened on-chain. The events from `r/sys/validators` -// do not pass data related to validator add / remove instances (who, what, how) -type validatorUpdate struct{} - -// validatorEventFilter filters the given event to determine if it -// is tied to a validator update -func validatorEventFilter(event events.Event) []validatorUpdate { - // Make sure the event is a new TX event - txResult, ok := event.(types.EventTx) - if !ok { - return nil - } - - // Make sure an add / remove event happened - for _, ev := range txResult.Result.Response.Events { - // Make sure the event is a GnoVM event - gnoEv, ok := ev.(chain.Event) - if !ok { - continue - } - - // Make sure the event is from `r/sys/validators` - if gnoEv.PkgPath != valRealm { - continue - } - - // Make sure the event is either an add / remove - switch gnoEv.Type { - case validatorAddedEvent, validatorRemovedEvent: - // We don't pass data around with the events, but a single - // notification is enough to "trigger" a VM scrape - return []validatorUpdate{{}} - default: - continue - } - } - - return nil -} diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go index 5f74b2c29aa..174af39138f 100644 --- a/gno.land/pkg/gnoweb/app.go +++ b/gno.land/pkg/gnoweb/app.go @@ -25,7 +25,7 @@ var DefaultAliases = map[string]AliasTarget{ "/license": {"/r/gnoland/pages:p/license", GnowebPath}, "/contribute": {"/r/gnoland/pages:p/contribute", GnowebPath}, "/links": {"/r/gnoland/pages:p/links", GnowebPath}, - "/events": {"/r/gnoland/events", GnowebPath}, + "/events": {"/r/devrels/events", GnowebPath}, "/partners": {"/r/gnoland/pages:p/partners", GnowebPath}, "/docs": {"/u/docs", GnowebPath}, } @@ -52,6 +52,8 @@ type AppConfig struct { FaucetURL string // Domain is the domain used by the node. Domain string + // Banner, if set, displays a site-wide banner above the header. + Banner components.BannerData // Aliases is a map of aliases pointing to another path or a static file. Aliases map[string]AliasTarget // RenderConfig defines the default configuration for rendering realms and source files. @@ -112,6 +114,7 @@ func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { ChainId: cfg.ChainID, Analytics: cfg.Analytics, BuildTime: buildTime, + Banner: cfg.Banner, } // Configure Markdown renderer diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index a0160f844f2..a761fca291c 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -115,6 +115,49 @@ func TestRoutes(t *testing.T) { } } +func TestStaticMarkdownDevLinks(t *testing.T) { + t.Parallel() + + logger := log.NewTestingLogger(t) + rootdir := gnoenv.RootDir() + genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) + config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) + node, remoteAddr := integration.TestingInMemoryNode(t, logger, config) + t.Cleanup(func() { node.Stop() }) + + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Aliases["/"] = AliasTarget{Value: "# Home Static", Kind: StaticMarkdown} + cfg.Aliases["/staticmd"] = AliasTarget{Value: "# Static Content", Kind: StaticMarkdown} + + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + + cases := []struct { + name string + route string + }{ + {"homepage static markdown", "/"}, + {"non-homepage static markdown", "/staticmd"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + request := httptest.NewRequest(http.MethodGet, tc.route, nil) + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + body := response.Body.String() + assert.Equal(t, http.StatusOK, response.Code) + assert.Contains(t, body, `link-label">Content<`, "static markdown pages should have Content link") + assert.NotContains(t, body, `link-label">Source<`, "static markdown pages should not have Source link") + assert.NotContains(t, body, `link-label">Actions<`, "static markdown pages should not have Actions link") + }) + } +} + func TestAnalytics(t *testing.T) { routes := []string{ // Special realms diff --git a/gno.land/pkg/gnoweb/components/layout_header.go b/gno.land/pkg/gnoweb/components/layout_header.go index 1ec4b7a3357..10916ad7c56 100644 --- a/gno.land/pkg/gnoweb/components/layout_header.go +++ b/gno.land/pkg/gnoweb/components/layout_header.go @@ -26,6 +26,7 @@ type HeaderData struct { ChainId string Remote string Mode ViewMode + Static bool } func StaticHeaderGeneralLinks() []HeaderLink { @@ -36,7 +37,7 @@ func StaticHeaderGeneralLinks() []HeaderLink { } } -func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode) []HeaderLink { +func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode, static bool) []HeaderLink { contentURL, sourceURL, helpURL := u, u, u contentURL.WebQuery = url.Values{} sourceURL.WebQuery = url.Values{"source": {""}} @@ -63,12 +64,14 @@ func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode) []HeaderLink { IsActive: isActive(u.WebQuery, "Actions"), } - switch mode { - case ViewModeExplorer: + switch { + case static: + return []HeaderLink{contentLink} + case mode == ViewModeExplorer: return []HeaderLink{} - case ViewModeUser: + case mode == ViewModeUser: return []HeaderLink{contentLink} - case ViewModePackage: + case mode == ViewModePackage: return []HeaderLink{contentLink, sourceLink} default: return []HeaderLink{contentLink, sourceLink, actionsLink} @@ -77,7 +80,7 @@ func StaticHeaderDevLinks(u weburl.GnoURL, mode ViewMode) []HeaderLink { func EnrichHeaderData(data HeaderData, mode ViewMode) HeaderData { data.RealmPath = data.RealmURL.EncodeURL() - data.Links.Dev = StaticHeaderDevLinks(data.RealmURL, mode) + data.Links.Dev = StaticHeaderDevLinks(data.RealmURL, mode, data.Static) data.Links.General = nil if mode.ShouldShowGeneralLinks() { diff --git a/gno.land/pkg/gnoweb/components/layout_index.go b/gno.land/pkg/gnoweb/components/layout_index.go index f68d69a3991..2a3172d4ecc 100644 --- a/gno.land/pkg/gnoweb/components/layout_index.go +++ b/gno.land/pkg/gnoweb/components/layout_index.go @@ -1,5 +1,17 @@ package components +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/text" +) + // ViewMode represents the current view mode of the application // It affects the layout, navigation, and display of content type ViewMode int @@ -43,6 +55,108 @@ type HeadData struct { BuildTime string } +// MaxBannerLength is the maximum character length for banner markdown source. +const MaxBannerLength = 400 + +// BannerData implements Component. +var _ Component = BannerData{} + +// BannerData holds pre-rendered inline HTML from markdown. +type BannerData struct { + content string + url string +} + +func (b BannerData) Enabled() bool { return b.content != "" } +func (b BannerData) HasURL() bool { return b.url != "" } +func (b BannerData) URL() string { return b.url } + +func (b BannerData) Render(w io.Writer) (err error) { + _, err = io.WriteString(w, b.content) + return err +} + +// NewBannerData parses inline markdown into a BannerData with pre-rendered HTML. +// Content after the first newline is discarded. Content is truncated to MaxBannerLength runes. +// If globalURL is non-empty (http/https only), the banner acts as a single clickable link +// and any inline markdown links are unwrapped to plain text. +func NewBannerData(markdown, globalURL string) (BannerData, error) { + // Keep only the first line + if i := strings.IndexAny(markdown, "\n\r"); i >= 0 { + markdown = markdown[:i] + } + markdown = strings.TrimSpace(markdown) + + if markdown == "" { + return BannerData{}, nil + } + + // Truncate to max length (rune-safe) + if runes := []rune(markdown); len(runes) > MaxBannerLength { + markdown = string(runes[:MaxBannerLength]) + } + + // Validate global URL: only http/https allowed. + globalURL = strings.TrimSpace(globalURL) + hasGlobalURL := strings.HasPrefix(globalURL, "https://") || strings.HasPrefix(globalURL, "http://") + + md := goldmark.New(goldmark.WithExtensions(extension.Strikethrough)) + src := []byte(markdown) + doc := md.Parser().Parse(text.NewReader(src)) + + // Keep only Paragraph nodes (the inline-content wrapper). All other + // block-level nodes (headings, code blocks, lists, HTML blocks, etc.) + // are removed so the banner contains only inline markup. + for c := doc.FirstChild(); c != nil; { + next := c.NextSibling() + if c.Kind() != ast.KindParagraph { + doc.RemoveChild(doc, c) + } + c = next + } + + ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering || n.Kind() != ast.KindLink { + return ast.WalkContinue, nil + } + + if hasGlobalURL { + // Replace link node with its children (keep text, drop the ). + parent := n.Parent() + for c := n.FirstChild(); c != nil; { + next := c.NextSibling() + parent.InsertBefore(parent, n, c) + c = next + } + parent.RemoveChild(parent, n) + return ast.WalkSkipChildren, nil + } + + n.SetAttributeString("target", "_blank") + n.SetAttributeString("rel", "noopener noreferrer") + return ast.WalkContinue, nil + }) + + var buf bytes.Buffer + if err := md.Renderer().Render(&buf, src, doc); err != nil { + return BannerData{}, fmt.Errorf("banner markdown rendering: %w", err) + } + + // Strip the

wrapper that goldmark adds for single-paragraph content. + result := strings.TrimSpace(buf.String()) + if after, ok := strings.CutPrefix(result, "

"); ok { + if inner, ok := strings.CutSuffix(after, "

"); ok { + result = inner + } + } + + bd := BannerData{content: result} + if hasGlobalURL { + bd.url = globalURL + } + return bd, nil +} + type IndexData struct { HeadData HeaderData @@ -50,6 +164,7 @@ type IndexData struct { BodyView *View Mode ViewMode Theme string + Banner BannerData } type indexLayoutParams struct { diff --git a/gno.land/pkg/gnoweb/components/layout_test.go b/gno.land/pkg/gnoweb/components/layout_test.go index 720ca9b0e96..a43093184f8 100644 --- a/gno.land/pkg/gnoweb/components/layout_test.go +++ b/gno.land/pkg/gnoweb/components/layout_test.go @@ -185,7 +185,7 @@ func TestStaticHeaderDevLinks_WithRealmMode(t *testing.T) { } // Test realm mode (default case) - links := StaticHeaderDevLinks(u, ViewModeRealm) + links := StaticHeaderDevLinks(u, ViewModeRealm, false) assert.Len(t, links, 3, "expected Content, Source, and Actions links") assert.Equal(t, "Content", links[0].Label) assert.Equal(t, "Source", links[1].Label) @@ -200,12 +200,24 @@ func TestStaticHeaderDevLinks_WithPackageMode(t *testing.T) { } // Test package mode - links := StaticHeaderDevLinks(u, ViewModePackage) + links := StaticHeaderDevLinks(u, ViewModePackage, false) assert.Len(t, links, 2, "expected Content and Source links only") assert.Equal(t, "Content", links[0].Label) assert.Equal(t, "Source", links[1].Label) } +func TestStaticHeaderDevLinks_StaticContent(t *testing.T) { + t.Parallel() + + u := weburl.GnoURL{ + Path: "/r/test/pkg", + } + + links := StaticHeaderDevLinks(u, ViewModeRealm, true) + require.Len(t, links, 1, "static content should only have Content link") + assert.Equal(t, "Content", links[0].Label) +} + func TestStaticHeaderDevLinks_WithExplorerMode(t *testing.T) { t.Parallel() @@ -214,7 +226,7 @@ func TestStaticHeaderDevLinks_WithExplorerMode(t *testing.T) { } // Test explorer mode - links := StaticHeaderDevLinks(u, ViewModeExplorer) + links := StaticHeaderDevLinks(u, ViewModeExplorer, false) assert.Empty(t, links, "expected no links in explorer mode") } @@ -357,3 +369,217 @@ func TestIndexLayout_ThemePropagation(t *testing.T) { }) } } + +func TestNewBannerData(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input string + globalURL string + wantEnabled bool + wantHasURL bool + wantContains string + wantNotContains string + }{ + { + name: "empty is disabled", + input: "", + wantEnabled: false, + }, + { + name: "plain text", + input: "Beta", + wantEnabled: true, + wantContains: "Beta", + }, + { + name: "markdown link gets target blank", + input: "[Beta](https://example.com)", + wantEnabled: true, + wantContains: `
Beta`, + }, + { + name: "bold and italic", + input: "This is **bold** and *italic*", + wantEnabled: true, + wantContains: "bold", + }, + { + name: "content after newline discarded", + input: "line one\nline two", + wantEnabled: true, + wantContains: "line one", + }, + { + name: "truncated over max length", + input: strings.Repeat("a", MaxBannerLength+50), + wantEnabled: true, + }, + { + name: "HTML block stripped", + input: ``, + wantEnabled: false, + }, + { + name: "javascript URL sanitized", + input: `[click](javascript:alert(1))`, + wantEnabled: true, + wantContains: `href=""`, + }, + { + name: "global URL strips inline links", + input: "[click](https://other.com)", + globalURL: "https://gno.land", + wantEnabled: true, + wantHasURL: true, + wantContains: "click", + wantNotContains: `href="https://other.com"`, + }, + { + name: "global javascript URL rejected", + input: "Hello", + globalURL: "javascript:alert(1)", + wantEnabled: true, + wantHasURL: false, + }, + { + name: "global ftp URL rejected", + input: "Hello", + globalURL: "ftp://bad.com", + wantEnabled: true, + wantHasURL: false, + }, + { + name: "heading block stripped", + input: "# Big Heading", + wantEnabled: false, + }, + { + name: "blockquote stripped", + input: "> quoted text", + wantEnabled: false, + }, + { + name: "thematic break stripped", + input: "---", + wantEnabled: false, + }, + { + name: "list item stripped", + input: "- list entry", + wantEnabled: false, + }, + { + name: "leading whitespace trimmed before parsing", + input: " code line", + wantEnabled: true, + wantContains: "code line", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + banner, err := NewBannerData(tc.input, tc.globalURL) + require.NoError(t, err) + assert.Equal(t, tc.wantEnabled, banner.Enabled()) + assert.Equal(t, tc.wantHasURL, banner.HasURL()) + + var buf strings.Builder + require.NoError(t, banner.Render(&buf)) + rendered := buf.String() + + if tc.wantContains != "" { + assert.Contains(t, rendered, tc.wantContains) + } + if tc.wantNotContains != "" { + assert.NotContains(t, rendered, tc.wantNotContains) + } + if banner.Enabled() { + assert.NotContains(t, rendered, "

") + } + }) + } +} + +func TestIndexLayout_Banner(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + markdown string + url string + wantBanner bool + wantContains string + wantNotContains string + }{ + { + name: "no banner when empty", + markdown: "", + wantBanner: false, + }, + { + name: "plain text renders in div", + markdown: "Maintenance", + wantBanner: true, + wantContains: "Maintenance", + }, + { + name: "markdown link renders inline", + markdown: "[Beta](https://example.com)", + wantBanner: true, + wantContains: `href="https://example.com"`, + }, + { + name: "global URL wraps banner in anchor", + markdown: "Beta release", + url: "https://gno.land", + wantBanner: true, + wantContains: ` {{ template "ui/icons" -}} + {{ if .IndexData.Banner.Enabled -}} + {{ if .IndexData.Banner.HasURL -}} + + {{ render .IndexData.Banner }} + + {{ else -}} +

+ {{ render .IndexData.Banner }} +
+ {{ end -}} + {{ end -}} {{ template "layouts/header" .IndexData.HeaderData -}}
diff --git a/gno.land/pkg/gnoweb/components/template.go b/gno.land/pkg/gnoweb/components/template.go index 20e31a77dea..4ebe808e3c7 100644 --- a/gno.land/pkg/gnoweb/components/template.go +++ b/gno.land/pkg/gnoweb/components/template.go @@ -6,6 +6,7 @@ import ( "fmt" "html/template" "net/url" + "strings" ) //go:embed ui/*.html views/*.html layouts/*.html @@ -34,6 +35,7 @@ func registerCommonFuncs(funcs template.FuncMap) { return vals.Has(key) } funcs["FormatRelativeTime"] = FormatRelativeTimeSince + funcs["hasPrefix"] = strings.HasPrefix // dict creates a map from key-value pairs for passing multiple values to templates funcs["dict"] = func(kv ...any) (map[string]any, error) { if len(kv)%2 != 0 { diff --git a/gno.land/pkg/gnoweb/components/ui/gnome.html b/gno.land/pkg/gnoweb/components/ui/gnome.html index d97bb255565..fef7edf6e20 100644 --- a/gno.land/pkg/gnoweb/components/ui/gnome.html +++ b/gno.land/pkg/gnoweb/components/ui/gnome.html @@ -1,10 +1,10 @@ {{ define "ui/gnome" }} -

{{ $pkgpath }}

diff --git a/gno.land/pkg/gnoweb/frontend/css/02-tools.css b/gno.land/pkg/gnoweb/frontend/css/02-tools.css index ac55da144fe..039941b9877 100644 --- a/gno.land/pkg/gnoweb/frontend/css/02-tools.css +++ b/gno.land/pkg/gnoweb/frontend/css/02-tools.css @@ -145,6 +145,10 @@ --s-border: var(--g-space-px, 1px) solid var(--s-color-border-primary); /* Small border width */ --s-border-secondary: var(--g-space-px, 1px) solid var(--s-color-border-secondary); /* Small border width */ + + /* Logo / gnome */ + --s-logo-hat: var(--g-color-green-600, #226c57); + --s-logo-beard: var(--g-color-gray-300, #999999); } /* ===== DARK THEME ===== */ diff --git a/gno.land/pkg/gnoweb/frontend/css/06-blocks.css b/gno.land/pkg/gnoweb/frontend/css/06-blocks.css index 0d266626aed..aaa5c8e76a9 100644 --- a/gno.land/pkg/gnoweb/frontend/css/06-blocks.css +++ b/gno.land/pkg/gnoweb/frontend/css/06-blocks.css @@ -2,6 +2,53 @@ 06-BLOCKS - Reusable UI components and blocks (CUBE CSS: Block) ========================================================================== */ +/* ===== GNOME / LOGO SVG ===== */ +.b-gnome, +.b-logo { + & .hat { + fill: var(--s-logo-hat); + } + + & .beard { + fill: var(--s-logo-beard); + } +} + +/* ===== BANNER ===== */ +.b-banner { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: var(--g-space-1-5) var(--g-space-4); + background-color: var(--s-color-bg-brand-default); + color: var(--s-color-text-base); + font-size: var(--g-font-size-50); + font-weight: var(--g-font-semibold); + text-align: center; + + @media (--md) { + font-size: var(--g-font-size-100); + } + + & a { + color: inherit; + text-decoration: underline; + } + + & a:hover, + &:is(a):hover { + opacity: 0.8; + } + + & code { + padding: var(--g-space-0-5) var(--g-space-1); + border-radius: var(--g-border-radius-sm); + background-color: var(--s-color-bg-brand-action); + font-size: 0.96em; + } +} + /* ===== HEADER COMPONENT ===== */ .b-header { position: sticky; @@ -859,7 +906,7 @@ border-top: var(--s-border); } - & > a { + & > :where(a, div) { display: flex; justify-content: space-between; align-items: center; @@ -872,6 +919,15 @@ .c-icon { margin-inline-start: 0; } + + & > a { + flex: 1; + min-width: 0; + + &:hover { + background-color: transparent; + } + } } } diff --git a/gno.land/pkg/gnoweb/handler_http.go b/gno.land/pkg/gnoweb/handler_http.go index 0ffff51fd2a..dc8499c806d 100644 --- a/gno.land/pkg/gnoweb/handler_http.go +++ b/gno.land/pkg/gnoweb/handler_http.go @@ -31,6 +31,7 @@ type StaticMetadata struct { ChainId string Analytics bool BuildTime string + Banner components.BannerData } type AliasKind int @@ -138,7 +139,8 @@ func (h *HTTPHandler) Get(w http.ResponseWriter, r *http.Request) { AssetsPath: h.Static.AssetsPath, BuildTime: h.Static.BuildTime, }, - Theme: theme, + Theme: theme, + Banner: h.Static.Banner, } // Parse the URL @@ -263,6 +265,7 @@ func (h *HTTPHandler) prepareIndexBodyView(r *http.Request, indexData *component switch { case aliasExists && aliasTarget.Kind == StaticMarkdown: + indexData.HeaderData.Static = true return h.GetMarkdownView(gnourl, aliasTarget.Value) case gnourl.IsRealm(), gnourl.IsPure(), gnourl.IsUser(): return h.GetPackageView(ctx, gnourl, indexData) diff --git a/gno.land/pkg/gnoweb/handler_http_test.go b/gno.land/pkg/gnoweb/handler_http_test.go index d48f0b7b217..d6110a27ae9 100644 --- a/gno.land/pkg/gnoweb/handler_http_test.go +++ b/gno.land/pkg/gnoweb/handler_http_test.go @@ -365,6 +365,30 @@ func TestHTTPHandler_DirectoryViewErrorTotal(t *testing.T) { assert.Contains(t, rr.Body.String(), "internal error") } +// TestHTTPHandler_RealmExplorerWithRender tests realms with Render() show realm icon and Source button. +func TestHTTPHandler_RealmExplorerWithRender(t *testing.T) { + t.Parallel() + + realmWithRender := &gnoweb.MockPackage{ + Domain: "gno.land", + Path: "/r/demo/withrender", + Files: map[string]string{"render.gno": `package withrender`}, + Functions: []*doc.JSONFunc{{ + Name: "Render", + Params: []*doc.JSONField{{Name: "path", Type: "string"}}, + Results: []*doc.JSONField{{Type: "string"}}, + }}, + } + + handler, _ := gnoweb.NewHTTPHandler(slog.New(slog.NewTextHandler(&testingLogger{t}, nil)), newTestHandlerConfig(t, gnoweb.NewMockClient(realmWithRender))) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/r/demo/withrender", nil)) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Contains(t, rr.Body.String(), "Source") + assert.Contains(t, rr.Body.String(), "Action") +} + // TestNewWebHandlerInvalidConfig ensures that NewWebHandler fails on invalid config. func TestHTTPHandler_NewInvalidConfig(t *testing.T) { t.Parallel() diff --git a/gno.land/pkg/gnoweb/markdown/ext_links.go b/gno.land/pkg/gnoweb/markdown/ext_links.go index 54fa2a3d2ca..a711b7a9379 100644 --- a/gno.land/pkg/gnoweb/markdown/ext_links.go +++ b/gno.land/pkg/gnoweb/markdown/ext_links.go @@ -87,34 +87,57 @@ func (*GnoLink) Kind() ast.NodeKind { // linkTransformer implements ASTTransformer type linkTransformer struct{} -// Transform replaces ast.Link nodes with GnoLink nodes in two passes. +// Transform replaces ast.Link and ast.AutoLink nodes with GnoLink nodes. func (t *linkTransformer) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) { orig, ok := getUrlFromContext(pc) if !ok { return } - // Traverse through the document and transform link nodes to GnoLink nodes. ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } - link, ok := node.(*ast.Link) - if !ok { + var ( + gnoLink *GnoLink + rawDest []byte + ) + + switch n := node.(type) { + case *ast.Link: + // Wrap the existing link node directly. + gnoLink = &GnoLink{Link: n} + rawDest = n.Destination + + case *ast.AutoLink: + // Build a synthetic ast.Link so the existing renderGnoLink handles + // IsDangerousURL, rel attributes, and icons for autolinks too. + source := reader.Source() + rawURL := n.URL(source) + if n.AutoLinkType == ast.AutoLinkEmail { + rawDest = append([]byte("mailto:"), rawURL...) + } else { + rawDest = rawURL + } + link := ast.NewLink() + link.Destination = rawDest + labelNode := ast.NewString(n.Label(source)) + labelNode.SetRaw(true) + link.AppendChild(link, labelNode) + gnoLink = &GnoLink{Link: link} + + default: return ast.WalkContinue, nil } - // Create a new GnoLink node wrapping the original link. - gnoLink := &GnoLink{Link: link} - - // Replace the original link with the GnoLink wrapper. + // Replace the original node with the GnoLink wrapper. parent, next := node.Parent(), node.NextSibling() parent.RemoveChild(parent, node) parent.InsertBefore(parent, next, gnoLink) // Parse destination URL and check for validity. - dest, err := url.Parse(string(link.Destination)) + dest, err := url.Parse(string(rawDest)) if err != nil { gnoLink.LinkType = GnoLinkTypeInvalid return ast.WalkContinue, nil diff --git a/gno.land/pkg/gnoweb/markdown/golden/ext_link/autolink.md.txtar b/gno.land/pkg/gnoweb/markdown/golden/ext_link/autolink.md.txtar new file mode 100644 index 00000000000..a07780e9f32 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/golden/ext_link/autolink.md.txtar @@ -0,0 +1,20 @@ +-- input.md -- + + + + + + + + + + + + +-- output.html -- +

https://example.com

+

user@example.com

+

javascript:alert(1)

+

vbscript:alert(1)

+

file:///etc/passwd

+

data:text/html,%3Cb%3Ehi%3C/b%3E

diff --git a/gno.land/pkg/gnoweb/public/main.css b/gno.land/pkg/gnoweb/public/main.css index 6063b3c47b8..86900eca7c6 100644 --- a/gno.land/pkg/gnoweb/public/main.css +++ b/gno.land/pkg/gnoweb/public/main.css @@ -1,5 +1,5 @@ -:root{--g-px-base:16;--g-space-mult:4;--g-space-base:calc(1rem/var(--g-space-mult));--g-breakpoint-max:calc(1580/var(--g-px-base)*1rem);--g-z-min:-1;--g-z-1:1;--g-z-max:9999;--g-duration-75:75ms;--g-duration-150:150ms;--g-opacity-50:0.5;--g-grid-1:repeat(1,minmax(0,1fr));--g-grid-10:repeat(10,minmax(0,1fr));--g-space-px:1px;--g-space-0-5:calc(var(--g-space-base)*0.5);--g-space-1:var(--g-space-base);--g-space-1-5:calc(var(--g-space-base)*1.5);--g-space-2:calc(var(--g-space-base)*2);--g-space-2-5:calc(var(--g-space-base)*2.5);--g-space-3:calc(var(--g-space-base)*3);--g-space-4:calc(var(--g-space-base)*4);--g-space-4-5:calc(var(--g-space-base)*4.5);--g-space-5:calc(var(--g-space-base)*5);--g-space-6:calc(var(--g-space-base)*6);--g-space-7:calc(var(--g-space-base)*7);--g-space-8:calc(var(--g-space-base)*8);--g-space-10:calc(var(--g-space-base)*10);--g-space-12:calc(var(--g-space-base)*12);--g-space-14:calc(var(--g-space-base)*14);--g-space-20:calc(var(--g-space-base)*20);--g-space-24:calc(var(--g-space-base)*24);--g-space-28:calc(var(--g-space-base)*28);--g-space-32:calc(var(--g-space-base)*32);--g-space-36:calc(var(--g-space-base)*36);--g-space-44:calc(var(--g-space-base)*44);--g-space-48:calc(var(--g-space-base)*48);--g-space-52:calc(var(--g-space-base)*52);--g-space-72:calc(var(--g-space-base)*72);--g-space-96:calc(var(--g-space-base)*96);--g-font-size-50:calc(12/var(--g-px-base)*1rem);--g-font-size-100:calc(14/var(--g-px-base)*1rem);--g-font-size-200:calc(16/var(--g-px-base)*1rem);--g-font-size-300:calc(18/var(--g-px-base)*1rem);--g-font-size-400:calc(20/var(--g-px-base)*1rem);--g-font-size-500:calc(22/var(--g-px-base)*1rem);--g-font-size-600:calc(24/var(--g-px-base)*1rem);--g-font-size-700:calc(32/var(--g-px-base)*1rem);--g-font-size-800:calc(38/var(--g-px-base)*1rem);--g-font-family-mono:"Roboto",'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace';--g-font-family-inter-var:"Inter",'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif';--g-font-normal:400;--g-font-medium:500;--g-font-semibold:600;--g-font-bold:700;--g-italic:oblique 14deg;--g-line-height-tight:1.25;--g-line-height-snug:1.375;--g-line-height-normal:1.5;--g-border-radius-sm:calc(4/var(--g-px-base)*1rem);--g-border-radius:calc(6/var(--g-px-base)*1rem);--g-border-radius-full:9999px;--g-color-light:#fff;--g-color-transparent:transparent;--g-color-gray-50:#f0f0f0;--g-color-gray-100:#e2e2e2;--g-color-gray-200:#bdbdbd;--g-color-gray-300:#999;--g-color-gray-400:#7c7c7c;--g-color-gray-500:#696969;--g-color-gray-600:#585858;--g-color-gray-700:#292929;--g-color-gray-750:#1f1f1f;--g-color-gray-800:#141414;--g-color-gray-850:#0e0e0e;--g-color-gray-900:#090909;--g-color-green-50:#e7efed;--g-color-green-400:#60ab96;--g-color-green-500:#277b63;--g-color-green-600:#226c57;--g-color-green-900:#144134;--g-color-green-950:#002c20;--g-color-blue-400:#49afeb;--g-color-blue-600:#3e96c9;--g-color-blue-900:#21506b;--g-color-yellow-50:#fff7eb;--g-color-yellow-400:#facc32;--g-color-yellow-600:#fbbf24;--g-color-yellow-900:#7b4807;--g-color-yellow-950:#362600;--g-color-red-400:#eb6c49;--g-color-red-600:#c95c3e;--g-color-red-900:#6b2521;--g-color-purple-400:#7f49eb;--g-color-purple-600:#6c3ec9;--g-color-purple-900:#39216b}@supports (color:color(display-p3 0 0 0%)){:root{--g-color-green-950:#002c20;--g-color-yellow-50:#fff7eb;--g-color-yellow-950:#362600}@media (color-gamut:p3){:root{--g-color-green-950:color(display-p3 0.04602 0.17026 0.1277);--g-color-yellow-50:color(display-p3 0.99709 0.97106 0.92232);--g-color-yellow-950:color(display-p3 0.2031 0.15112 0.01811)}}}:root{--s-color-bg-base:var(--g-color-light,#fff);--s-color-bg-base-dev:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary-hover:var(--g-color-gray-100,#f0f0f0);--s-color-bg-surface-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-bg-surface-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-bg-brand-default:var(--g-color-green-600,#226c57);--s-color-bg-brand-weak:var(--g-color-green-50,#f0f9ff);--s-color-bg-success-default:var(--g-color-green-600,#144134);--s-color-bg-info-default:var(--g-color-blue-600,#21506b);--s-color-bg-warning-default:var(--g-color-yellow-600,#665100);--s-color-bg-warning-weak:var(--g-color-yellow-50,#f9d985);--s-color-bg-warning-action:var(--g-color-yellow-400,#f9d985);--s-color-bg-caution-default:var(--g-color-red-600,#610);--s-color-bg-tip-default:var(--g-color-purple-600,#49216b);--s-color-bg-note-default:var(--g-color-gray-600,#21506b);--s-color-bg-input:var(--g-color-light,#fff);--s-color-text-base:var(--g-color-light,#fff);--s-color-text-primary:var(--g-color-gray-900,#080809);--s-color-text-secondary:var(--g-color-gray-600,#454a4e);--s-color-text-tertiary:var(--g-color-gray-400,#f0f0f0);--s-color-text-tertiary-hover:var(--g-color-gray-600,#e2e2e2);--s-color-text-quaternary:var(--g-color-gray-100,#f0f0f0);--s-color-text-brand-default:var(--g-color-light,#fff);--s-color-text-link:var(--g-color-green-600,#226c57);--s-color-text-link-hover:var(--g-color-green-600,#226c57);--s-color-text-success:var(--g-color-green-900,#144134);--s-color-text-info:var(--g-color-blue-900,#21506b);--s-color-text-warning:var(--g-color-yellow-900,#665100);--s-color-text-caution:var(--g-color-red-900,#610);--s-color-text-tip:var(--g-color-purple-900,#49216b);--s-color-border-primary:var(--g-color-gray-200,#bdbdbd);--s-color-border-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-border-tertiary:var(--g-color-gray-300,#999);--s-color-border-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-border-transparent:var(--g-color-transparent,transparent);--s-color-border-input:var(--g-color-gray-300,#999);--s-color-border-brand-default:var(--g-color-green-600,#226c57);--s-color-border-success:var(--g-color-green-600,#144134);--s-color-border-info:var(--g-color-blue-600,#21506b);--s-color-border-warning:var(--g-color-yellow-600,#665100);--s-color-border-error:var(--g-color-red-600,#610);--s-color-border-tip:var(--g-color-purple-600,#49216b);--s-color-border-note:var(--g-color-gray-600,#21506b);--s-rounded-sm:var(--g-border-radius-sm,4px);--s-rounded:var(--g-border-radius,6px);--s-rounded-full:var(--g-border-radius-full,9999px);--s-border:var(--g-space-px,1px) solid var(--s-color-border-primary);--s-border-secondary:var(--g-space-px,1px) solid var(--s-color-border-secondary)}[data-theme=dark]{--s-color-bg-base:var(--g-color-gray-850);--s-color-bg-base-dev:var(--g-color-gray-800);--s-color-bg-surface-primary:var(--g-color-gray-800);--s-color-bg-surface-primary-hover:var(--g-color-gray-750);--s-color-bg-surface-secondary:var(--g-color-gray-750);--s-color-bg-surface-quaternary:var(--g-color-gray-600);--s-color-bg-brand-weak:var(--g-color-green-950);--s-color-bg-warning-weak:var(--g-color-yellow-950);--s-color-bg-input:var(--g-color-gray-800);--s-color-text-primary:var(--g-color-gray-100);--s-color-text-secondary:var(--g-color-gray-200);--s-color-text-tertiary:var(--g-color-gray-400);--s-color-text-tertiary-hover:var(--g-color-gray-300);--s-color-text-quaternary:var(--g-color-gray-500);--s-color-text-brand-default:var(--g-color-light);--s-color-text-link:var(--g-color-green-500);--s-color-text-link-hover:var(--g-color-green-400);--s-color-text-success:var(--g-color-green-400);--s-color-text-info:var(--g-color-blue-400);--s-color-text-warning:var(--g-color-yellow-400);--s-color-text-caution:var(--g-color-red-400);--s-color-text-tip:var(--g-color-purple-400);--s-color-border-primary:var(--g-color-gray-700);--s-color-border-secondary:var(--g-color-gray-750);--s-color-border-tertiary:var(--g-color-gray-600);--s-color-border-quaternary:var(--g-color-gray-500);--s-color-border-input:var(--g-color-gray-700);--s-color-border-brand-default:var(--g-color-green-600);--s-color-border-success:var(--g-color-green-400);--s-color-border-info:var(--g-color-blue-400);--s-color-border-warning:var(--g-color-yellow-400);--s-color-border-error:var(--g-color-red-400);--s-color-border-tip:var(--g-color-purple-400);--s-color-border-note:var(--g-color-gray-600)}*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}h1,h2,h3{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub{bottom:-.25em;font-size:75%;line-height:0;position:relative;vertical-align:baseline}table{border-collapse:collapse;border-color:inherit;text-indent:0}summary{display:list-item}menu,ol,ul{list-style:none}embed,img,object,svg{display:block;vertical-align:middle}img{height:auto;max-width:100%}::file-selector-button,button,input,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}::file-selector-button{margin-right:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}::-webkit-calendar-picker-indicator{line-height:1}::file-selector-button,button,input:where([type=button],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}@font-face{font-display:swap;font-family:Roboto;font-style:normal;font-weight:900;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-display:block;font-family:Inter;font-style:oblique 0deg 10deg;font-variant:normal;font-weight:100 900;src:url(fonts/intervar/Intervar.woff2) format("woff2")}html{background-color:var(--s-color-bg-base);color:var(--s-color-text-secondary);font-family:var(--g-font-family-inter-var);font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on,contextual common-ligatures,"kern";-webkit-font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on;font-size:calc(var(--g-px-base)*1px);line-height:var(--g-line-height-normal);-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-kerning:normal;font-variant-ligatures:contextual common-ligatures;text-rendering:optimizeLegibility}body{display:flex;flex-direction:column;min-height:100vh}main{background-color:var(--s-color-bg-base);flex-grow:2;width:100%}main.dev-mode{background-color:var(--s-color-bg-base-dev)}main>section{display:grid;grid-auto-flow:dense;grid-template-columns:var(--g-grid-1);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20);min-height:100%;padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){main>section{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}@media (min-width:calc(820 / 16 * 1rem)){main>section{grid-template-columns:var(--g-grid-10)}}@media (min-width:calc(1366 / 16 * 1rem)){main>section{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}svg{max-height:100%;max-width:100%}form{margin-bottom:0;margin-top:0}code{font-family:var(--g-font-mono)}summary{cursor:pointer}md-renderer{margin-top:var(--g-space-4);padding-bottom:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){md-renderer{grid-column:span 7;margin-top:0}}::-moz-selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}::selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}summary::-webkit-details-marker{display:none}summary::marker{display:none}.c-stack{display:flex;flex-direction:column;justify-content:flex-start}.c-stack>*+*{margin-top:var(--g-space-4)}.c-inline{align-items:center;display:inline-flex;gap:var(--g-space-3)}.c-between{align-items:center;display:flex;justify-content:space-between}.c-center{box-sizing:border-box;margin-left:auto;margin-right:auto;max-width:var(--g-breakpoint-max);padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){.c-center{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}.c-full-screen{align-items:center;display:flex;flex-direction:column;grid-column:1/-1;height:100%;justify-content:center;margin-top:var(--g-space-10);padding-bottom:var(--g-space-24);width:100%}.c-reel{display:flex;overflow:scroll}.c-icon{flex-shrink:0;height:1.15em;width:1.15em}.c-with-icon{align-items:flex-start;display:inline-flex}.c-with-icon .c-icon,.c-with-icon--inline .c-icon{margin-left:.3em;margin-right:.3em;margin-top:.15em}.c-with-icon--inline{display:inline-block}.c-with-icon--inline>*{vertical-align:middle}.c-with-icon--inline .c-icon{margin-top:0}.c-view-grid{display:flex;flex-direction:column}@media (min-width:calc(640 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-8);column-gap:var(--g-space-8);flex-direction:row}}@media (min-width:calc(820 / 16 * 1rem)){.c-view-grid{display:grid;grid-template-columns:var(--g-grid-10);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20)}}@media (min-width:calc(1366 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}.c-toggle-btn>input{display:none}.c-toggle-btn label{visibility:hidden}.c-toggle-btn input:checked+label{visibility:visible}.c-readme-view,.c-realm-view{--cr-px-base:var(--g-px-base);--cr-space-mult:1;--cr-space-base:calc(1em/var(--g-space-mult)*var(--cr-space-mult));--cr-space-0:0;--cr-space-0-5:calc(var(--cr-space-base)*0.5);--cr-space-1:var(--cr-space-base);--cr-space-2:calc(var(--cr-space-base)*2);--cr-space-3:calc(var(--cr-space-base)*3);--cr-space-4:calc(var(--cr-space-base)*4);--cr-space-5:calc(var(--cr-space-base)*5);--cr-space-7:calc(var(--cr-space-base)*7);--cr-space-8:calc(var(--cr-space-base)*8);--cr-space-24:calc(var(--cr-space-base)*24);--cr-color-brand-default:var(--s-color-text-link);display:block;font-size:calc(var(--cr-px-base)*1px);padding-top:var(--g-space-4);word-break:break-word}.c-readme-view:empty,.c-realm-view:empty{display:none}.c-realm-view:has(.b-btn:only-child){display:none}.c-readme-view:has(.b-btn:only-child){display:none}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view,.c-realm-view{grid-row-start:1;padding-top:var(--g-space-6)}}.c-readme-view a,.c-realm-view a{color:var(--cr-color-brand-default);display:inline-block;font-weight:inherit;position:relative;text-wrap:balance;vertical-align:top}.c-readme-view a:hover,.c-realm-view a:hover{-webkit-text-decoration:underline;text-decoration:underline}.c-realm-view a:has(>img){vertical-align:middle}.c-readme-view a:has(>img){vertical-align:middle}.c-readme-view a>span,.c-realm-view a>span{margin-bottom:.1em}.c-readme-view a>.tooltip+.tooltip,.c-realm-view a>.tooltip+.tooltip{margin-left:.2em}.c-readme-view a>.tooltip:last-of-type,.c-realm-view a>.tooltip:last-of-type{margin-right:.2em}.c-realm-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view h1+h2,.c-readme-view h2+h3,.c-readme-view h3+h4,.c-realm-view h1+h2,.c-realm-view h2+h3,.c-realm-view h3+h4{margin-top:var(--cr-space-4)}.c-readme-view h1,.c-readme-view h2,.c-readme-view h3,.c-readme-view h4,.c-realm-view h1,.c-realm-view h2,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-primary);line-height:var(--g-line-height-tight);margin-top:var(--cr-space-4)}.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-800)}}.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-500);font-weight:var(--g-font-bold)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-600)}}.c-readme-view h2 *,.c-realm-view h2 *{font-weight:var(--g-font-bold)}.c-readme-view h3,.c-readme-view h4,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-secondary);font-weight:var(--g-font-semibold)}.c-readme-view h3,.c-realm-view h3{font-size:var(--g-font-size-400);margin-top:var(--cr-space-4)}.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300);margin-top:var(--cr-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300)}}.c-readme-view h3 *,.c-readme-view h4 *,.c-realm-view h3 *,.c-realm-view h4 *{font-weight:var(--g-font-semibold)}.c-readme-view h5,.c-readme-view h6,.c-realm-view h5,.c-realm-view h6{font-size:var(--g-font-size-300);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-0);margin-top:var(--cr-space-0)}.c-readme-view h5+p,.c-readme-view h6+p,.c-realm-view h5+p,.c-realm-view h6+p{margin-top:var(--cr-space-0)}.c-readme-view img,.c-realm-view img{border:1px solid var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);max-width:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.c-readme-view figure,.c-realm-view figure{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);text-align:center}.c-readme-view figcaption,.c-realm-view figcaption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100)}.c-readme-view video,.c-realm-view video{margin-bottom:var(--g-space-4);margin-top:var(--g-space-4);max-width:100%}.c-readme-view p,.c-realm-view p{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-realm-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-readme-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-realm-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view strong,.c-readme-view strong *,.c-realm-view strong,.c-realm-view strong *{font-weight:var(--g-font-bold)}.c-readme-view em,.c-realm-view em{font-style:var(--g-italic)}.c-readme-view blockquote,.c-realm-view blockquote{border-left:solid var(--g-space-0-5) var(--s-color-border-tertiary);color:var(--s-color-text-secondary);margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-3)}.c-readme-view blockquote>blockquote,.c-realm-view blockquote>blockquote{margin-bottom:var(--cr-space-7);margin-top:var(--cr-space-7)}.c-readme-view caption,.c-realm-view caption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);margin-top:var(--cr-space-2);text-align:left}.c-readme-view q,.c-realm-view q{quotes:"“" "”"}.c-readme-view q:before,.c-realm-view q:before{content:open-quote}.c-readme-view q:after,.c-realm-view q:after{content:close-quote}.c-readme-view details,.c-realm-view details{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-readme-view summary,.c-realm-view summary{cursor:pointer;font-weight:var(--g-font-bold)}.c-readme-view math,.c-realm-view math{font-family:var(--g-font-family-mono)}.c-readme-view small,.c-realm-view small{font-size:var(--g-font-size-100)}.c-readme-view del,.c-realm-view del{-webkit-text-decoration:line-through;text-decoration:line-through}.c-readme-view sub,.c-realm-view sub{font-size:var(--g-font-size-50);vertical-align:sub}.c-readme-view sup,.c-realm-view sup{font-size:var(--g-font-size-50);padding-left:var(--space-px);vertical-align:middle}.c-readme-view sup>a,.c-realm-view sup>a{vertical-align:middle}.c-readme-view ol,.c-readme-view ul,.c-realm-view ol,.c-realm-view ul{margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-4)}.c-readme-view ul,.c-realm-view ul{list-style:disc}.c-readme-view ol,.c-realm-view ol{list-style:decimal}.c-readme-view ol ol,.c-readme-view ol ul,.c-readme-view ul ol,.c-readme-view ul ul,.c-realm-view ol ol,.c-realm-view ol ul,.c-realm-view ul ol,.c-realm-view ul ul{margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);padding-left:var(--g-space-4)}.c-readme-view li,.c-realm-view li{margin-bottom:var(--cr-space-1);margin-top:var(--cr-space-1)}.c-readme-view code,.c-readme-view pre,.c-realm-view code,.c-realm-view pre{font-family:var(--g-font-family-mono)}.c-readme-view pre,.c-readme-view pre.chroma-chroma,.c-realm-view pre,.c-realm-view pre.chroma-chroma{background-color:var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);overflow-x:auto;padding:var(--cr-space-4)}.c-readme-view :not(pre)>code,.c-realm-view :not(pre)>code{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--g-border-radius-sm);font-size:.96em;padding:var(--cr-space-0-5) var(--cr-space-1)}.c-readme-view a code,.c-realm-view a code{color:inherit}.c-readme-view hr,.c-realm-view hr{border-top:var(--s-border-secondary);margin-bottom:var(--cr-space-8);margin-top:var(--cr-space-8)}.c-readme-view table,.c-realm-view table{border-collapse:collapse;display:block;margin-bottom:var(--cr-space-5);margin-top:var(--cr-space-5);max-width:100%;width:100%}.c-readme-view td,.c-readme-view th,.c-realm-view td,.c-realm-view th{border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4);white-space:normal;word-break:break-word}.c-readme-view th,.c-realm-view th{background-color:var(--s-color-bg-surface-secondary);font-weight:var(--g-font-bold)}.c-readme-view button,.c-readme-view input,.c-readme-view select,.c-readme-view textarea,.c-realm-view button,.c-realm-view input,.c-realm-view select,.c-realm-view textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-input);border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4)}.c-readme-view>.realm-view__btns:first-child+*,.c-readme-view>:first-child:not(.realm-view__btns),.c-realm-view>.realm-view__btns:first-child+*,.c-realm-view>:first-child:not(.realm-view__btns){margin-top:0!important}.c-readme-view .footnote-backref,.c-readme-view h1:not(.does-not-exist),.c-readme-view h2:not(.does-not-exist),.c-readme-view h3:not(.does-not-exist),.c-readme-view h4:not(.does-not-exist),.c-readme-view sup:not(.does-not-exist),.c-realm-view .footnote-backref,.c-realm-view h1:not(.does-not-exist),.c-realm-view h2:not(.does-not-exist),.c-realm-view h3:not(.does-not-exist),.c-realm-view h4:not(.does-not-exist),.c-realm-view sup:not(.does-not-exist){scroll-margin-top:var(--cr-space-24)}.c-readme-view .b-btn,.c-realm-view .b-btn{color:var(--s-color-text-secondary);display:inline-flex}.c-readme-view .b-btn:hover,.c-realm-view .b-btn:hover{-webkit-text-decoration:none;text-decoration:none}.c-readme-view .b-btn:first-child,.c-realm-view .b-btn:first-child{float:right;margin-top:var(--g-space-4)}.c-readme-view>.b-btn:first-child+*,.c-readme-view>:first-child:not(.b-btn),.c-realm-view>.b-btn:first-child+*,.c-realm-view>:first-child:not(.b-btn){margin-top:0}.c-readme-view{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius);margin-bottom:var(--g-space-6);padding:var(--g-space-6) var(--g-space-4) var(--g-space-4);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view{grid-row-start:auto}}.b-header{background-color:var(--s-color-bg-base);border-bottom:var(--s-border);font-size:var(--g-font-size-100);position:sticky;top:0;z-index:var(--g-z-max)}.b-header nav{align-items:stretch;height:auto}.b-header .main-nav{align-items:stretch;display:flex;flex:1 1 auto;gap:var(--g-space-1);height:100%;min-width:0;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-header .main-nav{grid-column:span 7}}.b-header .main-nav--explorer{grid-column:span 10}.b-header .user-picture{border:var(--s-border-secondary);border-radius:var(--s-rounded);cursor:pointer;flex-shrink:0;height:var(--g-space-10);width:var(--g-space-10)}.b-header .user-picture>svg{height:100%;width:100%}.b-main-navigation{color:var(--s-color-text-quaternary);height:auto;position:relative;width:100%}.b-main-navigation>.inner{align-items:center;background-color:var(--s-color-bg-surface-secondary);border:var(--s-border-secondary);border-radius:var(--s-rounded);height:100%;padding-left:var(--g-space-1-5);padding-right:var(--g-space-1-5);position:relative}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation>.inner{padding-right:var(--g-space-8)}}.b-main-navigation>.inner:has([data-role=header-input-search]:focus-within){border-color:var(--s-color-border-tertiary)}.b-main-navigation .searchbar{bottom:0;color:var(--s-color-text-secondary);font-size:var(--g-font-size-200);font-weight:var(--g-font-medium);left:0;padding:var(--g-space-1-5);padding-right:var(--g-space-8);position:absolute;right:0;top:0}.b-main-navigation .searchbar>input{background-color:transparent;height:100%;outline:none;width:100%}.b-main-navigation .searchbar:focus-within+.b-breadcrumb{display:none}.b-main-navigation .network-toggle{align-items:center;background-color:var(--g-color-transparent);border-radius:var(--g-border-radius);cursor:pointer;display:none;height:calc(100% - 2px);justify-content:center;padding:var(--g-space-1-5);position:absolute;right:1px;top:1px;z-index:var(--g-z-max)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation .network-toggle{display:flex}}.b-main-navigation .network-toggle>svg{color:var(--s-color-text-tertiary);height:var(--g-space-5);width:var(--g-space-5)}.b-main-navigation .network-toggle:hover>svg{color:var(--s-color-text-tertiary-hover)}.b-main-navigation .b-popup-dialog>.inner{color:var(--s-color-text-tertiary);width:var(--g-space-72)}.b-main-navigation .b-popup-dialog header>span{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-navigation .b-popup-dialog .item{display:flex;gap:var(--g-space-1)}.b-main-navigation .b-popup-dialog .item>svg{height:var(--g-space-4);width:var(--g-space-4)}.b-main-navigation .b-popup-dialog .item-content{display:flex;flex-direction:column}.b-main-navigation .b-popup-dialog .item-label{font-size:var(--g-font-size-50)}.b-main-navigation .b-popup-dialog .item-value{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-menu{display:flex;flex:0 0 auto;grid-column:span 3;height:var(--g-space-12)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-menu{height:auto}}.b-main-menu .menu-toggle{align-items:center;cursor:pointer;display:flex;margin-left:auto;order:3}.b-main-menu .menu-toggle>svg{height:var(--g-space-5);margin-left:var(--g-space-4);width:var(--g-space-5)}@media (min-width:calc(820 / 16 * 1rem)){.b-main-menu .menu-toggle>svg{margin-left:var(--g-space-2)}}.b-main-menu .menu-toggle-input~.menu-dev{display:none}.b-main-menu .menu-toggle-input:checked~.menu-dev{display:flex}.b-main-menu .menu-toggle-input:checked~.menu-general{display:none}.b-main-menu .menu-dev,.b-main-menu .menu-general{display:flex;height:100%;justify-content:flex-end}.b-menu-link:last-child,.b-menu-link:last-child .link{margin-right:0}.b-menu-link .link{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold);gap:var(--g-space-1);height:100%;margin-right:var(--g-space-3);position:relative}.b-menu-link .link:hover{color:var(--s-color-text-tertiary-hover)}.b-menu-link .link:after{background-color:var(--s-color-bg-brand-default);border-radius:var(--s-rounded) var(--s-rounded) 0 0;bottom:0;content:"";height:var(--g-space-1);left:0;position:absolute;transition:width var(--g-transition-fast);width:0}.b-menu-link .link>svg{flex-shrink:0;height:var(--g-space-5);min-width:var(--g-space-2);width:var(--g-space-5)}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link>svg{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link>svg{display:inline-block;height:var(--g-space-4-5);width:var(--g-space-4-5)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link{font-weight:var(--g-font-bold)}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link{margin-right:var(--g-space-6);padding-right:var(--g-space-1)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link-label{display:none}}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link-label{display:inline}}.b-menu-link .link--icon{font-weight:var(--g-font-regular);margin-right:var(--g-space-4)}@media (min-width:calc(480 / 16 * 1rem)){.b-menu-link .link--icon{margin-right:var(--g-space-6)}}.b-menu-link .link--is-active{color:var(--s-color-text-secondary)}.b-menu-link .link--is-active:after{width:100%}.b-menu-link .link--is-active>svg{color:var(--s-color-bg-brand-default)}.menu-general .link{color:var(--s-color-text-secondary)}.menu-general .link:hover{color:var(--s-color-text-link-hover)}.b-breadcrumb{display:flex}.b-breadcrumb,.b-breadcrumb:after{background-color:var(--s-color-bg-surface-secondary)}.b-breadcrumb:after{bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}.b-breadcrumb>ol{color:var(--s-color-text-primary);display:flex;font-weight:var(--g-font-semibold);line-height:var(--g-line-height-snug)}.b-breadcrumb .argument,.b-breadcrumb .element,.b-breadcrumb .query{align-items:center;display:flex;white-space:nowrap;z-index:var(--g-z-1)}.b-breadcrumb .argument:not(:first-child):before,.b-breadcrumb .element:not(:first-child):before,.b-breadcrumb .query:not(:first-child):before{color:var(--s-color-text-tertiary);content:"/";line-height:var(--g-line-height-normal);padding-left:.18rem;padding-right:.18rem;padding-top:var(--g-space-px)}.b-breadcrumb .argument a,.b-breadcrumb .element a,.b-breadcrumb .query a{background-color:var(--s-color-bg-base);border:1px solid var(--s-color-border-transparent);border-radius:var(--s-rounded-sm);display:inline-block;min-width:var(--g-space-4);padding:var(--g-space-0-5);text-align:center}.b-breadcrumb .argument a:hover,.b-breadcrumb .element a:hover,.b-breadcrumb .query a:hover{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}.b-breadcrumb .argument:not(:first-child):before{content:":"}.b-breadcrumb .argument a{background-color:var(--s-color-bg-surface-quaternary);color:var(--s-color-text-base)}.b-breadcrumb .query:not(:first-child):before{content:"&"}.b-breadcrumb .query:nth-child(1 of .query):before{content:"?"}.b-breadcrumb .query label{background-color:var(--s-color-bg-surface-primary);border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);cursor:text;display:inline-flex;height:100%;min-width:var(--g-space-4);padding:var(--g-space-0-5) var(--g-space-1);position:relative;text-align:center;width:100%}.b-breadcrumb .query label:focus-within{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query label:hover{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query input{background-color:var(--s-color-bg-surface-primary);max-width:10ch;order:3;outline:none;field-sizing:content}@supports not (field-sizing:content){.b-breadcrumb .query input{width:5rem!important}}.b-breadcrumb .query input::-moz-placeholder{opacity:0}.b-breadcrumb .query input::placeholder{opacity:0}.b-breadcrumb .query input:-moz-placeholder{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown::-moz-placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:-moz-placeholder::placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:placeholder-shown::placeholder{color:var(--g-color-transparent)}.b-footer{border-top:var(--s-border);font-size:var(--g-font-size-100);padding-bottom:var(--g-space-4);padding-top:var(--g-space-4);width:100%}.b-footer>nav{flex-direction:column;row-gap:var(--g-space-8)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer>nav{flex-wrap:wrap}}.b-footer .logo{color:var(--s-color-text-primary);grid-column:1/-1;width:var(--g-space-44)}.b-footer .logo:hover{color:var(--s-color-text-primary);-webkit-text-decoration:none;text-decoration:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .logo{align-self:center;grid-column:1/3;grid-row:1/1;width:60%}}.b-footer .nav-primary{display:flex;gap:var(--g-space-10);grid-column:1/-1;grid-row:2/3}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary{align-items:center;flex:1 0 0%;flex-direction:row;gap:var(--g-space-6);justify-content:space-between}}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .nav-primary{grid-column:2/8;grid-row:1/1}}.b-footer .nav-primary>ul{display:flex;flex:1;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary>ul{flex:initial;flex-direction:row}.b-footer .nav-social{margin-left:auto}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-social{grid-column:span 3;justify-self:end;margin-left:0}}.b-footer .nav-theme{align-items:center;display:flex;gap:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-theme{flex-basis:100%}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-theme{grid-column:span 3}}.b-footer .nav-theme .nav-theme-label{color:var(--s-color-text-secondary)}.b-footer .nav-theme:has([data-theme-target=sun]:not(.u-hidden)) .nav-theme-label:before{content:"Light"}.b-footer .nav-theme:has([data-theme-target=moon]:not(.u-hidden)) .nav-theme-label:before{content:"Dark"}.b-footer .nav-theme:has([data-theme-target=system]:not(.u-hidden)) - .nav-theme-label:before{content:"System"}.b-footer .legal{color:var(--s-color-text-tertiary);font-size:var(--g-font-size-50);margin-top:var(--g-space-3);padding-top:var(--g-space-3)}.b-footer .legal>nav{color:var(--s-color-text-secondary);display:flex;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3);margin-top:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .legal>nav{flex-direction:row}.b-footer .legal>nav>a+a:before{color:var(--s-color-text-quaternary);content:"|";margin-right:var(--g-space-3)}}.b-footer .legal>nav:nth-child(3){grid-column:span 2/span 2}.b-footer .legal>:last-child:not(ul),.b-footer .legal>nav li{margin-bottom:var(--g-space-2);margin-top:var(--g-space-2)}.b-footer .legal>:last-child:not(ul){flex-basis:100%}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .legal>:last-child:not(ul){flex-basis:auto;grid-column:span 1/span 1}}.b-footer a:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}.b-content-header{display:flex;flex-direction:column;gap:var(--g-space-3);grid-row:span 1/span 1;margin-bottom:var(--g-space-6);margin-top:var(--g-space-10)}@media (min-width:calc(820 / 16 * 1rem)){.b-content-header{grid-column:span 7/span 7;grid-row-start:1;justify-content:space-between;margin-top:var(--g-space-10)}}@media (min-width:calc(1020 / 16 * 1rem)){.b-content-header{align-items:center;flex-direction:row}}.b-content-header .title{align-items:center;display:flex;gap:var(--g-space-3)}.b-content-header .header-info{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);gap:var(--g-space-12);justify-content:space-between}.b-content-header .b-inline-btn>span{display:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-content-header .b-inline-btn>span{display:inline}}.b-content-h1{font-size:var(--g-font-size-600);text-align:center}.b-content-h1,.b-content-h2{color:var(--s-color-text-primary);font-weight:var(--g-font-bold)}.b-content-h2{font-size:var(--g-font-size-400);margin-bottom:var(--g-space-4)}.b-btns{align-items:center;display:flex;gap:var(--g-space-1)}@media (min-width:calc(1020 / 16 * 1rem)){.b-btns{gap:var(--g-space-2)}}.b-btn{border:var(--s-border);border-radius:var(--s-rounded-sm);cursor:pointer;display:inline-flex;gap:var(--g-space-1-5);min-width:-moz-max-content;min-width:max-content;padding:var(--g-space-1) var(--g-space-2)}.b-btn:hover{background-color:var(--s-color-bg-surface-primary-hover)}.b-btn .c-icon{margin-left:0;margin-right:0}.b-btn--secondary:hover{background-color:var(--s-color-bg-surface-primary)}.b-inline-btn{color:var(--s-color-text-tertiary);cursor:pointer}.b-inline-btn:hover{color:var(--s-color-text-tertiary-hover)}.b-switch input,.b-switch label:last-child{display:none}.b-switch input+label,.b-switch input:checked~label:last-child{display:block}.b-switch input:checked+label{display:none}.b-block-form,.b-inline-form{color:var(--s-color-text-tertiary);display:flex;flex-direction:column;gap:var(--g-space-2) var(--g-space-3)}@media (min-width:calc(820 / 16 * 1rem)){.b-block-form,.b-inline-form{flex-direction:row}}.b-block-form{align-items:stretch}@media (min-width:calc(820 / 16 * 1rem)){.b-block-form{flex-direction:column}}.b-input{border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);display:flex;font-size:var(--g-font-size-100);min-width:var(--g-space-48);overflow:hidden;position:relative}.b-input>svg{height:var(--g-space-4);pointer-events:none;position:absolute;top:50%;transform:translateY(-50%);width:var(--g-space-4)}.b-input>svg:first-child{left:var(--g-space-2)}.b-input>svg:last-child{right:var(--g-space-2)}.b-input:hover,.b-input>input:focus,.b-input>input:hover{border-color:var(--s-color-border-tertiary)}.b-input:has(input:focus),.b-input:hover,.b-input>input:focus,.b-input>input:hover{border-color:var(--s-color-border-tertiary)}.b-input:hover>label{background-color:var(--s-color-bg-surface-primary)}.b-input:has(input:focus)>label,.b-input:hover>label{background-color:var(--s-color-bg-surface-primary)}.b-input>label{align-items:center;background-color:var(--s-color-bg-surface-secondary);gap:var(--g-space-3);white-space:nowrap}.b-input>input,.b-input>label,.b-input>select{display:flex;padding:var(--g-space-1-5) var(--g-space-3)}.b-input>input,.b-input>select{color:inherit;outline:none;width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-input>input,.b-input>select{padding:var(--g-space-1-5) var(--g-space-2)}}.b-input>select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-surface-secondary);cursor:pointer}.b-input>select:hover{background-color:var(--s-color-bg-surface-primary)}.b-input>input{background-color:var(--s-color-bg-base);border-left:none}.b-input>label+input{border-left:var(--s-border)}.b-list{margin-bottom:var(--g-space-10)}.b-list>li{border-bottom:var(--s-border);color:var(--s-color-text-tertiary)}.b-list>li:first-child{border-top:var(--s-border)}.b-list>li>a{align-items:center;display:flex;justify-content:space-between;padding:var(--g-space-2)}.b-list>li>a:hover{background-color:var(--s-color-bg-surface-primary-hover)}.b-list>li>a .c-icon{margin-left:0}.b-list .name{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;color:var(--s-color-text-secondary);margin-left:var(--g-space-1);max-width:100%;overflow:hidden;text-overflow:ellipsis}.b-user-sidebar{margin-top:var(--g-space-4)}.b-user-sidebar>*+*{margin-top:var(--g-space-8)}.b-user-sidebar .user-avatar{border:var(--s-border);border-radius:var(--s-rounded);height:var(--g-space-24);width:var(--g-space-24)}@media (min-width:calc(640 / 16 * 1rem)){.b-user-sidebar .user-avatar{height:var(--g-space-36);width:var(--g-space-36)}}.b-user-sidebar .user-avatar img,.b-user-sidebar .user-avatar svg{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.b-user-sidebar .user-info{align-items:flex-start;display:flex;gap:var(--g-space-6)}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar .user-info{flex-direction:column}}.b-user-sidebar .user-info>div:last-child{align-self:flex-end}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar .user-info>div:last-child{align-self:flex-start}}.b-user-sidebar .title{color:var(--s-color-text-primary);display:bock;font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);line-height:var(--g-line-height-tight);text-transform:capitalize;word-break:break-all}@media (min-width:calc(640 / 16 * 1rem)){.b-user-sidebar .title{font-size:var(--g-font-size-800)}}.b-user-sidebar .subtitle{color:var(--s-color-text-secondary);display:block;font-size:var(--g-font-size-100);line-height:var(--g-line-height-tight);margin-top:var(--g-space-2)}.b-user-sidebar>a{align-items:center;display:flex;justify-content:center}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar>a{display:inline-flex}}.b-sidebar{border-bottom:var(--s-border);grid-column:span 1/span 1;padding-bottom:var(--g-space-10);position:relative}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar{border-bottom:none;grid-column:span 3/span 3;grid-row:span 2/span 2;grid-row-start:1;height:100%;margin-bottom:0;order:2;padding-bottom:0}.b-sidebar+md-renderer:empty+*{grid-row-start:1;padding-top:var(--g-space-6)}.b-sidebar+md-renderer:empty+*,.b-sidebar+md-renderer:has(.b-btn:only-child)+*{grid-row-start:1;padding-top:var(--g-space-6)}}.b-sidebar:first-child{margin-top:var(--g-space-8)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar:first-child{margin-top:0}}.b-sidebar>div{padding-top:var(--g-space-2);position:sticky;top:var(--g-space-14)}.b-sidebar>div:has(.inner):not(:has(nav li)){display:none}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar>div{padding-bottom:var(--g-space-2)}}.b-sidebar .inner{background-color:var(--s-color-bg-surface-primary);border-radius:var(--s-rounded-sm);max-height:100vh;overflow:scroll;scrollbar-width:none}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .inner{background-color:var(--g-color-transparent)}}.b-sidebar .inner>nav{display:none;font-size:var(--g-font-size-100);margin-top:var(--g-space-2);padding:var(--g-space-2) var(--g-space-4) var(--g-space-6)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .inner>nav{display:block;margin-top:0;padding-bottom:var(--g-space-28);padding-left:0;padding-right:0}.b-sidebar .inner>nav>*{padding-left:0}}.b-sidebar .b-expend-btn{align-items:center;background-color:var(--s-color-bg-base);border:var(--s-border);border-radius:var(--s-rounded-sm);cursor:pointer;display:flex;font-size:var(--g-font-size-100);justify-content:space-between;padding:var(--g-space-2) var(--g-space-4)}.b-sidebar .b-expend-btn:hover{background-color:var(--s-color-bg-surface-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .b-expend-btn{border:none;cursor:default;font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);margin-top:var(--g-space-10);padding:0}.b-sidebar .b-expend-btn,.b-sidebar .b-expend-btn:hover{background-color:var(--g-color-transparent)}}.b-sidebar .b-expend-btn:has(#toc-expend:checked)+nav{display:block}.b-sidebar .b-expend-btn>input{display:none}.b-sidebar .b-expend-btn>input:checked+.wrapper-icon:before{content:"close"}.b-sidebar .b-expend-btn>input:checked+.wrapper-icon>svg{transform:rotate(180deg)}.b-sidebar .wrapper-icon{align-items:center;display:flex;gap:var(--g-space-1-5)}.b-sidebar .wrapper-icon:before{content:"open"}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .wrapper-icon{display:none}}.dev-mode .b-sidebar .b-expend-btn{background-color:var(--s-color-bg-surface-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.dev-mode .b-sidebar .b-expend-btn{background-color:var(--g-color-transparent)}}.dev-mode .b-sidebar .b-expend-btn:hover{background-color:var(--s-color-bg-surface-primary)}.b-source-code{font-family:var(--g-font-mono)}.b-source-code>pre{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);font-size:var(--g-font-size-100);overflow:scroll;padding:var(--g-space-4) var(--g-space-1)}@media (min-width:calc(640 / 16 * 1rem)){.b-source-code>pre{font-size:var(--g-font-size-200);padding:var(--g-space-8) var(--g-space-3)}}.b-source-code>pre a:hover{-webkit-text-decoration:none;text-decoration:none}[data-theme=dark] .b-source-code>pre{background-color:var(--s-color-bg-base)}.b-toc{list-style:none;margin-top:var(--g-space-2)}.b-toc>*+*{margin-bottom:var(--g-space-1-5);margin-top:var(--g-space-1-5)}.b-toc .b-toc{border-left:1px solid var(--s-color-border-secondary);margin-bottom:var(--g-space-4);padding-left:var(--g-space-4)}.b-toc a>span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.b-toc a:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}main.dev-mode .b-toc a{word-break:break-all}.b-source-toc>.b-toc{margin-bottom:var(--g-space-4)}.b-source-toc>*+*{margin-top:var(--g-space-1-5)}.b-source-toc .accordion summary>svg{transform:rotate(-90deg)}.b-source-toc .accordion summary:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}.b-source-toc .accordion[open] summary>svg{transform:rotate(0deg)}.b-source-toc .accordion>.b-toc{padding-left:var(--g-space-5)}.b-source-toc .accordion h3{font-size:var(--g-font-size-100);font-weight:var(--g-font-medium);margin-top:0}.b-action-overview{margin-bottom:var(--g-space-12)}.b-action-overview>p{font-size:var(--g-font-size-200)}.b-action-function{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--s-rounded);margin-bottom:var(--g-space-3);padding:var(--g-space-4)}.b-action-function .title{align-items:baseline;display:flex;flex-wrap:wrap;font-size:var(--g-font-size-50);gap:var(--g-space-1) var(--g-space-4);margin-bottom:var(--g-space-1)}.b-action-function>header{align-items:flex-start;display:flex;font-size:var(--g-font-size-100);justify-content:space-between;margin-bottom:var(--g-space-4)}.b-action-function>header .signature>code{color:var(--s--text-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.b-action-function>header .signature{font-size:var(--g-font-size-50)}}.b-action-function>header h2{color:var(--s-color-text-primary);font-size:var(--g-font-size-300);font-weight:var(--g-font-semibold);line-height:var(--g-line-height-tight)}.b-action-function .description{color:var(--s-color-text-secondary);font-size:var(--g-font-size-200)}.b-action-function .params{align-items:stretch;color:var(--s-color-text-tertiary);display:flex;flex-direction:column;font-size:var(--g-font-size-100);gap:var(--g-space-1);margin-bottom:var(--g-space-1);margin-top:var(--g-space-6);width:100%}.b-action-function .params label{background-color:var(--s-color-bg-surface-primary)}.b-action-function .params .b-input:has(input:focus) label{background-color:var(--s-color-bg-surface-secondary)}.b-action-function .params .b-input:has(input:hover) label{background-color:var(--s-color-bg-surface-secondary)}.b-action-function .b-alert{background-color:var(--s-color-bg-warning-weak);border-left:var(--g-space-1) solid var(--s-color-border-tertiary);border-left-color:var(--s-color-border-warning);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);color:var(--s-color-text-warning);margin-bottom:var(--g-space-10);margin-top:var(--g-space-5);padding:var(--g-space-3) var(--g-space-4)}.b-action-function .b-alert>h1:first-child,.b-action-function .b-alert>h2:first-child,.b-action-function .b-alert>h3:first-child{font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);margin-bottom:var(--g-space-2)}.b-action-function .b-alert .b-btn,.b-action-function .b-alert label{background-color:var(--s-color-bg-warning-action);border:none;color:var(--s-color-bg-warning-weak);cursor:pointer}.b-action-function .b-alert .b-btn{margin-top:var(--g-space-4)}.b-code{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);font-size:var(--g-font-size-100);position:relative}.b-code pre{color:var(--s-color-text-secondary);padding:var(--g-space-4);padding-right:var(--g-space-10);white-space:pre-wrap}.b-code .btn-copy{background-color:var(--g-color-transparent);color:var(--s-color-text-tertiary);cursor:pointer;padding:0;position:absolute;right:var(--g-space-2);top:var(--g-space-2)}.b-code .btn-copy:hover{color:var(--s-color-text-primary)}.b-packages{min-height:var(--g-space-96);padding-bottom:var(--g-space-24);scroll-margin-block-start:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){.b-packages{grid-column:span 7/span 7}}.b-packages .title{color:var(--s-color-text-primary);display:block;font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--g-space-6)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .title{font-size:var(--g-font-size-800)}}.b-packages nav{display:grid;grid-template-columns:repeat(4,1fr);grid-gap:var(--g-space-3);gap:var(--g-space-3);margin-bottom:var(--g-space-6)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages nav{border-bottom:var(--s-border);padding-bottom:var(--g-space-2)}}.b-packages .packages-tabs{border-bottom:var(--s-border);color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);gap:var(--g-space-4);grid-column:span 4/span 4;padding-bottom:var(--g-space-2);width:auto}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-tabs{border-bottom:none;font-size:var(--g-font-size-100);grid-column:span 2/span 2;padding-bottom:0;width:100%}}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages .packages-tabs{gap:var(--g-space-6);margin-left:0;width:100%}}.b-packages .packages-tabs label{align-items:center;cursor:pointer;display:flex;gap:var(--g-space-1);position:relative}.b-packages .packages-tabs label:hover{color:var(--s-color-text-tertiary-hover)}.b-packages .packages-tabs label .b-tag--secondary{display:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages .packages-tabs label .b-tag--secondary{display:inline}}.b-packages .packages-filters{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);gap:var(--g-space-2);grid-column:span 2/span 2}@media (min-width:calc(480 / 16 * 1rem)){.b-packages .packages-filters{grid-column:span 1/span 1}}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-filters{justify-content:flex-end}}.b-packages .packages-filters>div{display:grid}.b-packages .packages-filters label{align-items:center;cursor:pointer;display:flex;gap:var(--g-space-0-5);grid-column:1/1;grid-row:1/1;justify-content:space-between}.b-packages .packages-filters label:hover>*{color:var(--s-color-text-tertiary-hover)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-filters label span{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-packages .packages-filters label span{display:inline}}.b-packages .packages-search{display:flex;font-size:var(--g-font-size-100);grid-column:span 2/span 2;position:relative}@media (min-width:calc(480 / 16 * 1rem)){.b-packages .packages-search{grid-column:span 3/span 3}}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-search{grid-column:span 1/span 1}}.b-packages .range{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-gap:var(--g-space-2);color:var(--s-color-text-tertiary);font-size:var(--g-font-size-100);gap:var(--g-space-2)}.b-packages .range:before{color:var(--s-color-text-tertiary);display:none;font-size:var(--g-font-size-200);font-weight:var(--g-font-weight-bold);grid-column:1/-1;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);text-align:center;width:100%}.b-packages .range:after{content:"Add a package to your namespace to get started";display:none;font-size:var(--g-font-size-100);grid-column:1/-1;text-align:center}.b-packages .range:empty:before{content:"No packages found";display:block}.b-packages .range:empty:after{content:"Add a package to your namespace to get started";display:block}.b-packages article{background-color:var(--s-color-bg-surface-primary);border-radius:var(--s-rounded);display:flex;flex-direction:column;gap:var(--g-space-6);padding:var(--g-space-1)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages article{gap:var(--g-space-2)}}.b-packages article .article-content{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded-sm);display:flex;flex-direction:column;height:100%;padding:var(--g-space-2);width:100%}.b-packages article .article-content .title{align-items:center;display:flex;gap:var(--g-space-2);margin-bottom:var(--g-space-1);overflow:hidden;width:100%}.b-packages article .article-content h3{font-size:var(--g-font-size-200);font-weight:var(--g-font-bold);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b-packages article .article-content h3>a{color:var(--s-color-text-link-hover)}.b-packages article .article-content h3>a:hover{-webkit-text-decoration:underline;text-decoration:underline}.b-packages article .article-content>p{overflow:hidden;text-overflow:ellipsis;width:100%}.b-packages article .article-content>p>a:hover{-webkit-text-decoration:underline;text-decoration:underline}.b-packages article footer{display:flex;font-size:var(--g-font-size-50);gap:var(--g-space-1);justify-content:space-between;padding-bottom:var(--g-space-1);padding-left:var(--g-space-2);padding-right:var(--g-space-2)}.b-packages article footer time{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b-packages article footer .size{text-align:right}.b-packages article,.b-packages li{display:none}.b-packages:has(input[value=packages]:checked) li{display:flex}.b-packages:has(input[value=packages]:checked) article{display:flex}.b-packages:has(input[value=realms]:checked) +:root{--g-px-base:16;--g-space-mult:4;--g-space-base:calc(1rem/var(--g-space-mult));--g-breakpoint-max:calc(1580/var(--g-px-base)*1rem);--g-z-min:-1;--g-z-1:1;--g-z-max:9999;--g-duration-75:75ms;--g-duration-150:150ms;--g-opacity-50:0.5;--g-grid-1:repeat(1,minmax(0,1fr));--g-grid-10:repeat(10,minmax(0,1fr));--g-space-px:1px;--g-space-0-5:calc(var(--g-space-base)*0.5);--g-space-1:var(--g-space-base);--g-space-1-5:calc(var(--g-space-base)*1.5);--g-space-2:calc(var(--g-space-base)*2);--g-space-2-5:calc(var(--g-space-base)*2.5);--g-space-3:calc(var(--g-space-base)*3);--g-space-4:calc(var(--g-space-base)*4);--g-space-4-5:calc(var(--g-space-base)*4.5);--g-space-5:calc(var(--g-space-base)*5);--g-space-6:calc(var(--g-space-base)*6);--g-space-7:calc(var(--g-space-base)*7);--g-space-8:calc(var(--g-space-base)*8);--g-space-10:calc(var(--g-space-base)*10);--g-space-12:calc(var(--g-space-base)*12);--g-space-14:calc(var(--g-space-base)*14);--g-space-20:calc(var(--g-space-base)*20);--g-space-24:calc(var(--g-space-base)*24);--g-space-28:calc(var(--g-space-base)*28);--g-space-32:calc(var(--g-space-base)*32);--g-space-36:calc(var(--g-space-base)*36);--g-space-44:calc(var(--g-space-base)*44);--g-space-48:calc(var(--g-space-base)*48);--g-space-52:calc(var(--g-space-base)*52);--g-space-72:calc(var(--g-space-base)*72);--g-space-96:calc(var(--g-space-base)*96);--g-font-size-50:calc(12/var(--g-px-base)*1rem);--g-font-size-100:calc(14/var(--g-px-base)*1rem);--g-font-size-200:calc(16/var(--g-px-base)*1rem);--g-font-size-300:calc(18/var(--g-px-base)*1rem);--g-font-size-400:calc(20/var(--g-px-base)*1rem);--g-font-size-500:calc(22/var(--g-px-base)*1rem);--g-font-size-600:calc(24/var(--g-px-base)*1rem);--g-font-size-700:calc(32/var(--g-px-base)*1rem);--g-font-size-800:calc(38/var(--g-px-base)*1rem);--g-font-family-mono:"Roboto",'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace';--g-font-family-inter-var:"Inter",'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif';--g-font-normal:400;--g-font-medium:500;--g-font-semibold:600;--g-font-bold:700;--g-italic:oblique 14deg;--g-line-height-tight:1.25;--g-line-height-snug:1.375;--g-line-height-normal:1.5;--g-border-radius-sm:calc(4/var(--g-px-base)*1rem);--g-border-radius:calc(6/var(--g-px-base)*1rem);--g-border-radius-full:9999px;--g-color-light:#fff;--g-color-transparent:transparent;--g-color-gray-50:#f0f0f0;--g-color-gray-100:#e2e2e2;--g-color-gray-200:#bdbdbd;--g-color-gray-300:#999;--g-color-gray-400:#7c7c7c;--g-color-gray-500:#696969;--g-color-gray-600:#585858;--g-color-gray-700:#292929;--g-color-gray-750:#1f1f1f;--g-color-gray-800:#141414;--g-color-gray-850:#0e0e0e;--g-color-gray-900:#090909;--g-color-green-50:#e7efed;--g-color-green-400:#60ab96;--g-color-green-500:#277b63;--g-color-green-600:#226c57;--g-color-green-900:#144134;--g-color-green-950:#002c20;--g-color-blue-400:#49afeb;--g-color-blue-600:#3e96c9;--g-color-blue-900:#21506b;--g-color-yellow-50:#fff7eb;--g-color-yellow-400:#facc32;--g-color-yellow-600:#fbbf24;--g-color-yellow-900:#7b4807;--g-color-yellow-950:#362600;--g-color-red-400:#eb6c49;--g-color-red-600:#c95c3e;--g-color-red-900:#6b2521;--g-color-purple-400:#7f49eb;--g-color-purple-600:#6c3ec9;--g-color-purple-900:#39216b}@supports (color:color(display-p3 0 0 0%)){:root{--g-color-green-950:#002c20;--g-color-yellow-50:#fff7eb;--g-color-yellow-950:#362600}@media (color-gamut:p3){:root{--g-color-green-950:color(display-p3 0.04602 0.17026 0.1277);--g-color-yellow-50:color(display-p3 0.99709 0.97106 0.92232);--g-color-yellow-950:color(display-p3 0.2031 0.15112 0.01811)}}}:root{--s-color-bg-base:var(--g-color-light,#fff);--s-color-bg-base-dev:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary:var(--g-color-gray-50,#f0f0f0);--s-color-bg-surface-primary-hover:var(--g-color-gray-100,#f0f0f0);--s-color-bg-surface-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-bg-surface-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-bg-brand-default:var(--g-color-green-600,#226c57);--s-color-bg-brand-weak:var(--g-color-green-50,#f0f9ff);--s-color-bg-brand-action:var(--g-color-green-400,#60ab96);--s-color-bg-success-default:var(--g-color-green-600,#144134);--s-color-bg-info-default:var(--g-color-blue-600,#21506b);--s-color-bg-warning-default:var(--g-color-yellow-600,#665100);--s-color-bg-warning-weak:var(--g-color-yellow-50,#f9d985);--s-color-bg-warning-action:var(--g-color-yellow-400,#f9d985);--s-color-bg-caution-default:var(--g-color-red-600,#610);--s-color-bg-tip-default:var(--g-color-purple-600,#49216b);--s-color-bg-note-default:var(--g-color-gray-600,#21506b);--s-color-bg-input:var(--g-color-light,#fff);--s-color-text-base:var(--g-color-light,#fff);--s-color-text-primary:var(--g-color-gray-900,#080809);--s-color-text-secondary:var(--g-color-gray-600,#454a4e);--s-color-text-tertiary:var(--g-color-gray-400,#f0f0f0);--s-color-text-tertiary-hover:var(--g-color-gray-600,#e2e2e2);--s-color-text-quaternary:var(--g-color-gray-100,#f0f0f0);--s-color-text-brand-default:var(--g-color-light,#fff);--s-color-text-link:var(--g-color-green-600,#226c57);--s-color-text-link-hover:var(--g-color-green-600,#226c57);--s-color-text-success:var(--g-color-green-900,#144134);--s-color-text-info:var(--g-color-blue-900,#21506b);--s-color-text-warning:var(--g-color-yellow-900,#665100);--s-color-text-caution:var(--g-color-red-900,#610);--s-color-text-tip:var(--g-color-purple-900,#49216b);--s-color-border-primary:var(--g-color-gray-200,#bdbdbd);--s-color-border-secondary:var(--g-color-gray-100,#e2e2e2);--s-color-border-tertiary:var(--g-color-gray-300,#999);--s-color-border-quaternary:var(--g-color-gray-400,#7c7c7c);--s-color-border-transparent:var(--g-color-transparent,transparent);--s-color-border-input:var(--g-color-gray-300,#999);--s-color-border-brand-default:var(--g-color-green-600,#226c57);--s-color-border-success:var(--g-color-green-600,#144134);--s-color-border-info:var(--g-color-blue-600,#21506b);--s-color-border-warning:var(--g-color-yellow-600,#665100);--s-color-border-error:var(--g-color-red-600,#610);--s-color-border-tip:var(--g-color-purple-600,#49216b);--s-color-border-note:var(--g-color-gray-600,#21506b);--s-rounded-sm:var(--g-border-radius-sm,4px);--s-rounded:var(--g-border-radius,6px);--s-rounded-full:var(--g-border-radius-full,9999px);--s-border:var(--g-space-px,1px) solid var(--s-color-border-primary);--s-border-secondary:var(--g-space-px,1px) solid var(--s-color-border-secondary);--s-logo-hat:var(--g-color-green-600,#226c57);--s-logo-beard:var(--g-color-gray-300,#999)}[data-theme=dark]{--s-color-bg-base:var(--g-color-gray-850);--s-color-bg-base-dev:var(--g-color-gray-800);--s-color-bg-surface-primary:var(--g-color-gray-800);--s-color-bg-surface-primary-hover:var(--g-color-gray-750);--s-color-bg-surface-secondary:var(--g-color-gray-750);--s-color-bg-surface-quaternary:var(--g-color-gray-600);--s-color-bg-brand-weak:var(--g-color-green-950);--s-color-bg-warning-weak:var(--g-color-yellow-950);--s-color-bg-input:var(--g-color-gray-800);--s-color-text-primary:var(--g-color-gray-100);--s-color-text-secondary:var(--g-color-gray-200);--s-color-text-tertiary:var(--g-color-gray-400);--s-color-text-tertiary-hover:var(--g-color-gray-300);--s-color-text-quaternary:var(--g-color-gray-500);--s-color-text-brand-default:var(--g-color-light);--s-color-text-link:var(--g-color-green-500);--s-color-text-link-hover:var(--g-color-green-400);--s-color-text-success:var(--g-color-green-400);--s-color-text-info:var(--g-color-blue-400);--s-color-text-warning:var(--g-color-yellow-400);--s-color-text-caution:var(--g-color-red-400);--s-color-text-tip:var(--g-color-purple-400);--s-color-border-primary:var(--g-color-gray-700);--s-color-border-secondary:var(--g-color-gray-750);--s-color-border-tertiary:var(--g-color-gray-600);--s-color-border-quaternary:var(--g-color-gray-500);--s-color-border-input:var(--g-color-gray-700);--s-color-border-brand-default:var(--g-color-green-600);--s-color-border-success:var(--g-color-green-400);--s-color-border-info:var(--g-color-blue-400);--s-color-border-warning:var(--g-color-yellow-400);--s-color-border-error:var(--g-color-red-400);--s-color-border-tip:var(--g-color-purple-400);--s-color-border-note:var(--g-color-gray-600);--s-logo-hat:#fff;--s-logo-beard:grey}*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}h1,h2,h3{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub{bottom:-.25em;font-size:75%;line-height:0;position:relative;vertical-align:baseline}table{border-collapse:collapse;border-color:inherit;text-indent:0}summary{display:list-item}menu,ol,ul{list-style:none}embed,img,object,svg{display:block;vertical-align:middle}img{height:auto;max-width:100%}::file-selector-button,button,input,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}::file-selector-button{margin-right:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}::-webkit-calendar-picker-indicator{line-height:1}::file-selector-button,button,input:where([type=button],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}@font-face{font-display:swap;font-family:Roboto;font-style:normal;font-weight:900;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-display:block;font-family:Inter;font-style:oblique 0deg 10deg;font-variant:normal;font-weight:100 900;src:url(fonts/intervar/Intervar.woff2) format("woff2")}html{background-color:var(--s-color-bg-base);color:var(--s-color-text-secondary);font-family:var(--g-font-family-inter-var);font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on,contextual common-ligatures,"kern";-webkit-font-feature-settings:"kern" on,"liga" on,"calt" off,"zero" on;font-size:calc(var(--g-px-base)*1px);line-height:var(--g-line-height-normal);-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-kerning:normal;font-variant-ligatures:contextual common-ligatures;text-rendering:optimizeLegibility}body{display:flex;flex-direction:column;min-height:100vh}main{background-color:var(--s-color-bg-base);flex-grow:2;width:100%}main.dev-mode{background-color:var(--s-color-bg-base-dev)}main>section{display:grid;grid-auto-flow:dense;grid-template-columns:var(--g-grid-1);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20);min-height:100%;padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){main>section{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}@media (min-width:calc(820 / 16 * 1rem)){main>section{grid-template-columns:var(--g-grid-10)}}@media (min-width:calc(1366 / 16 * 1rem)){main>section{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}svg{max-height:100%;max-width:100%}form{margin-bottom:0;margin-top:0}code{font-family:var(--g-font-mono)}summary{cursor:pointer}md-renderer{margin-top:var(--g-space-4);padding-bottom:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){md-renderer{grid-column:span 7;margin-top:0}}::-moz-selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}::selection{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}summary::-webkit-details-marker{display:none}summary::marker{display:none}.c-stack{display:flex;flex-direction:column;justify-content:flex-start}.c-stack>*+*{margin-top:var(--g-space-4)}.c-inline{align-items:center;display:inline-flex;gap:var(--g-space-3)}.c-between{align-items:center;display:flex;justify-content:space-between}.c-center{box-sizing:border-box;margin-left:auto;margin-right:auto;max-width:var(--g-breakpoint-max);padding-left:var(--g-space-4);padding-right:var(--g-space-4)}@media (min-width:calc(640 / 16 * 1rem)){.c-center{padding-left:var(--g-space-10);padding-right:var(--g-space-10)}}.c-full-screen{align-items:center;display:flex;flex-direction:column;grid-column:1/-1;height:100%;justify-content:center;margin-top:var(--g-space-10);padding-bottom:var(--g-space-24);width:100%}.c-reel{display:flex;overflow:scroll}.c-icon{flex-shrink:0;height:1.15em;width:1.15em}.c-with-icon{align-items:flex-start;display:inline-flex}.c-with-icon .c-icon,.c-with-icon--inline .c-icon{margin-left:.3em;margin-right:.3em;margin-top:.15em}.c-with-icon--inline{display:inline-block}.c-with-icon--inline>*{vertical-align:middle}.c-with-icon--inline .c-icon{margin-top:0}.c-view-grid{display:flex;flex-direction:column}@media (min-width:calc(640 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-8);column-gap:var(--g-space-8);flex-direction:row}}@media (min-width:calc(820 / 16 * 1rem)){.c-view-grid{display:grid;grid-template-columns:var(--g-grid-10);grid-column-gap:var(--g-space-20);-moz-column-gap:var(--g-space-20);column-gap:var(--g-space-20)}}@media (min-width:calc(1366 / 16 * 1rem)){.c-view-grid{-moz-column-gap:var(--g-space-32);column-gap:var(--g-space-32)}}.c-toggle-btn>input{display:none}.c-toggle-btn label{visibility:hidden}.c-toggle-btn input:checked+label{visibility:visible}.c-readme-view,.c-realm-view{--cr-px-base:var(--g-px-base);--cr-space-mult:1;--cr-space-base:calc(1em/var(--g-space-mult)*var(--cr-space-mult));--cr-space-0:0;--cr-space-0-5:calc(var(--cr-space-base)*0.5);--cr-space-1:var(--cr-space-base);--cr-space-2:calc(var(--cr-space-base)*2);--cr-space-3:calc(var(--cr-space-base)*3);--cr-space-4:calc(var(--cr-space-base)*4);--cr-space-5:calc(var(--cr-space-base)*5);--cr-space-7:calc(var(--cr-space-base)*7);--cr-space-8:calc(var(--cr-space-base)*8);--cr-space-24:calc(var(--cr-space-base)*24);--cr-color-brand-default:var(--s-color-text-link);display:block;font-size:calc(var(--cr-px-base)*1px);padding-top:var(--g-space-4);word-break:break-word}.c-readme-view:empty,.c-realm-view:empty{display:none}.c-realm-view:has(.b-btn:only-child){display:none}.c-readme-view:has(.b-btn:only-child){display:none}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view,.c-realm-view{grid-row-start:1;padding-top:var(--g-space-6)}}.c-readme-view a,.c-realm-view a{color:var(--cr-color-brand-default);display:inline-block;font-weight:inherit;position:relative;text-wrap:balance;vertical-align:top}.c-readme-view a:hover,.c-realm-view a:hover{-webkit-text-decoration:underline;text-decoration:underline}.c-realm-view a:has(>img){vertical-align:middle}.c-readme-view a:has(>img){vertical-align:middle}.c-readme-view a>span,.c-realm-view a>span{margin-bottom:.1em}.c-readme-view a>.tooltip+.tooltip,.c-realm-view a>.tooltip+.tooltip{margin-left:.2em}.c-readme-view a>.tooltip:last-of-type,.c-realm-view a>.tooltip:last-of-type{margin-right:.2em}.c-realm-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip:last-child):not(:has(>:nth-child(3)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius-full);bottom:var(--g-space-2);left:var(--g-space-2);margin-left:0;position:absolute}.c-realm-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view a:has(>img:first-child):has(.tooltip+.tooltip:last-child):not(:has(>:nth-child(4)))>.tooltip:first-of-type{bottom:var(--g-space-2);left:var(--g-space-7);position:absolute}.c-readme-view h1+h2,.c-readme-view h2+h3,.c-readme-view h3+h4,.c-realm-view h1+h2,.c-realm-view h2+h3,.c-realm-view h3+h4{margin-top:var(--cr-space-4)}.c-readme-view h1,.c-readme-view h2,.c-readme-view h3,.c-readme-view h4,.c-realm-view h1,.c-realm-view h2,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-primary);line-height:var(--g-line-height-tight);margin-top:var(--cr-space-4)}.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h1,.c-realm-view h1{font-size:var(--g-font-size-800)}}.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-500);font-weight:var(--g-font-bold)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h2,.c-realm-view h2{font-size:var(--g-font-size-600)}}.c-readme-view h2 *,.c-realm-view h2 *{font-weight:var(--g-font-bold)}.c-readme-view h3,.c-readme-view h4,.c-realm-view h3,.c-realm-view h4{color:var(--s-color-text-secondary);font-weight:var(--g-font-semibold)}.c-readme-view h3,.c-realm-view h3{font-size:var(--g-font-size-400);margin-top:var(--cr-space-4)}.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300);margin-top:var(--cr-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.c-readme-view h4,.c-realm-view h4{font-size:var(--g-font-size-300)}}.c-readme-view h3 *,.c-readme-view h4 *,.c-realm-view h3 *,.c-realm-view h4 *{font-weight:var(--g-font-semibold)}.c-readme-view h5,.c-readme-view h6,.c-realm-view h5,.c-realm-view h6{font-size:var(--g-font-size-300);font-weight:var(--g-font-bold);margin-bottom:var(--cr-space-0);margin-top:var(--cr-space-0)}.c-readme-view h5+p,.c-readme-view h6+p,.c-realm-view h5+p,.c-realm-view h6+p{margin-top:var(--cr-space-0)}.c-readme-view img,.c-realm-view img{border:1px solid var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);max-width:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.c-readme-view figure,.c-realm-view figure{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);text-align:center}.c-readme-view figcaption,.c-realm-view figcaption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100)}.c-readme-view video,.c-realm-view video{margin-bottom:var(--g-space-4);margin-top:var(--g-space-4);max-width:100%}.c-readme-view p,.c-realm-view p{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-realm-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-readme-view p:has(>a:only-child>img){margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4)}.c-realm-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view p:has(>a:only-child>img) img{margin-bottom:0;margin-top:0}.c-readme-view strong,.c-readme-view strong *,.c-realm-view strong,.c-realm-view strong *{font-weight:var(--g-font-bold)}.c-readme-view em,.c-realm-view em{font-style:var(--g-italic)}.c-readme-view blockquote,.c-realm-view blockquote{border-left:solid var(--g-space-0-5) var(--s-color-border-tertiary);color:var(--s-color-text-secondary);margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-3)}.c-readme-view blockquote>blockquote,.c-realm-view blockquote>blockquote{margin-bottom:var(--cr-space-7);margin-top:var(--cr-space-7)}.c-readme-view caption,.c-realm-view caption{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);margin-top:var(--cr-space-2);text-align:left}.c-readme-view q,.c-realm-view q{quotes:"“" "”"}.c-readme-view q:before,.c-realm-view q:before{content:open-quote}.c-readme-view q:after,.c-realm-view q:after{content:close-quote}.c-readme-view details,.c-realm-view details{margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3)}.c-readme-view summary,.c-realm-view summary{cursor:pointer;font-weight:var(--g-font-bold)}.c-readme-view math,.c-realm-view math{font-family:var(--g-font-family-mono)}.c-readme-view small,.c-realm-view small{font-size:var(--g-font-size-100)}.c-readme-view del,.c-realm-view del{-webkit-text-decoration:line-through;text-decoration:line-through}.c-readme-view sub,.c-realm-view sub{font-size:var(--g-font-size-50);vertical-align:sub}.c-readme-view sup,.c-realm-view sup{font-size:var(--g-font-size-50);padding-left:var(--space-px);vertical-align:middle}.c-readme-view sup>a,.c-realm-view sup>a{vertical-align:middle}.c-readme-view ol,.c-readme-view ul,.c-realm-view ol,.c-realm-view ul{margin-bottom:var(--cr-space-4);margin-top:var(--cr-space-4);padding-left:var(--g-space-4)}.c-readme-view ul,.c-realm-view ul{list-style:disc}.c-readme-view ol,.c-realm-view ol{list-style:decimal}.c-readme-view ol ol,.c-readme-view ol ul,.c-readme-view ul ol,.c-readme-view ul ul,.c-realm-view ol ol,.c-realm-view ol ul,.c-realm-view ul ol,.c-realm-view ul ul{margin-bottom:var(--cr-space-2);margin-top:var(--cr-space-2);padding-left:var(--g-space-4)}.c-readme-view li,.c-realm-view li{margin-bottom:var(--cr-space-1);margin-top:var(--cr-space-1)}.c-readme-view code,.c-readme-view pre,.c-realm-view code,.c-realm-view pre{font-family:var(--g-font-family-mono)}.c-readme-view pre,.c-readme-view pre.chroma-chroma,.c-realm-view pre,.c-realm-view pre.chroma-chroma{background-color:var(--s-color-bg-surface-primary);border-radius:var(--g-border-radius-sm);margin-bottom:var(--cr-space-3);margin-top:var(--cr-space-3);overflow-x:auto;padding:var(--cr-space-4)}.c-readme-view :not(pre)>code,.c-realm-view :not(pre)>code{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--g-border-radius-sm);font-size:.96em;padding:var(--cr-space-0-5) var(--cr-space-1)}.c-readme-view a code,.c-realm-view a code{color:inherit}.c-readme-view hr,.c-realm-view hr{border-top:var(--s-border-secondary);margin-bottom:var(--cr-space-8);margin-top:var(--cr-space-8)}.c-readme-view table,.c-realm-view table{border-collapse:collapse;display:block;margin-bottom:var(--cr-space-5);margin-top:var(--cr-space-5);max-width:100%;width:100%}.c-readme-view td,.c-readme-view th,.c-realm-view td,.c-realm-view th{border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4);white-space:normal;word-break:break-word}.c-readme-view th,.c-realm-view th{background-color:var(--s-color-bg-surface-secondary);font-weight:var(--g-font-bold)}.c-readme-view button,.c-readme-view input,.c-readme-view select,.c-readme-view textarea,.c-realm-view button,.c-realm-view input,.c-realm-view select,.c-realm-view textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-input);border:var(--s-border);padding:var(--cr-space-2) var(--cr-space-4)}.c-readme-view>.realm-view__btns:first-child+*,.c-readme-view>:first-child:not(.realm-view__btns),.c-realm-view>.realm-view__btns:first-child+*,.c-realm-view>:first-child:not(.realm-view__btns){margin-top:0!important}.c-readme-view .footnote-backref,.c-readme-view h1:not(.does-not-exist),.c-readme-view h2:not(.does-not-exist),.c-readme-view h3:not(.does-not-exist),.c-readme-view h4:not(.does-not-exist),.c-readme-view sup:not(.does-not-exist),.c-realm-view .footnote-backref,.c-realm-view h1:not(.does-not-exist),.c-realm-view h2:not(.does-not-exist),.c-realm-view h3:not(.does-not-exist),.c-realm-view h4:not(.does-not-exist),.c-realm-view sup:not(.does-not-exist){scroll-margin-top:var(--cr-space-24)}.c-readme-view .b-btn,.c-realm-view .b-btn{color:var(--s-color-text-secondary);display:inline-flex}.c-readme-view .b-btn:hover,.c-realm-view .b-btn:hover{-webkit-text-decoration:none;text-decoration:none}.c-readme-view .b-btn:first-child,.c-realm-view .b-btn:first-child{float:right;margin-top:var(--g-space-4)}.c-readme-view>.b-btn:first-child+*,.c-readme-view>:first-child:not(.b-btn),.c-realm-view>.b-btn:first-child+*,.c-realm-view>:first-child:not(.b-btn){margin-top:0}.c-readme-view{background-color:var(--s-color-bg-base);border-radius:var(--g-border-radius);margin-bottom:var(--g-space-6);padding:var(--g-space-6) var(--g-space-4) var(--g-space-4);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.c-readme-view{grid-row-start:auto}}.b-gnome .hat,.b-logo .hat{fill:var(--s-logo-hat)}.b-gnome .beard,.b-logo .beard{fill:var(--s-logo-beard)}.b-banner{align-items:center;background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base);display:flex;font-size:var(--g-font-size-50);font-weight:var(--g-font-semibold);justify-content:center;padding:var(--g-space-1-5) var(--g-space-4);text-align:center;width:100%}@media (min-width:calc(640 / 16 * 1rem)){.b-banner{font-size:var(--g-font-size-100)}}.b-banner a{color:inherit;-webkit-text-decoration:underline;text-decoration:underline}.b-banner a:hover,a.b-banner:hover{opacity:.8}.b-banner code{background-color:var(--s-color-bg-brand-action);border-radius:var(--g-border-radius-sm);font-size:.96em;padding:var(--g-space-0-5) var(--g-space-1)}.b-header{background-color:var(--s-color-bg-base);border-bottom:var(--s-border);font-size:var(--g-font-size-100);position:sticky;top:0;z-index:var(--g-z-max)}.b-header nav{align-items:stretch;height:auto}.b-header .main-nav{align-items:stretch;display:flex;flex:1 1 auto;gap:var(--g-space-1);height:100%;min-width:0;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-header .main-nav{grid-column:span 7}}.b-header .main-nav--explorer{grid-column:span 10}.b-header .user-picture{border:var(--s-border-secondary);border-radius:var(--s-rounded);cursor:pointer;flex-shrink:0;height:var(--g-space-10);width:var(--g-space-10)}.b-header .user-picture>svg{height:100%;width:100%}.b-main-navigation{color:var(--s-color-text-quaternary);height:auto;position:relative;width:100%}.b-main-navigation>.inner{align-items:center;background-color:var(--s-color-bg-surface-secondary);border:var(--s-border-secondary);border-radius:var(--s-rounded);height:100%;padding-left:var(--g-space-1-5);padding-right:var(--g-space-1-5);position:relative}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation>.inner{padding-right:var(--g-space-8)}}.b-main-navigation>.inner:has([data-role=header-input-search]:focus-within){border-color:var(--s-color-border-tertiary)}.b-main-navigation .searchbar{bottom:0;color:var(--s-color-text-secondary);font-size:var(--g-font-size-200);font-weight:var(--g-font-medium);left:0;padding:var(--g-space-1-5);padding-right:var(--g-space-8);position:absolute;right:0;top:0}.b-main-navigation .searchbar>input{background-color:transparent;height:100%;outline:none;width:100%}.b-main-navigation .searchbar:focus-within+.b-breadcrumb{display:none}.b-main-navigation .network-toggle{align-items:center;background-color:var(--g-color-transparent);border-radius:var(--g-border-radius);cursor:pointer;display:none;height:calc(100% - 2px);justify-content:center;padding:var(--g-space-1-5);position:absolute;right:1px;top:1px;z-index:var(--g-z-max)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-navigation .network-toggle{display:flex}}.b-main-navigation .network-toggle>svg{color:var(--s-color-text-tertiary);height:var(--g-space-5);width:var(--g-space-5)}.b-main-navigation .network-toggle:hover>svg{color:var(--s-color-text-tertiary-hover)}.b-main-navigation .b-popup-dialog>.inner{color:var(--s-color-text-tertiary);width:var(--g-space-72)}.b-main-navigation .b-popup-dialog header>span{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-navigation .b-popup-dialog .item{display:flex;gap:var(--g-space-1)}.b-main-navigation .b-popup-dialog .item>svg{height:var(--g-space-4);width:var(--g-space-4)}.b-main-navigation .b-popup-dialog .item-content{display:flex;flex-direction:column}.b-main-navigation .b-popup-dialog .item-label{font-size:var(--g-font-size-50)}.b-main-navigation .b-popup-dialog .item-value{color:var(--s-color-text-secondary);font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold)}.b-main-menu{display:flex;flex:0 0 auto;grid-column:span 3;height:var(--g-space-12)}@media (min-width:calc(640 / 16 * 1rem)){.b-main-menu{height:auto}}.b-main-menu .menu-toggle{align-items:center;cursor:pointer;display:flex;margin-left:auto;order:3}.b-main-menu .menu-toggle>svg{height:var(--g-space-5);margin-left:var(--g-space-4);width:var(--g-space-5)}@media (min-width:calc(820 / 16 * 1rem)){.b-main-menu .menu-toggle>svg{margin-left:var(--g-space-2)}}.b-main-menu .menu-toggle-input~.menu-dev{display:none}.b-main-menu .menu-toggle-input:checked~.menu-dev{display:flex}.b-main-menu .menu-toggle-input:checked~.menu-general{display:none}.b-main-menu .menu-dev,.b-main-menu .menu-general{display:flex;height:100%;justify-content:flex-end}.b-menu-link:last-child,.b-menu-link:last-child .link{margin-right:0}.b-menu-link .link{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);font-weight:var(--g-font-semibold);gap:var(--g-space-1);height:100%;margin-right:var(--g-space-3);position:relative}.b-menu-link .link:hover{color:var(--s-color-text-tertiary-hover)}.b-menu-link .link:after{background-color:var(--s-color-bg-brand-default);border-radius:var(--s-rounded) var(--s-rounded) 0 0;bottom:0;content:"";height:var(--g-space-1);left:0;position:absolute;transition:width var(--g-transition-fast);width:0}.b-menu-link .link>svg{flex-shrink:0;height:var(--g-space-5);min-width:var(--g-space-2);width:var(--g-space-5)}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link>svg{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link>svg{display:inline-block;height:var(--g-space-4-5);width:var(--g-space-4-5)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link{font-weight:var(--g-font-bold)}}@media (min-width:calc(1366 / 16 * 1rem)){.b-menu-link .link{margin-right:var(--g-space-6);padding-right:var(--g-space-1)}}@media (min-width:calc(640 / 16 * 1rem)){.b-menu-link .link-label{display:none}}@media (min-width:calc(1020 / 16 * 1rem)){.b-menu-link .link-label{display:inline}}.b-menu-link .link--icon{font-weight:var(--g-font-regular);margin-right:var(--g-space-4)}@media (min-width:calc(480 / 16 * 1rem)){.b-menu-link .link--icon{margin-right:var(--g-space-6)}}.b-menu-link .link--is-active{color:var(--s-color-text-secondary)}.b-menu-link .link--is-active:after{width:100%}.b-menu-link .link--is-active>svg{color:var(--s-color-bg-brand-default)}.menu-general .link{color:var(--s-color-text-secondary)}.menu-general .link:hover{color:var(--s-color-text-link-hover)}.b-breadcrumb{display:flex}.b-breadcrumb,.b-breadcrumb:after{background-color:var(--s-color-bg-surface-secondary)}.b-breadcrumb:after{bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}.b-breadcrumb>ol{color:var(--s-color-text-primary);display:flex;font-weight:var(--g-font-semibold);line-height:var(--g-line-height-snug)}.b-breadcrumb .argument,.b-breadcrumb .element,.b-breadcrumb .query{align-items:center;display:flex;white-space:nowrap;z-index:var(--g-z-1)}.b-breadcrumb .argument:not(:first-child):before,.b-breadcrumb .element:not(:first-child):before,.b-breadcrumb .query:not(:first-child):before{color:var(--s-color-text-tertiary);content:"/";line-height:var(--g-line-height-normal);padding-left:.18rem;padding-right:.18rem;padding-top:var(--g-space-px)}.b-breadcrumb .argument a,.b-breadcrumb .element a,.b-breadcrumb .query a{background-color:var(--s-color-bg-base);border:1px solid var(--s-color-border-transparent);border-radius:var(--s-rounded-sm);display:inline-block;min-width:var(--g-space-4);padding:var(--g-space-0-5);text-align:center}.b-breadcrumb .argument a:hover,.b-breadcrumb .element a:hover,.b-breadcrumb .query a:hover{background-color:var(--s-color-bg-brand-default);color:var(--s-color-text-base)}.b-breadcrumb .argument:not(:first-child):before{content:":"}.b-breadcrumb .argument a{background-color:var(--s-color-bg-surface-quaternary);color:var(--s-color-text-base)}.b-breadcrumb .query:not(:first-child):before{content:"&"}.b-breadcrumb .query:nth-child(1 of .query):before{content:"?"}.b-breadcrumb .query label{background-color:var(--s-color-bg-surface-primary);border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);cursor:text;display:inline-flex;height:100%;min-width:var(--g-space-4);padding:var(--g-space-0-5) var(--g-space-1);position:relative;text-align:center;width:100%}.b-breadcrumb .query label:focus-within{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query label:hover{border-color:var(--s-color-border-quaternary)}.b-breadcrumb .query input{background-color:var(--s-color-bg-surface-primary);max-width:10ch;order:3;outline:none;field-sizing:content}@supports not (field-sizing:content){.b-breadcrumb .query input{width:5rem!important}}.b-breadcrumb .query input::-moz-placeholder{opacity:0}.b-breadcrumb .query input::placeholder{opacity:0}.b-breadcrumb .query input:-moz-placeholder{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown{width:var(--g-space-px)}.b-breadcrumb .query input:placeholder-shown::-moz-placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:-moz-placeholder::placeholder{color:var(--g-color-transparent)}.b-breadcrumb .query input:placeholder-shown::placeholder{color:var(--g-color-transparent)}.b-footer{border-top:var(--s-border);font-size:var(--g-font-size-100);padding-bottom:var(--g-space-4);padding-top:var(--g-space-4);width:100%}.b-footer>nav{flex-direction:column;row-gap:var(--g-space-8)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer>nav{flex-wrap:wrap}}.b-footer .logo{color:var(--s-color-text-primary);grid-column:1/-1;width:var(--g-space-44)}.b-footer .logo:hover{color:var(--s-color-text-primary);-webkit-text-decoration:none;text-decoration:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .logo{align-self:center;grid-column:1/3;grid-row:1/1;width:60%}}.b-footer .nav-primary{display:flex;gap:var(--g-space-10);grid-column:1/-1;grid-row:2/3}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary{align-items:center;flex:1 0 0%;flex-direction:row;gap:var(--g-space-6);justify-content:space-between}}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .nav-primary{grid-column:2/8;grid-row:1/1}}.b-footer .nav-primary>ul{display:flex;flex:1;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-primary>ul{flex:initial;flex-direction:row}.b-footer .nav-social{margin-left:auto}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-social{grid-column:span 3;justify-self:end;margin-left:0}}.b-footer .nav-theme{align-items:center;display:flex;gap:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .nav-theme{flex-basis:100%}}@media (min-width:calc(820 / 16 * 1rem)){.b-footer .nav-theme{grid-column:span 3}}.b-footer .nav-theme .nav-theme-label{color:var(--s-color-text-secondary)}.b-footer .nav-theme:has([data-theme-target=sun]:not(.u-hidden)) .nav-theme-label:before{content:"Light"}.b-footer .nav-theme:has([data-theme-target=moon]:not(.u-hidden)) .nav-theme-label:before{content:"Dark"}.b-footer .nav-theme:has([data-theme-target=system]:not(.u-hidden)) + .nav-theme-label:before{content:"System"}.b-footer .legal{color:var(--s-color-text-tertiary);font-size:var(--g-font-size-50);margin-top:var(--g-space-3);padding-top:var(--g-space-3)}.b-footer .legal>nav{color:var(--s-color-text-secondary);display:flex;flex-direction:column;flex-wrap:wrap;gap:var(--g-space-1) var(--g-space-3);margin-top:var(--g-space-2)}@media (min-width:calc(640 / 16 * 1rem)){.b-footer .legal>nav{flex-direction:row}.b-footer .legal>nav>a+a:before{color:var(--s-color-text-quaternary);content:"|";margin-right:var(--g-space-3)}}.b-footer .legal>nav:nth-child(3){grid-column:span 2/span 2}.b-footer .legal>:last-child:not(ul),.b-footer .legal>nav li{margin-bottom:var(--g-space-2);margin-top:var(--g-space-2)}.b-footer .legal>:last-child:not(ul){flex-basis:100%}@media (min-width:calc(1020 / 16 * 1rem)){.b-footer .legal>:last-child:not(ul){flex-basis:auto;grid-column:span 1/span 1}}.b-footer a:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}.b-content-header{display:flex;flex-direction:column;gap:var(--g-space-3);grid-row:span 1/span 1;margin-bottom:var(--g-space-6);margin-top:var(--g-space-10)}@media (min-width:calc(820 / 16 * 1rem)){.b-content-header{grid-column:span 7/span 7;grid-row-start:1;justify-content:space-between;margin-top:var(--g-space-10)}}@media (min-width:calc(1020 / 16 * 1rem)){.b-content-header{align-items:center;flex-direction:row}}.b-content-header .title{align-items:center;display:flex;gap:var(--g-space-3)}.b-content-header .header-info{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);gap:var(--g-space-12);justify-content:space-between}.b-content-header .b-inline-btn>span{display:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-content-header .b-inline-btn>span{display:inline}}.b-content-h1{font-size:var(--g-font-size-600);text-align:center}.b-content-h1,.b-content-h2{color:var(--s-color-text-primary);font-weight:var(--g-font-bold)}.b-content-h2{font-size:var(--g-font-size-400);margin-bottom:var(--g-space-4)}.b-btns{align-items:center;display:flex;gap:var(--g-space-1)}@media (min-width:calc(1020 / 16 * 1rem)){.b-btns{gap:var(--g-space-2)}}.b-btn{border:var(--s-border);border-radius:var(--s-rounded-sm);cursor:pointer;display:inline-flex;gap:var(--g-space-1-5);min-width:-moz-max-content;min-width:max-content;padding:var(--g-space-1) var(--g-space-2)}.b-btn:hover{background-color:var(--s-color-bg-surface-primary-hover)}.b-btn .c-icon{margin-left:0;margin-right:0}.b-btn--secondary:hover{background-color:var(--s-color-bg-surface-primary)}.b-inline-btn{color:var(--s-color-text-tertiary);cursor:pointer}.b-inline-btn:hover{color:var(--s-color-text-tertiary-hover)}.b-switch input,.b-switch label:last-child{display:none}.b-switch input+label,.b-switch input:checked~label:last-child{display:block}.b-switch input:checked+label{display:none}.b-block-form,.b-inline-form{color:var(--s-color-text-tertiary);display:flex;flex-direction:column;gap:var(--g-space-2) var(--g-space-3)}@media (min-width:calc(820 / 16 * 1rem)){.b-block-form,.b-inline-form{flex-direction:row}}.b-block-form{align-items:stretch}@media (min-width:calc(820 / 16 * 1rem)){.b-block-form{flex-direction:column}}.b-input{border:var(--s-border);border-radius:var(--s-rounded-sm);color:var(--s-color-text-secondary);display:flex;font-size:var(--g-font-size-100);min-width:var(--g-space-48);overflow:hidden;position:relative}.b-input>svg{height:var(--g-space-4);pointer-events:none;position:absolute;top:50%;transform:translateY(-50%);width:var(--g-space-4)}.b-input>svg:first-child{left:var(--g-space-2)}.b-input>svg:last-child{right:var(--g-space-2)}.b-input:hover,.b-input>input:focus,.b-input>input:hover{border-color:var(--s-color-border-tertiary)}.b-input:has(input:focus),.b-input:hover,.b-input>input:focus,.b-input>input:hover{border-color:var(--s-color-border-tertiary)}.b-input:hover>label{background-color:var(--s-color-bg-surface-primary)}.b-input:has(input:focus)>label,.b-input:hover>label{background-color:var(--s-color-bg-surface-primary)}.b-input>label{align-items:center;background-color:var(--s-color-bg-surface-secondary);gap:var(--g-space-3);white-space:nowrap}.b-input>input,.b-input>label,.b-input>select{display:flex;padding:var(--g-space-1-5) var(--g-space-3)}.b-input>input,.b-input>select{color:inherit;outline:none;width:100%}@media (min-width:calc(820 / 16 * 1rem)){.b-input>input,.b-input>select{padding:var(--g-space-1-5) var(--g-space-2)}}.b-input>select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--s-color-bg-surface-secondary);cursor:pointer}.b-input>select:hover{background-color:var(--s-color-bg-surface-primary)}.b-input>input{background-color:var(--s-color-bg-base);border-left:none}.b-input>label+input{border-left:var(--s-border)}.b-list{margin-bottom:var(--g-space-10)}.b-list>li{border-bottom:var(--s-border);color:var(--s-color-text-tertiary)}.b-list>li:first-child{border-top:var(--s-border)}.b-list>li>:where(a,div){align-items:center;display:flex;justify-content:space-between;padding:var(--g-space-2)}.b-list>li>:where(a,div):hover{background-color:var(--s-color-bg-surface-primary-hover)}.b-list>li>:where(a,div) .c-icon{margin-left:0}.b-list>li>:where(a,div)>a{flex:1;min-width:0}.b-list>li>:where(a,div)>a:hover{background-color:transparent}.b-list .name{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;color:var(--s-color-text-secondary);margin-left:var(--g-space-1);max-width:100%;overflow:hidden;text-overflow:ellipsis}.b-user-sidebar{margin-top:var(--g-space-4)}.b-user-sidebar>*+*{margin-top:var(--g-space-8)}.b-user-sidebar .user-avatar{border:var(--s-border);border-radius:var(--s-rounded);height:var(--g-space-24);width:var(--g-space-24)}@media (min-width:calc(640 / 16 * 1rem)){.b-user-sidebar .user-avatar{height:var(--g-space-36);width:var(--g-space-36)}}.b-user-sidebar .user-avatar img,.b-user-sidebar .user-avatar svg{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.b-user-sidebar .user-info{align-items:flex-start;display:flex;gap:var(--g-space-6)}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar .user-info{flex-direction:column}}.b-user-sidebar .user-info>div:last-child{align-self:flex-end}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar .user-info>div:last-child{align-self:flex-start}}.b-user-sidebar .title{color:var(--s-color-text-primary);display:bock;font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);line-height:var(--g-line-height-tight);text-transform:capitalize;word-break:break-all}@media (min-width:calc(640 / 16 * 1rem)){.b-user-sidebar .title{font-size:var(--g-font-size-800)}}.b-user-sidebar .subtitle{color:var(--s-color-text-secondary);display:block;font-size:var(--g-font-size-100);line-height:var(--g-line-height-tight);margin-top:var(--g-space-2)}.b-user-sidebar>a{align-items:center;display:flex;justify-content:center}@media (min-width:calc(820 / 16 * 1rem)){.b-user-sidebar>a{display:inline-flex}}.b-sidebar{border-bottom:var(--s-border);grid-column:span 1/span 1;padding-bottom:var(--g-space-10);position:relative}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar{border-bottom:none;grid-column:span 3/span 3;grid-row:span 2/span 2;grid-row-start:1;height:100%;margin-bottom:0;order:2;padding-bottom:0}.b-sidebar+md-renderer:empty+*{grid-row-start:1;padding-top:var(--g-space-6)}.b-sidebar+md-renderer:empty+*,.b-sidebar+md-renderer:has(.b-btn:only-child)+*{grid-row-start:1;padding-top:var(--g-space-6)}}.b-sidebar:first-child{margin-top:var(--g-space-8)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar:first-child{margin-top:0}}.b-sidebar>div{padding-top:var(--g-space-2);position:sticky;top:var(--g-space-14)}.b-sidebar>div:has(.inner):not(:has(nav li)){display:none}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar>div{padding-bottom:var(--g-space-2)}}.b-sidebar .inner{background-color:var(--s-color-bg-surface-primary);border-radius:var(--s-rounded-sm);max-height:100vh;overflow:scroll;scrollbar-width:none}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .inner{background-color:var(--g-color-transparent)}}.b-sidebar .inner>nav{display:none;font-size:var(--g-font-size-100);margin-top:var(--g-space-2);padding:var(--g-space-2) var(--g-space-4) var(--g-space-6)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .inner>nav{display:block;margin-top:0;padding-bottom:var(--g-space-28);padding-left:0;padding-right:0}.b-sidebar .inner>nav>*{padding-left:0}}.b-sidebar .b-expend-btn{align-items:center;background-color:var(--s-color-bg-base);border:var(--s-border);border-radius:var(--s-rounded-sm);cursor:pointer;display:flex;font-size:var(--g-font-size-100);justify-content:space-between;padding:var(--g-space-2) var(--g-space-4)}.b-sidebar .b-expend-btn:hover{background-color:var(--s-color-bg-surface-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .b-expend-btn{border:none;cursor:default;font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);margin-top:var(--g-space-10);padding:0}.b-sidebar .b-expend-btn,.b-sidebar .b-expend-btn:hover{background-color:var(--g-color-transparent)}}.b-sidebar .b-expend-btn:has(#toc-expend:checked)+nav{display:block}.b-sidebar .b-expend-btn>input{display:none}.b-sidebar .b-expend-btn>input:checked+.wrapper-icon:before{content:"close"}.b-sidebar .b-expend-btn>input:checked+.wrapper-icon>svg{transform:rotate(180deg)}.b-sidebar .wrapper-icon{align-items:center;display:flex;gap:var(--g-space-1-5)}.b-sidebar .wrapper-icon:before{content:"open"}@media (min-width:calc(820 / 16 * 1rem)){.b-sidebar .wrapper-icon{display:none}}.dev-mode .b-sidebar .b-expend-btn{background-color:var(--s-color-bg-surface-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.dev-mode .b-sidebar .b-expend-btn{background-color:var(--g-color-transparent)}}.dev-mode .b-sidebar .b-expend-btn:hover{background-color:var(--s-color-bg-surface-primary)}.b-source-code{font-family:var(--g-font-mono)}.b-source-code>pre{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);font-size:var(--g-font-size-100);overflow:scroll;padding:var(--g-space-4) var(--g-space-1)}@media (min-width:calc(640 / 16 * 1rem)){.b-source-code>pre{font-size:var(--g-font-size-200);padding:var(--g-space-8) var(--g-space-3)}}.b-source-code>pre a:hover{-webkit-text-decoration:none;text-decoration:none}[data-theme=dark] .b-source-code>pre{background-color:var(--s-color-bg-base)}.b-toc{list-style:none;margin-top:var(--g-space-2)}.b-toc>*+*{margin-bottom:var(--g-space-1-5);margin-top:var(--g-space-1-5)}.b-toc .b-toc{border-left:1px solid var(--s-color-border-secondary);margin-bottom:var(--g-space-4);padding-left:var(--g-space-4)}.b-toc a>span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.b-toc a:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}main.dev-mode .b-toc a{word-break:break-all}.b-source-toc>.b-toc{margin-bottom:var(--g-space-4)}.b-source-toc>*+*{margin-top:var(--g-space-1-5)}.b-source-toc .accordion summary>svg{transform:rotate(-90deg)}.b-source-toc .accordion summary:hover{color:var(--s-color-text-link-hover);-webkit-text-decoration:underline;text-decoration:underline}.b-source-toc .accordion[open] summary>svg{transform:rotate(0deg)}.b-source-toc .accordion>.b-toc{padding-left:var(--g-space-5)}.b-source-toc .accordion h3{font-size:var(--g-font-size-100);font-weight:var(--g-font-medium);margin-top:0}.b-action-overview{margin-bottom:var(--g-space-12)}.b-action-overview>p{font-size:var(--g-font-size-200)}.b-action-function{background-color:var(--s-color-bg-surface-secondary);border-radius:var(--s-rounded);margin-bottom:var(--g-space-3);padding:var(--g-space-4)}.b-action-function .title{align-items:baseline;display:flex;flex-wrap:wrap;font-size:var(--g-font-size-50);gap:var(--g-space-1) var(--g-space-4);margin-bottom:var(--g-space-1)}.b-action-function>header{align-items:flex-start;display:flex;font-size:var(--g-font-size-100);justify-content:space-between;margin-bottom:var(--g-space-4)}.b-action-function>header .signature>code{color:var(--s--text-secondary)}@media (min-width:calc(820 / 16 * 1rem)){.b-action-function>header .signature{font-size:var(--g-font-size-50)}}.b-action-function>header h2{color:var(--s-color-text-primary);font-size:var(--g-font-size-300);font-weight:var(--g-font-semibold);line-height:var(--g-line-height-tight)}.b-action-function .description{color:var(--s-color-text-secondary);font-size:var(--g-font-size-200)}.b-action-function .params{align-items:stretch;color:var(--s-color-text-tertiary);display:flex;flex-direction:column;font-size:var(--g-font-size-100);gap:var(--g-space-1);margin-bottom:var(--g-space-1);margin-top:var(--g-space-6);width:100%}.b-action-function .params label{background-color:var(--s-color-bg-surface-primary)}.b-action-function .params .b-input:has(input:focus) label{background-color:var(--s-color-bg-surface-secondary)}.b-action-function .params .b-input:has(input:hover) label{background-color:var(--s-color-bg-surface-secondary)}.b-action-function .b-alert{background-color:var(--s-color-bg-warning-weak);border-left:var(--g-space-1) solid var(--s-color-border-tertiary);border-left-color:var(--s-color-border-warning);border-radius:var(--s-rounded);color:var(--s-color-text-secondary);color:var(--s-color-text-warning);margin-bottom:var(--g-space-10);margin-top:var(--g-space-5);padding:var(--g-space-3) var(--g-space-4)}.b-action-function .b-alert>h1:first-child,.b-action-function .b-alert>h2:first-child,.b-action-function .b-alert>h3:first-child{font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);margin-bottom:var(--g-space-2)}.b-action-function .b-alert .b-btn,.b-action-function .b-alert label{background-color:var(--s-color-bg-warning-action);border:none;color:var(--s-color-bg-warning-weak);cursor:pointer}.b-action-function .b-alert .b-btn{margin-top:var(--g-space-4)}.b-code{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded);font-size:var(--g-font-size-100);position:relative}.b-code pre{color:var(--s-color-text-secondary);padding:var(--g-space-4);padding-right:var(--g-space-10);white-space:pre-wrap}.b-code .btn-copy{background-color:var(--g-color-transparent);color:var(--s-color-text-tertiary);cursor:pointer;padding:0;position:absolute;right:var(--g-space-2);top:var(--g-space-2)}.b-code .btn-copy:hover{color:var(--s-color-text-primary)}.b-packages{min-height:var(--g-space-96);padding-bottom:var(--g-space-24);scroll-margin-block-start:var(--g-space-24)}@media (min-width:calc(820 / 16 * 1rem)){.b-packages{grid-column:span 7/span 7}}.b-packages .title{color:var(--s-color-text-primary);display:block;font-size:var(--g-font-size-700);font-weight:var(--g-font-bold);margin-bottom:var(--g-space-6)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .title{font-size:var(--g-font-size-800)}}.b-packages nav{display:grid;grid-template-columns:repeat(4,1fr);grid-gap:var(--g-space-3);gap:var(--g-space-3);margin-bottom:var(--g-space-6)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages nav{border-bottom:var(--s-border);padding-bottom:var(--g-space-2)}}.b-packages .packages-tabs{border-bottom:var(--s-border);color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-200);font-weight:var(--g-font-semibold);gap:var(--g-space-4);grid-column:span 4/span 4;padding-bottom:var(--g-space-2);width:auto}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-tabs{border-bottom:none;font-size:var(--g-font-size-100);grid-column:span 2/span 2;padding-bottom:0;width:100%}}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages .packages-tabs{gap:var(--g-space-6);margin-left:0;width:100%}}.b-packages .packages-tabs label{align-items:center;cursor:pointer;display:flex;gap:var(--g-space-1);position:relative}.b-packages .packages-tabs label:hover{color:var(--s-color-text-tertiary-hover)}.b-packages .packages-tabs label .b-tag--secondary{display:none}@media (min-width:calc(1020 / 16 * 1rem)){.b-packages .packages-tabs label .b-tag--secondary{display:inline}}.b-packages .packages-filters{align-items:center;color:var(--s-color-text-tertiary);display:flex;font-size:var(--g-font-size-100);gap:var(--g-space-2);grid-column:span 2/span 2}@media (min-width:calc(480 / 16 * 1rem)){.b-packages .packages-filters{grid-column:span 1/span 1}}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-filters{justify-content:flex-end}}.b-packages .packages-filters>div{display:grid}.b-packages .packages-filters label{align-items:center;cursor:pointer;display:flex;gap:var(--g-space-0-5);grid-column:1/1;grid-row:1/1;justify-content:space-between}.b-packages .packages-filters label:hover>*{color:var(--s-color-text-tertiary-hover)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-filters label span{display:none}}@media (min-width:calc(1366 / 16 * 1rem)){.b-packages .packages-filters label span{display:inline}}.b-packages .packages-search{display:flex;font-size:var(--g-font-size-100);grid-column:span 2/span 2;position:relative}@media (min-width:calc(480 / 16 * 1rem)){.b-packages .packages-search{grid-column:span 3/span 3}}@media (min-width:calc(640 / 16 * 1rem)){.b-packages .packages-search{grid-column:span 1/span 1}}.b-packages .range{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-gap:var(--g-space-2);color:var(--s-color-text-tertiary);font-size:var(--g-font-size-100);gap:var(--g-space-2)}.b-packages .range:before{color:var(--s-color-text-tertiary);display:none;font-size:var(--g-font-size-200);font-weight:var(--g-font-weight-bold);grid-column:1/-1;padding-bottom:var(--g-space-2);padding-top:var(--g-space-2);text-align:center;width:100%}.b-packages .range:after{content:"Add a package to your namespace to get started";display:none;font-size:var(--g-font-size-100);grid-column:1/-1;text-align:center}.b-packages .range:empty:before{content:"No packages found";display:block}.b-packages .range:empty:after{content:"Add a package to your namespace to get started";display:block}.b-packages article{background-color:var(--s-color-bg-surface-primary);border-radius:var(--s-rounded);display:flex;flex-direction:column;gap:var(--g-space-6);padding:var(--g-space-1)}@media (min-width:calc(640 / 16 * 1rem)){.b-packages article{gap:var(--g-space-2)}}.b-packages article .article-content{background-color:var(--s-color-bg-base);border-radius:var(--s-rounded-sm);display:flex;flex-direction:column;height:100%;padding:var(--g-space-2);width:100%}.b-packages article .article-content .title{align-items:center;display:flex;gap:var(--g-space-2);margin-bottom:var(--g-space-1);overflow:hidden;width:100%}.b-packages article .article-content h3{font-size:var(--g-font-size-200);font-weight:var(--g-font-bold);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b-packages article .article-content h3>a{color:var(--s-color-text-link-hover)}.b-packages article .article-content h3>a:hover{-webkit-text-decoration:underline;text-decoration:underline}.b-packages article .article-content>p{overflow:hidden;text-overflow:ellipsis;width:100%}.b-packages article .article-content>p>a:hover{-webkit-text-decoration:underline;text-decoration:underline}.b-packages article footer{display:flex;font-size:var(--g-font-size-50);gap:var(--g-space-1);justify-content:space-between;padding-bottom:var(--g-space-1);padding-left:var(--g-space-2);padding-right:var(--g-space-2)}.b-packages article footer time{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.b-packages article footer .size{text-align:right}.b-packages article,.b-packages li{display:none}.b-packages:has(input[value=packages]:checked) li{display:flex}.b-packages:has(input[value=packages]:checked) article{display:flex}.b-packages:has(input[value=realms]:checked) li[data-list-type-value=realm]{display:flex}.b-packages:has(input[value=realms]:checked) article[data-list-type-value=realm]{display:flex}.b-packages:has(input[value=pures]:checked) li[data-list-type-value=pure]{display:flex}.b-packages:has(input[value=pures]:checked) diff --git a/gno.land/pkg/integration/malformed_typeurl_test.go b/gno.land/pkg/integration/malformed_typeurl_test.go new file mode 100644 index 00000000000..4e2c5f56a37 --- /dev/null +++ b/gno.land/pkg/integration/malformed_typeurl_test.go @@ -0,0 +1,158 @@ +package integration + +// Tests for the malformed amino type_url attack path: a transaction whose +// type_url field contains no forward slash used to trigger a hard panic in +// typeURLtoFullname() before the runTx recover block was ever entered. +// +// After the fix, typeURLtoFullname returns an error instead of panicking, and +// BaseApp.CheckTx / DeliverTx recover from any remaining codec panics. Both +// paths must return a tx-decode error rather than crashing. + +import ( + "bytes" + "io" + "log/slog" + "testing" + "time" + + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/amino" + bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/require" +) + +// buildMalformedTxBytes encodes a valid bank.MsgSend transaction and flips the +// leading '/' (0x2F) in the amino type_url to '}' (0x7D). The result passes +// the IsASCIIText check in the binary decode path but contains no slash, +// which previously triggered a panic in typeURLtoFullname(). +func buildMalformedTxBytes(t *testing.T) []byte { + t.Helper() + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: crypto.Address{}, + ToAddress: crypto.Address{}, + Amount: std.NewCoins(std.NewCoin("ugnot", 1)), + }, + }, + Fee: std.NewFee(100000, std.NewCoin("ugnot", 1)), + } + validBz, err := amino.Marshal(tx) + require.NoError(t, err) + + typeURL := amino.GetTypeURL(bank.MsgSend{}) + idx := bytes.Index(validBz, []byte(typeURL)) + require.True(t, idx >= 0, "type_url not found in binary payload") + + mutated := make([]byte, len(validBz)) + copy(mutated, validBz) + mutated[idx] = '}' // '/' (0x2F) → '}' (0x7D): no slash, previously caused panic + return mutated +} + +// TestMalformedTypeURL_ConsensusDoesNotHalt verifies that a block containing a +// transaction with a malformed amino type_url (no slash) does not halt the +// consensus goroutine. The transaction must be rejected with a decode error and +// the node must continue processing subsequent blocks normally. +// +// Before the fix, the panic in typeURLtoFullname() propagated through: +// +// BaseApp.DeliverTx → amino.Unmarshal → typeURLtoFullname (panic) +// → localClient.DeliverTxAsync (no recover) +// → execBlockOnProxyApp → ApplyBlock → finalizeCommit +// → receiveRoutine defer/recover → logs CONSENSUS FAILURE!!! → onExit() +// +// A single malicious proposer could deterministically halt every validator by +// including one such transaction in a block. +func TestMalformedTypeURL_ConsensusDoesNotHalt(t *testing.T) { + t.Parallel() + + rootdir := gnoenv.RootDir() + config := TestingMinimalNodeConfig(rootdir) + // Disable empty blocks so the node stays in enterNewRound at heights > 1 + // with an empty mempool, giving a clean window to inject our proposal. + config.TMConfig.Consensus.CreateEmptyBlocks = false + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + node, _ := TestingInMemoryNode(t, logger, config) + defer node.Stop() + + mutatedBz := buildMalformedTxBytes(t) + cs := node.ConsensusState() + pv := node.PrivValidator() + + // consensusDead is closed by cs.Wait() if receiveRoutine ever exits — the + // definitive signal that the consensus goroutine has terminated. + consensusDead := make(chan struct{}) + go func() { cs.Wait(); close(consensusDead) }() + + // Wait for height ≥ 2 with no existing proposal. Height 1 always commits + // automatically (needProofBlock(1)=true). At height 2+ with + // CreateEmptyBlocks=false and an empty mempool, the node idles in + // enterNewRound — cs.Proposal remains nil — providing a clean injection + // window. + var ( + targetHeight int64 + targetCommit *bfttypes.Commit + ) + deadline := time.Now().Add(15 * time.Second) + for time.Now().Before(deadline) { + rs := cs.GetRoundState() + if rs.Height < 2 || rs.Proposal != nil { + time.Sleep(20 * time.Millisecond) + continue + } + if c := cs.LoadCommit(rs.Height - 1); c != nil { + targetHeight = rs.Height + targetCommit = c + break + } + time.Sleep(20 * time.Millisecond) + } + require.NotZero(t, targetHeight, "timed out waiting for injectable consensus height") + + state := cs.GetState() + + // Subscribe before injection so we don't miss the commit event. + newBlockSub := events.SubscribeToEvent(node.EventSwitch(), "test-malformed-typeurl", bfttypes.EventNewBlock{}) + + // Build a block that passes structural validation but contains the + // amino-malformed transaction. state.MakeBlock uses MedianTime for + // block.Time, which is exactly what ValidateBlock expects. + proposerAddr := pv.PubKey().Address() + block, blockParts := state.MakeBlock( + targetHeight, + []bfttypes.Tx{bfttypes.Tx(mutatedBz)}, + targetCommit, + proposerAddr, + ) + + proposal := bfttypes.NewProposal( + targetHeight, 0, -1, + bfttypes.BlockID{Hash: block.Hash(), PartsHeader: blockParts.Header()}, + ) + require.NoError(t, pv.SignProposal(state.ChainID, proposal)) + + // Inject the signed proposal. receiveRoutine picks it up via peerMsgQueue. + // With the fix applied, DeliverTx returns a decode error; the block is + // committed with the tx marked as errored and consensus continues. + require.NoError(t, cs.SetProposalAndBlock(proposal, block, blockParts, "attacker")) + + // Wait for an EventNewBlock at targetHeight: this fires only after the + // block is fully committed, proving consensus is still alive. If the fix + // is absent, consensusDead closes first. + select { + case <-consensusDead: + t.Fatal("consensus goroutine terminated — malformed tx caused an unrecovered panic") + case ev := <-newBlockSub: + got := ev.(bfttypes.EventNewBlock) + require.GreaterOrEqual(t, got.Block.Height, targetHeight) + t.Logf("OK: consensus survived, committed block %d with malformed type_url transaction", got.Block.Height) + case <-time.After(15 * time.Second): + t.Fatal("timed out waiting for consensus to commit block with malformed type_url transaction") + } +} diff --git a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar index d0dde24e05b..c848534e0f1 100644 --- a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar +++ b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar @@ -3,7 +3,7 @@ loadpkg gno.land/r/sys/names adduser admin adduser gui -patchpkg "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p" $admin_user_addr # use our custom admin +patchpkg "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" $admin_user_addr # use our custom admin gnoland start diff --git a/gno.land/pkg/integration/testdata/addpkg_private.txtar b/gno.land/pkg/integration/testdata/addpkg_private.txtar index c34607c877e..44f7e4c3015 100644 --- a/gno.land/pkg/integration/testdata/addpkg_private.txtar +++ b/gno.land/pkg/integration/testdata/addpkg_private.txtar @@ -531,7 +531,6 @@ var savedInterfaceMap map[string]interface{} var savedInterface interface{} var savedArray [3]*avl.Tree var savedArray2 [3]interface{} -var savedChannel chan *avl.Tree type PublicInterface interface { DoSomething() interface{} diff --git a/gno.land/pkg/integration/testdata/addpkg_public.txtar b/gno.land/pkg/integration/testdata/addpkg_public.txtar index 1f779a79cda..69ed449c106 100644 --- a/gno.land/pkg/integration/testdata/addpkg_public.txtar +++ b/gno.land/pkg/integration/testdata/addpkg_public.txtar @@ -213,7 +213,6 @@ var savedSlice []string var savedMap map[string]int var savedInterface interface{} var savedArray [3]*avl.Tree -var savedChannel chan *avl.Tree type PublicStruct struct { Name string diff --git a/gno.land/pkg/integration/testdata/alloc_call.txtar b/gno.land/pkg/integration/testdata/alloc_call.txtar deleted file mode 100644 index d0ea2430926..00000000000 --- a/gno.land/pkg/integration/testdata/alloc_call.txtar +++ /dev/null @@ -1,38 +0,0 @@ -# load the package -loadpkg gno.land/r/ext $WORK/ext -loadpkg gno.land/r/alloc $WORK/alloc - -# start a new node -gnoland start - -# this works fine, mostly for observing allocations and garbage collection. -gnokey maketx call -pkgpath gno.land/r/alloc -func DoAlloc -gas-fee 1000000ugnot -gas-wanted 50000000 -broadcast -chainid=tendermint_test test1 -stdout 'OK!' - --- alloc/alloc.gno -- -// MAXALLOC: 100000000 -package alloc - -import ( - "runtime" - - "gno.land/r/ext" -) - -var s = "hello world" - -func DoAlloc(_ realm) { - runtime.GC() - println("MemStats: ", runtime.MemStats()) - ext.GetVar() -} - --- ext/ext.gno -- -package ext - -var Global int - -func GetVar() int { - return Global -} - diff --git a/gno.land/pkg/integration/testdata/atomicswap.txtar b/gno.land/pkg/integration/testdata/atomicswap.txtar index 164892fe569..b31215e587d 100644 --- a/gno.land/pkg/integration/testdata/atomicswap.txtar +++ b/gno.land/pkg/integration/testdata/atomicswap.txtar @@ -20,12 +20,12 @@ gnokey maketx call -pkgpath gno.land/r/demo/defi/atomicswap -func NewCoinSwap -g stdout '(1 int)' stdout ".*$test2_user_addr.*$test3_user_addr.*12345ugnot.*" stdout 'OK!' -stdout 'EVENTS: \[.*"fee_delta":\{"denom":"ugnot","amount":454000\}.*\]' +stdout 'EVENTS: \[.*"fee_delta":\{"denom":"ugnot","amount":453800\}.*\]' gnokey query vm/qrender --data 'gno.land/r/demo/defi/atomicswap:' gnokey query auth/accounts/$test2_user_addr -stdout 'coins.*:.*1008533655ugnot' +stdout 'coins.*:.*1008533855ugnot' gnokey query auth/accounts/$test3_user_addr stdout 'coins.*:.*1010000000ugnot' @@ -34,6 +34,6 @@ stdout 'OK!' stdout 'EVENTS: \[.*"fee_delta":\{"denom":"ugnot","amount":500\}.*\]' gnokey query auth/accounts/$test2_user_addr -stdout 'coins.*:.*1008533655ugnot' +stdout 'coins.*:.*1008533855ugnot' gnokey query auth/accounts/$test3_user_addr stdout 'coins.*:.*1009011845ugnot' diff --git a/gno.land/pkg/integration/testdata/crossrealm_assign_recover.txtar b/gno.land/pkg/integration/testdata/crossrealm_assign_recover.txtar new file mode 100644 index 00000000000..90f207942d5 --- /dev/null +++ b/gno.land/pkg/integration/testdata/crossrealm_assign_recover.txtar @@ -0,0 +1,148 @@ +# Test: cross-realm assign+recover cannot corrupt state or steal funds. +# +# A vault realm has an exported SetOwner (non-crossing) that was meant +# to be unexported. An attacker deploys a realm that: +# 1. Calls SetOwner(myAddr) + recover — blocked by the pre-mutation +# readonly check so ownership never changes in memory. +# 2. Calls Withdraw(cross) — fails with "unauthorized" because +# the owner is still the original DAO address. +# +# Without the fix, step 1 would land in memory before DidUpdate panics. +# After recover(), the attacker owns the vault in memory and Withdraw +# succeeds — but the ownership change reverts after the tx, leaving +# cross-realm state inconsistent. + +adduser attacker + +## start a new node +gnoland start + +## deploy vault realm +gnokey maketx addpkg -pkgdir $WORK/vault -pkgpath gno.land/r/test/vault -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test test1 + +## verify initial owner is the DAO address +gnokey query "vm/qrender" --data "gno.land/r/test/vault:" +stdout 'owner: g1dao0000000000000000000000000000000000' + +## deploy attack realm +gnokey maketx addpkg -pkgdir $WORK/attack -pkgpath gno.land/r/test/attack -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test attacker + +## TEST 1: assign+recover then Withdraw — must fail with "unauthorized" +## The attacker does SetOwner(myAddr) + recover, then Withdraw(cross). +## With the fix, SetOwner is blocked before the write, so Withdraw sees +## the original owner and rejects the caller. +! gnokey maketx call -pkgpath gno.land/r/test/attack -func Attack -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test attacker +stderr 'unauthorized' + +## verify owner is unchanged after the attack attempt +gnokey query "vm/qrender" --data "gno.land/r/test/vault:" +stdout 'owner: g1dao0000000000000000000000000000000000' + +## TEST 2: assign without recover — the readonly panic aborts the tx +! gnokey maketx call -pkgpath gno.land/r/test/attack -func AttackNoRecover -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test attacker +stderr 'readonly tainted object' + +## verify owner is still unchanged +gnokey query "vm/qrender" --data "gno.land/r/test/vault:" +stdout 'owner: g1dao0000000000000000000000000000000000' + +## TEST 3: the crossing setter correctly rejects non-owners (positive control) +! gnokey maketx call -pkgpath gno.land/r/test/vault -func TransferOwnership -args ${attacker_user_addr} -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test attacker +stderr 'unauthorized' + +## owner unchanged +gnokey query "vm/qrender" --data "gno.land/r/test/vault:" +stdout 'owner: g1dao0000000000000000000000000000000000' + +-- vault/gnomod.toml -- +module = "gno.land/r/test/vault" +gno = "0.9" + +-- vault/vault.gno -- +package vault + +import ( + "chain" + "chain/banker" + "chain/runtime" +) + +var owner address + +func init() { + // Set initial owner to a DAO address via internal helper. + SetOwner(address("g1dao0000000000000000000000000000000000")) +} + +// SetOwner is an internal helper that was exported by mistake +// (should be setOwner). Without the pre-mutation readonly check, +// a cross-realm caller could call SetOwner + recover to silently +// hijack ownership in memory, then call Withdraw to steal funds. +func SetOwner(o address) { + owner = o +} + +func GetOwner(cur realm) address { + return owner +} + +// TransferOwnership is the legitimate crossing setter. +func TransferOwnership(cur realm, newOwner address) { + caller := runtime.PreviousRealm().Address() + if caller != owner { + panic("unauthorized") + } + owner = newOwner +} + +func Withdraw(cur realm, amount int64) { + caller := runtime.PreviousRealm().Address() + if caller != owner { + panic("unauthorized") + } + b := banker.NewBanker(banker.BankerTypeRealmSend) + pkgAddr := runtime.CurrentRealm().Address() + b.SendCoins(pkgAddr, caller, chain.Coins{{"ugnot", amount}}) +} + +func Render(_ string) string { + return "owner: " + string(owner) +} + +-- attack/gnomod.toml -- +module = "gno.land/r/test/attack" +gno = "0.9" + +-- attack/attack.gno -- +package attack + +import ( + "chain/runtime" + + "gno.land/r/test/vault" +) + +// Attack attempts to hijack ownership via assign+recover, then withdraw. +func Attack(cur realm) { + myAddr := runtime.CurrentRealm().Address() + + // Step 1: try to hijack ownership. The readonly check blocks the + // assignment before it lands in memory. recover() catches the panic. + func() { + defer func() { _ = recover() }() + vault.SetOwner(myAddr) + }() + + // Step 2: try to withdraw. Since owner is unchanged, this fails + // with "unauthorized". + vault.Withdraw(cross, 1000000) +} + +// AttackNoRecover is the negative control: without recover, the +// readonly panic aborts the entire transaction. +func AttackNoRecover(cur realm) { + myAddr := runtime.CurrentRealm().Address() + vault.SetOwner(myAddr) + // Never reached. + vault.Withdraw(cross, 1000000) +} diff --git a/gno.land/pkg/integration/testdata/gc.txtar b/gno.land/pkg/integration/testdata/gc.txtar index 2af7d95a789..5d07c6d0e17 100644 --- a/gno.land/pkg/integration/testdata/gc.txtar +++ b/gno.land/pkg/integration/testdata/gc.txtar @@ -6,8 +6,8 @@ loadpkg gno.land/r/gc $WORK/r/gc gnoland start -no-parallel -gnokey maketx call -pkgpath gno.land/r/gc -func Alloc -gas-fee 10000000ugnot -gas-wanted 300000000 -simulate skip -broadcast -chainid tendermint_test test1 -stdout 'GAS USED: 262693098' +gnokey maketx call -pkgpath gno.land/r/gc -func Alloc -gas-fee 10000000ugnot -gas-wanted 3000000000 -simulate skip -broadcast -chainid tendermint_test test1 +stdout 'GAS USED: 1048705180' -- r/gc/gc.gno -- package gc @@ -17,7 +17,7 @@ func gen() { } func Alloc(cur realm) { - for i := 0; i < 100; i++ { + for i := 0; i < 2; i++ { gen() gen() } diff --git a/gno.land/pkg/integration/testdata/gnokey_gasfee.txtar b/gno.land/pkg/integration/testdata/gnokey_gasfee.txtar index 3bc4edb9df4..f11707ee2fe 100644 --- a/gno.land/pkg/integration/testdata/gnokey_gasfee.txtar +++ b/gno.land/pkg/integration/testdata/gnokey_gasfee.txtar @@ -11,8 +11,8 @@ stdout '"coins": "10000000000000ugnot"' # Tx add package -simulate only, estimate gas used and gas fee gnokey maketx addpkg -pkgdir $WORK/hello -pkgpath gno.land/r/hello -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1 -stdout 'GAS USED: 269942' -stdout 'INFO: estimated gas usage: 269942, gas fee: 283ugnot, current gas price: 1ugnot/1000gas' +stdout 'GAS USED: 270022' +stdout 'INFO: estimated gas usage: 270022, gas fee: 284ugnot, current gas price: 1ugnot/1000gas' ## No fee was charged, and the sequence number did not change. gnokey query auth/accounts/$test1_user_addr @@ -20,7 +20,7 @@ stdout '"sequence": "0"' stdout '"coins": "10000000000000ugnot"' # Using the simulated gas and estimated gas fee should ensure the transaction executes successfully. -gnokey maketx addpkg -pkgdir $WORK/hello -pkgpath gno.land/r/hello -gas-wanted 269914 -gas-fee 282ugnot -broadcast -chainid tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/hello -pkgpath gno.land/r/hello -gas-wanted 269994 -gas-fee 282ugnot -broadcast -chainid tendermint_test test1 stdout 'OK' stdout 'EVENTS: \[.*"fee_delta":\{"denom":"ugnot","amount":207700\}.*\]' diff --git a/gno.land/pkg/integration/testdata/govdao_execute_reject_proposal.txtar b/gno.land/pkg/integration/testdata/govdao_execute_reject_proposal.txtar new file mode 100644 index 00000000000..a21667639be --- /dev/null +++ b/gno.land/pkg/integration/testdata/govdao_execute_reject_proposal.txtar @@ -0,0 +1,74 @@ +loadpkg gno.land/r/gov/dao/v3/init +loadpkg gno.land/r/gov/dao + +gnoland start + +# Init GovDAO members +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 95000000 -broadcast -chainid=tendermint_test test1 $WORK/run/init_govdao_members.gno + +# Deploy the realm that defines the proposal request +gnokey maketx addpkg -pkgdir $WORK/request -pkgpath gno.land/r/test/request -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Create proposal that fails on execution +gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 $WORK/run/create_proposal.gno +stdout OK! + +# Vote on proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func MustVoteOnProposalSimple -gas-fee 1000000ugnot -gas-wanted 10000000 -args 0 -args YES -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Try to execute proposal, it should fail because execution returns an error +! gnokey maketx call -pkgpath gno.land/r/gov/dao -func ExecuteProposal -gas-fee 1000000ugnot -gas-wanted 20000000 -args 0 -broadcast -chainid=tendermint_test test1 +stderr 'Boom!' + +# Execute proposal changing its status to rejected +gnokey maketx call -pkgpath gno.land/r/gov/dao -func ExecuteOrRejectProposal -gas-fee 1000000ugnot -gas-wanted 20000000 -args 0 -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Check that proposal was rejected +gnokey query vm/qeval --data "gno.land/r/gov/dao.Render(\"0\")" +stdout 'Stats\\n- \*\*PROPOSAL HAS BEEN DENIED\*\*\\nREASON: execution failed: Boom!' + +-- run/init_govdao_members.gno -- +package main + +import dao "gno.land/r/gov/dao/v3/init" + +func main() { + dao.InitWithUsers("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +} + +-- run/create_proposal.gno -- +package main + +import ( + "gno.land/r/gov/dao" + "gno.land/r/test/request" +) + +func main() { + dao.MustCreateProposal(cross, request.NewProposalRequest()) +} + +-- request/gnomod.toml -- +module = "gno.land/r/test/request" +gno = "0.9" + +-- request/request.gno -- +package request + +import ( + "errors" + + "gno.land/r/gov/dao" +) + +func NewProposalRequest() dao.ProposalRequest { + cb := func(realm) error { + return errors.New("Boom!") + } + + e := dao.NewSimpleExecutor(cb, "") + return dao.NewProposalRequest("Test Proposal", "This proposal always fails on execution", e) +} diff --git a/gno.land/pkg/integration/testdata/govdao_proposal_users_register_user.txtar b/gno.land/pkg/integration/testdata/govdao_proposal_users_register_user.txtar new file mode 100644 index 00000000000..d41d7f5051f --- /dev/null +++ b/gno.land/pkg/integration/testdata/govdao_proposal_users_register_user.txtar @@ -0,0 +1,125 @@ +loadpkg gno.land/r/gov/dao +loadpkg gno.land/r/gov/dao/v3/impl +loadpkg gno.land/r/gov/dao/v3/init +loadpkg gno.land/r/sys/users + +adduserfrom user1 'source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast' 1 +stdout 'g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4' + +gnoland start + +# Initialize GovDAO members +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 95000000 -broadcast -chainid=tendermint_test test1 $WORK/run/init_govdao.gno +stdout OK! + +# Render GovDAO to check it's working +gnokey query vm/qrender --data 'gno.land/r/gov/dao:' +stdout 'data: # GovDAO' + +# Create proposal to register @user1 +gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 $WORK/run/create_register_user_proposal.gno +stdout OK! + +# Check proposal exists +gnokey query vm/qrender --data 'gno.land/r/gov/dao:0' +stdout 'Register User to \"sys/users\" Realm' + +# Vote on proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func MustVoteOnProposalSimple -gas-fee 1000000ugnot -gas-wanted 10000000 -args 0 -args YES -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Execute proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func ExecuteProposal -gas-fee 1000000ugnot -gas-wanted 20000000 -args 0 -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Check user is registered +gnokey query vm/qeval --data "gno.land/r/sys/users.ResolveName(\"user1\")" +stdout 'g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4' + +# Create proposal for name alias @user1alias +gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 $WORK/run/create_update_name_proposal.gno +stdout OK! + +# Check proposal exists +gnokey query vm/qrender --data 'gno.land/r/gov/dao:1' +stdout 'Update Name Alias in \"sys/users\" Realm' + +# Vote on proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func MustVoteOnProposalSimple -gas-fee 1000000ugnot -gas-wanted 10000000 -args 1 -args YES -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Execute proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func ExecuteProposal -gas-fee 1000000ugnot -gas-wanted 20000000 -args 1 -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Check alias is registered +gnokey query vm/qeval --data "gno.land/r/sys/users.ResolveName(\"user1alias\")" +stdout 'g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4' + +# Create proposal for to delete @user1 +gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 $WORK/run/create_delete_user_proposal.gno +stdout OK! + +# Check proposal exists +gnokey query vm/qrender --data 'gno.land/r/gov/dao:2' +stdout 'Delete User in \"sys/users\" Realm' + +# Vote on proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func MustVoteOnProposalSimple -gas-fee 1000000ugnot -gas-wanted 10000000 -args 2 -args YES -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Execute proposal +gnokey maketx call -pkgpath gno.land/r/gov/dao -func ExecuteProposal -gas-fee 1000000ugnot -gas-wanted 20000000 -args 2 -broadcast -chainid=tendermint_test test1 +stdout OK! + +# Check user is deleted +gnokey query vm/qeval --data "gno.land/r/sys/users.ResolveName(\"user1\")" +stdout '(nil \*gno\.land/r/sys/users\.UserData)' + +-- run/init_govdao.gno -- +package main + +import dao "gno.land/r/gov/dao/v3/init" + +func main() { + dao.InitWithUsers("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +} + +-- run/create_register_user_proposal.gno -- +package main + +import ( + "gno.land/r/sys/users" + "gno.land/r/gov/dao" +) + +func main() { + r := users.ProposeRegisterUser("user1", "g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4") + dao.MustCreateProposal(cross, r) +} + +-- run/create_update_name_proposal.gno -- +package main + +import ( + "gno.land/r/sys/users" + "gno.land/r/gov/dao" +) + +func main() { + r := users.ProposeUpdateName("g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4", "user1alias") + dao.MustCreateProposal(cross, r) +} + +-- run/create_delete_user_proposal.gno -- +package main + +import ( + "gno.land/r/sys/users" + "gno.land/r/gov/dao" +) + +func main() { + r := users.ProposeDeleteUser("g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4") + dao.MustCreateProposal(cross, r) +} diff --git a/gno.land/pkg/integration/testdata/interrealm_final.txtar b/gno.land/pkg/integration/testdata/interrealm_final.txtar index 200b16c499b..ab28c9d5738 100644 --- a/gno.land/pkg/integration/testdata/interrealm_final.txtar +++ b/gno.land/pkg/integration/testdata/interrealm_final.txtar @@ -23,31 +23,31 @@ gnokey maketx addpkg -pkgdir $WORK/callerrealm -pkgpath gno.land/r/test/callerre ## test CASE_rA1 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rA1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rA2 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rA2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rA3 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rA3 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rB1 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rB1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rB2 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rB2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rB3 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rB3 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rB4 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rB4 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rC1 gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rC1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner @@ -55,7 +55,7 @@ stdout 'OK' ## test CASE_rC2 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rC2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rC3 gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rC3 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner @@ -63,11 +63,11 @@ stdout 'OK' ## test CASE_rC4 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rC4 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rC5 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_rC5 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_rD1 ! gnokey maketx addpkg -pkgdir $WORK/callerrealm_rd1 -pkgpath gno.land/r/test/callerrealm_rd1 -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test test1 @@ -79,30 +79,30 @@ stderr 'cannot directly mutate gno.land/r/test/bob.AllowedList from gno.land/r/t ## test CASE_pA1 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pA1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pA2 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pA2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pA3 -- omitted (illegal crossing function) # ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pA3 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner ## test CASE_pB1 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pB1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pB2 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pB2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pB3 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pB3 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pB4 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pB4 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pC1 gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pC1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner @@ -110,7 +110,7 @@ stdout 'OK' ## test CASE_pC2 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pC2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pC3 gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pC3 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner @@ -118,11 +118,11 @@ stdout 'OK' ## test CASE_pC4 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pC4 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## test CASE_pC5 ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func CASE_pC5 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test runner -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' ## entry diff --git a/gno.land/pkg/integration/testdata/interrealm_mix_call.txtar b/gno.land/pkg/integration/testdata/interrealm_mix_call.txtar index 2bd91a4cee9..f79f0dfff2e 100644 --- a/gno.land/pkg/integration/testdata/interrealm_mix_call.txtar +++ b/gno.land/pkg/integration/testdata/interrealm_mix_call.txtar @@ -18,7 +18,7 @@ stdout 'obj\.value = 0' ## execute NonCrossingMutation ! gnokey maketx call -pkgpath gno.land/r/test/callerrealm -func NonCrossingMutation -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' gnokey query "vm/qrender" --data "gno.land/r/test/borrowrealm:" stdout 'Object\.value = 0, value = 0' diff --git a/gno.land/pkg/integration/testdata/interrealm_mix_run.txtar b/gno.land/pkg/integration/testdata/interrealm_mix_run.txtar index 9c1fc680e3e..4f82e937174 100644 --- a/gno.land/pkg/integration/testdata/interrealm_mix_run.txtar +++ b/gno.land/pkg/integration/testdata/interrealm_mix_run.txtar @@ -16,7 +16,7 @@ stdout 'Object\.value = 0, value = 0' ## run non_crossing_mutation ! gnokey maketx run runner $WORK/run/non_crossing_mutation.gno -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test -stderr 'cannot modify external-realm or non-realm object' +stderr 'readonly tainted object' gnokey query "vm/qrender" --data "gno.land/r/test/borrowrealm:" stdout 'Object\.value = 0, value = 0' diff --git a/gno.land/pkg/integration/testdata/maketx_call_send.txtar b/gno.land/pkg/integration/testdata/maketx_call_send.txtar index a9a50296c59..27444cb8945 100644 --- a/gno.land/pkg/integration/testdata/maketx_call_send.txtar +++ b/gno.land/pkg/integration/testdata/maketx_call_send.txtar @@ -1,36 +1,102 @@ -# load the package -loadpkg gno.land/r/foo/call_realm $WORK/realm +# This test verifies the behavior of the -send flag in maketx call transactions, +# and contrasts realm.SentCoins() with banker.OriginSend(). +# The call chain is: OriginCall -> firstrealm -> secondrealm -> firstrealm (re-entrant) +# +# The re-entrant call is achieved via a coinchecker.Caller interface defined in a +# package (not a realm), which breaks the circular import problem: secondrealm +# imports the package but not firstrealm; firstrealm passes itself as a Caller +# to secondrealm, which calls back into firstrealm through the interface. +# +# - When -send is not provided, both APIs return empty coins in all realms. +# +# - realm.SentCoins() returns the coins sent via the tx -send field, but ONLY +# for the realm that is the tx recipient (identified by address). It returns +# empty coins for any other realm, even re-entrantly called ones. firstrealm +# sees coins in both its initial and re-entrant invocations because it is the +# tx recipient either way; secondrealm never sees them. +# +# - banker.OriginSend() returns the tx -send coins regardless of which realm +# calls it. It is not a reliable way to know if the current realm received +# the coins. + +# load the packages +loadpkg gno.land/p/foo/coinchecker $WORK/coinchecker +loadpkg gno.land/r/foo/secondrealm $WORK/secondrealm +loadpkg gno.land/r/foo/firstrealm $WORK/firstrealm # start a new node gnoland start -## user balance before realm send -gnokey query auth/accounts/$test1_user_addr -stdout '"coins": "9999998810000ugnot"' +# call to firstrealm without -send: both APIs return empty in all realms +gnokey maketx call -pkgpath gno.land/r/foo/firstrealm -func GetSentCoins -gas-fee 1000000ugnot -gas-wanted 3000000 -broadcast -chainid=tendermint_test test1 +stdout '("firstrealm\[realm.SentCoins: , banker.OriginSend: \] secondrealm\[realm.SentCoins: , banker.OriginSend: \] firstrealm\(reentrant\)\[realm.SentCoins: , banker.OriginSend: \]")' -## realm balance before realm send -gnokey query auth/accounts/g1x4ykzcqksj2hc5qpvr8kd9zaffkd82rvmzqup7 +## firstrealm balance: no coins sent +gnokey query auth/accounts/g1d260hvgfya26huxscg80xckxh7ydsyy0tve4sp stdout '"coins": ""' -# call to realm with -send -gnokey maketx call -send 42ugnot -pkgpath gno.land/r/foo/call_realm -func GimmeMoney -gas-fee 1000000ugnot -gas-wanted 3000000 -broadcast -chainid=tendermint_test test1 -stdout '("send: 42ugnot")' +## secondrealm balance: no coins sent +gnokey query auth/accounts/g17xvzxkz7lqm2mtmw3tvq0ajgx79phg24h60a9x +stdout '"coins": ""' -## user balance after realm send -# reduced by -gas-fee AND -send -gnokey query auth/accounts/$test1_user_addr -stdout '"coins": "9999997809958ugnot"' +# call to firstrealm with -send +gnokey maketx call -send 42ugnot -pkgpath gno.land/r/foo/firstrealm -func GetSentCoins -gas-fee 1000000ugnot -gas-wanted 3000000 -broadcast -chainid=tendermint_test test1 +# realm.SentCoins() is tied to the realm address, not the call order: firstrealm +# sees the coins in both its initial and re-entrant invocations because it is +# the tx recipient either way. secondrealm never sees them. +# banker.OriginSend() returns the coins in all realms regardless of call depth. +stdout '("firstrealm\[realm.SentCoins: 42ugnot, banker.OriginSend: 42ugnot\] secondrealm\[realm.SentCoins: , banker.OriginSend: 42ugnot\] firstrealm\(reentrant\)\[realm.SentCoins: 42ugnot, banker.OriginSend: 42ugnot\]")' -## realm balance after realm send -gnokey query auth/accounts/g1x4ykzcqksj2hc5qpvr8kd9zaffkd82rvmzqup7 +## firstrealm balance after realm send +gnokey query auth/accounts/g1d260hvgfya26huxscg80xckxh7ydsyy0tve4sp stdout '"coins": "42ugnot"' +## secondrealm balance after realm send +gnokey query auth/accounts/g17xvzxkz7lqm2mtmw3tvq0ajgx79phg24h60a9x +stdout '"coins": ""' + + +-- coinchecker/coinchecker.gno -- +package coinchecker + +type Caller interface { + Report(cur realm) string +} +-- firstrealm/realm.gno -- +package firstrealm + +import ( + "chain/banker" + + "gno.land/p/foo/coinchecker" + "gno.land/r/foo/secondrealm" +) + +type self struct{} + +func (s self) Report(cur realm) string { + return "firstrealm(reentrant)[realm.SentCoins: " + cur.SentCoins().String() + + ", banker.OriginSend: " + banker.OriginSend().String() + "]" +} + +func GetSentCoins(cur realm) string { + return "firstrealm[realm.SentCoins: " + cur.SentCoins().String() + + ", banker.OriginSend: " + banker.OriginSend().String() + "] " + + secondrealm.GetSentCoins(cross, self{}) +} + +var _ coinchecker.Caller = self{} +-- secondrealm/realm.gno -- +package secondrealm --- realm/realm.gno -- -package call_realm +import ( + "chain/banker" -import "chain/banker" + "gno.land/p/foo/coinchecker" +) -func GimmeMoney(cur realm) string { - return "send: " + banker.OriginSend().String() +func GetSentCoins(cur realm, callback coinchecker.Caller) string { + return "secondrealm[realm.SentCoins: " + cur.SentCoins().String() + + ", banker.OriginSend: " + banker.OriginSend().String() + "] " + + callback.Report(cross) } diff --git a/gno.land/pkg/integration/testdata/restart_gas.txtar b/gno.land/pkg/integration/testdata/restart_gas.txtar index b4d76990618..d3b71d87d8e 100644 --- a/gno.land/pkg/integration/testdata/restart_gas.txtar +++ b/gno.land/pkg/integration/testdata/restart_gas.txtar @@ -1,15 +1,15 @@ gnoland start gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/$test1_user_addr/bar -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 -stdout 'GAS USED: +593591' +stdout 'GAS USED: +593679' gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/$test1_user_addr/foo -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 -stdout 'GAS USED: +593618' +stdout 'GAS USED: +593706' gnoland restart gnokey maketx addpkg -pkgdir $WORK/baz -pkgpath gno.land/r/$test1_user_addr/baz -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 -stdout 'GAS USED: +593618' +stdout 'GAS USED: +593706' -- bar/gnomod.toml -- module = "bar" diff --git a/gno.land/pkg/integration/testdata/transfer_unlock.txtar b/gno.land/pkg/integration/testdata/transfer_unlock.txtar index f9448a712d5..2465e3094c1 100644 --- a/gno.land/pkg/integration/testdata/transfer_unlock.txtar +++ b/gno.land/pkg/integration/testdata/transfer_unlock.txtar @@ -88,7 +88,7 @@ import ( ) func main() { - ok := dao.ExecuteProposal(cross, dao.ProposalID(0)) + ok := dao.ExecuteProposal(cross, dao.ProposalID(0)) if ok { println("OK!") } diff --git a/gno.land/pkg/integration/testdata/transfer_unrestricted.txtar b/gno.land/pkg/integration/testdata/transfer_unrestricted.txtar index a09668d7253..f038f701960 100644 --- a/gno.land/pkg/integration/testdata/transfer_unrestricted.txtar +++ b/gno.land/pkg/integration/testdata/transfer_unrestricted.txtar @@ -168,7 +168,7 @@ import ( ) func main() { - ok := dao.ExecuteProposal(cross,dao.ProposalID(0)) + ok := dao.ExecuteProposal(cross,dao.ProposalID(0)) if ok { println("OK!") } @@ -195,7 +195,7 @@ import ( ) func main() { - ok := dao.ExecuteProposal(cross,dao.ProposalID(1)) + ok := dao.ExecuteProposal(cross, dao.ProposalID(1)) if ok { println("OK!") } diff --git a/gno.land/pkg/integration/testdata/update_storage_params.txtar b/gno.land/pkg/integration/testdata/update_storage_params.txtar index c7858d0fc70..ec4ecc89b45 100644 --- a/gno.land/pkg/integration/testdata/update_storage_params.txtar +++ b/gno.land/pkg/integration/testdata/update_storage_params.txtar @@ -102,7 +102,7 @@ import ( ) func main() { - dao.ExecuteProposal(cross,dao.ProposalID(0)) + dao.ExecuteProposal(cross, dao.ProposalID(0)) } diff --git a/gno.land/pkg/integration/testdata/user_journey.txtar b/gno.land/pkg/integration/testdata/user_journey.txtar index bcc1e9430f3..529f2ff1ab0 100644 --- a/gno.land/pkg/integration/testdata/user_journey.txtar +++ b/gno.land/pkg/integration/testdata/user_journey.txtar @@ -11,7 +11,7 @@ loadpkg gno.land/r/sys/names genesiscall gno.land/r/sys/users/init Bootstrap # Override admin address in r/sys/names with test1 address -patchpkg "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p" $test1_user_addr +patchpkg "g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh" $test1_user_addr # Add 3 users with different balances adduser user1 1ugnot diff --git a/gno.land/pkg/integration/testdata/variadic.txtar b/gno.land/pkg/integration/testdata/variadic.txtar new file mode 100644 index 00000000000..17cd10f5252 --- /dev/null +++ b/gno.land/pkg/integration/testdata/variadic.txtar @@ -0,0 +1,32 @@ +# create member and receiver users with known addresses +adduserfrom member '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' + + +loadpkg gno.land/r/tests/vm/variadic + +gnoland start + +# Call variadic func with 2 variadic arguments +gnokey maketx call -pkgpath gno.land/r/tests/vm/variadic -func Echo -args "mem123" -args "123mem" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test member +stdout 'OK!' +stdout '("mem123 123mem" string)' + +# Call variadic func with 0 variadic arguments +gnokey maketx call -pkgpath gno.land/r/tests/vm/variadic -func Echo -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test member +stdout 'OK!' +stdout '("" string)' + +# Call variadic func with 2 int variadic arguments +gnokey maketx call -pkgpath gno.land/r/tests/vm/variadic -func Add -args 10 -args 20 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test member +stdout 'OK!' +stdout '(30 int)' + +# Call variadic func with 2 invalid arguments +! gnokey maketx call -pkgpath gno.land/r/tests/vm/variadic -func And -args test -args arguments -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test member +stderr 'unexpected bool value' + +# Call variadic func with 0 int variadic arguments +gnokey maketx call -pkgpath gno.land/r/tests/vm/variadic -func Add -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test member +stdout 'OK!' +stdout '(0 int)' diff --git a/gno.land/pkg/sdk/vm/builtins.go b/gno.land/pkg/sdk/vm/builtins.go index 127ea524b16..dc576d11c1a 100644 --- a/gno.land/pkg/sdk/vm/builtins.go +++ b/gno.land/pkg/sdk/vm/builtins.go @@ -142,6 +142,42 @@ func (prm *SDKParams) UpdateStrings(key string, vals []string, add bool) { prm.SetStrings(key, updatedList) } +func (prm *SDKParams) GetString(key string) (string, bool) { + var val string + prm.pmk.GetString(prm.ctx, key, &val) + return val, true // impossible to determine if value is missing or just empty +} + +func (prm *SDKParams) GetBool(key string) (bool, bool) { + var val bool + prm.pmk.GetBool(prm.ctx, key, &val) + return val, true // impossible to determine if value is missing or just false +} + +func (prm *SDKParams) GetInt64(key string) (int64, bool) { + var val int64 + prm.pmk.GetInt64(prm.ctx, key, &val) + return val, true // impossible to determine if value is missing or just 0 +} + +func (prm *SDKParams) GetUint64(key string) (uint64, bool) { + var val uint64 + prm.pmk.GetUint64(prm.ctx, key, &val) + return val, true // impossible to determine if value is missing or just 0 +} + +func (prm *SDKParams) GetBytes(key string) ([]byte, bool) { + var val []byte + prm.pmk.GetBytes(prm.ctx, key, &val) + return val, val != nil +} + +func (prm *SDKParams) GetStrings(key string) ([]string, bool) { + var val []string + prm.pmk.GetStrings(prm.ctx, key, &val) + return val, val != nil +} + func (prm *SDKParams) setWithCheck(key string, set func()) { prm.mustHaveModuleKeeper(key) set() diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 3094b3671f7..c67b5da1d05 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -72,7 +72,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 250_000 :) - assert.Equal(t, int64(226738), gasDeliver) + assert.Equal(t, int64(226778), gasDeliver) } // Enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/handler_test.go b/gno.land/pkg/sdk/vm/handler_test.go index 07f7ac0f6fa..d0ecb1b50ec 100644 --- a/gno.land/pkg/sdk/vm/handler_test.go +++ b/gno.land/pkg/sdk/vm/handler_test.go @@ -99,7 +99,7 @@ func TestVmHandlerQuery_Eval(t *testing.T) { {input: []byte(`gno.land/r/hello.doesnotexist`), expectedErrorMatch: `^:0:0: name doesnotexist not declared:`}, // multiline error {input: []byte(`gno.land/r/doesnotexist.Foo`), expectedErrorMatch: `^invalid package path$`}, {input: []byte(`gno.land/r/hello.Panic()`), expectedErrorMatch: `^foo$`}, - {input: []byte(`gno.land/r/hello.sl[6]`), expectedErrorMatch: `^slice index out of bounds: 6 \(len=5\)$`}, + {input: []byte(`gno.land/r/hello.sl[6]`), expectedErrorMatch: `^runtime error: slice index out of bounds: 6 \(len=5\)$`}, {input: []byte(`gno.land/r/hello.func(){ for {} }()`), expectedErrorMatch: `out of gas in location: CPUCycles`}, } diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 31571a4afcb..6f71c891a69 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -154,7 +154,7 @@ func (vm *VMKeeper) Initialize( opts.Cache[stdlib] = pkg } - logger.Debug("GnoVM packages preprocessed", + logger.Info("GnoVM packages preprocessed", "elapsed", time.Since(start)) } } @@ -727,19 +727,39 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { if err != nil { return "", err } - // Convert Args to gno values. cx := xn.(*gno.CallExpr) - if cx.Varg { - panic("variadic calls not yet supported") - } - if nargs := len(msg.Args) + 1; nargs != len(ft.Params) { // NOTE: nargs = `cur` + user's len(args) - panic(fmt.Sprintf("wrong number of arguments in call to %s: want %d got %d", fnc, len(ft.Params), nargs)) + hasVarg := ft.HasVarg() + // NOTE: nargs = `cur` + user's len(args) + nargs := len(msg.Args) + 1 + var vargType gno.Type + // If function is not variadic, it must have the same number of arguments. + if !hasVarg { + if nargs != len(ft.Params) { + panic(fmt.Sprintf("wrong number of arguments in call to %s: want %d got %d", fnc, len(ft.Params), nargs)) + } + } else { + if nargs < len(ft.Params)-1 { + // If function is variadic, it must have at least the number of arguments-1. + // on the function we can simply avoid the variadic argument. + panic(fmt.Sprintf("insufficient number of arguments in call to %s: must be at least %d, got %d", fnc, len(ft.Params)-1, nargs)) + } + + // For the variadic argument, we need to use the type of the + // elements contained on the slice. + vargType = ft.Params[len(ft.Params)-1].Type.(*gno.SliceType).Elt } + + // Convert Args to gno values. for i, arg := range msg.Args { - argType := ft.Params[i+1].Type - atv := convertArgToGno(arg, argType) - cx.Args[i+1] = &gno.ConstExpr{ - TypedValue: atv, + paramIndex := i + 1 + var argType gno.Type + if hasVarg && paramIndex >= len(ft.Params)-1 { + argType = vargType + } else { + argType = ft.Params[paramIndex].Type + } + cx.Args[paramIndex] = &gno.ConstExpr{ + TypedValue: convertArgToGno(arg, argType), } } defer m.Release() diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go index 515ba5711e1..d6bb381940c 100644 --- a/gno.land/pkg/sdk/vm/params.go +++ b/gno.land/pkg/sdk/vm/params.go @@ -3,6 +3,7 @@ package vm import ( "fmt" "regexp" + "strconv" "strings" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" @@ -20,6 +21,10 @@ const ( depositDefault = "600000000ugnot" storagePriceDefault = "100ugnot" // cost per byte (1 gnot per 10KB) 1B GNOT == 10TB storageFeeCollectorNameDefault = "storage_fee_collector" + + // ValsetRealmDefault is the default realm path for on-chain validator set management. + // Keep in sync with examples/gno.land/r/sys/validators/v3/poc.gno + ValsetRealmDefault = "gno.land/r/sys/validators/v3" ) var ASCIIDomain = regexp.MustCompile(`^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$`) @@ -32,6 +37,7 @@ type Params struct { DefaultDeposit string `json:"default_deposit" yaml:"default_deposit"` StoragePrice string `json:"storage_price" yaml:"storage_price"` StorageFeeCollector crypto.Address `json:"storage_fee_collector" yaml:"storage_fee_collector"` + ValsetRealmPath string `json:"valset_realm_path" yaml:"valset_realm_path"` } // NewParams creates a new Params object @@ -43,6 +49,7 @@ func NewParams(namesPkgPath, claPkgPath, chainDomain, defaultDeposit, storagePri DefaultDeposit: defaultDeposit, StoragePrice: storagePrice, StorageFeeCollector: storageFeeCollector, + ValsetRealmPath: ValsetRealmDefault, } } @@ -62,6 +69,7 @@ func (p Params) String() string { sb.WriteString(fmt.Sprintf("DefaultDeposit: %q\n", p.DefaultDeposit)) sb.WriteString(fmt.Sprintf("StoragePrice: %q\n", p.StoragePrice)) sb.WriteString(fmt.Sprintf("StorageFeeCollector: %q\n", p.StorageFeeCollector.String())) + sb.WriteString(fmt.Sprintf("ValsetRealmPath: %q\n", p.ValsetRealmPath)) return sb.String() } @@ -86,6 +94,9 @@ func (p Params) Validate() error { if p.StorageFeeCollector.IsZero() { return fmt.Errorf("invalid storage fee collector, cannot be empty") } + if p.ValsetRealmPath != "" && !gno.IsRealmPath(p.ValsetRealmPath) { + return fmt.Errorf("invalid valset realm path %q", p.ValsetRealmPath) + } return nil } @@ -109,9 +120,15 @@ func (vm *VMKeeper) GetParams(ctx sdk.Context) Params { } const ( - sysUsersPkgParamPath = "vm:p:sysnames_pkgpath" - sysCLAPkgParamPath = "vm:p:syscla_pkgpath" - chainDomainParamPath = "vm:p:chain_domain" + moduleParamPrefix = "vm" + + sysUsersPkgParamPath = moduleParamPrefix + ":p:sysnames_pkgpath" + sysCLAPkgParamPath = moduleParamPrefix + ":p:syscla_pkgpath" + chainDomainParamPath = moduleParamPrefix + ":p:chain_domain" + + // ValsetRealmParamPath is the param key that stores the path of the + // realm responsible for on-chain validator set management. + ValsetRealmParamPath = moduleParamPrefix + ":p:valset_realm_path" ) func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string { @@ -132,6 +149,12 @@ func (vm *VMKeeper) getSysCLAPkgParam(ctx sdk.Context) string { return sysCLAPkg } +func (vm *VMKeeper) getValsetRealmParam(ctx sdk.Context) string { + valsetRealm := ValsetRealmDefault + vm.prmk.GetString(ctx, ValsetRealmParamPath, &valsetRealm) + return valsetRealm +} + func (vm *VMKeeper) WillSetParam(ctx sdk.Context, key string, value any) { params := vm.GetParams(ctx) switch key { @@ -152,10 +175,27 @@ func (vm *VMKeeper) WillSetParam(ctx sdk.Context, key string, value any) { panic(fmt.Sprintf("invalid storage_fee_collector address: %v", err)) } params.StorageFeeCollector = addr + case "p:valset_realm_path": + params.ValsetRealmPath = sdkparams.MustParamString("valset_realm_path", value) default: if strings.HasPrefix(key, "p:") { panic(fmt.Sprintf("unknown vm param key: %q", key)) } + // Validate valset updates if the key targets the valset realm's valset_new param. + valsetRealm := vm.getValsetRealmParam(ctx) + if strings.HasPrefix(key, valsetRealm+":valset_new") { + changes, ok := value.([]string) + if !ok { + panic(fmt.Sprintf( + "value for VM param %s update is an invalid type (%T)", + key, + value, + )) + } + if err := validateValsetUpdate(changes); err != nil { + panic(err) + } + } // Allow realm-scoped params through without validation. return } @@ -163,3 +203,44 @@ func (vm *VMKeeper) WillSetParam(ctx sdk.Context, key string, value any) { panic("invalid param: " + err.Error()) } } + +// validateValsetUpdate validates the validator set updates, +// which are serialized in the form: +// -
:: +// - voting power == 0 => validator removal +// - voting power != 0 => validator power update / validator addition +func validateValsetUpdate(changes []string) error { + for _, change := range changes { + changeParts := strings.Split(change, ":") + if len(changeParts) != 3 { + return fmt.Errorf( + "valset update is not in the format
::, but %q", + change, + ) + } + + address, err := crypto.AddressFromBech32(changeParts[0]) + if err != nil { + return fmt.Errorf("invalid validator address: %w", err) + } + + pubKey, err := crypto.PubKeyFromBech32(changeParts[1]) + if err != nil { + return fmt.Errorf("invalid validator pubkey: %w", err) + } + + if pubKey.Address().Compare(address) != 0 { + return fmt.Errorf( + "address (%s) does not match public key address (%s)", + address, + pubKey.Address(), + ) + } + + if _, err = strconv.ParseUint(changeParts[2], 10, 64); err != nil { + return fmt.Errorf("invalid voting power: %w", err) + } + } + + return nil +} diff --git a/gno.land/pkg/sdk/vm/params_test.go b/gno.land/pkg/sdk/vm/params_test.go index 945aebaef8d..5ee971f21fe 100644 --- a/gno.land/pkg/sdk/vm/params_test.go +++ b/gno.land/pkg/sdk/vm/params_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" "github.com/stretchr/testify/assert" ) @@ -24,7 +25,8 @@ func TestParamsString(t *testing.T) { fmt.Sprintf("ChainDomain: %q\n", p.ChainDomain) + fmt.Sprintf("DefaultDeposit: %q\n", p.DefaultDeposit) + fmt.Sprintf("StoragePrice: %q\n", p.StoragePrice) + - fmt.Sprintf("StorageFeeCollector: %q\n", p.StorageFeeCollector) + fmt.Sprintf("StorageFeeCollector: %q\n", p.StorageFeeCollector) + + fmt.Sprintf("ValsetRealmPath: %q\n", p.ValsetRealmPath) // Assert: check if the result matches the expected string. if result != expected { @@ -233,6 +235,106 @@ func TestWillSetParamExhaustive(t *testing.T) { } } +func TestWillSetParam_ValsetUpdate(t *testing.T) { + t.Parallel() + + // valsetNewPath returns the raw key (without vm: prefix) for valset_new param. + // "valset_new" must be kept in sync with examples/gno.land/r/sys/validators/v3/poc.gno. + valsetNewPath := func() string { + return ValsetRealmDefault + ":valset_new" + } + + t.Run("non-valset key passes through", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + assert.NotPanics(t, func() { + env.vmk.WillSetParam(ctx, "some_realm:arbitrary_key", nil) + }) + }) + + t.Run("invalid value type", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + assert.Panics(t, func() { + env.vmk.WillSetParam(ctx, valsetNewPath(), "not a slice") + }) + }) + + t.Run("malformed entry", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + assert.Panics(t, func() { + env.vmk.WillSetParam(ctx, valsetNewPath(), []string{"addr:pubkey:power:extra"}) + }) + }) + + t.Run("invalid address", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + key := secp256k1.GenPrivKey() + + assert.Panics(t, func() { + env.vmk.WillSetParam(ctx, valsetNewPath(), []string{ + fmt.Sprintf("notabech32:%s:10", key.PubKey()), + }) + }) + }) + + t.Run("address pubkey mismatch", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + key1 := secp256k1.GenPrivKey() + key2 := secp256k1.GenPrivKey() + + assert.Panics(t, func() { + env.vmk.WillSetParam(ctx, valsetNewPath(), []string{ + fmt.Sprintf("%s:%s:10", key1.PubKey().Address(), key2.PubKey()), + }) + }) + }) + + t.Run("invalid voting power", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + key := secp256k1.GenPrivKey() + + assert.Panics(t, func() { + env.vmk.WillSetParam(ctx, valsetNewPath(), []string{ + fmt.Sprintf("%s:%s:notanumber", key.PubKey().Address(), key.PubKey()), + }) + }) + }) + + t.Run("valid valset update", func(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + key := secp256k1.GenPrivKey() + + assert.NotPanics(t, func() { + env.vmk.WillSetParam(ctx, valsetNewPath(), []string{ + fmt.Sprintf("%s:%s:10", key.PubKey().Address(), key.PubKey()), + }) + }) + }) +} + func TestParamsValidate(t *testing.T) { valid := DefaultParams() diff --git a/gnovm/adr/pr5247_initialization_order.md b/gnovm/adr/pr5247_initialization_order.md new file mode 100644 index 00000000000..ba9dccaa76c --- /dev/null +++ b/gnovm/adr/pr5247_initialization_order.md @@ -0,0 +1,161 @@ +# PR5247: Go-Compliant Variable Initialization Order + +## Context + +The Go specification mandates that package-level variables are initialized +stepwise, with each step selecting the variable earliest in declaration order +whose dependencies are all satisfied. GnoVM's original implementation used a +recursive depth-first `runDeclarationFor` function that computed dependencies +via `findDependentNames` (an AST walker) at initialization time. This had +several problems: + +1. **Incorrect ordering.** The depth-first recursive approach did not implement + the Go spec's "earliest in declaration order" rule. It processed variables + in file-iteration order and recursively resolved deps depth-first, which + could produce a different order than Go. + +2. **Non-determinism.** Dependency sets were stored in `map[Name]struct{}` + and iterated with `for dep := range deps`, making initialization order + depend on Go's non-deterministic map iteration. + +3. **Missed method dependencies.** `findDependentNames` relied on the + `Externs` mechanism (`GetExternNames`) to discover names referenced from + inside function bodies. However, method names were not tracked as externs, + so their transitive dependencies were invisible. For example: + + ```go + type T struct{} + func (T) GetB() int { return B } + var A = T{}.GetB() // dependency on B was not discovered + var B = 42 + ``` + +4. **Shallow `Externs` tracking.** More broadly, `findDependentNames` depended + on the `Externs` implementation on `StaticBlock`, which did not descend + into function bodies. It only tracked names that crossed block boundaries + during `GetPathForName`, not all names referenced within a function. This + meant transitive dependencies through function calls could be missed. + +## Decision + +The fix has two parts: (A) how dependencies are syntactically recorded, and +(B) how the initialization order is computed from those dependencies. + +### Part A: Syntactic Dependency Recording via `codaInitOrderDeps` + +Replace the post-hoc `findDependentNames` AST walker with a single dedicated +coda pass: `codaInitOrderDeps`. + +The pass runs after `preprocess1` (all `NameExpr` paths are filled) and before +`codaPackageSelectors` (which replaces `NameExpr`s with `SelectorExpr`s). It +uses `TranscribeB` to traverse the full AST of each file, with the ancestor +node stack always available. + +For each `*NameExpr` at `TRANS_LEAVE`: if the path type is `VPBlock`, the name +is not blank, not a package reference, not the LHS of a declaration, and is +defined at package level (not a type declaration), the name is recorded via +`addDependencyToTopDecl(ns, name)` as an entry in `ATTR_DECL_DEPS` on the +nearest enclosing `*ValueDecl` or `*FuncDecl`. + +For each `*SelectorExpr` with a method path (`VPValMethod`, `VPPtrMethod`, +`VPDerefValMethod`, `VPDerefPtrMethod`): the cached `ATTR_TYPEOF_VALUE` is +read from the receiver expression (unwrapping auto-generated `RefExpr` +wrappers), and if the result is a `*DeclaredType` from the current package, +`"TypeName.MethodName"` is recorded as a dep. This allows the resolution phase +to transitively discover variables referenced inside method bodies. + +### Part B: Initialization Order via Memoized DFS + Kahn's Algorithm + +**`resolveEffectiveDeps`** (memoized DFS, O(V+E)): For every declaration +reachable from the pending list, computes the set of `*ValueDecl` dependencies +by collapsing `FuncDecl` edges. FuncDecls are transparent pass-throughs: their +effective `*ValueDecl` deps are inherited by callers. Each `Decl` is visited at +most once thanks to a shared `cache` map, so total work is O(V+E) regardless +of the number of declarations. Circular variable dependencies are detected +during this DFS (via an `onStack` set) and produce a panic with the full +dependency chain. + +**[Kahn's topological sort][kahn]** (in `runFileDecls`): Builds a +reverse-dependency index and unsatisfied-count array from the effective deps. +A min-heap keyed on declaration index ensures the Go spec's "earliest in +declaration order" tiebreaking. Each declaration enters and leaves the heap at +most once, giving O(V + E + V log V) total. + +[kahn]: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + +### Removed Code + +- `findDependentNames`: recursive AST walker that needed a case for every node + type, replaced by `codaInitOrderDeps` (which uses the existing `TranscribeB` + infrastructure to walk the full AST generically). +- `GetExternNames` / `addExternName` / `isFile`: the `Externs` tracking on + `StaticBlock` was only used by `findDependentNames` via `FuncLitExpr` and + `FuncDecl`. The `Externs` field is retained for amino serialization + backward-compatibility but is no longer populated. +- `runDeclarationFor` / `loopfindr`: the recursive initialization loop in + `runFileDecls`, replaced by Kahn's algorithm. + +## Alternatives Considered + +### Iteration 1: Rewrite `findDependentNames` + stepwise loop + +The first approach kept the `findDependentNames` AST walker but rewrote +`runFileDecls` to use a stepwise "find earliest ready" loop instead of +recursive DFS. This fixed the ordering but kept the incomplete walker and did +not address the method dependency problem or the non-determinism from map +iteration. + +### Iteration 2: Full rewrite with inline dep recording + +Rewrote `findDependentNames` from scratch to work on the preprocessed AST +(using filled `NameExpr` paths instead of raw names) and moved dependency +recording inline into `preprocess1`. This fixed many issues but failed to +cover dependencies through methods: the `SelectorExpr` handling required +knowing the receiver's declared type to look up the method's `FuncDecl`, but +the inline approach did not reliably have this information for all receiver +patterns (value vs pointer, auto-addressed, etc.). + +### Iteration 3: `codaInitOrderDeps` + per-decl `findUnresolvedDeps` + +Moved dep recording to a dedicated post-preprocess coda pass +(`codaInitOrderDeps`) using `TranscribeB`, fixing the method dependency issue +by reading `ATTR_TYPEOF_VALUE` from the already-preprocessed receiver +expression. Used `findUnresolvedDeps` (a per-declaration DFS) to resolve +transitive deps and a stepwise scanning loop to find the earliest ready +variable. This was correct but O(n²): `findUnresolvedDeps` was called +independently for each declaration with no shared state, re-traversing the +same `FuncDecl` bodies repeatedly, and the ready-variable loop scanned all +pending entries each iteration. + +### Other alternatives not pursued + +**Patch `predefineRecursively` to pass the outer `ns`**: would require +threading `ns` through many call sites and would not fix the analogous problem +for other isolated `Preprocess` calls. + +**Record deps during `findUndefinedV`**: already has access to the full +context but walks un-preprocessed ASTs; mixing dep-recording there would +conflate two concerns. + +## Consequences + +- Variable initialization order is now correct and deterministic, matching + the Go specification's stepwise algorithm. +- O(V+E) dependency resolution (memoized DFS) and O(V + E + V log V) + initialization (Kahn's with min-heap) replace O(n²) algorithms. Packages + with thousands of top-level declarations are no longer a performance concern. +- `ATTR_TYPEOF_VALUE` must continue to be set on receiver expressions during + `preprocess1` (currently guaranteed by the `evalStaticTypeOf` call at the + SelectorExpr `TRANS_LEAVE`). +- Interface dispatch (`Getter(T{}).GetB()`) does not trace into the concrete + method body, since the static receiver type is `*InterfaceType`. This is + spec-compliant but diverges from gc's behavior in simple cases. + +## Key Files + +- `gnovm/pkg/gnolang/preprocess.go`: `codaInitOrderDeps`, + `addDependencyToTopDecl`, `resolveEffectiveDeps`, `resolveDeclDep` +- `gnovm/pkg/gnolang/machine.go`: `initHeap`, Kahn's loop in `runFileDecls` +- `gnovm/tests/files/var_initorder*.gno`: 19 filetests +- `gnovm/pkg/gnolang/preprocess_test.go`: `TestInitOrderDeterminism`, + `TestCircDepDeterminism` diff --git a/gnovm/adr/pr5269_const_conversion_overflow.md b/gnovm/adr/pr5269_const_conversion_overflow.md new file mode 100644 index 00000000000..93f6094b045 --- /dev/null +++ b/gnovm/adr/pr5269_const_conversion_overflow.md @@ -0,0 +1,52 @@ +# PR5269: Constant Conversion Overflow Checks + +## Context + +Large `uint64` constants (e.g., `math.MaxUint64`) were bypassing overflow validation +when converting to smaller types. The root cause was that `validate` comparisons +cast `uint64` values to `int64` first, which silently wrapped large values and +made the bounds check pass incorrectly. + +## Decision + +### Fix: Remove `int64()` cast in uint64 overflow comparisons + +In `gnovm/pkg/gnolang/values_conversions.go`, the `Uint64Kind` conversion cases +compared `int64(tv.GetUint64()) <= math.MaxInt8` etc. Casting a large uint64 to +int64 wraps it to a negative number, which is always `<= MaxInt8`. The fix removes +the `int64()` cast so the comparison stays in uint64 space. + +### Error messages: match Go's logic + +Before all validation failures used a single generic message: +`"cannot convert constant of type IntKind to UintKind"` — no actual value shown. + +After, matches Go's type checker (`src/go/types/conversions.go`): +- **Integer→integer:** `"constant -1 overflows UintKind"` (shows the value) +- **Float→integer:** `"cannot convert constant of type Float32Kind to Int32Kind"` + +The `validate` closure uses `isIntegerKind` (package-level in `type_check.go`) to pick the format. + +### int→string const conversions are valid + +`string(typed_int_const)` is valid Go (produces a rune string, e.g., `string(int8(65))` → `"A"`). +All `validate(XxxKind, StringKind, nil)` calls were removed as they would reject valid Go code. + +Note: the preprocessing path in `preprocess.go:1539` has an `isIntNum(ct)` guard that +skips `ConvertTo` with `isConst=true` when the target is `StringType`. So even if +validate calls existed, they would be dead code through the normal preprocessing path. +The actual int→string conversion happens at runtime via `op_expressions.go:727`. + +## Key files + +| File | Role | +|------|------| +| `gnovm/pkg/gnolang/values_conversions.go` | `ConvertTo` function with `validate` closure | +| `gnovm/pkg/gnolang/preprocess.go:1539` | `isIntNum(ct)` guard — controls which const conversions go through `ConvertTo` | +| `gnovm/tests/files/convert9*.gno` | Filetests for uint64 overflow and int→string | + +## Consequences + +- `const a uint64 = math.MaxUint64; int8(a)` now correctly errors at preprocess time +- Float→int const errors use "cannot convert" (not "overflows"), matching Go +- `string(const_typed_int)` works correctly, matching Go behavior diff --git a/gnovm/adr/pr5436_fix_gc_alloc_mismatch.md b/gnovm/adr/pr5436_fix_gc_alloc_mismatch.md new file mode 100644 index 00000000000..daa373b2c3b --- /dev/null +++ b/gnovm/adr/pr5436_fix_gc_alloc_mismatch.md @@ -0,0 +1,57 @@ +# Fix GC allocation/recount mismatch + +## Context + +When Gno code triggers infinite recursion (e.g., `func f() int { return f() }`), +the VM panics with `"should not happen, allocation limit exceeded while gc."` +instead of the expected `"allocation limit exceeded"`. + +The allocator tracks memory via `alloc.bytes`. When the limit is reached, GC runs: +it resets `alloc.bytes` to 0 and walks all live objects, recounting their sizes via +`GetShallowSize()`. If GC recounts more bytes than were originally tracked, it +concludes the limit was exceeded during GC itself -- a path marked as "should not happen". + +The root cause is in `PrepareNewValues` (`nodes.go`): when new package-level +declarations (functions, variables) are added after initial package creation, +their block items are appended to the package block's `Values` slice via +`block.Values = append(block.Values, nvs...)` **without** calling +`AllocateBlockItems`. This means: + +- **During allocation**: `alloc.bytes` does not account for the new block items +- **During GC recount**: `Block.GetShallowSize()` uses `len(b.Values)` which + includes the appended items + +The mismatch is small (e.g., 80 bytes for 2 functions), but in infinite recursion +the allocator fills to near-max with block allocations, and the untracked bytes +tip the GC recount past `maxBytes`. + +## Decision + +Add `alloc.AllocateBlockItems(int64(len(nvs)))` in `PrepareNewValues` before +appending to `block.Values`. This ensures the allocator tracks the same bytes +that GC will recount. + +## Alternatives considered + +1. **Fix GC to not stop early when recount exceeds maxBytes** -- this masks the + mismatch rather than fixing it. GC would always succeed, but the allocator + state would be inconsistent. + +2. **Track allocations in preprocessing** (adding `AllocateFunc` calls in + `preprocess.go` for FuncValues created during `tryPredefine`) -- this was + explored but turned out to be unnecessary. The FuncValues are properly + allocated via `fv.Copy(alloc)` in `PrepareNewValues`. The actual mismatch + was only the block items. + +3. **Change the panic message** from "should not happen" to "allocation limit + exceeded" -- this was considered as a minimal fix but doesn't address the + underlying inconsistency. + +## Consequences + +- Infinite recursion now correctly panics with `"allocation limit exceeded"` +- GC recount is consistent with allocator tracking for package block items +- Gas values for some tests change slightly because `AllocateBlockItems` charges + gas during package setup +- The "should not happen" panic path remains as a safety net for any future + mismatches -- it should now truly never trigger diff --git a/gnovm/adr/pr5439_iterative_exception_recovery.md b/gnovm/adr/pr5439_iterative_exception_recovery.md new file mode 100644 index 00000000000..e5cc92f608c --- /dev/null +++ b/gnovm/adr/pr5439_iterative_exception_recovery.md @@ -0,0 +1,78 @@ +# PR5439: Iterative Exception Recovery in Machine.Run() + +## Context + +`Machine.Run()` contained a `defer/recover` handler that caught Go-level `*Exception` +panics and recursively called `m.Run(st)`. This design meant each panicking deferred +function added a new Go stack frame. An attacker could register enough deferred closures +that each trigger a nil pointer dereference (a Go-level `panic(&Exception{})`), causing +unbounded Go stack growth that exceeds the Go runtime's 1GB goroutine stack limit +(~500K defers are sufficient to crash the process). + +The resulting `runtime.throw("stack overflow")` is a fatal error that bypasses all +`recover()` handlers in the call chain — including the VM keeper's `doRecover` and +`BaseApp.runTx()` — killing the node process. + +The GnoVM has two panic mechanisms: +1. **Cooperative path** (`pushPanic`): pushes `OpReturnCallDefers` + `OpPanic2` onto the + op stack and returns. The main `for` loop processes defers iteratively — no Go stack growth. +2. **Go-level path** (`panic(&Exception{...})`): used at ~19 call sites in `values.go`, + `alloc.go`, and `realm.go`. These trigger real Go panics that unwind past the `for` loop + and are caught by `Run()`'s defer/recover. + +The old code converted Go-level exceptions back into the cooperative path via `pushPanic`, +but then re-entered the op loop by recursively calling `m.Run(st)` — accumulating one Go +stack frame per exception. + +## Decision + +Split `Machine.Run()` into two methods: + +- **`Run(st Stage)`** — outer method, contains the benchmark defers and an iterative loop. + When `runOnce()` returns a caught `*Exception`, it calls `pushPanic` and loops back. + No recursion, O(1) Go stack frames regardless of the number of panicking defers. + +- **`runOnce() *Exception`** — inner method with its own `defer/recover`. Runs the op loop + until `OpHalt` (returns nil) or a Go-level `*Exception` panic is caught (returns the + exception). Non-Exception panics are re-raised. + +This preserves the existing semantics: Go-level `*Exception` panics are still converted +to the cooperative `pushPanic` path, and the op loop still processes `OpReturnCallDefers` +iteratively. The only change is that re-entering the op loop after catching an exception +no longer adds a Go stack frame. + +### Alternatives considered + +1. **Depth counter on recursive `Run()`**: Would limit recursion depth, but choosing the + right limit is fragile and the recursive design is fundamentally unnecessary. + +2. **Convert all 19 Go-level `panic(&Exception{})` sites to use `pushPanic`**: Would + eliminate the problem at the source, but is a much larger change that touches many + files and risks subtle behavioral differences. The iterative approach is a minimal, + surgical fix. + +## Key files + +| File | Role | +|------|------| +| `gnovm/pkg/gnolang/machine.go:1268` | `Run()` — outer iterative loop | +| `gnovm/pkg/gnolang/machine.go:1300` | `runOnce()` — inner op loop with defer/recover | +| `gnovm/tests/files/defer_panic_many.gno` | Regression filetest — 500K panicking defers | + +## Testing + +A dedicated filetest (`gnovm/tests/files/defer_panic_many.gno`) registers 500K deferred +closures that each trigger a nil pointer dereference — a Go-level `panic(&Exception{})`. +Before the fix, this would exhaust the Go goroutine stack via recursive `m.Run(st)` calls, +crashing the process with `runtime.throw("stack overflow")`. With the iterative recovery +loop, all 500K panicking defers complete in ~1s and the final panic is recovered normally. + +The fix is also validated by the existing 96 panic/defer/recover file tests in +`gnovm/tests/files/`, which exercise the `Run()`/`runOnce()` iterative recovery path +on every run. + +## Consequences + +- Node processes can no longer be crashed by transactions with many panicking defers. +- The Gno-level panic semantics are preserved — all 96 panic/defer/recover file tests pass. +- `runOnce` is unexported, keeping the public API unchanged. diff --git a/gnovm/pkg/benchops/gno/opcodes/opcode.gno b/gnovm/pkg/benchops/gno/opcodes/opcode.gno index ac08a2443ca..8f94b99c321 100644 --- a/gnovm/pkg/benchops/gno/opcodes/opcode.gno +++ b/gnovm/pkg/benchops/gno/opcodes/opcode.gno @@ -986,8 +986,8 @@ func OpForLoop() { } /* -OpEval, struct { a [](const-type string), b map[(const-type string)] (const-type string), c <-chan (const-type string), d func(), e interface { } }{} -OpEval, struct { a [](const-type string), b map[(const-type string)] (const-type string), c <-chan (const-type string), d func(), e interface { } } +OpEval, struct { a [](const-type string), b map[(const-type string)] (const-type string), c *(const-type string), d func(), e interface { } }{} +OpEval, struct { a [](const-type string), b map[(const-type string)] (const-type string), c *(const-type string), d func(), e interface { } } OpEval, a [](const-type string) OpEval, [](const-type string) OpEval, (const-type string) @@ -999,11 +999,11 @@ OpEval, (const-type string) OpEval, (const-type string) OpMapType, (typeval{string} type{}) OpFieldType, b map[(const-type string)] (const-type string) -OpEval, c <-chan (const-type string) -OpEval, <-chan (const-type string) +OpEval, c *(const-type string) +OpEval, *(const-type string) OpEval, (const-type string) -OpChanType, <-chan (const-type string) -OpFieldType, c <-chan (const-type string) +OpStarType, *(const-type string) +OpFieldType, c *(const-type string) OpEval, d func() OpEval, func() OpFuncType, func() @@ -1012,10 +1012,10 @@ OpEval, e interface { } OpEval, interface { } OpInterfaceType, interface { } OpFieldType, e interface { } -OpStructType, struct { a [](const-type string), b map[(const-type string)] (const-type string), c <-chan (const-type string), d func(), e interface { } } -OpCompositeLit, struct { a [](const-type string), b map[(const-type string)] (const-type string), c <-chan (const-type string), d func(), e interface { } }{} -OpStructLit, struct { a [](const-type string), b map[(const-type string)] (const-type string), c <-chan (const-type string), d func(), e interface { } }{} -OpDefine, t := struct { a [](const-type string), b map[(const-type string)] (const-type string), c <-chan (const-type string), d func(), e interface +OpStructType, struct { a [](const-type string), b map[(const-type string)] (const-type string), c *(const-type string), d func(), e interface { } } +OpCompositeLit, struct { a [](const-type string), b map[(const-type string)] (const-type string), c *(const-type string), d func(), e interface { } }{} +OpStructLit, struct { a [](const-type string), b map[(const-type string)] (const-type string), c *(const-type string), d func(), e interface { } }{} +OpDefine, t := struct { a [](const-type string), b map[(const-type string)] (const-type string), c *(const-type string), d func(), e interface OpExec, bodyStmt[0/0/1]=(end) OpExec, return OpReturnFromBlock, [FRAME FUNC:OpTypes RECV:(undefined) (0 args) 1/0/0/0/1 LASTPKG:main LASTRLM:Realm(nil)] @@ -1025,7 +1025,7 @@ func OpTypes() { t := struct { a []string b map[string]string - c chan string + c *string d func() e any }{} diff --git a/gnovm/pkg/gnolang/alloc.go b/gnovm/pkg/gnolang/alloc.go index 6e07623d32d..ede2799a739 100644 --- a/gnovm/pkg/gnolang/alloc.go +++ b/gnovm/pkg/gnolang/alloc.go @@ -14,13 +14,8 @@ import ( type Allocator struct { maxBytes int64 bytes int64 - // `peakBytes` represents the maximum memory - // usage during a single transaction, and is used - // to calculate the corresponding gas usage. - // It increases monotonically. - peakBytes int64 - collect func() (left int64, ok bool) // gc callback - gasMeter store.GasMeter + collect func() (left int64, ok bool) // gc callback + gasMeter store.GasMeter } // for gonative, which doesn't consider the allocator. @@ -120,6 +115,13 @@ func (alloc *Allocator) Reset() *Allocator { return alloc } +// Recount adds size to bytes without charging gas. +// Used during GC re-walk to re-count surviving objects +// without double-charging for already-paid allocations. +func (alloc *Allocator) Recount(size int64) { + alloc.bytes += size +} + func (alloc *Allocator) Fork() *Allocator { if alloc == nil { return nil @@ -151,15 +153,11 @@ func (alloc *Allocator) Allocate(size int64) { } else { alloc.bytes += size } - // The value of `bytes` decreases during GC, and fees - // are only charged when it exceeds peakBytes (again). - if alloc.bytes > alloc.peakBytes { - if alloc.gasMeter != nil { - change := alloc.bytes - alloc.peakBytes - alloc.gasMeter.ConsumeGas(overflow.Mulp(change, GasCostPerByte), "memory allocation") - } - alloc.peakBytes = alloc.bytes + // Charge gas for every allocation unconditionally (cpu/throughput). + // This ensures repeated allocate-then-GC cycles are not free. + if alloc.gasMeter != nil { + alloc.gasMeter.ConsumeGas(overflow.Mulp(size, GasCostPerByte), "memory allocation (cpu)") } } @@ -244,7 +242,7 @@ func (alloc *Allocator) NewString(s string) StringValue { func (alloc *Allocator) NewListArray(n int) *ArrayValue { if n < 0 { - panic(&Exception{Value: typedString("len out of range")}) + panic("NewListArray: n must not be negative") } alloc.AllocateListArray(int64(n)) return &ArrayValue{ @@ -254,11 +252,11 @@ func (alloc *Allocator) NewListArray(n int) *ArrayValue { func (alloc *Allocator) NewListArray2(l, c int) *ArrayValue { if l < 0 || c < 0 { - panic(&Exception{Value: typedString("len or cap out of range")}) + panic("NewListArray2: l and c must not be negative") } if c < l { - panic(&Exception{Value: typedString("length and capacity swapped")}) + panic("NewListArray2: c must not be less than l") } alloc.AllocateListArray(int64(c)) @@ -269,7 +267,7 @@ func (alloc *Allocator) NewListArray2(l, c int) *ArrayValue { func (alloc *Allocator) NewDataArray(n int) *ArrayValue { if n < 0 { - panic(&Exception{Value: typedString("len out of range")}) + panic("NewDataArray: n must not be negative") } alloc.AllocateDataArray(int64(n)) @@ -355,6 +353,9 @@ func (alloc *Allocator) NewStructWithFields(fields ...TypedValue) *StructValue { } func (alloc *Allocator) NewMap(size int) *MapValue { + if size < 0 { + size = 0 + } alloc.AllocateMap(int64(size)) mv := &MapValue{} mv.MakeMap(size) @@ -416,10 +417,8 @@ func (b *Block) GetShallowSize() int64 { } var ss int64 - // RefNode is not value, put it here - // for convinence if _, ok := b.Source.(RefNode); ok { - ss += allocRefValue + ss += allocRefNode } ss += allocBlock + allocBlockItem*int64(len(b.Values)) @@ -504,3 +503,101 @@ func (dbv DataByteValue) GetShallowSize() int64 { func (tv TypeValue) GetShallowSize() int64 { return 0 } + +// Returns the size of object's RefValue(s). +func internalRefSize(val Value) int64 { + var size int64 + switch v := val.(type) { + case *PackageValue: + if _, ok := v.Block.(RefValue); ok { + size += allocRefValue // .Block ref + } + + // include RefValue size + for _, fb := range v.FBlocks { + if _, ok := fb.(RefValue); !ok { + continue + } + size += allocRefValue + } + case *Block: + for _, v := range v.Values { + if _, ok := v.V.(RefValue); ok { + size += allocRefValue + } + } + + if _, ok := v.Parent.(RefValue); ok { + size += allocRefValue + } + + case *ArrayValue: + if v.Data == nil { + for _, tv := range v.List { + if _, ok := tv.V.(RefValue); ok { + size += allocRefValue + } + } + } + case *StructValue: + for _, tv := range v.Fields { + if _, ok := tv.V.(RefValue); ok { + size += allocRefValue + } + } + case *MapValue: + for cur := v.List.Head; cur != nil; cur = cur.Next { + if _, ok := cur.Key.V.(RefValue); ok { + size += allocRefValue + } + + if _, ok := cur.Value.V.(RefValue); ok { + size += allocRefValue + } + } + case *BoundMethodValue: + if _, ok := v.Receiver.V.(RefValue); ok { + size += allocRefValue + } + case *HeapItemValue: + if _, ok := v.Value.V.(RefValue); ok { + size += allocRefValue + } + case RefValue: + // do nothing + case *PointerValue: + if _, ok := v.Base.(RefValue); ok { + size += allocRefValue + } + case *SliceValue: + if _, ok := v.Base.(RefValue); ok { + size += allocRefValue + } + case *FuncValue: + for _, tv := range v.Captures { + if _, ok := tv.V.(RefValue); ok { + size += allocRefValue + } + } + + if _, ok := v.Parent.(RefValue); ok { + size += allocRefValue + } + + case StringValue: + // do nothing + case BigintValue: + // do nothing + case BigdecValue: + // do nothing + case DataByteValue: + // do nothing + case TypeValue: + // do nothing + default: + panic(fmt.Sprintf( + "unexpected type %T", + val)) + } + return size +} diff --git a/gnovm/pkg/gnolang/alloc_test.go b/gnovm/pkg/gnolang/alloc_test.go index 70ad1f2ed4c..d6f4823eafd 100644 --- a/gnovm/pkg/gnolang/alloc_test.go +++ b/gnovm/pkg/gnolang/alloc_test.go @@ -41,9 +41,9 @@ func TestBlockGetShallowSize_WithRefNodeSource(t *testing.T) { normalSize := normalBlock.GetShallowSize() refNodeSize := refNodeBlock.GetShallowSize() - expectedRefNodeSize := normalSize + allocRefValue + expectedRefNodeSize := normalSize + allocRefNode if refNodeSize != expectedRefNodeSize { - t.Errorf("Block with RefNode source: GetShallowSize() = %d, want %d (normal %d + allocRefValue %d)", - refNodeSize, expectedRefNodeSize, normalSize, allocRefValue) + t.Errorf("Block with RefNode source: GetShallowSize() = %d, want %d (normal %d + allocRefNode %d)", + refNodeSize, expectedRefNodeSize, normalSize, allocRefNode) } } diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index 8c624c9516e..5eefd58dfa6 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -728,7 +728,7 @@ func debugEvalExpr(m *Machine, node ast.Node) (tv TypedValue, err error) { if err != nil { return tv, err } - return x.GetPointerAtIndex(m.Realm, m.Alloc, m.Store, &index).Deref(), nil + return x.GetPointerAtIndex(nilRealm, m.Alloc, m.Store, &index).Deref(), nil default: err = fmt.Errorf("expression not supported: %v", n) } diff --git a/gnovm/pkg/gnolang/garbage_collector.go b/gnovm/pkg/gnolang/garbage_collector.go index d3d5cb3e7e3..32cf3f9c48f 100644 --- a/gnovm/pkg/gnolang/garbage_collector.go +++ b/gnovm/pkg/gnolang/garbage_collector.go @@ -160,7 +160,7 @@ func GCVisitorFn(gcCycle int64, alloc *Allocator, visitCount *int64) Visitor { return true } - alloc.Allocate(size) + alloc.Recount(size) // bump before visiting associated, // this avoids infinite recursion. @@ -406,7 +406,7 @@ func (tv TypeValue) VisitAssociated(vis Visitor) (stop bool) { func (fr *Frame) Visit(alloc *Allocator, vis Visitor) (stop bool) { // vis receiver if fr.Receiver.IsDefined() { - alloc.Allocate(allocTypedValue) // alloc shallowly + alloc.Recount(allocTypedValue) // reclaim shallowly if v := fr.Receiver.V; v != nil { stop = vis(v) @@ -435,7 +435,7 @@ func (fr *Frame) Visit(alloc *Allocator, vis Visitor) (stop bool) { } for _, arg := range dfr.Args { - alloc.Allocate(allocTypedValue) + alloc.Recount(allocTypedValue) if arg.V != nil { stop = vis(arg.V) @@ -466,7 +466,7 @@ func (fr *Frame) Visit(alloc *Allocator, vis Visitor) (stop bool) { func (e *Exception) Visit(alloc *Allocator, vis Visitor) (stop bool) { // vis value - alloc.Allocate(allocTypedValue) + alloc.Recount(allocTypedValue) if v := e.Value.V; v != nil { stop = vis(v) } diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go index 4c6000f7b63..293da58cd77 100644 --- a/gnovm/pkg/gnolang/gno_test.go +++ b/gnovm/pkg/gnolang/gno_test.go @@ -323,94 +323,6 @@ func TestConvertTo(t *testing.T) { } `, `test/main.go:6:14-22: cannot convert interface{} to test.MyInt: need type assertion`, }, - { - `package test - - func main() { - const a int = -1 - println(uint(a)) - }`, - `test/main.go:5:14-21: cannot convert constant of type IntKind to UintKind`, - }, - { - `package test - - func main() { - const a int = -1 - println(uint8(a)) - }`, - `test/main.go:5:14-22: cannot convert constant of type IntKind to Uint8Kind`, - }, - { - `package test - - func main() { - const a int = -1 - println(uint16(a)) - }`, - `test/main.go:5:14-23: cannot convert constant of type IntKind to Uint16Kind`, - }, - { - `package test - - func main() { - const a int = -1 - println(uint32(a)) - }`, - `test/main.go:5:14-23: cannot convert constant of type IntKind to Uint32Kind`, - }, - { - `package test - - func main() { - const a int = -1 - println(uint64(a)) - }`, - `test/main.go:5:14-23: cannot convert constant of type IntKind to Uint64Kind`, - }, - { - `package test - - func main() { - const a float32 = 1.5 - println(int32(a)) - }`, - `test/main.go:5:14-22: cannot convert constant of type Float32Kind to Int32Kind`, - }, - { - `package test - - func main() { - println(int32(1.5)) - }`, - `test/main.go:4:14-24: cannot convert (const (1.5 bigdec)) to integer type`, - }, - { - `package test - - func main() { - const a float64 = 1.5 - println(int64(a)) - }`, - `test/main.go:5:14-22: cannot convert constant of type Float64Kind to Int64Kind`, - }, - { - `package test - - func main() { - println(int64(1.5)) - }`, - `test/main.go:4:14-24: cannot convert (const (1.5 bigdec)) to integer type`, - }, - { - `package test - - func main() { - const f = float64(1.0) - println(int64(f)) - }`, - ``, - }, } for _, tc := range tests { diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 2117135ca7d..1d8148128b5 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -312,11 +312,14 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { Type: toExpr(fs, gon.Type), } case *ast.UnaryExpr: - if gon.Op == token.AND { + switch gon.Op { + case token.AND: return &RefExpr{ X: toExpr(fs, gon.X), } - } else { + case token.ARROW: + panicWithPos("channel receive is not permitted") + default: return &UnaryExpr{ X: toExpr(fs, gon.X), Op: toWord(gon.Op), @@ -386,18 +389,6 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { return &InterfaceTypeExpr{ Methods: toFieldsFromList(fs, gon.Methods), } - case *ast.ChanType: - var dir ChanDir - if gon.Dir&ast.SEND > 0 { - dir |= SEND - } - if gon.Dir&ast.RECV > 0 { - dir |= RECV - } - return &ChanTypeExpr{ - Dir: dir, - Value: toExpr(fs, gon.Value), - } case *ast.FuncType: return &FuncTypeExpr{ Params: toFieldsFromList(fs, gon.Params), @@ -578,8 +569,14 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { panicWithPos("invalid operation: more than one index") } panicWithPos("invalid operation: indexList is not permitted in Gno") + case *ast.ChanType: + panicWithPos("channels are not permitted") case *ast.GoStmt: panicWithPos("goroutines are not permitted") + case *ast.SendStmt: + panicWithPos("send statements are not permitted") + case *ast.SelectStmt: + panicWithPos("select statements are not permitted") default: panicWithPos("unknown Go type %v: %s\n", reflect.TypeOf(gon), diff --git a/gnovm/pkg/gnolang/gotypecheck.go b/gnovm/pkg/gnolang/gotypecheck.go index 167f2f2a41f..b2006961e2f 100644 --- a/gnovm/pkg/gnolang/gotypecheck.go +++ b/gnovm/pkg/gnolang/gotypecheck.go @@ -10,9 +10,10 @@ import ( "slices" "strings" - "github.com/gnolang/gno/tm2/pkg/std" "go.uber.org/multierr" "golang.org/x/tools/go/ast/astutil" + + "github.com/gnolang/gno/tm2/pkg/std" ) /* @@ -42,6 +43,7 @@ type realm interface { Address() address PkgPath() string Coins() gnocoins + SentCoins() gnocoins Send(coins gnocoins, to address) error Previous() realm Origin() realm @@ -55,12 +57,16 @@ func (a address) IsValid() bool { return false } // shim type Address = address type gnocoins []gnocoin +func (cz gnocoins) String() string { return "" } // shim + type Gnocoins = gnocoins type gnocoin struct { Denom string Amount int64 } +func (c gnocoin) String() string { return "" } // shim + type Gnocoin = gnocoin `) default: diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index d8e6bfeddb8..af9738a7151 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -1,6 +1,7 @@ package gnolang import ( + "container/heap" "fmt" "io" "path" @@ -196,6 +197,21 @@ func (m *Machine) SetActivePackage(pv *PackageValue) { func (m *Machine) PreprocessAllFilesAndSaveBlockNodes() { ch := m.Store.IterMemPackage() for mpkg := range ch { + // AddMemPackage writes the path index (baseStore / pebble) and the + // package body (iavlStore) in two separate Set calls. If the process + // is SIGKILLed between them (OOM, container stop, OrbStack restart), + // the index can point at a path whose body is absent, and + // GetMemPackage then returns nil. ParseMemPackage(nil) would SIGSEGV + // on `nil.Type.(MemPackageType)` (offset 0x38) and the node cannot + // boot. Skip with a warning so an operator can still reach a working + // node; the real fix is atomic index+body writes in AddMemPackage. + if mpkg == nil { + fmt.Fprintln(m.Output, + "WARNING: IterMemPackage returned nil — index points at a "+ + "package whose body is not persisted (likely a prior "+ + "crash mid-AddMemPackage). Skipping.") + continue + } mpkg = MPFProd.FilterMemPackage(mpkg) fset := m.ParseMemPackage(mpkg) pn := NewPackageNode(Name(mpkg.Name), mpkg.Path, fset) @@ -524,6 +540,23 @@ func (m *Machine) PreprocessFiles(pkgName, pkgPath string, fset *FileSet, save, return pn, pv } +// initHeap is a min-heap of pending-declaration indices, used by runFileDecls +// to always pick the earliest-in-declaration-order ready entry. +type initHeap []int + +func (h initHeap) Len() int { return len(h) } +func (h initHeap) Less(i, j int) bool { return h[i] < h[j] } +func (h initHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h *initHeap) Push(x any) { *h = append(*h, x.(int)) } + +func (h *initHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[:n-1] + return x +} + // Add files to the package's *FileSet and run decls in them. // This will also run each init function encountered. // Returns the updated typed values of package. @@ -594,71 +627,86 @@ func (m *Machine) runFileDecls(withOverrides bool, fns ...*FileNode) []TypedValu // Get new values across all files in package. updates := pn.PrepareNewValues(m.Alloc, pv) - // to detect loops in var declarations. - loopfindr := []Name{} - // recursive function for var declarations. - var runDeclarationFor func(fn *FileNode, decl Decl) - runDeclarationFor = func(fn *FileNode, decl Decl) { - // get fileblock of fn. - // fb := pv.GetFileBlock(nil, fn.FileName) - // get dependencies of decl. - deps := make(map[Name]struct{}) - findDependentNames(decl, deps) - for dep := range deps { - // if dep already defined as import, skip. - if _, ok := fn.GetLocalIndex(dep); ok { - continue - } - // if dep already in fdeclared, skip. - if _, ok := fdeclared[dep]; ok { + // To initialize package variables, Go's spec says the following: + // Within a package, package-level variable initialization proceeds + // stepwise, with each step selecting the variable earliest in declaration + // order which has no dependencies on uninitialized variables. + // + // Implementation: Kahn's topological sort with declaration-order tiebreaking + // via a min-heap keyed on declaration index. + // + // Phase 1: Collect all non-FuncDecl declarations in source order and compute + // effective deps (collapsing FuncDecl edges) once for all declarations. + // Phase 2: Build reverse-dep index and unsatisfied counts, then use a + // min-heap to always pick the earliest-in-declaration-order ready entry. + + // Build ordered pending list from all non-FuncDecl decls, preserving source + // declaration order. declFiles tracks which FileNode each decl belongs to. + var pending []Decl + var declFiles []*FileNode + for _, fn := range fns { + for _, decl := range fn.Decls { + if _, ok := decl.(*FuncDecl); ok { continue } - fn, depdecl, exists := pn.FileSet.GetDeclForSafe(dep) - // special case: if doesn't exist: - if !exists { - if isUverseName(dep) { // then is reserved keyword in uverse. - continue - } else { // is an undefined dependency. - panic(fmt.Sprintf( - "%s/%s:%s: dependency %s not defined in fileset with files %v", - pv.PkgPath, fn.FileName, decl.GetPos().String(), dep, fs.FileNames())) - } - } - // if dep already in loopfindr, abort. - if slices.Contains(loopfindr, dep) { - if _, ok := (*depdecl).(*FuncDecl); ok { - // recursive function dependencies - // are OK with func decls. - continue - } else { - panic(fmt.Sprintf( - "%s/%s:%s: loop in variable initialization: dependency trail %v circularly depends on %s", - pv.PkgPath, fn.FileName, decl.GetPos().String(), loopfindr, dep)) - } - } - // run dependency declaration - loopfindr = append(loopfindr, dep) - runDeclarationFor(fn, *depdecl) - loopfindr = loopfindr[:len(loopfindr)-1] + pending = append(pending, decl) + declFiles = append(declFiles, fn) + } + } + + // Compute effective deps for all decls at once (memoized DFS, O(V+E)). + effectiveDeps := resolveEffectiveDeps(pending, pn, fdeclared) + + // Build reverse deps and unsatisfied counts. reverseDeps maps a ValueDecl + // to the indices of pending entries that depend on it. + unsatisfied := make([]int, len(pending)) + reverseDeps := map[*ValueDecl][]int{} + for i, decl := range pending { + deps := effectiveDeps[decl] + unsatisfied[i] = len(deps) + for _, dep := range deps { + reverseDeps[dep] = append(reverseDeps[dep], i) } - // run declaration - fb := pv.GetFileBlock(m.Store, fn.FileName) + } + + // Seed heap with zero-dep entries. + ready := &initHeap{} + for i := range pending { + if unsatisfied[i] == 0 { + heap.Push(ready, i) + } + } + + // Kahn's loop: always pop the earliest-in-declaration-order ready entry. + for ready.Len() > 0 { + idx := heap.Pop(ready).(int) + decl := pending[idx] + fb := pv.GetFileBlock(m.Store, declFiles[idx].FileName) m.PushBlock(fb) m.runDeclaration(decl) m.PopBlock() for _, n := range decl.GetDeclNames() { fdeclared[n] = struct{}{} } + // Notify dependents; enqueue newly-ready ones. + if vd, ok := decl.(*ValueDecl); ok { + for _, depIdx := range reverseDeps[vd] { + unsatisfied[depIdx]-- + if unsatisfied[depIdx] == 0 { + heap.Push(ready, depIdx) + } + } + } } - // Declarations (and variable initializations). This must happen - // after all files are preprocessed, because value decl may be out of - // order and depend on other files. - - // Run declarations. - for _, fn := range fns { - for _, decl := range fn.Decls { - runDeclarationFor(fn, decl) + // Sanity check: all entries must have been processed. If any remain, + // it means resolveEffectiveDeps missed a cycle or the reverse-dep + // notification has a gap. + for i, decl := range pending { + if unsatisfied[i] > 0 { + panic(fmt.Sprintf( + "incomplete initialization: %v still has %d unsatisfied deps", + decl.GetDeclNames(), unsatisfied[i])) } } @@ -1246,18 +1294,30 @@ func (m *Machine) Run(st Stage) { bm.FinishNative() }() } - defer func() { - r := recover() - if r != nil { - switch r := r.(type) { - case *Exception: - if r.Stacktrace.IsZero() { - r.Stacktrace = m.Stacktrace() - } - m.pushPanic(r.Value) - m.Run(st) - default: + // Iterative exception recovery: catch Go-level *Exception panics and + // convert them to the cooperative pushPanic path without recursion. + for { + caught := m.runOnce() + if caught == nil { + return + } + if caught.Stacktrace.IsZero() { + caught.Stacktrace = m.Stacktrace() + } + m.pushPanic(caught.Value) + } +} + +// runOnce executes the op loop until it completes (OpHalt) or a Go-level +// *Exception panic is caught. Returns the caught exception, or nil if the +// loop completed normally. Non-Exception panics are re-raised. +func (m *Machine) runOnce() (caught *Exception) { + defer func() { + if r := recover(); r != nil { + if ex, ok := r.(*Exception); ok { + caught = ex + } else { panic(r) } } @@ -1328,11 +1388,9 @@ func (m *Machine) Run(st Stage) { m.incrCPU(OpCPUCallDeferNativeBody) m.doOpCallDeferNativeBody() case OpGo: - m.incrCPU(OpCPUGo) - panic("not yet implemented") + panic("goroutines are not yet supported") case OpSelect: - m.incrCPU(OpCPUSelect) - panic("not yet implemented") + panic("select is not yet supported") case OpSwitchClause: m.incrCPU(OpCPUSwitchClause) m.doOpSwitchClause() @@ -1371,8 +1429,7 @@ func (m *Machine) Run(st Stage) { m.incrCPU(OpCPUUxor) m.doOpUxor() case OpUrecv: - m.incrCPU(OpCPUUrecv) - m.doOpUrecv() + panic("channel type is not yet supported") /* Binary operators */ case OpLor: m.incrCPU(OpCPULor) @@ -1500,8 +1557,7 @@ func (m *Machine) Run(st Stage) { m.incrCPU(OpCPUSliceType) m.doOpSliceType() case OpChanType: - m.incrCPU(OpCPUChanType) - m.doOpChanType() + panic("channel type is not yet supported") case OpFuncType: m.incrCPU(OpCPUFuncType) m.doOpFuncType() @@ -2107,6 +2163,22 @@ func (m *Machine) NumFrames() int { return len(m.Frames) } +// NumCallFrames returns the number of actual function call frames, +// excluding closure frames (func literals) and control-flow basic +// frames (for/range/switch where Func is nil). Only named, non-closure +// function calls count as separate call boundaries for origin-call +// purposes. +func (m *Machine) NumCallFrames() int { + count := 0 + for i := range m.Frames { + fr := &m.Frames[i] + if fr.Func != nil && !fr.Func.IsClosure { + count++ + } + } + return count +} + // Returns the current frame. func (m *Machine) LastFrame() *Frame { return &m.Frames[len(m.Frames)-1] @@ -2218,19 +2290,18 @@ func (m *Machine) PopAsPointer(lx Expr) PointerValue { } func readonlyAccessPanic(x Expr) string { - return "cannot directly modify readonly tainted object (w/o method): " + x.String() + return "cannot directly modify readonly tainted object (use a method or crossing function): " + x.String() } -// Returns true iff: -// - m.Realm is nil (single user mode), or +// Returns false if m.Realm is nil (single user mode, nothing is readonly). +// Otherwise returns true iff: // - tv is a ref to (external) package path, or // - tv is N_Readonly, or // - tv is not an object ("first object" ID is zero), or // - tv is an unreal object (no object id), or // - tv is an object residing in external realm func (m *Machine) IsReadonly(tv *TypedValue) bool { - // Returns true iff: - // - m.Realm is nil (single user mode) + // m.Realm is nil → single user mode, nothing is readonly if m.Realm == nil { return false } @@ -2249,9 +2320,26 @@ func (m *Machine) IsReadonly(tv *TypedValue) bool { return tv.IsReadonlyBy(m.Realm.ID) } +// isExternalRealm returns true if base is a real Object belonging to +// a different realm than m.Realm. Used for NameExpr cross-realm checks +// where we have a Base (Block) rather than a TypedValue. +func (m *Machine) isExternalRealm(base Value) bool { + if m.Realm == nil { + return false + } + obj, ok := base.(Object) + if !ok { + return false + } + oid := obj.GetObjectID() + if oid.IsZero() { + return false // transient (local var, unreal block) + } + return oid.PkgID != m.Realm.ID +} + // Returns ro = true if the base is readonly, // or if the base's storage realm != m.Realm and both are non-nil, -// and the lx isn't a name (base is a block), // and the lx isn't a composite lit expr. func (m *Machine) PopAsPointer2(lx Expr) (pv PointerValue, ro bool) { switch lx := lx.(type) { @@ -2260,11 +2348,11 @@ func (m *Machine) PopAsPointer2(lx Expr) (pv PointerValue, ro bool) { case NameExprTypeNormal: lb := m.LastBlock() pv = lb.GetPointerTo(m.Store, lx.Path) - ro = false // always mutable + ro = m.isExternalRealm(pv.Base) case NameExprTypeHeapUse: lb := m.LastBlock() pv = lb.GetPointerTo(m.Store, lx.Path) - ro = false // always mutable + ro = m.isExternalRealm(pv.Base) case NameExprTypeHeapClosure: panic("should not happen") default: @@ -2295,7 +2383,7 @@ func (m *Machine) PopAsPointer2(lx Expr) (pv PointerValue, ro bool) { var ok bool if pv, ok = xv.V.(PointerValue); !ok { if xv.V == nil { - m.Panic(typedString("nil pointer dereference")) + m.Panic(typedString("runtime error: nil pointer dereference")) } panic("should not happen, not pointer nor nil") } @@ -2308,7 +2396,7 @@ func (m *Machine) PopAsPointer2(lx Expr) (pv PointerValue, ro bool) { Base: hv, Index: 0, } - ro = false // always mutable + ro = false // always mutable; composite literals are freshly allocated (unreal) values not yet owned by any realm. default: panic("should not happen") } diff --git a/gnovm/pkg/gnolang/misc.go b/gnovm/pkg/gnolang/misc.go index 67386b2ba58..169e8debfdd 100644 --- a/gnovm/pkg/gnolang/misc.go +++ b/gnovm/pkg/gnolang/misc.go @@ -82,7 +82,7 @@ func word2UnaryOp(w Word) Op { case BAND: panic("unexpected unary operation & - use RefExpr instead") case ARROW: - return OpUrecv + panic("channel type is not yet supported") default: panic("unexpected unary operation") } diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index c5098ff5932..6a68f8b1293 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -139,9 +139,12 @@ const ( ATTR_LAST_BLOCK_STMT GnoAttribute = "ATTR_LAST_BLOCK_STMT" ATTR_PACKAGE_REF GnoAttribute = "ATTR_PACKAGE_REF" ATTR_PACKAGE_DECL GnoAttribute = "ATTR_PACKAGE_DECL" - ATTR_PACKAGE_PATH GnoAttribute = "ATTR_PACKAGE_PATH" // if name expr refers to package. - ATTR_FIX_FROM GnoAttribute = "ATTR_FIX_FROM" // gno fix this version. - ATTR_LOOPVAR_SKIP GnoAttribute = "ATTR_LOOPVAR_SKIP" // temp only + ATTR_PACKAGE_PATH GnoAttribute = "ATTR_PACKAGE_PATH" // if name expr refers to package. + ATTR_FIX_FROM GnoAttribute = "ATTR_FIX_FROM" // gno fix this version. + ATTR_LOOPVAR_SKIP GnoAttribute = "ATTR_LOOPVAR_SKIP" // temp only + ATTR_REF_ELEM_TYPE GnoAttribute = "ATTR_REF_ELEM_TYPE" // static element type of &x, set on the RefExpr node during preprocessing. + // For top level declarations, a map[Name]struct{} of other dependencies + ATTR_DECL_DEPS GnoAttribute = "ATTR_DECL_DEPS" ) // Embedded in each Node. @@ -1432,6 +1435,7 @@ func (pn *PackageNode) PrepareNewValues(alloc *Allocator, pv *PackageValue) []Ty } } } + alloc.AllocateBlockItems(int64(len(nvs))) block.Values = append(block.Values, nvs...) return block.Values[pvl:] } else if pvl > pnl { @@ -1547,7 +1551,6 @@ type BlockNode interface { Define2(bool, Name, Type, TypedValue, NameSource) GetPathForName(Store, Name) ValuePath GetBlockNames() []Name - GetExternNames() []Name GetNumNames() uint16 GetIsConst(Store, Name) bool GetIsConstAt(Store, ValuePath) bool @@ -1606,7 +1609,7 @@ type StaticBlock struct { HeapItems []bool UnassignableNames []Name Consts []Name // TODO consider merging with Names. - Externs []Name + Externs []Name // TODO: remove, this only exists for amino backward-compat. Parent BlockNode // temporary storage for rolling back redefinitions. @@ -1697,7 +1700,6 @@ func (sb *StaticBlock) InitStaticBlock(source BlockNode, parent BlockNode) { sb.NameSources = make([]NameSource, 0, 16) sb.HeapItems = make([]bool, 0, 16) sb.Consts = make([]Name, 0, 16) - sb.Externs = make([]Name, 0, 16) sb.Parent = parent } @@ -1722,20 +1724,6 @@ func (sb *StaticBlock) GetBlockNames() (ns []Name) { return sb.Names } -// Implements BlockNode. -// NOTE: Extern names may also be local, if declared after usage as an extern -// (thus shadowing the extern name). -func (sb *StaticBlock) GetExternNames() (ns []Name) { - return sb.Externs -} - -func (sb *StaticBlock) addExternName(n Name) { - if slices.Contains(sb.Externs, n) { - return - } - sb.Externs = append(sb.Externs, n) -} - // Implements BlockNode. func (sb *StaticBlock) GetNumNames() (nn uint16) { return sb.NumNames @@ -1761,7 +1749,6 @@ func (sb *StaticBlock) GetParentNode(store Store) BlockNode { } // Implements BlockNode. -// As a side effect, notes externally defined names. // Slow, for precompile only. func (sb *StaticBlock) GetPathForName(store Store, n Name) ValuePath { if n == blankIdentifier { @@ -1773,14 +1760,6 @@ func (sb *StaticBlock) GetPathForName(store Store, n Name) ValuePath { return NewValuePathBlock(uint8(gen), idx, n) } sn := sb.GetSource(store) - // Register as extern. - // NOTE: uverse names are externs too. - // NOTE: externs may also be shadowed later in the block. Thus, usages - // before the declaration will have depth > 1; following it, depth == 1, - // matching the two different identifiers they refer to. - if !isFile(sn) { - sb.GetStaticBlock().addExternName(n) - } // Check ancestors. gen++ fauxChild := 0 @@ -1795,9 +1774,6 @@ func (sb *StaticBlock) GetPathForName(store Store, n Name) ValuePath { } return NewValuePathBlock(uint8(gen-fauxChild), idx, n) } else { - if !isFile(sn) { - sn.GetStaticBlock().addExternName(n) - } gen++ if fauxChildBlockNode(sn) { fauxChild++ diff --git a/gnovm/pkg/gnolang/op_binary.go b/gnovm/pkg/gnolang/op_binary.go index 7a971ed37b0..dd0acc34b06 100644 --- a/gnovm/pkg/gnolang/op_binary.go +++ b/gnovm/pkg/gnolang/op_binary.go @@ -826,7 +826,7 @@ func mulAssign(lv, rv *TypedValue) { // for doOpQuo and doOpQuoAssign. func quoAssign(lv, rv *TypedValue) *Exception { expt := &Exception{ - Value: typedString("division by zero"), + Value: typedString("runtime error: division by zero"), } // set the result in lv. @@ -925,7 +925,7 @@ func quoAssign(lv, rv *TypedValue) *Exception { // for doOpRem and doOpRemAssign. func remAssign(lv, rv *TypedValue) *Exception { expt := &Exception{ - Value: typedString("division by zero"), + Value: typedString("runtime error: division by zero"), } // set the result in lv. @@ -1162,7 +1162,9 @@ func xorAssign(lv, rv *TypedValue) { // for doOpShl and doOpShlAssign. func shlAssign(m *Machine, lv, rv *TypedValue) { - rv.AssertNonNegative("runtime error: negative shift amount") + if rv.Sign() < 0 { + m.Panic(typedString(fmt.Sprintf("runtime error: negative shift amount: %v", rv))) + } checkOverflow := func(v func() bool) { if m.Stage == StagePre && !v() { @@ -1286,7 +1288,9 @@ func shlAssign(m *Machine, lv, rv *TypedValue) { // for doOpShr and doOpShrAssign. func shrAssign(m *Machine, lv, rv *TypedValue) { - rv.AssertNonNegative("runtime error: negative shift amount") + if rv.Sign() < 0 { + m.Panic(typedString(fmt.Sprintf("runtime error: negative shift amount: %v", rv))) + } checkOverflow := func(v func() bool) { if m.Stage == StagePre && !v() { diff --git a/gnovm/pkg/gnolang/op_call.go b/gnovm/pkg/gnolang/op_call.go index 161f4845925..55197438422 100644 --- a/gnovm/pkg/gnolang/op_call.go +++ b/gnovm/pkg/gnolang/op_call.go @@ -66,8 +66,13 @@ func (m *Machine) doOpPrecall() { // Do not pop type yet. // No need for frames. xv := m.PeekValue(1) + // When the preprocessor wraps a shift RHS in uint(), + // it sets ATTR_SHIFT_RHS so we can reject negative + // values before the conversion. if cx.GetAttribute(ATTR_SHIFT_RHS) == true { - xv.AssertNonNegative("runtime error: negative shift amount") + if xv.Sign() < 0 { + m.Panic(typedString(fmt.Sprintf("runtime error: negative shift amount: %v", xv))) + } } m.PushOp(OpConvert) if debug { diff --git a/gnovm/pkg/gnolang/op_eval.go b/gnovm/pkg/gnolang/op_eval.go index 4f8fe8e64c0..49700bd3269 100644 --- a/gnovm/pkg/gnolang/op_eval.go +++ b/gnovm/pkg/gnolang/op_eval.go @@ -400,10 +400,6 @@ func (m *Machine) doOpEval() { // evaluate x m.PushExpr(x.X) m.PushOp(OpEval) - case *ChanTypeExpr: - m.PushOp(OpChanType) - m.PushExpr(x.Value) - m.PushOp(OpEval) // OpEvalType? default: panic(fmt.Sprintf("unexpected expression %#v", x)) } diff --git a/gnovm/pkg/gnolang/op_exec.go b/gnovm/pkg/gnolang/op_exec.go index 26dc061d439..02dd3911cf4 100644 --- a/gnovm/pkg/gnolang/op_exec.go +++ b/gnovm/pkg/gnolang/op_exec.go @@ -161,7 +161,7 @@ func (m *Machine) doOpExec(op Op) { var dv *TypedValue if op == OpRangeIterArrayPtr { if xv.V == nil { - m.pushPanic(typedString("nil pointer dereference")) + m.pushPanic(typedString("runtime error: nil pointer dereference")) return } dv = xv.V.(PointerValue).TV diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index 5e5839bc7ed..3e0e5a97e88 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -27,7 +27,7 @@ func (m *Machine) doOpIndex1() { } } default: - // NOTE: nilRealm is OK, not setting a map (w/ new key). + // Read-only: pass nilRealm so map key attach DidUpdate is a no-op. res := xv.GetPointerAtIndex(nilRealm, m.Alloc, m.Store, iv) *xv = res.Deref() // reuse as result } @@ -103,7 +103,7 @@ func (m *Machine) doOpSlice() { xv.T.Elem().Kind() == ArrayKind { // simply deref xv. if xv.V == nil { - m.pushPanic(typedString("nil pointer dereference")) + m.pushPanic(typedString("runtime error: nil pointer dereference")) return } *xv = xv.V.(PointerValue).Deref() @@ -146,7 +146,7 @@ func (m *Machine) doOpStar() { switch bt := baseOf(xv.T).(type) { case *PointerType: if xv.V == nil { - m.pushPanic(typedString("nil pointer dereference")) + m.pushPanic(typedString("runtime error: nil pointer dereference")) return } @@ -160,9 +160,17 @@ func (m *Machine) doOpStar() { ro := m.IsReadonly(xv) pvtv := (*pv.TV).WithReadonly(ro) if xpt, ok := baseOf(xv.T).(*PointerType); ok { - // e.g. type Foo; type Bar; - // *((*Foo)(&Bar{})) should be Bar, not Foo. - pvtv.T = xpt.Elem() + // When a pointer was converted to a different + // declared pointer type, the dereferenced value + // should have the element type of the pointer, + // not the original stored type. + // e.g. type Foo struct{X int}; type Bar struct{X int}; + // *((*Foo)(&Bar{})) is Foo, not Bar. + // But do not overwrite for interface element + // types; the concrete type must be preserved. + if xpt.Elem().Kind() != InterfaceKind { + pvtv.T = xpt.Elem() + } } m.PushValue(pvtv) } @@ -177,13 +185,19 @@ func (m *Machine) doOpStar() { } } -// XXX this is wrong, for var i interface{}; &i is *interface{}. +// doOpRef implements the & (address-of) operator. +// The element type for the resulting pointer is taken from +// ATTR_REF_ELEM_TYPE on the RefExpr, not from the runtime +// xv.TV.T. This distinction matters for interface variables: +// var i interface{} = 42; &i must yield *interface{}, not *int. +// ATTR_REF_ELEM_TYPE is set during preprocessing in +// TRANS_LEAVE *RefExpr and at each synthetic RefExpr site. func (m *Machine) doOpRef() { rx := m.PopExpr().(*RefExpr) xv, ro := m.PopAsPointer2(rx.X) - elt := xv.TV.T - if elt == DataByteType { - elt = xv.TV.V.(DataByteValue).ElemType + elt, ok := rx.GetAttribute(ATTR_REF_ELEM_TYPE).(Type) + if !ok { + panic("ATTR_REF_ELEM_TYPE not set during preprocessing") } m.Alloc.AllocatePointer() m.PushValue(TypedValue{ @@ -698,7 +712,7 @@ func (m *Machine) doOpConvert() { // These protect against inter-realm conversion exploits. // Case 1. - // Do not allow conversion of value stored in eternal realm. + // Do not allow conversion of value stored in external realm. // Otherwise anyone could convert an external object insecurely. if xv.T != nil && !xv.T.IsImmutable() && m.IsReadonly(&xv) { if xvdt, ok := xv.T.(*DeclaredType); ok && diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index 0a26365667d..bb83ade5e2a 100644 --- a/gnovm/pkg/gnolang/op_types.go +++ b/gnovm/pkg/gnolang/op_types.go @@ -152,19 +152,6 @@ func (m *Machine) doOpInterfaceType() { }) } -func (m *Machine) doOpChanType() { - x := m.PopExpr().(*ChanTypeExpr) - tv := m.PeekValue(1) // re-use as result. - ct := &ChanType{ - Dir: x.Dir, - Elt: tv.GetType(), - } - *tv = TypedValue{ - T: gTypeType, - V: toTypeValue(ct), - } -} - // Evaluate the type of a typed (i.e. not untyped) value. // This function expects const expressions to have been // already swapped for *ConstExpr in the preprocessor. If not, panics. @@ -199,6 +186,10 @@ func (m *Machine) doOpStaticTypeOf() { m.PushValue(asValue(UntypedBoolType)) } case *CallExpr: + // ATTR_TYPEOF_VALUE must already be set on every CallExpr + // during preprocessing: for type conversions (TRANS_LEAVE + // *CallExpr conversion paths), for generic/specialized + // calls, and for plain function calls via the general case. t := getTypeOf(x) m.PushValue(asValue(t)) case *IndexExpr: @@ -412,12 +403,12 @@ func (m *Machine) doOpStaticTypeOf() { panic("unexpected star expression") } case *RefExpr: - start := len(m.Values) - m.PushOp(OpHalt) - m.PushExpr(x.X) - m.PushOp(OpStaticTypeOf) - m.Run(StageRun) - xt := m.ReapValues(start)[0].GetType() + // The static type of &x is *typeof(x). + // ATTR_REF_ELEM_TYPE is set during preprocessing. + xt, ok := x.GetAttribute(ATTR_REF_ELEM_TYPE).(Type) + if !ok { + panic("ATTR_REF_ELEM_TYPE not set during preprocessing") + } m.PushValue(asValue(&PointerType{Elt: xt})) case *TypeAssertExpr: if x.HasOK { diff --git a/gnovm/pkg/gnolang/op_unary.go b/gnovm/pkg/gnolang/op_unary.go index b597d0dd58e..86e595ffb10 100644 --- a/gnovm/pkg/gnolang/op_unary.go +++ b/gnovm/pkg/gnolang/op_unary.go @@ -120,7 +120,3 @@ func (m *Machine) doOpUxor() { baseOf(xv.T))) } } - -func (m *Machine) doOpUrecv() { - panic("not yet implemented") -} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index c63f0acac37..932702171d9 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2,6 +2,7 @@ package gnolang import ( "fmt" + "maps" "math" "math/big" "reflect" @@ -135,7 +136,7 @@ func PredefineFileSet(store Store, pn *PackageNode, fset *FileSet) { } } } - split := make([]Decl, len(vd.NameExprs)) + split := make([]Decl, 0, len(vd.NameExprs)) for j := range vd.NameExprs { part := &ValueDecl{ NameExprs: NameExprs{{ @@ -151,10 +152,19 @@ func PredefineFileSet(store Store, pn *PackageNode, fset *FileSet) { if iota_ != nil { part.SetAttribute(ATTR_IOTA, iota_) } - predefineRecursively(store, fn, part) - split[j] = part + split = append(split, part) + } + // Apply the split to fn.Decls BEFORE calling predefineRecursively, + // so that GetDeclFor resolves each split name to its own individual + // decl rather than the original multi-value decl, avoiding false + // cycle detection. + fn.Decls = append(fn.Decls[:i], append(split, fn.Decls[i+1:]...)...) + for j := range split { + if split[j].GetAttribute(ATTR_PREDEFINED) == true { + continue + } + predefineRecursively(store, fn, split[j]) } - fn.Decls = append(fn.Decls[:i], append(split, fn.Decls[i+1:]...)...) //nolint:makezero i += len(vd.NameExprs) - 1 continue } else { @@ -219,6 +229,8 @@ func initStaticBlocks1(store Store, ctx BlockNode, nn Node) { switch n := n.(type) { case *NameExpr: switch ftype { + case TRANS_COMPOSITE_KEY: + return n, TRANS_CONTINUE case TRANS_ASSIGN_LHS: as := ns[len(ns)-1].(*AssignStmt) if as.Op == DEFINE { @@ -312,39 +324,11 @@ func initStaticBlocks1(store Store, ctx BlockNode, nn Node) { nx.Name += ".loopvar" replaceAllLoopvar(last, n, ln) } - case *SendStmt: - panic("not yet implemented") } case *RangeStmt: if n.Op != DEFINE { return n, TRANS_CONTINUE } - if n.Key != nil { - ln := n.Key.(*NameExpr).Name - if ln == blankIdentifier { - return n, TRANS_CONTINUE - } - if strings.HasSuffix(string(ln), ".loopvar") { - // for idempotency (already converted) - return n, TRANS_CONTINUE - } - // replace all n.Key w/ .loopvar - n.Key.(*NameExpr).Name += ".loopvar" - replaceAllLoopvar(last, n, ln) - } - if n.Value != nil { - ln := n.Value.(*NameExpr).Name - if ln == blankIdentifier { - return n, TRANS_CONTINUE - } - if strings.HasSuffix(string(ln), ".loopvar") { - // for idempotency (already converted) - return n, TRANS_CONTINUE - } - // replace all n.Value w/ .loopvar - n.Value.(*NameExpr).Name += ".loopvar" - replaceAllLoopvar(last, n, ln) - } } } return n, TRANS_CONTINUE @@ -382,11 +366,13 @@ func initStaticBlocks2(store Store, ctx BlockNode, nn Node) { if ln == blankIdentifier { continue } - if !isLocallyDefined2(last, ln) { + if !isLocallyReserved(last, ln) { // if loopvar, will promote to // NameExprTypeHeapDefine later. nx.Type = NameExprTypeDefine last.Reserve(false, nx, n, NSDefine, i) + } else { + nx.Type = NameExprTypeDefine } } } @@ -673,6 +659,12 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { // XXX check node lines and locations checkNodeLinesLocations("XXXpkgPath", "XXXfileName", n) + // Record package-level initialization order dependencies. + // Must run before codaPackageSelectors replaces NameExprs. + if fn, ok := n.(*FileNode); ok { + codaInitOrderDeps(packageOf(ctx), fn) + } + // "coda" means "conclusion". // NOTE: need to use Transcribe() here instead of `bn, ok := n.(BlockNode)` // because say n may be a *CallExpr containing an anonymous function. @@ -955,10 +947,6 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { n.Type.Results[i].Path = n.GetPathForName(nil, name) } - // TRANS_BLOCK ----------------------- - case *SelectCaseStmt: - pushInitBlock(n, &last, &stack) - // TRANS_BLOCK ----------------------- case *SwitchStmt: // create faux block to store .Init/.Varname. @@ -1249,17 +1237,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { cx := evalConst(store, last, n) return cx, TRANS_CONTINUE } - // If name refers to a package, and this is not in - // the context of a selector, fail. Packages cannot - // be used as a value, for go compatibility but also - // to preserve the security expectation regarding imports. - nt := evalStaticTypeOf(store, last, n) - if nt.Kind() == PackageKind { - panic(fmt.Sprintf( - "package %s cannot only be referred to in a selector expression", - n.Name)) - } - return n, TRANS_CONTINUE + panic("slice/array literals may not contain non-const keys") } } // specific and general cases @@ -1398,6 +1376,8 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { Op: n.Op, Right: rn, } + // Mark the uint() conversion as a shift RHS so + // doOpCall can assert non-negative at runtime. n2.Right.SetAttribute(ATTR_SHIFT_RHS, true) resn := Preprocess(store, last, n2) return resn, TRANS_CONTINUE @@ -1538,7 +1518,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Out of bounds errors are usually handled during evalConst(). if isIntNum(ct) { if bd, ok := arg0.TypedValue.V.(BigdecValue); ok { - if !isInteger(bd.V) { + if !isDecimalInteger(bd.V) { panic(fmt.Sprintf( "cannot convert %s to integer type", arg0)) @@ -1566,7 +1546,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // check legal type for nil if arg0.IsUndefined() { switch ct.Kind() { // special case for nil conversion check. - case SliceKind, PointerKind, FuncKind, MapKind, InterfaceKind, ChanKind: + case SliceKind, PointerKind, FuncKind, MapKind, InterfaceKind: convertConst(store, last, n, arg0, ct) default: panic(fmt.Sprintf( @@ -1717,9 +1697,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // NOTE: these appear to be actually special cases in go. // In general, a string is not assignable to []bytes // without conversion. - if cx, ok := n.Func.(*ConstExpr); ok { + if cx, ok := n.Func.(*ConstExpr); ok && cx.GetFunc().PkgPath == uversePkgPath { fv := cx.GetFunc() - if fv.PkgPath == uversePkgPath && fv.Name == "append" { + switch fv.Name { + case "append": if n.Varg && len(n.Args) == 2 { // If the second argument is a string, // convert to byteslice. @@ -1760,7 +1741,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { n.Args[i+1] = Preprocess(nil, last, arg1).(Expr) } } - } else if fv.PkgPath == uversePkgPath && fv.Name == "copy" { + case "copy": if len(n.Args) == 2 { // If the second argument is a string, // convert to byteslice. @@ -1772,9 +1753,112 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { n.Args[1] = args1 } } - } else if fv.PkgPath == uversePkgPath && fv.Name == "cross" { - panic("cross(fn)(...) syntax is deprecated, use fn(cross,...)") - } else if fv.PkgPath == uversePkgPath && fv.Name == "_cross_gno0p0" { + case "make": + // Self-contained handling for make() builtin. + // Validate argument count based on target type, + // check size arguments are integers, resolve generics, + // then skip the general case via TRANS_CONTINUE. + // NOTE: If the general *FuncType call-expr path below changes + // (e.g. embedded-call expansion, generic Specify semantics, or + // checkOrConvertType behaviour), this block may need updating. + ft := bnft + n.NumArgs = countNumArgs(store, last, n) + + if n.Varg { + panic("make does not accept variadic spread (...)") + } + if len(n.Args) == 0 { + panic("missing argument to make") + } + + // Validate arg count per target type. + tt := evalStaticType(store, last, n.Args[0]) + switch baseOf(tt).(type) { + case *SliceType: + if len(n.Args) < 2 || len(n.Args) > 3 { + panic(fmt.Sprintf( + "invalid operation: make(%s) expects 2 or 3 arguments; found %d", + tt, len(n.Args))) + } + case *MapType: + if len(n.Args) > 2 { + panic(fmt.Sprintf( + "invalid operation: make(%s) expects 1 or 2 arguments; found %d", + tt, len(n.Args))) + } + case *ChanType: + panic("channel type is not yet supported") + default: + panic(fmt.Sprintf( + "invalid argument: cannot make %s; type must be slice, map", tt)) + } + + // Reject negative constant size arguments (len, cap, hint). + // Skip n.Args[0] which is the type argument. + for _, arg := range n.Args[1:] { + if cx, ok := arg.(*ConstExpr); ok { + tv := cx.TypedValue + if tv.T == nil || !isNumeric(tv.T) { + panic(fmt.Sprintf( + "cannot use %v as type int in argument to make", tv)) + } + if tv.Sign() < 0 { + panic(fmt.Sprintf( + "invalid argument: index %v must not be negative", tv)) + } + } + } + + // Specify function param/result generics. + argTVs := evalStaticTypedValues(store, last, n.Args...) + isVarg := n.Varg + sft := ft.Specify(store, n, argTVs, isVarg) + spts := sft.Params + srts := FieldTypeList(sft.Results).Types() + + // Update func attributes with specified types. + n.Func.SetAttribute(ATTR_TYPEOF_VALUE, sft) + cx := n.Func.(*ConstExpr) + fv2 := cx.V.(*FuncValue).Copy(nilAllocator) + fv2.Type = sft + cx.T = sft + cx.V = fv2 + n.SetAttribute(ATTR_TYPEOF_VALUE, &tupleType{Elts: srts}) + + // Type-check arguments. + // First arg is the type -- check against resolved param type. + checkOrConvertType(store, last, n, &n.Args[0], spts[0].Type) + + // make's variadic params are declared as Vrd(AnyT()), so untyped + // constants won't be automatically coerced to int; enforce it here. + for i := 1; i < len(n.Args); i++ { + expectedType := spts[len(spts)-1].Type.Elem() + at := evalStaticTypeOf(store, last, n.Args[i]) + switch { + case isUntyped(at): + expectedType = IntType + case !isInteger(at): + panic(fmt.Sprintf( + "invalid argument: index %v (variable of type %v) must be integer", + n.Args[i], at)) + } + checkOrConvertType(store, last, n, &n.Args[i], expectedType) + } + + // For slices with 3 args, check len <= cap when both are constants. + if _, ok := baseOf(tt).(*SliceType); ok && len(n.Args) == 3 { + lcx, lOk := n.Args[1].(*ConstExpr) + ccx, cOk := n.Args[2].(*ConstExpr) + if lOk && cOk { + if lcx.TypedValue.GetInt() > ccx.TypedValue.GetInt() { + panic(fmt.Sprintf( + "invalid argument: len larger than cap in make(%s)", tt)) + } + } + } + + return n, TRANS_CONTINUE + case "_cross_gno0p0": if ctxpn.GetAttribute(ATTR_FIX_FROM) == GnoVerMissing { // This is only backwards compatibility for the gno 0.9 // transpiler/fixer. cross() is no longer used. @@ -1790,11 +1874,13 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // only way _cross_gno0p0 appears is panic("_cross_gno0p0 is reserved") } - } else if fv.PkgPath == uversePkgPath && fv.Name == "crossing" { + case "cross": + panic("cross(fn)(...) syntax is deprecated, use fn(cross,...)") + case "crossing": if ctxpn.GetAttribute(ATTR_FIX_FROM) != GnoVerMissing { panic("crossing() is reserved and deprecated") } - } else if fv.PkgPath == uversePkgPath && fv.Name == "attach" { + case "attach": // reserve attach() so we can support it later. panic("attach() not yet supported") } @@ -2013,8 +2099,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type) } else { - checkOrConvertType(store, last, n, &n.Args[i], - spts[len(spts)-1].Type.Elem()) + checkOrConvertType(store, last, n, &n.Args[i], spts[len(spts)-1].Type.Elem()) } } else { checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type) @@ -2045,7 +2130,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case StringKind, ArrayKind, SliceKind: // Replace const index with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n, n.Index) + checkOrConvertIndexKind(store, last, n, n.Index) case MapKind: mt := baseOf(dt).(*MapType) checkOrConvertType(store, last, n, &n.Index, mt.Key) @@ -2059,9 +2144,9 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case *SliceExpr: // Replace const L/H/M with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n, n.Low) - checkOrConvertIntegerKind(store, last, n, n.High) - checkOrConvertIntegerKind(store, last, n, n.Max) + checkOrConvertIndexKind(store, last, n, n.Low) + checkOrConvertIndexKind(store, last, n, n.High) + checkOrConvertIndexKind(store, last, n, n.Max) t := evalStaticTypeOf(store, last, n.X) switch t.Kind() { @@ -2144,18 +2229,38 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } case *ArrayType: for i := range n.Elts { + if cx, ok := n.Elts[i].Key.(*ConstExpr); ok && cx.TypedValue.Sign() < 0 { + panic(fmt.Sprintf("invalid argument: index must not be negative: %v", cx.TypedValue)) + } convertType(store, last, n, &n.Elts[i].Key, IntType) checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt) } case *SliceType: for i := range n.Elts { + if cx, ok := n.Elts[i].Key.(*ConstExpr); ok && cx.TypedValue.Sign() < 0 { + panic(fmt.Sprintf("invalid argument: index must not be negative: %v", cx.TypedValue)) + } convertType(store, last, n, &n.Elts[i].Key, IntType) checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt) } case *MapType: - for i := range n.Elts { + if !isComparable(cclt.Key) { + panic(fmt.Sprintf("invalid map key type %v", cclt.Key)) + } + + // kset tracks seen const keys for duplicate detection. + // checkOrConvertType must be called before the check so that + // values are stored in N (not V), making TypedValue comparable. + kset := make(map[TypedValue]struct{}) + for i, elt := range n.Elts { checkOrConvertType(store, last, n, &n.Elts[i].Key, cclt.Key) checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Value) + if cx, ok := elt.Key.(*ConstExpr); ok && !cx.TypedValue.IsUndefined() { + if _, ok := kset[cx.TypedValue]; ok { + panic(fmt.Sprintf("duplicate key %v in map literal", cx.TypedValue)) + } + kset[cx.TypedValue] = struct{}{} + } } default: panic(fmt.Sprintf( @@ -2202,6 +2307,18 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { panic(fmt.Sprintf("invalid operation: cannot indirect %s (variable of type %s)", n.X.String(), xt.String())) } // TRANS_LEAVE ----------------------- + case *RefExpr: + // Cache the static type of X as ATTR_REF_ELEM_TYPE + // on the RefExpr so doOpRef can use it (instead of + // the runtime type, which is wrong for interface variables). + xt := evalStaticTypeOf(store, last, n.X) + if tt, ok := xt.(*tupleType); ok { + panic(fmt.Sprintf( + "cannot take address of multi-value call (results: %s)", + tt.String())) + } + n.SetAttribute(ATTR_REF_ELEM_TYPE, xt) + // TRANS_LEAVE ----------------------- case *SelectorExpr: xt := evalStaticTypeOf(store, last, n.X) @@ -2255,7 +2372,9 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // value: t.Mp is equivalent to (&t).Mp." // // convert to (&x).m, but leave xt as is. - n.X = &RefExpr{X: n.X} + rx := &RefExpr{X: n.X} + rx.SetAttribute(ATTR_REF_ELEM_TYPE, nxt2) + n.X = rx setPreprocessed(n.X) switch tr[len(tr)-1].Type { case VPDerefPtrMethod: @@ -2281,12 +2400,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Case 2: If tr[0] is deref type, but xt // is not pointer type, replace n.X with // &RefExpr{X: n.X}. - n.X = &RefExpr{X: n.X} + rx := &RefExpr{X: n.X} + rx.SetAttribute(ATTR_REF_ELEM_TYPE, nxt2) + n.X = rx setPreprocessed(n.X) } // bound method or underlying. // TODO check for unexported fields. n.Path = tr[len(tr)-1] + // n.Path = cxt.GetPathForName(n.Sel) case *PackageType: var pv *PackageValue @@ -2380,10 +2502,6 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case *InterfaceTypeExpr: evalStaticType(store, last, n) - // TRANS_LEAVE ----------------------- - case *ChanTypeExpr: - evalStaticType(store, last, n) - // TRANS_LEAVE ----------------------- case *FuncTypeExpr: evalStaticType(store, last, n) @@ -2677,18 +2795,6 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } } - // TRANS_LEAVE ----------------------- - case *SendStmt: - // Value consts become default *ConstExprs. - checkOrConvertType(store, last, n, &n.Value, nil) - - // TRANS_LEAVE ----------------------- - case *SelectCaseStmt: - // maybe receive defines. - // if as, ok := n.Comm.(*AssignStmt); ok { - // handled by case *AssignStmt. - // } - // TRANS_LEAVE ----------------------- case *SwitchStmt: // Ensure type switch cases are unique. @@ -2777,8 +2883,6 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { *dstT = *(tmp.(*SliceType)) case *InterfaceType: *dstT = *(tmp.(*InterfaceType)) - case *ChanType: - *dstT = *(tmp.(*ChanType)) case *MapType: *dstT = *(tmp.(*MapType)) case *StructType: @@ -2844,6 +2948,12 @@ func defineOrDecl( if numVals > 1 && numNames != numVals { panic(fmt.Sprintf("assignment mismatch: %d variable(s) but %d value(s)", numNames, numVals)) } + if isConst && numVals == 0 && numNames > 0 && typeExpr == nil { + // This occurs when a const group line inherits values from the previous + // line but the number of names doesn't match (go2gno sets Values to nil + // in that case). Report a proper error instead of a confusing internal panic. + panic(fmt.Sprintf("assignment mismatch: %d variable(s) but 0 value(s)", numNames)) + } sts := make([]Type, numNames) // static types tvs := make([]TypedValue, numNames) @@ -4278,6 +4388,7 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type) { // Convert untyped to typed. checkOrConvertType(store, last, n, &bx.Left, t) + bx.SetAttribute(ATTR_TYPEOF_VALUE, t) // propagate converted type from left operand to shift expr. } else { mustAssignableTo(n, xt, t) } @@ -4656,8 +4767,6 @@ func findUndefinedAny(store Store, last BlockNode, x Expr, stack []Name, definin return } } - case *ChanTypeExpr: - return findUndefinedT(store, last, cx.Value, stack, defining, isalias, astype && isalias) case *FuncTypeExpr: for i := range cx.Params { un, directR = findUndefinedT(store, last, &cx.Params[i], stack, defining, isalias, astype && isalias) @@ -4745,6 +4854,15 @@ func checkBoolKind(xt Type) { } } +// checkOrConvertIndexKind ensures the expression evaluates to an integer +// and, if it is a constant, ensures it is non-negative. +func checkOrConvertIndexKind(store Store, last BlockNode, n Node, x Expr) { + if cx, ok := x.(*ConstExpr); ok && cx.TypedValue.Sign() < 0 { + panic(fmt.Sprintf("invalid argument: index must not be negative: %v", cx.TypedValue)) + } + checkOrConvertIntegerKind(store, last, n, x) +} + // like checkOrConvertType() but for any typed integer kind. func checkOrConvertIntegerKind(store Store, last BlockNode, n Node, x Expr) { if cx, ok := x.(*ConstExpr); ok { @@ -4805,8 +4923,11 @@ func predefineRecursively(store Store, last BlockNode, d Decl) bool { func predefineRecursively2(store Store, last BlockNode, d Decl, stack []Name, defining map[Name]struct{}, direct bool) bool { pkg := packageOf(last) - // NOTE: predefine fileset breaks up circular definitions like - // `var a, b, c = 1, a, b` which is only legal at the file level. + // NOTE: PredefineFileSet splits multi-value decls like `var a, b = c, d` + // into individual decls before calling this function, so that GetDeclFor + // resolves each name to its own standalone decl. Without the split, + // all names in the original multi-value decl would be added to `defining`, + // causing false cycle detection for valid DAG dependencies. for _, dn := range d.GetDeclNames() { if isUverseName(dn) { panic(fmt.Sprintf( @@ -5004,8 +5125,6 @@ func tryPredefine(store Store, pkg *PackageNode, last BlockNode, d Decl, stack [ t = &SliceType{} case *InterfaceTypeExpr: t = &InterfaceType{} - case *ChanTypeExpr: - t = &ChanType{} case *MapTypeExpr: t = &MapType{} case *StructTypeExpr: @@ -5318,14 +5437,6 @@ func fillNameExprPath(last BlockNode, nx *NameExpr, isDefineLHS bool) { nx.Path = last.GetPathForName(nil, nx.Name) } -func isFile(n BlockNode) bool { - if _, ok := n.(*FileNode); ok { - return true - } else { - return false - } -} - func skipFile(n BlockNode) BlockNode { if fn, ok := n.(*FileNode); ok { return packageOf(fn) @@ -5405,6 +5516,7 @@ func elideCompositeExpr(last BlockNode, x *Expr, t Type) { if t.Kind() == PointerKind { clx.Type = toConstTypeExpr(last, tx, t.Elem()) refx := &RefExpr{X: clx} + refx.SetAttribute(ATTR_REF_ELEM_TYPE, t.Elem()) refx.SetSpan(clx.GetSpan()) *x = refx elideCompositeElements(last, clx, t.Elem()) // recurse @@ -5435,134 +5547,258 @@ func countNumArgs(store Store, last BlockNode, n *CallExpr) (numArgs int) { } } -// This is to be run *after* preprocessing is done, -// to determine the order of var decl execution -// (which may include functions which may refer to package vars). -func findDependentNames(n Node, dst map[Name]struct{}) { - switch cn := n.(type) { - case *NameExpr: - dst[cn.Name] = struct{}{} - case *BasicLitExpr: - case *BinaryExpr: - findDependentNames(cn.Left, dst) - findDependentNames(cn.Right, dst) - case *SelectorExpr: - findDependentNames(cn.X, dst) - case *SliceExpr: - findDependentNames(cn.X, dst) - if cn.Low != nil { - findDependentNames(cn.Low, dst) - } - if cn.High != nil { - findDependentNames(cn.High, dst) +// codaInitOrderDeps records ATTR_DECL_DEPS on package-level *ValueDecl and +// *FuncDecl nodes by scanning for all references to other package-level names. +// It must run after preprocess1 (so all NameExpr paths are filled) and before +// codaPackageSelectors (which replaces NameExprs with SelectorExprs). +// +// This is implemented as a separate post-preprocess pass (rather than inline +// within preprocess1) because preprocess1 recursively calls Preprocess for +// inner function literal bodies with a fresh context, losing the enclosing +// declaration. By iterating fn.Decls and transcribing each one, the target +// declaration is always known from the outer loop. +func codaInitOrderDeps(pn *PackageNode, fn *FileNode) { + if pn.PkgPath == ".uverse" { + return + } + for _, decl := range fn.Decls { + switch decl.(type) { + case *FuncDecl, *ValueDecl: + default: + continue } - if cn.Max != nil { - findDependentNames(cn.Max, dst) + + var deps map[Name]struct{} + addDep := func(name Name) { + if deps == nil { + deps = make(map[Name]struct{}) + } + deps[name] = struct{}{} } - case *StarExpr: - findDependentNames(cn.X, dst) - case *RefExpr: - findDependentNames(cn.X, dst) - case *TypeAssertExpr: - findDependentNames(cn.X, dst) - findDependentNames(cn.Type, dst) - case *UnaryExpr: - findDependentNames(cn.X, dst) - case *CompositeLitExpr: - findDependentNames(cn.Type, dst) - ct := getType(cn.Type) - switch ct.Kind() { - case ArrayKind, SliceKind, MapKind: - for _, kvx := range cn.Elts { - if kvx.Key != nil { - findDependentNames(kvx.Key, dst) + + _ = TranscribeB(fn, decl, func( + ns []Node, + stack []BlockNode, + last BlockNode, + ftype TransField, + index int, + n Node, + stage TransStage, + ) (Node, TransCtrl) { + switch stage { + case TRANS_ENTER: + // Skip function literal bodies that were not preprocessed; + // same guard as the main coda Transcribe in Preprocess. + if flx, ok := n.(*FuncLitExpr); ok { + if flx.GetAttribute(ATTR_PREPROCESS_SKIPPED) == AttrPreprocessFuncLitExpr { + return n, TRANS_SKIP + } + } + case TRANS_LEAVE: + switch n := n.(type) { + case *NameExpr: + // Only track names resolved via the block hierarchy. + if n.Path.Type != VPBlock { + return n, TRANS_CONTINUE + } + // Ignore blank identifiers. + if n.Name == blankIdentifier { + return n, TRANS_CONTINUE + } + // Ignore package-name references (e.g. `fmt` in `fmt.Println`). + if n.GetAttribute(ATTR_PACKAGE_REF) != nil { + return n, TRANS_CONTINUE + } + // Ignore the declaration name itself (LHS of var/const decl). + if ftype == TRANS_VAR_NAME { + return n, TRANS_CONTINUE + } + // Check that the name is defined at package level. + dbn := last.GetBlockNodeForPath(nil, n.Path) + if dbn != pn { + return n, TRANS_CONTINUE + } + // Ignore type declarations; they have no runtime + // initialization order. + if li, ok := pn.GetLocalIndex(n.Name); ok { + if pn.NameSources[li].Type == NSTypeDecl { + return n, TRANS_CONTINUE + } + } + addDep(n.Name) + case *SelectorExpr: + // Track same-package method calls so that resolveEffectiveDeps + // can transitively discover vars referenced in method bodies. + // e.g. `A = T{}.GetB()` records "T.GetB" as a dep of A; + // resolveEffectiveDeps then walks GetB's body to find B. + switch n.Path.Type { + case VPValMethod, VPPtrMethod, VPDerefValMethod, VPDerefPtrMethod: + // Get the receiver type from ATTR_REF_ELEM_TYPE + // on the RefExpr, or ATTR_TYPEOF_VALUE on n.X. + // + // For auto-addressed receivers (n.X is a synthetic *RefExpr), + // read from ATTR_REF_ELEM_TYPE. For already-pointer receivers + // (VPDerefValMethod/VPDerefPtrMethod), n.X is not a RefExpr; + // fall back to ATTR_TYPEOF_VALUE on the expression itself. + var xt Type + var ok bool + if rx, ok2 := n.X.(*RefExpr); ok2 { + xt, ok = rx.GetAttribute(ATTR_REF_ELEM_TYPE).(Type) + } + if !ok { + xt, ok = n.X.GetAttribute(ATTR_TYPEOF_VALUE).(Type) + } + if !ok { + break + } + // Dereference pointer receiver types. + if pt, ok2 := xt.(*PointerType); ok2 { + xt = pt.Elt + } + dt, ok := xt.(*DeclaredType) + if !ok || dt.PkgPath != pn.PkgPath { + break + } + addDep(dt.Name + "." + n.Sel) + } } - findDependentNames(kvx.Value, dst) - } - case StructKind: - for _, kvx := range cn.Elts { - findDependentNames(kvx.Value, dst) } - default: - panic(fmt.Sprintf( - "unexpected composite lit type %s", - ct.String())) - } - case *FieldTypeExpr: - findDependentNames(cn.Type, dst) - case *ArrayTypeExpr: - findDependentNames(cn.Elt, dst) - if cn.Len != nil { - findDependentNames(cn.Len, dst) - } - case *SliceTypeExpr: - findDependentNames(cn.Elt, dst) - case *InterfaceTypeExpr: - for i := range cn.Methods { - findDependentNames(&cn.Methods[i], dst) - } - case *ChanTypeExpr: - findDependentNames(cn.Value, dst) - case *FuncTypeExpr: - for i := range cn.Params { - findDependentNames(&cn.Params[i], dst) - } - for i := range cn.Results { - findDependentNames(&cn.Results[i], dst) + return n, TRANS_CONTINUE + }) + + if deps != nil { + decl.SetAttribute(ATTR_DECL_DEPS, deps) } - case *MapTypeExpr: - findDependentNames(cn.Key, dst) - findDependentNames(cn.Value, dst) - case *StructTypeExpr: - for i := range cn.Fields { - findDependentNames(&cn.Fields[i], dst) + } +} + +// resolveDeclDep resolves a dependency name (as stored in ATTR_DECL_DEPS) to +// the corresponding Decl in pn. Method dependencies are encoded as "Type.Method". +func resolveDeclDep(name Name, pn *PackageNode) Decl { + id, sel, isMethod := strings.Cut(string(name), ".") + if isMethod { + li, found := pn.GetLocalIndex(Name(id)) + if !found || pn.NameSources[li].Type != NSTypeDecl { + panic(fmt.Sprintf("type %s not found in package %s", id, pn.PkgName)) } - case *CallExpr: - findDependentNames(cn.Func, dst) - for i := range cn.Args { - findDependentNames(cn.Args[i], dst) + dt, ok := pn.Types[li].(*DeclaredType) + if !ok { + panic(fmt.Sprintf("type %s is not a *DeclaredType in package %s", id, pn.PkgName)) } - case *IndexExpr: - findDependentNames(cn.X, dst) - findDependentNames(cn.Index, dst) - case *FuncLitExpr: - findDependentNames(&cn.Type, dst) - for _, n := range cn.GetExternNames() { - dst[n] = struct{}{} + idx := slices.IndexFunc(dt.Methods, func(m TypedValue) bool { + return m.V.(*FuncValue).Name == Name(sel) + }) + if idx < 0 { + panic(fmt.Sprintf("method %s not found in type %s", sel, id)) } - case *constTypeExpr: - case *ConstExpr: - case *ImportDecl: - case *ValueDecl: - if cn.Type != nil { - findDependentNames(cn.Type, dst) + return dt.Methods[idx].V.(*FuncValue).Source.(*FuncDecl) + } + li, found := pn.GetLocalIndex(name) + if !found { + panic(fmt.Sprintf("name %s not found in package %s", name, pn.PkgName)) + } + return pn.NameSources[li].Origin.(Decl) +} + +// resolveEffectiveDeps computes, for every Decl reachable from the given +// declarations, the set of *ValueDecl dependencies obtained by collapsing +// FuncDecl edges (FuncDecls are transparent pass-throughs). +// +// The result is a shared cache: cache[d] = list of *ValueDecl that d +// (transitively through FuncDecls) depends on. Each Decl is visited at most +// once across all calls, so total work is O(V+E). +// +// Circular variable dependencies are detected and cause a panic with a +// descriptive chain. +func resolveEffectiveDeps(decls []Decl, pn *PackageNode, fdeclared map[Name]struct{}) map[Decl][]*ValueDecl { + cache := map[Decl][]*ValueDecl{} // fully resolved + onStack := map[Decl]bool{} // grey: currently in DFS path + + // inFDeclared reports whether all names declared by d are already in + // fdeclared, meaning d has already been initialized. + inFDeclared := func(d *ValueDecl) bool { + for _, n := range d.GetDeclNames() { + if _, ok := fdeclared[n]; !ok { + return false + } } - for _, vx := range cn.Values { - findDependentNames(vx, dst) + return true + } + + var walk func(d Decl, path []Name) []*ValueDecl + walk = func(d Decl, path []Name) []*ValueDecl { + if res, ok := cache[d]; ok { + return res } - case *TypeDecl: - findDependentNames(cn.Type, dst) - case *FuncDecl: - findDependentNames(&cn.Type, dst) - if cn.IsMethod { - findDependentNames(&cn.Recv, dst) - for _, n := range cn.GetExternNames() { - dst[n] = struct{}{} - } - } else { - for _, n := range cn.GetExternNames() { - if n == cn.Name { - // top-level function referring to itself - } else { - dst[n] = struct{}{} + onStack[d] = true + m, _ := d.GetAttribute(ATTR_DECL_DEPS).(map[Name]struct{}) + // Sort dependency names for deterministic DFS traversal order. + names := slices.Collect(maps.Keys(m)) + slices.Sort(names) + + var result []*ValueDecl + for _, name := range names { + dep := resolveDeclDep(name, pn) + switch dep := dep.(type) { + case *FuncDecl: + if onStack[dep] { + // Mutually recursive functions are fine; skip. + continue + } + // Collapse: inherit effective deps from the FuncDecl. + for _, vd := range walk(dep, append(path, name)) { + if !slices.Contains(result, vd) { + result = append(result, vd) + } + } + case *ValueDecl: + if onStack[dep] { + bld := strings.Builder{} + if fn, sourceDecl, ok := pn.FileSet.GetDeclForSafe(path[0]); ok { + fmt.Fprintf(&bld, "%s/%s:%s: ", pn.PkgPath, fn.FileName, (*sourceDecl).GetSpan().Pos.String()) + } + bld.WriteString("circular dependency: ") + for _, n := range path { + bld.WriteString(string(n)) + bld.WriteString(" -> ") + } + bld.WriteString(string(name)) + panic(bld.String()) } + // Skip already-initialized decls entirely. + if len(fdeclared) > 0 && inFDeclared(dep) { + continue + } + if !slices.Contains(result, dep) { + result = append(result, dep) + } + // Recurse into ValueDecl deps to detect cycles and to + // discover transitive deps through FuncDecl chains rooted + // in this ValueDecl. Kahn's handles direct ValueDecl→ValueDecl + // transitivity, but we still need to walk through for cycle + // detection and for FuncDecl collapse. + walk(dep, append(path, name)) + default: + panic(fmt.Sprintf("unexpected gnolang.Decl: %#v", dep)) } } - default: - panic(fmt.Sprintf( - "unexpected node: %v (%v)", - n, reflect.TypeOf(n))) + delete(onStack, d) + cache[d] = result + return result + } + + for _, d := range decls { + if _, ok := cache[d]; ok { + continue + } + rootNames := d.GetDeclNames() + rootName := Name("_") + if len(rootNames) > 0 { + rootName = rootNames[0] + } + walk(d, []Name{rootName}) } + return cache } // A name is locally defined on a block node @@ -5579,9 +5815,8 @@ func isLocallyDefined(bn BlockNode, n Name) bool { return t != nil } -// r := 0 -// r, ok := 1, true -func isLocallyDefined2(bn BlockNode, n Name) bool { +// if name is is reserved. +func isLocallyReserved(bn BlockNode, n Name) bool { _, isLocal := bn.GetLocalIndex(n) return isLocal } diff --git a/gnovm/pkg/gnolang/preprocess_test.go b/gnovm/pkg/gnolang/preprocess_test.go new file mode 100644 index 00000000000..8fb2bef4793 --- /dev/null +++ b/gnovm/pkg/gnolang/preprocess_test.go @@ -0,0 +1,164 @@ +package gnolang_test + +import ( + "fmt" + "io" + "path/filepath" + "strings" + "testing" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestInitOrderDeterminism verifies that package-level variable initialization +// always produces the same result across many runs, regardless of Go's +// non-deterministic map iteration over internal dependency sets. +// It runs a program with complex cross-variable dependencies 100 times and +// checks that the initialization order is always Go-spec-compliant. +func TestInitOrderDeterminism(t *testing.T) { + // This source has vars that all depend (transitively, through emit) on + // 'events', but Z also depends on A,B,C,D,E. The Go spec mandates that + // the earliest-in-source-order ready variable is initialized next. + // Expected order: events, B, A, C, G, D, E, Z (emit("L")), F. + const src = `package main + +var events []string +func emit(s string) string { events = append(events, s); return s } +var ( + Z = A + "-" + B + "-" + C + "-" + emit("L") + D + "-" + E + B = emit("B") + A = emit("A") + C = emit("C") + G = emit("G") + D = emit("D") + E = emit("E") + F = emit("F") +) +func main() { + for _, e := range events { println(e) } +} + +// Output: +// B +// A +// C +// G +// D +// E +// L +// F +` + rootDir, err := filepath.Abs("../../../") + require.NoError(t, err) + + newOpts := func() *test.TestOptions { + opts := test.NewTestOptions(rootDir, io.Discard, io.Discard, nil) + return opts + } + sharedOpts := newOpts() + + const iters = 100 + for i := 0; i < iters; i++ { + t.Run(fmt.Sprintf("iter%d", i), func(t *testing.T) { + _, _, err := sharedOpts.RunFiletest("init_order_det.gno", []byte(src), sharedOpts.TestStore) + require.NoError(t, err, "init order non-determinism or mismatch detected on iteration %d", i) + }) + } +} + +// TestCircDepDeterminism verifies that circular-dependency error messages are +// produced in a deterministic order even when a declaration has multiple +// function dependencies (each of which independently reads the same variable). +// +// Without sorting: `var a = B() + C()` causes a's ATTR_DECL_DEPS to hold +// {B, C}. Because Go map iteration is non-deterministic, the DFS in +// findUnresolvedDeps could traverse B first ("circular dependency: a -> B") +// or C first ("circular dependency: a -> C"), making the panic message flaky. +// +// After the fix (sorting ATTR_DECL_DEPS keys before DFS), the message is +// always deterministic ("a -> B" because "B" < "C" lexicographically). +func TestCircDepDeterminism(t *testing.T) { + const src = `package main + +var a = B() + C() + +func B() int { return a } + +func C() int { return a } + +func main() {}` + + rootDir, err := filepath.Abs("../../../") + require.NoError(t, err) + + opts := test.NewTestOptions(rootDir, io.Discard, io.Discard, nil) + + const iters = 100 + var seen []string + for i := 0; i < iters; i++ { + _, _, runErr := opts.RunFiletest("circ_dep_det.gno", []byte(src), opts.TestStore) + // A circular dep error is expected; extract just the "circular dependency: ..." line. + if runErr == nil { + t.Fatalf("iteration %d: expected circular dep error, got none", i) + } + errMsg := runErr.Error() + idx := strings.Index(errMsg, "circular dependency: ") + if idx < 0 { + // May be a TypeCheckError mismatch with no circular dep in the message; skip. + continue + } + end := strings.IndexByte(errMsg[idx:], '\n') + var circdep string + if end >= 0 { + circdep = errMsg[idx : idx+end] + } else { + circdep = errMsg[idx:] + } + found := false + for _, s := range seen { + if s == circdep { + found = true + break + } + } + if !found { + seen = append(seen, circdep) + } + } + if len(seen) > 1 { + t.Errorf("circular dependency error message is non-deterministic across %d runs: %v", iters, seen) + } +} + +func TestShiftExprAttrTypeOfValue(t *testing.T) { + t.Parallel() + + m := gno.NewMachine("test", nil) + c := `package test +func main() { + var a uint = 1 + b := make([]byte, 1< 0 { + res += "," + } + res += gnoCoinString(base.List[sv.Offset+i].V.(*StructValue)) + } + m.PushValue(typedString(res)) + }, + ) def("realm", asValue(gRealmType)) def(".grealm", asValue(gConcreteRealmType)) defNativeMethod(".grealm", "Address", @@ -1042,6 +1106,61 @@ func makeUverseNode() { panic("not yet implemented") }, ) + defNativeMethod(".grealm", "SentCoins", + nil, // params + Flds( // results + "", "gnocoins", + ), + func(m *Machine) { + // Only return coins if the caller of SentCoins() is the first realm + // in the call stack, i.e. the realm that actually received the funds. + // + // Frame.LastPackage is the package that was active before the frame + // was pushed (the caller's package). So the innermost frame's + // LastPackage is the package that invoked SentCoins(). + var callerPkg string + if lp := m.Frames[m.NumFrames()-1].LastPackage; lp != nil { + callerPkg = lp.PkgPath + } + // Walk frames from oldest to newest; the first LastPackage that is + // a realm path is the first realm that appeared in the call chain. + var firstRealmPkg string + for i := 1; i < m.NumFrames(); i++ { + lp := m.Frames[i].LastPackage + if lp == nil { + continue + } + if pkg := lp.PkgPath; IsRealmPath(pkg) { + firstRealmPkg = pkg + break + } + } + var coins std.Coins + if callerPkg != "" && firstRealmPkg == callerPkg { + if osp, ok := m.Context.(OriginSendProvider); ok { + coins = osp.GetOriginSend() + } + } + // Manually construct a gnocoins. + n := len(coins) + baseArray := m.Alloc.NewListArray(n) + for i, coin := range coins { + fields := m.Alloc.NewStructFields(2) + fields[0] = TypedValue{T: StringType} + fields[0].V = m.Alloc.NewString(coin.Denom) + fields[1] = TypedValue{T: Int64Type} + fields[1].SetInt64(coin.Amount) + baseArray.List[i] = TypedValue{ + T: gCoinType, + V: m.Alloc.NewStruct(fields), + } + } + m.PushValue(TypedValue{ + T: gCoinsType, + V: m.Alloc.NewSlice(baseArray, 0, n, n), + }) + }, + ) defNativeMethod(".grealm", "Send", Flds( // params "coins", "gnocoins", diff --git a/gnovm/pkg/gnolang/uverse_test.go b/gnovm/pkg/gnolang/uverse_test.go index cfbce67b06f..1f93b88e70e 100644 --- a/gnovm/pkg/gnolang/uverse_test.go +++ b/gnovm/pkg/gnolang/uverse_test.go @@ -100,7 +100,7 @@ func TestIssue1337PrintNilSliceAsUndefined(t *testing.T) { name: "print composite slice", code: `package test func main() { - a, b, c, d := 1, 2, 3, 4 + const a, b, c, d = 1, 2, 3, 4 x := []int{ a: b, c: d, diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index b3dd763f652..f322e4f7fd5 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1,7 +1,5 @@ package gnolang -// XXX TODO address "this is wrong, for var i interface{}; &i is *interface{}." - import ( "encoding/binary" "fmt" @@ -219,6 +217,9 @@ func (pv PointerValue) Assign2(alloc *Allocator, store Store, rlm *Realm, tv2 Ty if pv.TV.T == DataByteType { // Special case of DataByte into (base=*SliceValue).Data. pv.TV.SetDataByte(tv2.GetUint8()) + if rlm != nil && pv.Base != nil { + rlm.DidUpdate(pv.Base.(Object), nil, nil) + } return } // General case @@ -328,7 +329,9 @@ func (av *ArrayValue) Copy(alloc *Allocator) *ArrayValue { */ if av.Data == nil { av2 := alloc.NewListArray(len(av.List)) - copy(av2.List, av.List) + for i, tv := range av.List { + av2.List[i] = tv.Copy(alloc) + } return av2 } av2 := alloc.NewDataArray(len(av.Data)) @@ -375,13 +378,13 @@ func (sv *SliceValue) GetPointerAtIndexInt2(store Store, ii int, et Type) Pointe if ii < 0 { excpt := &Exception{ Value: typedString(fmt.Sprintf( - "slice index out of bounds: %d", ii)), + "runtime error: slice index out of bounds: %d", ii)), } panic(excpt) } else if sv.Length <= ii { excpt := &Exception{ Value: typedString(fmt.Sprintf( - "slice index out of bounds: %d (len=%d)", + "runtime error: slice index out of bounds: %d (len=%d)", ii, sv.Length)), } panic(excpt) @@ -656,15 +659,16 @@ func (ml MapList) MarshalAmino() (MapListImage, error) { func (ml *MapList) UnmarshalAmino(mlimg MapListImage) error { for i, item := range mlimg.List { if i == 0 { + item.Prev = nil ml.Head = item ml.Tail = item - item.Prev = nil + ml.Size = 1 } else { item.Prev = ml.Tail ml.Tail.Next = item ml.Tail = item + ml.Size++ } - ml.Size++ } return nil } @@ -681,15 +685,19 @@ func (ml *MapList) Append(alloc *Allocator, key TypedValue) *MapListItem { if ml.Head == nil { ml.Head = item ml.Tail = item + ml.Size = 1 } else { ml.Tail.Next = item ml.Tail = item + ml.Size++ } - ml.Size++ return item } func (ml *MapList) Remove(mli *MapListItem) { + if ml.Size == 0 { + return + } prev, next := mli.Prev, mli.Next if prev == nil { ml.Head = next @@ -996,7 +1004,7 @@ func (tv *TypedValue) IsTypedNil() bool { } if tv.T != nil { switch tv.T.Kind() { - case SliceKind, FuncKind, MapKind, InterfaceKind, PointerKind, ChanKind: + case SliceKind, FuncKind, MapKind, InterfaceKind, PointerKind: return true } } @@ -1552,12 +1560,6 @@ func (tv *TypedValue) Sign() int { } } -func (tv *TypedValue) AssertNonNegative(msg string) { - if tv.Sign() < 0 { - panic(fmt.Sprintf("%s: %v", msg, tv)) - } -} - // ComputeMapKey returns the value of tv, encoded as a string for usage inside // of a map. // @@ -1614,7 +1616,7 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) (key MapKey, isN } } case FieldType: - panic("field (pseudo)type cannot be used as map key") + panic(&Exception{Value: typedString("runtime error: field (pseudo)type cannot be used as map key")}) case *ArrayType: av := tv.V.(*ArrayValue) al := av.GetLength() @@ -1641,7 +1643,7 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) (key MapKey, isN } bz = append(bz, ']') case *SliceType: - panic("slice type cannot be used as map key") + panic(&Exception{Value: typedString("runtime error: slice type cannot be used as map key")}) case *StructType: sv := tv.V.(*StructValue) sl := len(sv.Fields) @@ -1661,7 +1663,7 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) (key MapKey, isN } bz = append(bz, '}') case *ChanType: - panic("not yet implemented") + panic("channel type is not yet supported") default: panic(fmt.Sprintf( "unexpected map key type %s", @@ -1782,7 +1784,7 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val path.SetDepth(0) case 2: if tv.V == nil { - panic(&Exception{Value: typedString("nil pointer dereference")}) + panic(&Exception{Value: typedString("runtime error: nil pointer dereference")}) } dtv = tv.V.(PointerValue).TV isPtr = true @@ -1798,7 +1800,7 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val } case VPDerefValMethod: if tv.V == nil { - panic(&Exception{Value: typedString("nil pointer dereference")}) + panic(&Exception{Value: typedString("runtime error: nil pointer dereference")}) } dtv2 := tv.V.(PointerValue).TV dtv = &TypedValue{ // In case method is called on converted type, like ((*othertype)x).Method(). @@ -1985,10 +1987,10 @@ func (tv *TypedValue) GetPointerAtIndex(rlm *Realm, alloc *Allocator, store Stor } if ii >= len(sv) { - panic(&Exception{Value: typedString(fmt.Sprintf("index out of range [%d] with length %d", ii, len(sv)))}) + panic(&Exception{Value: typedString(fmt.Sprintf("runtime error: index out of range [%d] with length %d", ii, len(sv)))}) } if ii < 0 { - panic(&Exception{Value: typedString(fmt.Sprintf("invalid slice index %d (index must be non-negative)", ii))}) + panic(&Exception{Value: typedString(fmt.Sprintf("runtime error: invalid slice index %d (index must be non-negative)", ii))}) } btv.SetUint8(sv[ii]) @@ -2006,14 +2008,14 @@ func (tv *TypedValue) GetPointerAtIndex(rlm *Realm, alloc *Allocator, store Stor return av.GetPointerAtIndexInt2(store, ii, bt.Elt) case *SliceType: if tv.V == nil { - panic("nil slice index (out of bounds)") + panic(&Exception{Value: typedString("runtime error: nil slice index (out of bounds)")}) } sv := tv.V.(*SliceValue) ii := int(iv.ConvertGetInt()) return sv.GetPointerAtIndexInt2(store, ii, bt.Elt) case *MapType: if tv.V == nil { - panic(&Exception{Value: typedString("uninitialized map index")}) + panic(&Exception{Value: typedString("runtime error: uninitialized map index")}) } mv := tv.V.(*MapValue) @@ -2038,7 +2040,10 @@ func (tv *TypedValue) GetPointerAtIndex(rlm *Realm, alloc *Allocator, store Stor *(pv.TV) = defaultTypedValue(nil, vt) } } - // attach mapkey object, if changed + // Attach mapkey object to the map's ownership tree if changed. + // Only PopAsPointer2 (write path) reaches here with non-nil rlm, + // and it checks readonly before calling. + // Read paths (doOpIndex, debugger) pass nilRealm → DidUpdate is a no-op. newObject := ivk.GetFirstObject(store) if oldObject != newObject { rlm.DidUpdate(mv, oldObject, newObject) @@ -2161,24 +2166,24 @@ func (tv *TypedValue) GetCapacity() int { func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { if low < 0 { panic(&Exception{Value: typedString(fmt.Sprintf( - "invalid slice index %d (index must be non-negative)", + "runtime error: invalid slice index %d (index must be non-negative)", low))}) } if high < 0 { panic(&Exception{Value: typedString(fmt.Sprintf( - "invalid slice index %d (index must be non-negative)", + "runtime error: invalid slice index %d (index must be non-negative)", low))}) } if low > high { panic(&Exception{Value: typedString(fmt.Sprintf( - "invalid slice index %d > %d", + "runtime error: invalid slice index %d > %d", low, high))}) } switch t := baseOf(tv.T).(type) { case PrimitiveType: if tv.GetLength() < high { panic(&Exception{Value: typedString(fmt.Sprintf( - "slice bounds out of range [%d:%d] with string length %d", + "runtime error: slice bounds out of range [%d:%d] with string length %d", low, high, tv.GetLength()))}) } if t == StringType || t == UntypedStringType { @@ -2193,7 +2198,7 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { case *ArrayType: if tv.GetLength() < high { panic(&Exception{Value: typedString(fmt.Sprintf( - "slice bounds out of range [%d:%d] with array length %d", + "runtime error: slice bounds out of range [%d:%d] with array length %d", low, high, tv.GetLength()))}) } av := tv.V.(*ArrayValue) @@ -2214,12 +2219,12 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { // XXX consider restricting slice expansion if slice is readonly. if tv.GetCapacity() < high { panic(&Exception{Value: typedString(fmt.Sprintf( - "slice bounds out of range [%d:%d] with capacity %d", + "runtime error: slice bounds out of range [%d:%d] with capacity %d", low, high, tv.GetCapacity()))}) } if tv.V == nil { if low != 0 || high != 0 { - panic(&Exception{Value: typedString("nil slice index out of range")}) + panic(&Exception{Value: typedString("runtime error: nil slice index out of range")}) } return TypedValue{ T: tv.T, @@ -2244,39 +2249,39 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { func (tv *TypedValue) GetSlice2(alloc *Allocator, lowVal, highVal, maxVal int) TypedValue { if lowVal < 0 { - panic(fmt.Sprintf( - "invalid slice index %d (index must be non-negative)", - lowVal)) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: invalid slice index %d (index must be non-negative)", + lowVal))}) } if highVal < 0 { - panic(fmt.Sprintf( - "invalid slice index %d (index must be non-negative)", - highVal)) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: invalid slice index %d (index must be non-negative)", + highVal))}) } if maxVal < 0 { - panic(fmt.Sprintf( - "invalid slice index %d (index must be non-negative)", - maxVal)) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: invalid slice index %d (index must be non-negative)", + maxVal))}) } if lowVal > highVal { - panic(fmt.Sprintf( - "invalid slice index %d > %d", - lowVal, highVal)) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: invalid slice index %d > %d", + lowVal, highVal))}) } if highVal > maxVal { - panic(fmt.Sprintf( - "invalid slice index %d > %d", - highVal, maxVal)) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: invalid slice index %d > %d", + highVal, maxVal))}) } if tv.GetCapacity() < highVal { - panic(fmt.Sprintf( - "slice bounds out of range [%d:%d:%d] with capacity %d", - lowVal, highVal, maxVal, tv.GetCapacity())) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: slice bounds out of range [%d:%d:%d] with capacity %d", + lowVal, highVal, maxVal, tv.GetCapacity()))}) } if tv.GetCapacity() < maxVal { - panic(fmt.Sprintf( - "slice bounds out of range [%d:%d:%d] with capacity %d", - lowVal, highVal, maxVal, tv.GetCapacity())) + panic(&Exception{Value: typedString(fmt.Sprintf( + "runtime error: slice bounds out of range [%d:%d:%d] with capacity %d", + lowVal, highVal, maxVal, tv.GetCapacity()))}) } switch bt := baseOf(tv.T).(type) { case *ArrayType: @@ -2298,7 +2303,7 @@ func (tv *TypedValue) GetSlice2(alloc *Allocator, lowVal, highVal, maxVal int) T // XXX consider restricting slice expansion if slice is readonly. if tv.V == nil { if lowVal != 0 || highVal != 0 || maxVal != 0 { - panic("nil slice index out of range") + panic(&Exception{Value: typedString("runtime error: nil slice index out of range")}) } return TypedValue{ T: tv.T, diff --git a/gnovm/pkg/gnolang/values_conversions.go b/gnovm/pkg/gnolang/values_conversions.go index 593862f2097..861a53a3efc 100644 --- a/gnovm/pkg/gnolang/values_conversions.go +++ b/gnovm/pkg/gnolang/values_conversions.go @@ -51,11 +51,15 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo validate := func(from Kind, to Kind, cmp func() bool) { if isConst { - msg := fmt.Sprintf("cannot convert constant of type %s to %s", from, to) - if cmp != nil && cmp() { - return + if cmp == nil { + panic(fmt.Sprintf("unexpected conversion from %s to %s", from, to)) + } + if !cmp() { + if isIntegerKind(from) && isIntegerKind(to) { + panic(fmt.Sprintf("constant %s overflows %s", tv.ProtectedSprint(newSeenValues(), false), to)) + } + panic(fmt.Sprintf("cannot convert constant of type %s to %s", from, to)) } - panic(msg) } } @@ -127,7 +131,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(IntKind, StringKind, nil) tv.V = alloc.NewString(string(rune(tv.GetInt()))) tv.T = t tv.ClearNum() @@ -268,8 +271,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Int16Kind, StringKind, nil) - tv.V = alloc.NewString(string(rune(tv.GetInt16()))) tv.T = t tv.ClearNum() @@ -343,8 +344,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Int32Kind, StringKind, nil) - tv.V = alloc.NewString(string(tv.GetInt32())) tv.T = t tv.ClearNum() @@ -420,8 +419,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Int64Kind, Uint64Kind, nil) - tv.V = alloc.NewString(string(rune(tv.GetInt64()))) tv.T = t tv.ClearNum() @@ -497,8 +494,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(UintKind, StringKind, nil) - tv.V = alloc.NewString(string(rune(tv.GetUint()))) tv.T = t tv.ClearNum() @@ -566,8 +561,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Uint8Kind, StringKind, nil) - tv.V = alloc.NewString(string(rune(tv.GetUint8()))) tv.T = t tv.ClearNum() @@ -637,8 +630,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Uint16Kind, StringKind, nil) - tv.V = alloc.NewString(string(rune(tv.GetUint16()))) tv.T = t tv.ClearNum() @@ -710,8 +701,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Uint32Kind, StringKind, nil) - tv.V = alloc.NewString(string(rune(tv.GetUint32()))) tv.T = t tv.ClearNum() @@ -723,25 +712,25 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Uint64Kind: switch k { case IntKind: - validate(Uint64Kind, IntKind, func() bool { return int64(tv.GetUint64()) <= math.MaxInt }) + validate(Uint64Kind, IntKind, func() bool { return tv.GetUint64() <= math.MaxInt }) x := int64(tv.GetUint64()) tv.T = t tv.SetInt(x) case Int8Kind: - validate(Uint64Kind, Int8Kind, func() bool { return int64(tv.GetUint64()) <= math.MaxInt8 }) + validate(Uint64Kind, Int8Kind, func() bool { return tv.GetUint64() <= math.MaxInt8 }) x := int8(tv.GetUint64()) tv.T = t tv.SetInt8(x) case Int16Kind: - validate(Uint64Kind, Int16Kind, func() bool { return int64(tv.GetUint64()) <= math.MaxInt16 }) + validate(Uint64Kind, Int16Kind, func() bool { return tv.GetUint64() <= math.MaxInt16 }) x := int16(tv.GetUint64()) tv.T = t tv.SetInt16(x) case Int32Kind: - validate(Uint64Kind, Int32Kind, func() bool { return int64(tv.GetUint64()) <= math.MaxInt32 }) + validate(Uint64Kind, Int32Kind, func() bool { return tv.GetUint64() <= math.MaxInt32 }) x := int32(tv.GetUint64()) tv.T = t @@ -759,13 +748,13 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetUint64(x) case Uint8Kind: - validate(Uint64Kind, Uint8Kind, func() bool { return int64(tv.GetUint64()) <= math.MaxUint8 }) + validate(Uint64Kind, Uint8Kind, func() bool { return tv.GetUint64() <= math.MaxUint8 }) x := uint8(tv.GetUint64()) tv.T = t tv.SetUint8(x) case Uint16Kind: - validate(Uint64Kind, Uint16Kind, func() bool { return int64(tv.GetUint64()) <= math.MaxUint16 }) + validate(Uint64Kind, Uint16Kind, func() bool { return tv.GetUint64() <= math.MaxUint16 }) x := uint16(tv.GetUint64()) tv.T = t @@ -789,8 +778,6 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat64(x) case StringKind: - validate(Uint64Kind, StringKind, nil) - tv.V = alloc.NewString(string(rune(tv.GetUint64()))) tv.T = t tv.ClearNum() @@ -813,7 +800,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Int8Kind: validate(Float32Kind, Int8Kind, func() bool { f32 := tv.GetFloat32() - trunc := int8(softfloat.F32toint64(f32)) + trunc := int8(softfloat.F32toint32(f32)) return softfloat.Fint64to32(int64(trunc)) == f32 }) @@ -823,7 +810,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Int16Kind: validate(Float32Kind, Int16Kind, func() bool { f32 := tv.GetFloat32() - trunc := int16(softfloat.F32toint64(f32)) + trunc := int16(softfloat.F32toint32(f32)) return softfloat.Fint64to32(int64(trunc)) == f32 }) @@ -833,7 +820,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Int32Kind: validate(Float32Kind, Int32Kind, func() bool { f32 := tv.GetFloat32() - trunc := int32(softfloat.F32toint64(f32)) + trunc := softfloat.F32toint32(f32) return softfloat.Fint64to32(int64(trunc)) == f32 }) @@ -898,9 +885,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetUint64(x) case Float32Kind: - x := tv.GetFloat32() // ??? tv.T = t - tv.SetFloat32(x) case Float64Kind: x := softfloat.F32to64(tv.GetFloat32()) tv.T = t @@ -924,7 +909,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Int8Kind: validate(Float64Kind, Int8Kind, func() bool { f64 := tv.GetFloat64() - trunc := int8(softfloat.F64toint64(f64)) + trunc := int8(softfloat.F64toint32(f64)) return softfloat.Fint64to64(int64(trunc)) == f64 }) @@ -934,7 +919,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Int16Kind: validate(Float64Kind, Int16Kind, func() bool { f64 := tv.GetFloat64() - trunc := int16(softfloat.F64toint64(f64)) + trunc := int16(softfloat.F64toint32(f64)) return softfloat.Fint64to64(int64(trunc)) == f64 }) @@ -944,7 +929,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo case Int32Kind: validate(Float64Kind, Int32Kind, func() bool { f64 := tv.GetFloat64() - trunc := int32(softfloat.F64toint64(f64)) + trunc := softfloat.F64toint32(f64) return softfloat.Fint64to64(int64(trunc)) == f64 }) @@ -1016,9 +1001,7 @@ func ConvertTo(alloc *Allocator, store Store, tv *TypedValue, t Type, isConst bo tv.T = t tv.SetFloat32(x) case Float64Kind: - x := tv.GetFloat64() // ??? tv.T = t - tv.SetFloat64(x) default: panic(fmt.Sprintf( "cannot convert %s to %s", @@ -1403,7 +1386,7 @@ func ConvertUntypedBigdecTo(dst *TypedValue, bdv BigdecValue, t Type) { bd := bdv.V switch k { case BigintKind: - if !isInteger(bd) { + if !isDecimalInteger(bd) { panic(fmt.Sprintf( "cannot convert untyped bigdec to integer -- %s not an exact integer", bd.String(), @@ -1423,7 +1406,7 @@ func ConvertUntypedBigdecTo(dst *TypedValue, bdv BigdecValue, t Type) { case IntKind, Int8Kind, Int16Kind, Int32Kind, Int64Kind: fallthrough case UintKind, Uint8Kind, Uint16Kind, Uint32Kind, Uint64Kind: - if !isInteger(bd) { + if !isDecimalInteger(bd) { panic(fmt.Sprintf( "cannot convert untyped bigdec to integer -- %s not an exact integer", bd.String(), @@ -1468,7 +1451,7 @@ func ConvertUntypedBigdecTo(dst *TypedValue, bdv BigdecValue, t Type) { // ---------------------------------------- // apd.Decimal utility -func isInteger(d *apd.Decimal) bool { +func isDecimalInteger(d *apd.Decimal) bool { d2 := apd.New(0, 0) res, err := apd.BaseContext.RoundToIntegralExact(d2, d) if err != nil { @@ -1500,7 +1483,7 @@ func toBigInt(d *apd.Decimal) *big.Int { // underlying value has no fractional component. func IsExactBigDec(v Value) bool { if bd, ok := v.(BigdecValue); ok { - return isInteger(bd.V) + return isDecimalInteger(bd.V) } return false } diff --git a/gnovm/stdlibs/builtin/shims.go b/gnovm/stdlibs/builtin/shims.go index b2d6df68049..f94610fd258 100644 --- a/gnovm/stdlibs/builtin/shims.go +++ b/gnovm/stdlibs/builtin/shims.go @@ -4,6 +4,7 @@ type Realm interface { Address() Address PkgPath() string Coins() Gnocoins + SentCoins() Gnocoins Send(coins Gnocoins, to Address) error Previous() Realm Origin() Realm @@ -17,7 +18,11 @@ func (a Address) IsValid() bool { return false } type Gnocoins []Gnocoin +func (cz Gnocoins) String() string { return "" } + type Gnocoin struct { Denom string Amount int64 } + +func (c Gnocoin) String() string { return "" } diff --git a/gnovm/stdlibs/chain/coins.gno b/gnovm/stdlibs/chain/coins.gno index 7dfbb339603..4480d392ca1 100644 --- a/gnovm/stdlibs/chain/coins.gno +++ b/gnovm/stdlibs/chain/coins.gno @@ -163,15 +163,21 @@ func (cz Coins) String() string { return res } -// AmountOf returns the amount of a specific coin from the Coins set +// AmountOf returns the amount of a specific coin from the Coins set. +// Returns 0 if the denom is not found. Panics if there are duplicate coins with +// the given denom. func (cz Coins) AmountOf(denom string) int64 { + amt, set := int64(0), false for _, c := range cz { if c.Denom == denom { - return c.Amount + if set { + panic("duplicate denom " + denom + " in coin set") + } + amt, set = c.Amount, true } } - return 0 + return amt } // Add adds the given Coins to the set. diff --git a/gnovm/stdlibs/chain/coins_test.gno b/gnovm/stdlibs/chain/coins_test.gno index 36e45634dcf..a01863bb7fe 100644 --- a/gnovm/stdlibs/chain/coins_test.gno +++ b/gnovm/stdlibs/chain/coins_test.gno @@ -70,6 +70,39 @@ func TestCoinsIsZero(t *testing.T) { } } +func TestCoins_AmountOf(t *testing.T) { + coins := chain.Coins{ + chain.NewCoin("denom1", 1), + chain.NewCoin("denom2", 2), + } + + amount := coins.AmountOf("denom1") + if amount != 1 { + t.Fatalf("expected amount=1 for denom1, got %d", amount) + } + + amount = coins.AmountOf("denom2") + if amount != 2 { + t.Fatalf("expected amount=2 for denom2, got %d", amount) + } + + amount = coins.AmountOf("denom3") + if amount != 0 { + t.Fatalf("expected amount=0 for denom3, got %d", amount) + } + + coins = append(coins, chain.NewCoin("denom1", 1)) + + func() { + defer func() { + if err := recover(); err == nil { + t.Fatal("expected panic when getting duplicate denom") + } + }() + coins.AmountOf("denom1") + }() +} + const maxInt64 int64 = (1 << 63) - 1 func shouldPanic(t *testing.T, f func()) { diff --git a/gnovm/stdlibs/chain/runtime/native.go b/gnovm/stdlibs/chain/runtime/native.go index fe45c5208b0..ac9b6ed1288 100644 --- a/gnovm/stdlibs/chain/runtime/native.go +++ b/gnovm/stdlibs/chain/runtime/native.go @@ -18,7 +18,12 @@ func isOriginCall(m *gno.Machine) bool { } firstPkg := m.Frames[0].LastPackage isMsgCall := firstPkg != nil && firstPkg.PkgPath == "" - return n <= 2 && isMsgCall + if !isMsgCall { + return false + } + // Count only actual function call frames (excludes closures + // and control-flow basic frames like for/range/switch). + return m.NumCallFrames() <= 2 } func ChainID(m *gno.Machine) string { diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index 254536c0c9b..20ae1882075 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -14,7 +14,6 @@ import ( libs_crypto_ed25519 "github.com/gnolang/gno/gnovm/stdlibs/crypto/ed25519" libs_crypto_sha256 "github.com/gnolang/gno/gnovm/stdlibs/crypto/sha256" libs_math "github.com/gnolang/gno/gnovm/stdlibs/math" - libs_runtime "github.com/gnolang/gno/gnovm/stdlibs/runtime" libs_sys_params "github.com/gnolang/gno/gnovm/stdlibs/sys/params" libs_time "github.com/gnolang/gno/gnovm/stdlibs/time" ) @@ -918,38 +917,6 @@ var nativeFuncs = [...]NativeFunc{ )) }, }, - { - "runtime", - "GC", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{}, - true, - func(m *gno.Machine) { - libs_runtime.GC( - m, - ) - }, - }, - { - "runtime", - "MemStats", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {NameExpr: *gno.Nx("r0"), Type: gno.X("string")}, - }, - true, - func(m *gno.Machine) { - r0 := libs_runtime.MemStats( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "sys/params", "setSysParamString", @@ -1360,7 +1327,6 @@ var initOrder = [...]string{ "net/url", "regexp/syntax", "regexp", - "runtime", "sys/params", "time", "unicode/utf16", diff --git a/gnovm/stdlibs/internal/execctx/context.go b/gnovm/stdlibs/internal/execctx/context.go index d19ac7d3be8..d4fd4f4354e 100644 --- a/gnovm/stdlibs/internal/execctx/context.go +++ b/gnovm/stdlibs/internal/execctx/context.go @@ -46,7 +46,14 @@ func (e ExecContext) GetExecContext() ExecContext { return e } +// GetOriginSend returns the OriginSend coins. +// This implements gno.OriginSendProvider to avoid import cycles. +func (e ExecContext) GetOriginSend() std.Coins { + return e.OriginSend +} + var _ ExecContexter = ExecContext{} +var _ gno.OriginSendProvider = ExecContext{} // ExecContexter is a type capable of returning the parent [ExecContext]. When // using these standard libraries, m.Context should always implement this diff --git a/gnovm/stdlibs/runtime/gnomod.toml b/gnovm/stdlibs/runtime/gnomod.toml deleted file mode 100644 index f3aa73ec2f6..00000000000 --- a/gnovm/stdlibs/runtime/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "runtime" -gno = "0.9" diff --git a/gnovm/tests/files/addressable_7a_err.gno b/gnovm/tests/files/addressable_7a_err.gno index 811e1a07daf..564e20a5556 100644 --- a/gnovm/tests/files/addressable_7a_err.gno +++ b/gnovm/tests/files/addressable_7a_err.gno @@ -9,7 +9,7 @@ func main() { } // Error: -// main/addressable_7a_err.gno:8:2-12: getTypeOf() only supports *CallExpr with 1 result, got ([]int,[]string) +// main/addressable_7a_err.gno:8:6-12: cannot take address of multi-value call (results: ([]int,[]string)) // TypeCheckError: // main/addressable_7a_err.gno:8:7: multiple-value foo() (value of type ([]int, []string)) in single-value context diff --git a/gnovm/tests/files/alloc_0.gno b/gnovm/tests/files/alloc_0.gno index 1b17645c9d9..1d87b501794 100644 --- a/gnovm/tests/files/alloc_0.gno +++ b/gnovm/tests/files/alloc_0.gno @@ -16,7 +16,7 @@ func main() { } // Output: -// MemStats: Allocator{maxBytes:100000000, bytes:6494} +// MemStats: Allocator{maxBytes:100000000, bytes:6510} // TypeCheckError: // main/alloc_0.gno:13:2: declared and not used: f1 diff --git a/gnovm/tests/files/alloc_1.gno b/gnovm/tests/files/alloc_1.gno index 5bc46d2ca96..6e373210941 100644 --- a/gnovm/tests/files/alloc_1.gno +++ b/gnovm/tests/files/alloc_1.gno @@ -21,7 +21,7 @@ func main() { } // Output: -// MemStats: Allocator{maxBytes:100000000, bytes:7744} +// MemStats: Allocator{maxBytes:100000000, bytes:7760} // TypeCheckError: // main/alloc_1.gno:18:2: declared and not used: S1 diff --git a/gnovm/tests/files/alloc_10_long.gno b/gnovm/tests/files/alloc_10_long.gno index dbc58815300..2efdc6990fa 100644 --- a/gnovm/tests/files/alloc_10_long.gno +++ b/gnovm/tests/files/alloc_10_long.gno @@ -11,4 +11,4 @@ func main() { } // Error: -// main/alloc_10_long.gno:6:2-29: should not happen, allocation limit exceeded while gc. +// main/alloc_10_long.gno:6:2-29: allocation limit exceeded diff --git a/gnovm/tests/files/alloc_10c.gno b/gnovm/tests/files/alloc_10c.gno new file mode 100644 index 00000000000..43f95f32a54 --- /dev/null +++ b/gnovm/tests/files/alloc_10c.gno @@ -0,0 +1,35 @@ +// MAXALLOC: 20000 +// SEND: 1000000ugnot + +package main + +import ( + "testing" + + "gno.land/p/nt/testutils/v0" + "gno.land/r/archive/boards" + users "gno.land/r/sys/users/init" +) + +func main() { + aliceAddr := testutils.TestAddress("alice_123") + + // TestSetRealm sets the realm caller, in this case Alice + testing.SetRealm(testing.NewUserRealm(aliceAddr)) + + users.RegisterUser(cross, "alice_123", aliceAddr) + + bid := boards.CreateBoard(cross, "testboard") + boards.CreateThread(cross, bid, "testthread", "testbody") + n := 5000 + for i := 0; i < n; i++ { + boards.CreateReply(cross, bid, boards.PostID(1), boards.PostID(1), "reply") + } + println("ok") +} + +// allocation limit exceeded during importing. + +// Error: +// main/alloc_10c.gno:7:2-11: allocation limit exceeded + diff --git a/gnovm/tests/files/alloc_12.gno b/gnovm/tests/files/alloc_12.gno new file mode 100644 index 00000000000..6165b0a20f6 --- /dev/null +++ b/gnovm/tests/files/alloc_12.gno @@ -0,0 +1,18 @@ +// MAXALLOC: 500000000 + +package main + +// Test that infinite recursion triggers "allocation limit exceeded" +// and NOT "should not happen, allocation limit exceeded while gc." +// This verifies that GC recount is consistent with allocator tracking. + +func f() int { + return f() +} + +func main() { + f() +} + +// Error: +// allocation limit exceeded diff --git a/gnovm/tests/files/alloc_3.gno b/gnovm/tests/files/alloc_3.gno index 083d11b9298..3662a556064 100644 --- a/gnovm/tests/files/alloc_3.gno +++ b/gnovm/tests/files/alloc_3.gno @@ -11,7 +11,7 @@ func main() { } // Output: -// MemStats after GC: Allocator{maxBytes:110000000, bytes:5840} +// MemStats after GC: Allocator{maxBytes:110000000, bytes:5856} // TypeCheckError: // main/alloc_3.gno:7:2: declared and not used: data diff --git a/gnovm/tests/files/alloc_4.gno b/gnovm/tests/files/alloc_4.gno index 12d08593d95..35d6fcd7054 100644 --- a/gnovm/tests/files/alloc_4.gno +++ b/gnovm/tests/files/alloc_4.gno @@ -22,5 +22,5 @@ func main() { } // Output: -// memstats in main after first GC: Allocator{maxBytes:50000, bytes:17428} -// memstats in main after second GC: Allocator{maxBytes:50000, bytes:6997} +// memstats in main after first GC: Allocator{maxBytes:50000, bytes:17444} +// memstats in main after second GC: Allocator{maxBytes:50000, bytes:7013} diff --git a/gnovm/tests/files/alloc_5.gno b/gnovm/tests/files/alloc_5.gno index 7d7ba0efb99..db06a961130 100644 --- a/gnovm/tests/files/alloc_5.gno +++ b/gnovm/tests/files/alloc_5.gno @@ -22,4 +22,4 @@ func main() { } // Output: -// memstats in main after GC: Allocator{maxBytes:100000000, bytes:6184} +// memstats in main after GC: Allocator{maxBytes:100000000, bytes:6200} diff --git a/gnovm/tests/files/alloc_6.gno b/gnovm/tests/files/alloc_6.gno index 5c29b8915cf..34af2a5fd89 100644 --- a/gnovm/tests/files/alloc_6.gno +++ b/gnovm/tests/files/alloc_6.gno @@ -16,4 +16,4 @@ func main() { } // Output: -// memstats in main after GC: Allocator{maxBytes:100000000, bytes:6184} +// memstats in main after GC: Allocator{maxBytes:100000000, bytes:6200} diff --git a/gnovm/tests/files/alloc_6a.gno b/gnovm/tests/files/alloc_6a.gno index de119084182..f92ee22d958 100644 --- a/gnovm/tests/files/alloc_6a.gno +++ b/gnovm/tests/files/alloc_6a.gno @@ -18,4 +18,4 @@ func main() { } // Output: -// memstats in main after GC: Allocator{maxBytes:100000000, bytes:6688} +// memstats in main after GC: Allocator{maxBytes:100000000, bytes:6704} diff --git a/gnovm/tests/files/alloc_7.gno b/gnovm/tests/files/alloc_7.gno index fe5fdeb1c16..c6ef06d79bf 100644 --- a/gnovm/tests/files/alloc_7.gno +++ b/gnovm/tests/files/alloc_7.gno @@ -13,4 +13,4 @@ func main() { } // Output: -// MemStats: Allocator{maxBytes:100000000, bytes:5947} +// MemStats: Allocator{maxBytes:100000000, bytes:5963} diff --git a/gnovm/tests/files/alloc_7a.gno b/gnovm/tests/files/alloc_7a.gno index d5a14db8f0e..342d69a176c 100644 --- a/gnovm/tests/files/alloc_7a.gno +++ b/gnovm/tests/files/alloc_7a.gno @@ -17,4 +17,4 @@ func main() { } // Output: -// MemStats: Allocator{maxBytes:100000000, bytes:8155} +// MemStats: Allocator{maxBytes:100000000, bytes:8171} diff --git a/gnovm/tests/files/array6.gno b/gnovm/tests/files/array6.gno new file mode 100644 index 00000000000..212bc3c90a3 --- /dev/null +++ b/gnovm/tests/files/array6.gno @@ -0,0 +1,42 @@ +package main + +type Record struct { + Value int + Label string +} + +func main() { + var a [2]Record + a[0] = Record{Value: 100, Label: "original-0"} + a[1] = Record{Value: 200, Label: "original-1"} + + // Array assignment must produce a deep copy (Go value semantics). + // Mutating b must never affect a. + b := a + b[0].Value = 999 + b[0].Label = "modified-0" + b[1].Value = 888 + b[1].Label = "modified-1" + + println(a[0].Label, a[0].Value) + println(a[1].Label, a[1].Value) + println(b[0].Label, b[0].Value) + println(b[1].Label, b[1].Value) + + // Also test pass-by-value: modifying a function's array parameter + // must not affect the caller's copy. + modify(a) + println(a[0].Label, a[0].Value) +} + +func modify(arr [2]Record) { + arr[0].Value = 777 + arr[0].Label = "param-modified" +} + +// Output: +// original-0 100 +// original-1 200 +// modified-0 999 +// modified-1 888 +// original-0 100 diff --git a/gnovm/tests/files/array7.gno b/gnovm/tests/files/array7.gno new file mode 100644 index 00000000000..127d821de48 --- /dev/null +++ b/gnovm/tests/files/array7.gno @@ -0,0 +1,35 @@ +package main + +// Nested array assignment must deep-copy (not shallow alias). + +func main() { + // Direct assignment + a := [1][1]int{{1}} + b := a + b[0][0] = 2 + println(a[0][0], b[0][0]) + + // Function argument passing + c := [1][1]int{{1}} + f(c) + println(c[0][0]) + + // Return by value + d := [1][1]int{{1}} + e := g(d) + e[0][0] = 2 + println(d[0][0], e[0][0]) +} + +func f(x [1][1]int) { + x[0][0] = 2 +} + +func g(a [1][1]int) [1][1]int { + return a +} + +// Output: +// 1 2 +// 1 +// 1 2 diff --git a/gnovm/tests/files/assign39.gno b/gnovm/tests/files/assign39.gno new file mode 100644 index 00000000000..23cf463eb5b --- /dev/null +++ b/gnovm/tests/files/assign39.gno @@ -0,0 +1,21 @@ +package main + +// Verifies that in an AssignStmt, all RHS values are evaluated before LHS +// is written (Go spec §Assignments). This exercises the new interleaved +// Rhs[i]→Lhs[i] transcription order in transcribe.go. +func main() { + x, y := 1, 2 + + // Swap: both RHS (y and x) are evaluated before either LHS is written. + x, y = y, x + println(x, y) // 2 1 + + // a=b (old), b=a+b (old a + old b) + a, b := 3, 5 + a, b = b, a+b + println(a, b) // 5 8 +} + +// Output: +// 2 1 +// 5 8 diff --git a/gnovm/tests/files/assign_unnamed_type/unnamedtype7_filetest.gno b/gnovm/tests/files/assign_unnamed_type/unnamedtype7_filetest.gno index 939428bd192..dc80934802a 100644 --- a/gnovm/tests/files/assign_unnamed_type/unnamedtype7_filetest.gno +++ b/gnovm/tests/files/assign_unnamed_type/unnamedtype7_filetest.gno @@ -1,15 +1,15 @@ package main -type mychan chan int +type myptr *int -// chan int is unmamed +// *int is unnamed func main() { - var n mychan = nil - var u chan int = nil + var n myptr = nil + var u *int = nil n = u println(n) } // Output: -// (nil main.mychan) +// (nil main.myptr) diff --git a/gnovm/tests/files/chan_make0.gno b/gnovm/tests/files/chan_make0.gno new file mode 100644 index 00000000000..4f0bbbb9d60 --- /dev/null +++ b/gnovm/tests/files/chan_make0.gno @@ -0,0 +1,11 @@ +// https://github.com/gnolang/gno/issues/5233 +// make(chan T) is rejected because channel type is not yet supported. +package main + +func main() { + ch := make(chan int) + _ = ch +} + +// Error: +// chan_make0.gno:6:13: channels are not permitted diff --git a/gnovm/tests/files/chan_select0.gno b/gnovm/tests/files/chan_select0.gno new file mode 100644 index 00000000000..3c718f90dcd --- /dev/null +++ b/gnovm/tests/files/chan_select0.gno @@ -0,0 +1,9 @@ +// select statement is rejected at parse time. +package main + +func main() { + select {} +} + +// Error: +// chan_select0.gno:5:2: select statements are not permitted diff --git a/gnovm/tests/files/chan_type0.gno b/gnovm/tests/files/chan_type0.gno new file mode 100644 index 00000000000..f2b41111bc9 --- /dev/null +++ b/gnovm/tests/files/chan_type0.gno @@ -0,0 +1,10 @@ +// channel type in variable declaration is rejected at parse time. +package main + +func main() { + var ch chan int + _ = ch +} + +// Error: +// chan_type0.gno:5:9: channels are not permitted diff --git a/gnovm/tests/files/chan_type1.gno b/gnovm/tests/files/chan_type1.gno new file mode 100644 index 00000000000..ff70a16cd42 --- /dev/null +++ b/gnovm/tests/files/chan_type1.gno @@ -0,0 +1,10 @@ +// named channel type declaration is rejected at parse time. +package main + +type C chan int + +func main() { +} + +// Error: +// chan_type1.gno:4:8: channels are not permitted diff --git a/gnovm/tests/files/chan_type2.gno b/gnovm/tests/files/chan_type2.gno new file mode 100644 index 00000000000..0a9fe15c156 --- /dev/null +++ b/gnovm/tests/files/chan_type2.gno @@ -0,0 +1,12 @@ +// channel type in function signature is rejected at parse time. +package main + +func foo(ch chan int) { +} + +func main() { + foo(nil) +} + +// Error: +// chan_type2.gno:4:13: channels are not permitted diff --git a/gnovm/tests/files/chan_type3.gno b/gnovm/tests/files/chan_type3.gno new file mode 100644 index 00000000000..a5cc41a2865 --- /dev/null +++ b/gnovm/tests/files/chan_type3.gno @@ -0,0 +1,12 @@ +// channel type in struct field is rejected at parse time. +package main + +type S struct { + ch chan int +} + +func main() { +} + +// Error: +// chan_type3.gno:5:5: channels are not permitted diff --git a/gnovm/tests/files/closure.gno b/gnovm/tests/files/closure.gno index 3804ef8dc7d..2185522951d 100644 --- a/gnovm/tests/files/closure.gno +++ b/gnovm/tests/files/closure.gno @@ -13,7 +13,7 @@ var b = func() { } // Error: -// main/closure.gno:7:5: loop in variable initialization: dependency trail [b a] circularly depends on b +// main/closure.gno:7:5: circular dependency: a -> b -> a // TypeCheckError: // main/closure.gno:7:5: initialization cycle for a; main/closure.gno:7:5: a refers to b; main/closure.gno:11:5: b refers to a diff --git a/gnovm/tests/files/composite15.gno b/gnovm/tests/files/composite15.gno index de8cac0d09d..0bc2f2273a7 100644 --- a/gnovm/tests/files/composite15.gno +++ b/gnovm/tests/files/composite15.gno @@ -9,8 +9,8 @@ func main() { println(x) } -// Output: -// slice[(0 int),(2 int),(0 int),(4 int)] - // TypeCheckError: // main/composite15.gno:6:3: index a must be integer constant; main/composite15.gno:7:3: index c must be integer constant + +// Error: +// main/composite15.gno:6:3-4: slice/array literals may not contain non-const keys diff --git a/gnovm/tests/files/const55.gno b/gnovm/tests/files/const55.gno index d2c7a80f9bb..8989995bcb7 100644 --- a/gnovm/tests/files/const55.gno +++ b/gnovm/tests/files/const55.gno @@ -11,7 +11,7 @@ func main() { } // Error: -// main/const55.gno:10:10-11: name not defined: m +// main/const55.gno:5:2-3: assignment mismatch: 1 variable(s) but 0 value(s) // TypeCheckError: // main/const55.gno:5:2: extra init expr at main/const55.gno:4:2 diff --git a/gnovm/tests/files/const55a.gno b/gnovm/tests/files/const55a.gno index 275caf13163..a292fc375dd 100644 --- a/gnovm/tests/files/const55a.gno +++ b/gnovm/tests/files/const55a.gno @@ -11,7 +11,7 @@ func main() { } // Error: -// main/const55a.gno:10:10-11: name not defined: m +// main/const55a.gno:5:2-9: assignment mismatch: 3 variable(s) but 0 value(s) // TypeCheckError: // main/const55a.gno:5:8: missing init expr for l diff --git a/gnovm/tests/files/const62.gno b/gnovm/tests/files/const62.gno new file mode 100644 index 00000000000..fef3ee0b5dc --- /dev/null +++ b/gnovm/tests/files/const62.gno @@ -0,0 +1,19 @@ +package main + +// Const group where a subsequent line has fewer names than the previous iota +// expression. Should produce a clear assignment mismatch error, not a +// confusing internal panic. +const ( + d, e = 1, "hello" + m +) + +func main() { + println(d, e) +} + +// Error: +// main/const62.gno:8:2-3: assignment mismatch: 1 variable(s) but 0 value(s) + +// TypeCheckError: +// main/const62.gno:8:2: extra init expr at main/const62.gno:7:2 diff --git a/gnovm/tests/files/convert6.gno b/gnovm/tests/files/convert6.gno index df3dcb286c1..09096729178 100644 --- a/gnovm/tests/files/convert6.gno +++ b/gnovm/tests/files/convert6.gno @@ -6,7 +6,7 @@ func main() { } // Error: -// main/convert6.gno:5:10-17: cannot convert constant of type IntKind to UintKind +// main/convert6.gno:5:10-17: constant -1 overflows UintKind // TypeCheckError: // main/convert6.gno:5:15: constant -1 overflows uint diff --git a/gnovm/tests/files/convert6a.gno b/gnovm/tests/files/convert6a.gno index ac60fdaa3f6..1a78067dc2a 100644 --- a/gnovm/tests/files/convert6a.gno +++ b/gnovm/tests/files/convert6a.gno @@ -6,7 +6,7 @@ func main() { } // Error: -// main/convert6a.gno:5:10-18: cannot convert constant of type IntKind to Uint8Kind +// main/convert6a.gno:5:10-18: constant -1 overflows Uint8Kind // TypeCheckError: // main/convert6a.gno:5:16: constant -1 overflows uint8 diff --git a/gnovm/tests/files/convert6b.gno b/gnovm/tests/files/convert6b.gno index 04e672215a1..7d5de68631b 100644 --- a/gnovm/tests/files/convert6b.gno +++ b/gnovm/tests/files/convert6b.gno @@ -6,7 +6,7 @@ func main() { } // Error: -// main/convert6b.gno:5:10-19: cannot convert constant of type IntKind to Uint16Kind +// main/convert6b.gno:5:10-19: constant -1 overflows Uint16Kind // TypeCheckError: // main/convert6b.gno:5:17: constant -1 overflows uint16 diff --git a/gnovm/tests/files/convert6c.gno b/gnovm/tests/files/convert6c.gno index 9701854dce9..17cde1b4d21 100644 --- a/gnovm/tests/files/convert6c.gno +++ b/gnovm/tests/files/convert6c.gno @@ -6,7 +6,7 @@ func main() { } // Error: -// main/convert6c.gno:5:10-19: cannot convert constant of type IntKind to Uint32Kind +// main/convert6c.gno:5:10-19: constant -1 overflows Uint32Kind // TypeCheckError: // main/convert6c.gno:5:17: constant -1 overflows uint32 diff --git a/gnovm/tests/files/convert6d.gno b/gnovm/tests/files/convert6d.gno index 2514263a254..65436822cc2 100644 --- a/gnovm/tests/files/convert6d.gno +++ b/gnovm/tests/files/convert6d.gno @@ -6,7 +6,7 @@ func main() { } // Error: -// main/convert6d.gno:5:10-19: cannot convert constant of type IntKind to Uint64Kind +// main/convert6d.gno:5:10-19: constant -1 overflows Uint64Kind // TypeCheckError: // main/convert6d.gno:5:17: constant -1 overflows uint64 diff --git a/gnovm/tests/files/convert9.gno b/gnovm/tests/files/convert9.gno new file mode 100644 index 00000000000..1d7c7372e5e --- /dev/null +++ b/gnovm/tests/files/convert9.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(int(a)) +} + +// Error: +// main/convert9.gno:5:10-16: constant 18446744073709551615 overflows IntKind + +// TypeCheckError: +// main/convert9.gno:5:14: constant 18446744073709551615 overflows int diff --git a/gnovm/tests/files/convert9a.gno b/gnovm/tests/files/convert9a.gno new file mode 100644 index 00000000000..cc8697e5344 --- /dev/null +++ b/gnovm/tests/files/convert9a.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(int8(a)) +} + +// Error: +// main/convert9a.gno:5:10-17: constant 18446744073709551615 overflows Int8Kind + +// TypeCheckError: +// main/convert9a.gno:5:15: constant 18446744073709551615 overflows int8 diff --git a/gnovm/tests/files/convert9b.gno b/gnovm/tests/files/convert9b.gno new file mode 100644 index 00000000000..25579cc8963 --- /dev/null +++ b/gnovm/tests/files/convert9b.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(int16(a)) +} + +// Error: +// main/convert9b.gno:5:10-18: constant 18446744073709551615 overflows Int16Kind + +// TypeCheckError: +// main/convert9b.gno:5:16: constant 18446744073709551615 overflows int16 diff --git a/gnovm/tests/files/convert9c.gno b/gnovm/tests/files/convert9c.gno new file mode 100644 index 00000000000..1bfb928003c --- /dev/null +++ b/gnovm/tests/files/convert9c.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(int32(a)) +} + +// Error: +// main/convert9c.gno:5:10-18: constant 18446744073709551615 overflows Int32Kind + +// TypeCheckError: +// main/convert9c.gno:5:16: constant 18446744073709551615 overflows int32 diff --git a/gnovm/tests/files/convert9d.gno b/gnovm/tests/files/convert9d.gno new file mode 100644 index 00000000000..dad717ea323 --- /dev/null +++ b/gnovm/tests/files/convert9d.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(int64(a)) +} + +// Error: +// main/convert9d.gno:5:10-18: constant 18446744073709551615 overflows Int64Kind + +// TypeCheckError: +// main/convert9d.gno:5:16: constant 18446744073709551615 overflows int64 diff --git a/gnovm/tests/files/convert9e.gno b/gnovm/tests/files/convert9e.gno new file mode 100644 index 00000000000..59b5802cca2 --- /dev/null +++ b/gnovm/tests/files/convert9e.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(uint8(a)) +} + +// Error: +// main/convert9e.gno:5:10-18: constant 18446744073709551615 overflows Uint8Kind + +// TypeCheckError: +// main/convert9e.gno:5:16: constant 18446744073709551615 overflows uint8 diff --git a/gnovm/tests/files/convert9f.gno b/gnovm/tests/files/convert9f.gno new file mode 100644 index 00000000000..6a4d4de1917 --- /dev/null +++ b/gnovm/tests/files/convert9f.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(uint16(a)) +} + +// Error: +// main/convert9f.gno:5:10-19: constant 18446744073709551615 overflows Uint16Kind + +// TypeCheckError: +// main/convert9f.gno:5:17: constant 18446744073709551615 overflows uint16 diff --git a/gnovm/tests/files/convert9g.gno b/gnovm/tests/files/convert9g.gno new file mode 100644 index 00000000000..c1028100922 --- /dev/null +++ b/gnovm/tests/files/convert9g.gno @@ -0,0 +1,12 @@ +package main + +func main() { + const a uint64 = 18446744073709551615 + println(uint32(a)) +} + +// Error: +// main/convert9g.gno:5:10-19: constant 18446744073709551615 overflows Uint32Kind + +// TypeCheckError: +// main/convert9g.gno:5:17: constant 18446744073709551615 overflows uint32 diff --git a/gnovm/tests/files/convert9h.gno b/gnovm/tests/files/convert9h.gno new file mode 100644 index 00000000000..962067d9218 --- /dev/null +++ b/gnovm/tests/files/convert9h.gno @@ -0,0 +1,9 @@ +package main + +func main() { + const a int8 = 65 + println(string(a)) +} + +// Output: +// A diff --git a/gnovm/tests/files/convert9i.gno b/gnovm/tests/files/convert9i.gno new file mode 100644 index 00000000000..3d7ca6643df --- /dev/null +++ b/gnovm/tests/files/convert9i.gno @@ -0,0 +1,9 @@ +package main + +func main() { + const a int64 = 65 + println(string(a)) +} + +// Output: +// A diff --git a/gnovm/tests/files/convert9j.gno b/gnovm/tests/files/convert9j.gno new file mode 100644 index 00000000000..ba678705b5a --- /dev/null +++ b/gnovm/tests/files/convert9j.gno @@ -0,0 +1,9 @@ +package main + +func main() { + const a uint64 = 100 + println(int8(a)) +} + +// Output: +// 100 diff --git a/gnovm/tests/files/defer11.gno b/gnovm/tests/files/defer11.gno new file mode 100644 index 00000000000..df261198574 --- /dev/null +++ b/gnovm/tests/files/defer11.gno @@ -0,0 +1,29 @@ +package main + +// Regression test for iterative exception recovery in Machine.Run(). +// Before the fix, each panicking defer added a Go stack frame via recursive +// m.Run(st), and ~500K defers would exceed the 1GB goroutine stack limit, +// crashing the process with runtime.throw("stack overflow"). +// The iterative recovery loop handles this in O(1) Go stack frames. + +type S struct { + X int +} + +func main() { + defer func() { + // Recover the final panic after all 500K panicking defers complete. + println(recover()) + }() + var p *S + for i := 0; i < 500000; i++ { + defer func() { + // Nil pointer dereference triggers a Go-level panic(&Exception{}), + // not the cooperative pushPanic path. + _ = p.X + }() + } +} + +// Output: +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/extern/initorder_crossfn/a.gno b/gnovm/tests/files/extern/initorder_crossfn/a.gno new file mode 100644 index 00000000000..63e2d29251f --- /dev/null +++ b/gnovm/tests/files/extern/initorder_crossfn/a.gno @@ -0,0 +1,7 @@ +package initorder_crossfn + +// A depends on GetB which is defined in b.gno. +// findDependentNames must recurse into GetB's body (via GetExternNames) to +// discover that A transitively depends on B, even across file boundaries. + +var A = GetB() diff --git a/gnovm/tests/files/extern/initorder_crossfn/b.gno b/gnovm/tests/files/extern/initorder_crossfn/b.gno new file mode 100644 index 00000000000..a863ed6c382 --- /dev/null +++ b/gnovm/tests/files/extern/initorder_crossfn/b.gno @@ -0,0 +1,5 @@ +package initorder_crossfn + +var B = 42 + +func GetB() int { return B } diff --git a/gnovm/tests/files/extern/initorder_xpkgmethod/pkg.gno b/gnovm/tests/files/extern/initorder_xpkgmethod/pkg.gno new file mode 100644 index 00000000000..91f6217126c --- /dev/null +++ b/gnovm/tests/files/extern/initorder_xpkgmethod/pkg.gno @@ -0,0 +1,13 @@ +package initorder_xpkgmethod + +// B is a package-level variable. GetB references it so that any caller of +// GetB transitively depends on B. A is declared after GetB here to stress +// that the intra-package init order is also correct (B before A). + +var B = 42 + +type T struct{} + +func (T) GetB() int { return B } + +var A = T{}.GetB() diff --git a/gnovm/tests/files/extern/net/http/http.gno b/gnovm/tests/files/extern/net/http/http.gno index 7dec7c90923..e9d1b5f4d0b 100644 --- a/gnovm/tests/files/extern/net/http/http.gno +++ b/gnovm/tests/files/extern/net/http/http.gno @@ -117,10 +117,6 @@ type ResponseWriter interface { WriteHeader(statusCode int) } -type CloseNotifier interface { - CloseNotify() <-chan bool -} - // XXX dummy type Server struct { // Addr optionally specifies the TCP address for the server to listen on, diff --git a/gnovm/tests/files/gas/const.gno b/gnovm/tests/files/gas/const.gno index ce7b5b85843..8c0db7e5abe 100644 --- a/gnovm/tests/files/gas/const.gno +++ b/gnovm/tests/files/gas/const.gno @@ -8,4 +8,4 @@ func main() { } // Gas: -// 2966 +// 3046 diff --git a/gnovm/tests/files/gas/nested_alloc.gno b/gnovm/tests/files/gas/nested_alloc.gno index ec379eda4c8..71563a2c74f 100644 --- a/gnovm/tests/files/gas/nested_alloc.gno +++ b/gnovm/tests/files/gas/nested_alloc.gno @@ -9,4 +9,4 @@ func main() { } // Gas: -// 13273861 +// 24810947885 diff --git a/gnovm/tests/files/gas/slice_alloc.gno b/gnovm/tests/files/gas/slice_alloc.gno index e8c615844a8..e26eb829af3 100644 --- a/gnovm/tests/files/gas/slice_alloc.gno +++ b/gnovm/tests/files/gas/slice_alloc.gno @@ -2,7 +2,7 @@ package main func main() { - alloc(12499894) // 12499894 is the threshold to reach the allocation limit + alloc(12499880) // 12499880 is the threshold to reach the allocation limit } func alloc(n int) { @@ -11,4 +11,4 @@ func alloc(n int) { } // Gas: -// 500003015 +// 500002511 diff --git a/gnovm/tests/files/heap_alloc_forloop1.gno b/gnovm/tests/files/heap_alloc_forloop1.gno index 8455c427a43..e5809718b0a 100644 --- a/gnovm/tests/files/heap_alloc_forloop1.gno +++ b/gnovm/tests/files/heap_alloc_forloop1.gno @@ -21,7 +21,7 @@ func main() { } // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); for i.loopvar := (const (0 int)); i.loopvar<~VPBlock(1,0)> < (const (3 int)); i.loopvar<~VPBlock(1,0)>++ { s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); for i.loopvar := (const (0 int)); i.loopvar<~VPBlock(1,0)> < (const (3 int)); i.loopvar<~VPBlock(1,0)>++ { s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } // Output: // s1[0] is: 0 diff --git a/gnovm/tests/files/heap_alloc_forloop1a.gno b/gnovm/tests/files/heap_alloc_forloop1a.gno index af468eb91f7..7ae79575446 100644 --- a/gnovm/tests/files/heap_alloc_forloop1a.gno +++ b/gnovm/tests/files/heap_alloc_forloop1a.gno @@ -27,7 +27,7 @@ func main() { } // Preprocessed: -// file{ package main; import fmt fmt; type Int (const-type main.Int); var s1 []*(typeval{main.Int}); func inc2(j *(typeval{main.Int})) { *(j) = *(j) + (const (2 main.Int)) }; func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); for i.loopvar := (const (0 main.Int)); i.loopvar<~VPBlock(1,0)> < (const (10 main.Int)); inc2(&(i.loopvar<~VPBlock(1,0)>)) { s1<~VPBlock(4,1)> = (const (append func([]*main.Int, ...*main.Int) []*main.Int))(s1<~VPBlock(4,1)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; type Int (const-type main.Int); var s1 []*(typeval{main.Int}); func inc2(j *(typeval{main.Int})) { *(j) = *(j) + (const (2 main.Int)) }; func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); for i.loopvar := (const (0 main.Int)); i.loopvar<~VPBlock(1,0)> < (const (10 main.Int)); inc2(&(i.loopvar<~VPBlock(1,0)>)) { s1<~VPBlock(4,1)> = (const (append func([]*main.Int, ...*main.Int) []*main.Int))(s1<~VPBlock(4,1)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } // Output: // s1[0] is: 0 diff --git a/gnovm/tests/files/heap_alloc_forloop1b.gno b/gnovm/tests/files/heap_alloc_forloop1b.gno index 2031427a1e3..63f41ca6860 100644 --- a/gnovm/tests/files/heap_alloc_forloop1b.gno +++ b/gnovm/tests/files/heap_alloc_forloop1b.gno @@ -26,7 +26,7 @@ func main() { // go 1.22 loop var is not supported for now. // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); for i.loopvar := (const (0 int)); i.loopvar<~VPBlock(1,0)> < (const (3 int)); i.loopvar<~VPBlock(1,0)>++ { r := i.loopvar<~VPBlock(1,0)>; r, ok := (const (0 int)), (const (true bool)); (const (println func(...interface {})))(ok, r); s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); for i.loopvar := (const (0 int)); i.loopvar<~VPBlock(1,0)> < (const (3 int)); i.loopvar<~VPBlock(1,0)>++ { r := i.loopvar<~VPBlock(1,0)>; r, ok := (const (0 int)), (const (true bool)); (const (println func(...interface {})))(ok, r); s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } // Output: // true 0 diff --git a/gnovm/tests/files/heap_alloc_forloop2.gno b/gnovm/tests/files/heap_alloc_forloop2.gno index 09b52d3d4cf..9d557602528 100644 --- a/gnovm/tests/files/heap_alloc_forloop2.gno +++ b/gnovm/tests/files/heap_alloc_forloop2.gno @@ -25,7 +25,7 @@ func main() { // You can tell by the preprocess printout of z and z<~...>. // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); for i.loopvar := (const (0 int)); i.loopvar < (const (3 int)); i.loopvar++ { z := i.loopvar + (const (1 int)); s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(z<~VPBlock(1,1)>)) } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); for i.loopvar := (const (0 int)); i.loopvar < (const (3 int)); i.loopvar++ { z := i.loopvar + (const (1 int)); s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(z<~VPBlock(1,1)>)) } }; func main() { forLoopRef() } } // Output: // s1[0] is: 1 diff --git a/gnovm/tests/files/heap_alloc_forloop2a.gno b/gnovm/tests/files/heap_alloc_forloop2a.gno index 538ee199325..0fc9358ec46 100644 --- a/gnovm/tests/files/heap_alloc_forloop2a.gno +++ b/gnovm/tests/files/heap_alloc_forloop2a.gno @@ -26,7 +26,7 @@ func main() { // You can tell by the preprocess printout of z and z<~...>. // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); for i.loopvar := (const (0 int)); i.loopvar < (const (3 int)); i.loopvar++ { z := i.loopvar; s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(z<~VPBlock(1,1)>)); z<~VPBlock(1,1)>++ } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); for i.loopvar := (const (0 int)); i.loopvar < (const (3 int)); i.loopvar++ { z := i.loopvar; s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(z<~VPBlock(1,1)>)); z<~VPBlock(1,1)>++ } }; func main() { forLoopRef() } } // Output: // s1[0] is: 1 diff --git a/gnovm/tests/files/heap_alloc_forloop9.gno b/gnovm/tests/files/heap_alloc_forloop9.gno index 2d97e09f239..9fc67f446e1 100644 --- a/gnovm/tests/files/heap_alloc_forloop9.gno +++ b/gnovm/tests/files/heap_alloc_forloop9.gno @@ -26,7 +26,7 @@ func main() { } // Preprocessed: -// file{ package main; import fmt fmt; func main() { var fns []func(.arg_0 (const-type int)) .res.0 (const-type int); var recursiveFunc func(.arg_0 (const-type int)) .res.0 (const-type int); for i.loopvar := (const (0 int)); i.loopvar<~VPBlock(1,0)> < (const (3 int)); i.loopvar<~VPBlock(1,0)>++ { recursiveFunc<~VPBlock(2,1)> = func func(num (const-type int)) .res.0 (const-type int){ x := i.loopvar<~VPBlock(1,3)>; (const (println func(...interface {})))((const ("value of x: " string)), x); if num <= (const (0 int)) { return (const (1 int)) }; return num * recursiveFunc<~VPBlock(1,4)>(num - (const (1 int))) }, recursiveFunc<()~VPBlock(2,1)>>; fns = (const (append func([]func(int) int, ...func(int) int) []func(int) int))(fns, recursiveFunc<~VPBlock(2,1)>) }; for i.loopvar, r.loopvar := range fns { result := r.loopvar(i.loopvar); (const (ref(fmt) package{})).Printf((const ("Factorial of %d is: %d\n" string)), i.loopvar, result) } } } +// file{ package main; import fmt fmt; func main() { var fns []func(.arg_0 (const-type int)) .res.0 (const-type int); var recursiveFunc func(.arg_0 (const-type int)) .res.0 (const-type int); for i.loopvar := (const (0 int)); i.loopvar<~VPBlock(1,0)> < (const (3 int)); i.loopvar<~VPBlock(1,0)>++ { recursiveFunc<~VPBlock(2,1)> = func func(num (const-type int)) .res.0 (const-type int){ x := i.loopvar<~VPBlock(1,3)>; (const (println func(...interface {})))((const ("value of x: " string)), x); if num <= (const (0 int)) { return (const (1 int)) }; return num * recursiveFunc<~VPBlock(1,4)>(num - (const (1 int))) }, recursiveFunc<()~VPBlock(2,1)>>; fns = (const (append func([]func(int) int, ...func(int) int) []func(int) int))(fns, recursiveFunc<~VPBlock(2,1)>) }; for i, r := range fns { result := r(i); (const (ref(fmt) package{})).Printf((const ("Factorial of %d is: %d\n" string)), i, result) } } } // Output: // value of x: 0 diff --git a/gnovm/tests/files/heap_alloc_gotoloop9_10.gno b/gnovm/tests/files/heap_alloc_gotoloop9_10.gno index 9f1aa8cd2d7..7058252c703 100644 --- a/gnovm/tests/files/heap_alloc_gotoloop9_10.gno +++ b/gnovm/tests/files/heap_alloc_gotoloop9_10.gno @@ -41,7 +41,7 @@ LOOP_2: } // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); var s2 []*((const-type int)); func main() { defer func func(){ for i.loopvar, v.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is %d\n" string)), i.loopvar, *(v.loopvar)) }; for i.loopvar, v.loopvar := range (const (ref(main) package{})).s2 { (const (ref(fmt) package{})).Printf((const ("s2[%d] is %d\n" string)), i.loopvar, *(v.loopvar)) } }(); var c1, c2 (const-type int); x := c1; s1<~VPBlock(3,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(3,0)>, &(x<~VPBlock(1,2)>)); (const (println func(...interface {})))((const ("loop_1" string)), c1); c1++; y := c2; s2<~VPBlock(3,1)> = (const (append func([]*int, ...*int) []*int))(s2<~VPBlock(3,1)>, &(y<~VPBlock(1,3)>)); (const (println func(...interface {})))((const ("loop_2" string)), c2); c2++; if c1 < (const (3 int)) { goto LOOP_1<1,0,2> }; if c2 < (const (6 int)) { goto LOOP_2<1,0,6> } } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); var s2 []*((const-type int)); func main() { defer func func(){ for i, v := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is %d\n" string)), i, *(v)) }; for i, v := range (const (ref(main) package{})).s2 { (const (ref(fmt) package{})).Printf((const ("s2[%d] is %d\n" string)), i, *(v)) } }(); var c1, c2 (const-type int); x := c1; s1<~VPBlock(3,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(3,0)>, &(x<~VPBlock(1,2)>)); (const (println func(...interface {})))((const ("loop_1" string)), c1); c1++; y := c2; s2<~VPBlock(3,1)> = (const (append func([]*int, ...*int) []*int))(s2<~VPBlock(3,1)>, &(y<~VPBlock(1,3)>)); (const (println func(...interface {})))((const ("loop_2" string)), c2); c2++; if c1 < (const (3 int)) { goto LOOP_1<1,0,2> }; if c2 < (const (6 int)) { goto LOOP_2<1,0,6> } } } // Output: // loop_1 0 diff --git a/gnovm/tests/files/heap_alloc_range1.gno b/gnovm/tests/files/heap_alloc_range1.gno index 45785ba2d29..c20de43f4ef 100644 --- a/gnovm/tests/files/heap_alloc_range1.gno +++ b/gnovm/tests/files/heap_alloc_range1.gno @@ -22,7 +22,7 @@ func main() { } // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); s := (const-type []int){(const (0 int)), (const (1 int)), (const (2 int))}; for i.loopvar, _ := range s { s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(i.loopvar<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); s := (const-type []int){(const (0 int)), (const (1 int)), (const (2 int))}; for i, _ := range s { s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(i<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } // Output: // s1[0] is: 0 diff --git a/gnovm/tests/files/heap_alloc_range2.gno b/gnovm/tests/files/heap_alloc_range2.gno index 5d85988b26e..88d6ec9d928 100644 --- a/gnovm/tests/files/heap_alloc_range2.gno +++ b/gnovm/tests/files/heap_alloc_range2.gno @@ -22,7 +22,7 @@ func main() { } // Preprocessed: -// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i.loopvar, e.loopvar := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i.loopvar, *(e.loopvar)) } }(); s := (const-type []int){(const (0 int)), (const (1 int)), (const (2 int))}; for _, v := range s { s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(v<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } +// file{ package main; import fmt fmt; var s1 []*((const-type int)); func forLoopRef() { defer func func(){ for i, e := range (const (ref(main) package{})).s1 { (const (ref(fmt) package{})).Printf((const ("s1[%d] is: %d\n" string)), i, *(e)) } }(); s := (const-type []int){(const (0 int)), (const (1 int)), (const (2 int))}; for _, v := range s { s1<~VPBlock(4,0)> = (const (append func([]*int, ...*int) []*int))(s1<~VPBlock(4,0)>, &(v<~VPBlock(1,0)>)) } }; func main() { forLoopRef() } } // Output: // s1[0] is: 0 diff --git a/gnovm/tests/files/heap_alloc_range3.gno b/gnovm/tests/files/heap_alloc_range3.gno index 08454e01199..7174267ee58 100644 --- a/gnovm/tests/files/heap_alloc_range3.gno +++ b/gnovm/tests/files/heap_alloc_range3.gno @@ -14,7 +14,7 @@ func main() { } // Preprocessed: -// file{ package main; func main() { s := (const-type []int){(const (1 int)), (const (2 int))}; f := func func(){ for i.loopvar, v.loopvar := range s<~VPBlock(2,0)> { (const (println func(...interface {})))(i.loopvar); (const (println func(...interface {})))(v.loopvar) } }>; f() } } +// file{ package main; func main() { s := (const-type []int){(const (1 int)), (const (2 int))}; f := func func(){ for i, v := range s<~VPBlock(2,0)> { (const (println func(...interface {})))(i); (const (println func(...interface {})))(v) } }>; f() } } // Output: // 0 diff --git a/gnovm/tests/files/heap_alloc_range4.gno b/gnovm/tests/files/heap_alloc_range4.gno index 51dc6b82546..942d1763b4a 100644 --- a/gnovm/tests/files/heap_alloc_range4.gno +++ b/gnovm/tests/files/heap_alloc_range4.gno @@ -16,7 +16,7 @@ func main() { } // Preprocessed: -// file{ package main; func main() { var fns []func() .res.0 (const-type int); s := (const-type []int){(const (1 int)), (const (2 int)), (const (3 int))}; for i.loopvar, _ := range s { x := i.loopvar; f := func func() .res.0 (const-type int){ return x<~VPBlock(1,1)> }>; fns = (const (append func([]func() int, ...func() int) []func() int))(fns, f) }; for _, fn := range fns { (const (println func(...interface {})))(fn()) } } } +// file{ package main; func main() { var fns []func() .res.0 (const-type int); s := (const-type []int){(const (1 int)), (const (2 int)), (const (3 int))}; for i, _ := range s { x := i; f := func func() .res.0 (const-type int){ return x<~VPBlock(1,1)> }>; fns = (const (append func([]func() int, ...func() int) []func() int))(fns, f) }; for _, fn := range fns { (const (println func(...interface {})))(fn()) } } } // Output: // 0 diff --git a/gnovm/tests/files/heap_alloc_range4b.gno b/gnovm/tests/files/heap_alloc_range4b.gno index 488670fc74e..6e399b9a99f 100644 --- a/gnovm/tests/files/heap_alloc_range4b.gno +++ b/gnovm/tests/files/heap_alloc_range4b.gno @@ -16,7 +16,7 @@ func main() { } // Preprocessed: -// file{ package main; func main() { var fns []func() .res.0 (const-type int); s := (const ("hello" string)); for i.loopvar, _ := range s { x := i.loopvar; f := func func() .res.0 (const-type int){ return x<~VPBlock(1,1)> }>; fns = (const (append func([]func() int, ...func() int) []func() int))(fns, f) }; for _, fn := range fns { (const (println func(...interface {})))(fn()) } } } +// file{ package main; func main() { var fns []func() .res.0 (const-type int); s := (const ("hello" string)); for i, _ := range s { x := i; f := func func() .res.0 (const-type int){ return x<~VPBlock(1,1)> }>; fns = (const (append func([]func() int, ...func() int) []func() int))(fns, f) }; for _, fn := range fns { (const (println func(...interface {})))(fn()) } } } // Output: // 0 diff --git a/gnovm/tests/files/heap_alloc_range4b1.gno b/gnovm/tests/files/heap_alloc_range4b1.gno index f6eadfc5f4f..e8600abd983 100644 --- a/gnovm/tests/files/heap_alloc_range4b1.gno +++ b/gnovm/tests/files/heap_alloc_range4b1.gno @@ -15,7 +15,7 @@ func main() { } // Preprocessed: -// file{ package main; func main() { var fns []func() .res.0 (const-type int); s := (const ("hello" string)); for i.loopvar, _ := range s { f := func func() .res.0 (const-type int){ return i.loopvar<~VPBlock(1,1)> }>; fns = (const (append func([]func() int, ...func() int) []func() int))(fns, f) }; for _, fn := range fns { (const (println func(...interface {})))(fn()) } } } +// file{ package main; func main() { var fns []func() .res.0 (const-type int); s := (const ("hello" string)); for i, _ := range s { f := func func() .res.0 (const-type int){ return i<~VPBlock(1,1)> }>; fns = (const (append func([]func() int, ...func() int) []func() int))(fns, f) }; for _, fn := range fns { (const (println func(...interface {})))(fn()) } } } // Output: // 0 diff --git a/gnovm/tests/files/index1.gno b/gnovm/tests/files/index1.gno new file mode 100644 index 00000000000..1d0505117fa --- /dev/null +++ b/gnovm/tests/files/index1.gno @@ -0,0 +1,10 @@ +package main + +func main() { + var a [1024]byte + i := -1 + _ = a[i] +} + +// Error: +// runtime error: index out of range [-1] diff --git a/gnovm/tests/files/index10.gno b/gnovm/tests/files/index10.gno new file mode 100644 index 00000000000..4b484aaea84 --- /dev/null +++ b/gnovm/tests/files/index10.gno @@ -0,0 +1,11 @@ +package main + +func main() { + _ = []int{-1: 5} +} + +// Error: +// main/index10.gno:4:6-18: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index10.gno:4:12: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index11.gno b/gnovm/tests/files/index11.gno new file mode 100644 index 00000000000..4ed3467d675 --- /dev/null +++ b/gnovm/tests/files/index11.gno @@ -0,0 +1,11 @@ +package main + +func main() { + _ = [5]int{-1: 5} +} + +// Error: +// main/index11.gno:4:6-19: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index11.gno:4:13: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index2.gno b/gnovm/tests/files/index2.gno new file mode 100644 index 00000000000..9e87df0a00b --- /dev/null +++ b/gnovm/tests/files/index2.gno @@ -0,0 +1,12 @@ +package main + +func main() { + var a [1024]byte + _ = a[-1] +} + +// Error: +// main/index2.gno:5:6-11: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index2.gno:5:8: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index3.gno b/gnovm/tests/files/index3.gno new file mode 100644 index 00000000000..7c6b03eab94 --- /dev/null +++ b/gnovm/tests/files/index3.gno @@ -0,0 +1,17 @@ +package main + +func main() { + defer func() { + r := recover() + println("recover:", r) // not recoverable. + }() + + s := []int{1, 2, 3} + _ = s[-1:] // Panics because of negative index +} + +// Error: +// main/index3.gno:10:6-12: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index3.gno:10:8: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index4.gno b/gnovm/tests/files/index4.gno new file mode 100644 index 00000000000..94268b4441c --- /dev/null +++ b/gnovm/tests/files/index4.gno @@ -0,0 +1,12 @@ +package main + +func main() { + var a = []int{1, 2, 3} + _ = a[-1] +} + +// Error: +// main/index4.gno:5:6-11: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index4.gno:5:8: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index5.gno b/gnovm/tests/files/index5.gno new file mode 100644 index 00000000000..03e791bd3d0 --- /dev/null +++ b/gnovm/tests/files/index5.gno @@ -0,0 +1,12 @@ +package main + +func main() { + s := "hello" + _ = s[-1] +} + +// Error: +// main/index5.gno:5:6-11: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index5.gno:5:8: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index6.gno b/gnovm/tests/files/index6.gno new file mode 100644 index 00000000000..23dadd004e8 --- /dev/null +++ b/gnovm/tests/files/index6.gno @@ -0,0 +1,12 @@ +package main + +func main() { + s := []int{1, 2, 3} + _ = s[:-1:2] // Panics because of negative index +} + +// Error: +// main/index6.gno:5:6-14: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index6.gno:5:9: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index7.gno b/gnovm/tests/files/index7.gno new file mode 100644 index 00000000000..4c6a080293b --- /dev/null +++ b/gnovm/tests/files/index7.gno @@ -0,0 +1,12 @@ +package main + +func main() { + s := []int{1, 2, 3} + _ = s[:1:-1] // Panics because of negative index +} + +// Error: +// main/index7.gno:5:6-14: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index7.gno:5:11: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/index8.gno b/gnovm/tests/files/index8.gno new file mode 100644 index 00000000000..e9663064988 --- /dev/null +++ b/gnovm/tests/files/index8.gno @@ -0,0 +1,14 @@ +package main + +const c = -1 + +func main() { + var a [5]int + _ = a[c] +} + +// Error: +// main/index8.gno:7:6-10: invalid argument: index must not be negative: (-1 bigint) + +// TypeCheckError: +// main/index8.gno:7:8: invalid argument: index c (constant -1 of type int) must not be negative diff --git a/gnovm/tests/files/index9.gno b/gnovm/tests/files/index9.gno new file mode 100644 index 00000000000..878f8620226 --- /dev/null +++ b/gnovm/tests/files/index9.gno @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + m := map[int]int{-1: 1} + fmt.Println(m[-1]) +} + +// Output: +// 1 diff --git a/gnovm/tests/files/loopvar_err_1.gno b/gnovm/tests/files/loopvar_err_1.gno new file mode 100644 index 00000000000..b161a67ecb8 --- /dev/null +++ b/gnovm/tests/files/loopvar_err_1.gno @@ -0,0 +1,16 @@ +package main + +// Type error inside a for loop: check that the error message does NOT +// expose the internal ".loopvar" name suffix to users. +func main() { + for i := 0; i < 3; i++ { + var s string = i + _ = s + } +} + +// Error: +// main/loopvar_err_1.gno:7:7-19: cannot use int as string + +// TypeCheckError: +// main/loopvar_err_1.gno:7:18: cannot use i (variable of type int) as string value in variable declaration diff --git a/gnovm/tests/files/loopvar_err_2.gno b/gnovm/tests/files/loopvar_err_2.gno new file mode 100644 index 00000000000..da485f542b9 --- /dev/null +++ b/gnovm/tests/files/loopvar_err_2.gno @@ -0,0 +1,16 @@ +package main + +// Using a loop variable after the loop exits should report "name not defined: i" +// not "name not defined: i.loopvar". +func main() { + for i := 0; i < 3; i++ { + _ = i + } + println(i) // i is out of scope here +} + +// Error: +// main/loopvar_err_2.gno:9:10-11: name i not declared + +// TypeCheckError: +// main/loopvar_err_2.gno:9:10: undefined: i diff --git a/gnovm/tests/files/loopvar_goto_1.gno b/gnovm/tests/files/loopvar_goto_1.gno new file mode 100644 index 00000000000..ab77923ec71 --- /dev/null +++ b/gnovm/tests/files/loopvar_goto_1.gno @@ -0,0 +1,20 @@ +package main + +// goto label inside for loop body, with loop var references on both sides of the label. +func main() { + for i := 0; i < 3; i++ { + if i == 1 { + goto SKIP + } + println("body:", i) + SKIP: + println("end:", i) + } +} + +// Output: +// body: 0 +// end: 0 +// end: 1 +// body: 2 +// end: 2 diff --git a/gnovm/tests/files/loopvar_map_key_1.gno b/gnovm/tests/files/loopvar_map_key_1.gno new file mode 100644 index 00000000000..dbd8ff5a12b --- /dev/null +++ b/gnovm/tests/files/loopvar_map_key_1.gno @@ -0,0 +1,16 @@ +package main + +// Map composite literal: the loop var name is used as a map key expression. +// Unlike struct fields, map keys ARE variable references and MUST be renamed. +// This also exercises the TRANS_COMPOSITE_KEY fix for struct field names. +func main() { + for k := range []string{"a", "b", "c"} { + m := map[int]string{k: "val"} + println(k, m[k]) + } +} + +// Output: +// 0 val +// 1 val +// 2 val diff --git a/gnovm/tests/files/loopvar_modify_body_1.gno b/gnovm/tests/files/loopvar_modify_body_1.gno new file mode 100644 index 00000000000..a1724923e7f --- /dev/null +++ b/gnovm/tests/files/loopvar_modify_body_1.gno @@ -0,0 +1,18 @@ +package main + +// Modifying the loop init var in the body affects the condition check, +// causing early termination. The closure captures the value after the modification. +func main() { + var fns []func() int + for i := 0; i < 10; i++ { + i += 5 // skip ahead; within same iteration + fns = append(fns, func() int { return i }) + } + for _, fn := range fns { + println(fn()) + } +} + +// Output: +// 5 +// 11 diff --git a/gnovm/tests/files/loopvar_multi_init_1.gno b/gnovm/tests/files/loopvar_multi_init_1.gno new file mode 100644 index 00000000000..06f2549f802 --- /dev/null +++ b/gnovm/tests/files/loopvar_multi_init_1.gno @@ -0,0 +1,23 @@ +package main + +// Multiple for-init vars directly captured by closures (no shadowing). +// Exercises NumInit==2 heap-copy path. j is mutated in the body; +// the closure for each iteration sees j's within-iteration modified value. +// i is not mutated in the body; closure sees pre-post-stmt value (0, 1, 2). +func main() { + var fns []func() (int, int) + for i, j := 0, 10; i < 3; i++ { + f := func() (int, int) { return i, j } + fns = append(fns, f) + j += 5 // modify loop var j in body + } + for _, fn := range fns { + a, b := fn() + println(a, b) + } +} + +// Output: +// 0 15 +// 1 20 +// 2 25 diff --git a/gnovm/tests/files/loopvar_multi_init_2.gno b/gnovm/tests/files/loopvar_multi_init_2.gno new file mode 100644 index 00000000000..811ded62082 --- /dev/null +++ b/gnovm/tests/files/loopvar_multi_init_2.gno @@ -0,0 +1,21 @@ +package main + +// Multiple for-init vars: both i and j are loop vars (NumInit==2). +// Shadow them to get per-iteration snapshots without body mutation. +func main() { + var fns []func() int + for i, j := 0, 10; i < 3; i++ { + i := i + j := j + fns = append(fns, func() int { return i*100 + j }) + j += 5 // modifies body-scope j after snapshot; closure sees it (j=15) + } + for _, fn := range fns { + println(fn()) + } +} + +// Output: +// 15 +// 115 +// 215 diff --git a/gnovm/tests/files/loopvar_outer_shadow_1.gno b/gnovm/tests/files/loopvar_outer_shadow_1.gno new file mode 100644 index 00000000000..b94accfe286 --- /dev/null +++ b/gnovm/tests/files/loopvar_outer_shadow_1.gno @@ -0,0 +1,18 @@ +package main + +// Outer var with same name as loop var should not be affected by the rename. +// Inside the loop, i refers to i.loopvar (the loop var). +// After the loop, i refers to the outer var (99). +func main() { + i := 99 + for i := 0; i < 3; i++ { + println("loop:", i) + } + println("outer:", i) +} + +// Output: +// loop: 0 +// loop: 1 +// loop: 2 +// outer: 99 diff --git a/gnovm/tests/files/loopvar_range_capture_mutate_1.gno b/gnovm/tests/files/loopvar_range_capture_mutate_1.gno new file mode 100644 index 00000000000..efd7d876644 --- /dev/null +++ b/gnovm/tests/files/loopvar_range_capture_mutate_1.gno @@ -0,0 +1,20 @@ +package main + +// Range loop: closure captures the range value var, then it's mutated. +// Each iteration has its own per-iteration v; mutation stays local. +func main() { + var fns []func() int + for _, v := range []int{1, 2, 3} { + f := func() int { return v } // capture first + v += 100 // mutate after; same iteration's v + fns = append(fns, f) + } + for _, fn := range fns { + println(fn()) + } +} + +// Output: +// 101 +// 102 +// 103 diff --git a/gnovm/tests/files/loopvar_range_kv_shadow_2.gno b/gnovm/tests/files/loopvar_range_kv_shadow_2.gno new file mode 100644 index 00000000000..95508816c65 --- /dev/null +++ b/gnovm/tests/files/loopvar_range_kv_shadow_2.gno @@ -0,0 +1,21 @@ +package main + +func main() { + for i, v := range []int{10, 20, 30} { + i := i+1 + v := v+1 + println("inner i:", i) + println("inner v:", v) + } +} + +// Output: +// inner i: 1 +// inner v: 11 +// inner i: 2 +// inner v: 21 +// inner i: 3 +// inner v: 31 + +// Preprocessed: +// file{ package main; func main() { for i, v := range (const-type []int){(const (10 int)), (const (20 int)), (const (30 int))} { i := i + (const (1 int)); v := v + (const (1 int)); (const (println func(...interface {})))((const ("inner i:" string)), i); (const (println func(...interface {})))((const ("inner v:" string)), v) } } } diff --git a/gnovm/tests/files/loopvar_range_outer_shadow_1.gno b/gnovm/tests/files/loopvar_range_outer_shadow_1.gno new file mode 100644 index 00000000000..61bff352733 --- /dev/null +++ b/gnovm/tests/files/loopvar_range_outer_shadow_1.gno @@ -0,0 +1,14 @@ +package main + +// Range loop var with same name as an outer var: outer var should be unchanged. +func main() { + i := 99 + for i, v := range []int{10, 20, 30} { + _ = v + _ = i + } + println("outer i:", i) // 99 +} + +// Output: +// outer i: 99 diff --git a/gnovm/tests/files/loopvar_redefine_4.gno b/gnovm/tests/files/loopvar_redefine_4.gno index 2dccdf40ce7..aa25a6a9695 100644 --- a/gnovm/tests/files/loopvar_redefine_4.gno +++ b/gnovm/tests/files/loopvar_redefine_4.gno @@ -9,7 +9,7 @@ func main() { } // Preprocessed: -// file{ package main; func main() { for i.loopvar, v.loopvar := range (const-type []int){(const (1 int)), (const (2 int)), (const (3 int))} { i := i.loopvar; v := v.loopvar; (const (println func(...interface {})))(i, v) } } } +// file{ package main; func main() { for i, v := range (const-type []int){(const (1 int)), (const (2 int)), (const (3 int))} { i := i; v := v; (const (println func(...interface {})))(i, v) } } } // Output: // 0 1 diff --git a/gnovm/tests/files/loopvar_struct_field_1.gno b/gnovm/tests/files/loopvar_struct_field_1.gno new file mode 100644 index 00000000000..3051a550521 --- /dev/null +++ b/gnovm/tests/files/loopvar_struct_field_1.gno @@ -0,0 +1,30 @@ +package main + +// Struct composite literal: field names (i, v) match range loop var names. +// Field names must NOT be renamed to i.loopvar / v.loopvar. +// Value expressions (i, v) must be renamed correctly. +type S struct { + i int + v string +} + +func main() { + var fns []func() S + for i, v := range []string{"a", "b", "c"} { + i := i + v := v + fns = append(fns, func() S { return S{i: i, v: v} }) + } + for _, fn := range fns { + s := fn() + println(s.i, s.v) + } +} + +// Preprocessed: +// file{ package main; type S (const-type main.S); func main() { var fns []func() .res.0 typeval{main.S}; for i, v := range (const-type []string){(const ("a" string)), (const ("b" string)), (const ("c" string))} { i := i<~VPBlock(1,0)>; v := v<~VPBlock(1,1)>; fns = (const (append func([]func() main.S, ...func() main.S) []func() main.S))(fns, func func() .res.0 typeval{main.S}{ return (const-type main.S){i: i<~VPBlock(1,1)>, v: v<~VPBlock(1,2)>} }, v<()~VPBlock(1,1)>>) }; for _, fn := range fns { s := fn(); (const (println func(...interface {})))(s.i, s.v) } } } + +// Output: +// 0 a +// 1 b +// 2 c diff --git a/gnovm/tests/files/loopvar_struct_field_2.gno b/gnovm/tests/files/loopvar_struct_field_2.gno new file mode 100644 index 00000000000..e1b2df9e55a --- /dev/null +++ b/gnovm/tests/files/loopvar_struct_field_2.gno @@ -0,0 +1,19 @@ +package main + +// Struct composite literal: field names that exactly match for-loop init vars. +// Tests that TRANS_COMPOSITE_KEY NameExprs are not renamed to i.loopvar. +type S struct { + i int +} + +func main() { + for i := 0; i < 3; i++ { + s := S{i: i * 10} + println(s.i) + } +} + +// Output: +// 0 +// 10 +// 20 diff --git a/gnovm/tests/files/loopvar_switch_1.gno b/gnovm/tests/files/loopvar_switch_1.gno new file mode 100644 index 00000000000..d527b53c73c --- /dev/null +++ b/gnovm/tests/files/loopvar_switch_1.gno @@ -0,0 +1,28 @@ +package main + +// switch on loop var: the loop var expression is used in the switch tag. +// Closures capture a body-scope label derived from the switch result. +func main() { + var fns []func() string + for i := 0; i < 4; i++ { + var label string + switch i { + case 0: + label = "zero" + case 1: + label = "one" + default: + label = "other" + } + fns = append(fns, func() string { return label }) + } + for _, fn := range fns { + println(fn()) + } +} + +// Output: +// zero +// one +// other +// other diff --git a/gnovm/tests/files/make10.gno b/gnovm/tests/files/make10.gno new file mode 100644 index 00000000000..efdaa663244 --- /dev/null +++ b/gnovm/tests/files/make10.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, -1, 5) + println(t) +} + +// Error: +// main/make10.gno:4:7-25: invalid argument: index (-1 bigint) must not be negative + +// TypeCheckError: +// main/make10.gno:4:19: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/make11.gno b/gnovm/tests/files/make11.gno new file mode 100644 index 00000000000..1ffd761abc4 --- /dev/null +++ b/gnovm/tests/files/make11.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, 5, -1) + println(t) +} + +// Error: +// main/make11.gno:4:7-25: invalid argument: index (-1 bigint) must not be negative + +// TypeCheckError: +// main/make11.gno:4:22: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/make12.gno b/gnovm/tests/files/make12.gno new file mode 100644 index 00000000000..18439a88dc3 --- /dev/null +++ b/gnovm/tests/files/make12.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make(-1) + println(t) +} + +// Error: +// main/make12.gno:4:7-15: (const (-1 bigint)) is not a type + +// TypeCheckError: +// main/make12.gno:4:12: -1 is not a type diff --git a/gnovm/tests/files/make13.gno b/gnovm/tests/files/make13.gno new file mode 100644 index 00000000000..306b91e4509 --- /dev/null +++ b/gnovm/tests/files/make13.gno @@ -0,0 +1,10 @@ +package main + +func main() { + i := -1 + t := make([]int, 5, i) + println(t) +} + +// Error: +// runtime error: makeslice: cap out of range diff --git a/gnovm/tests/files/make14.gno b/gnovm/tests/files/make14.gno new file mode 100644 index 00000000000..733ff96802d --- /dev/null +++ b/gnovm/tests/files/make14.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, 6, 3) + println(t) +} + +// Error: +// main/make14.gno:4:7-24: invalid argument: len larger than cap in make([]int) + +// TypeCheckError: +// main/make14.gno:4:19: invalid argument: length and capacity swapped diff --git a/gnovm/tests/files/make15.gno b/gnovm/tests/files/make15.gno new file mode 100644 index 00000000000..f9cf943a272 --- /dev/null +++ b/gnovm/tests/files/make15.gno @@ -0,0 +1,9 @@ +package main + +func main() { + l := 2 + _ = make([]int, l, 1) +} + +// Error: +// runtime error: makeslice: cap out of range diff --git a/gnovm/tests/files/make16.gno b/gnovm/tests/files/make16.gno new file mode 100644 index 00000000000..913cab7a09e --- /dev/null +++ b/gnovm/tests/files/make16.gno @@ -0,0 +1,9 @@ +package main + +func main() { + l := -1 + _ = make([]int, l) +} + +// Error: +// runtime error: makeslice: len out of range diff --git a/gnovm/tests/files/make17.gno b/gnovm/tests/files/make17.gno new file mode 100644 index 00000000000..2efb97d6662 --- /dev/null +++ b/gnovm/tests/files/make17.gno @@ -0,0 +1,9 @@ +package main + +func main() { + c := -1 + _ = make([]int, 0, c) +} + +// Error: +// runtime error: makeslice: cap out of range diff --git a/gnovm/tests/files/make18.gno b/gnovm/tests/files/make18.gno new file mode 100644 index 00000000000..1b65afac087 --- /dev/null +++ b/gnovm/tests/files/make18.gno @@ -0,0 +1,9 @@ +package main + +func main() { + l := -1 + _ = make([]int, l, 10) +} + +// Error: +// runtime error: makeslice: len out of range diff --git a/gnovm/tests/files/make3.gno b/gnovm/tests/files/make3.gno new file mode 100644 index 00000000000..455a3783b7b --- /dev/null +++ b/gnovm/tests/files/make3.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, -1) + println(t) +} + +// Error: +// main/make3.gno:4:7-22: invalid argument: index (-1 bigint) must not be negative + +// TypeCheckError: +// main/make3.gno:4:19: invalid argument: index -1 (constant of type int) must not be negative diff --git a/gnovm/tests/files/make4.gno b/gnovm/tests/files/make4.gno new file mode 100644 index 00000000000..49afe555d33 --- /dev/null +++ b/gnovm/tests/files/make4.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, 1, -1.0) + println(t) +} + +// Error: +// main/make4.gno:4:7-27: invalid argument: index (-1.0 bigdec) must not be negative + +// TypeCheckError: +// main/make4.gno:4:22: invalid argument: index -1.0 (constant -1 of type int) must not be negative diff --git a/gnovm/tests/files/make5.gno b/gnovm/tests/files/make5.gno new file mode 100644 index 00000000000..54a0df54fa0 --- /dev/null +++ b/gnovm/tests/files/make5.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, 1, -1.2) + println(t) +} + +// Error: +// main/make5.gno:4:7-27: invalid argument: index (-1.2 bigdec) must not be negative + +// TypeCheckError: +// main/make5.gno:4:22: -1.2 (untyped float constant) truncated to int diff --git a/gnovm/tests/files/make6.gno b/gnovm/tests/files/make6.gno new file mode 100644 index 00000000000..21042422909 --- /dev/null +++ b/gnovm/tests/files/make6.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make([]int, 1, 1.2) + println(t) +} + +// TypeCheckError: +// main/make6.gno:4:22: 1.2 (untyped float constant) truncated to int + +// Error: +// main/make6.gno:4:7-26: cannot convert untyped bigdec to integer -- 1.2 not an exact integer diff --git a/gnovm/tests/files/make7.gno b/gnovm/tests/files/make7.gno new file mode 100644 index 00000000000..20a6fb042f0 --- /dev/null +++ b/gnovm/tests/files/make7.gno @@ -0,0 +1,12 @@ +package main + +func main() { + t := make(map[int]string, -1) + println(t) +} + +// TypeCheckError: +// main/make7.gno:4:28: invalid argument: index -1 (constant of type int) must not be negative + +// Error: +// main/make7.gno:4:7-31: invalid argument: index (-1 bigint) must not be negative diff --git a/gnovm/tests/files/make8.gno b/gnovm/tests/files/make8.gno new file mode 100644 index 00000000000..f47bafaa3c3 --- /dev/null +++ b/gnovm/tests/files/make8.gno @@ -0,0 +1,10 @@ +package main + +func main() { + i := -1 + t := make(map[int]string, i) + println(t) +} + +// Output: +// map{} diff --git a/gnovm/tests/files/make9.gno b/gnovm/tests/files/make9.gno new file mode 100644 index 00000000000..ad85982daa8 --- /dev/null +++ b/gnovm/tests/files/make9.gno @@ -0,0 +1,13 @@ +package main + +func main() { + const i = -1 + t := make(map[int]string, i) + println(t) +} + +// Error: +// main/make9.gno:5:7-30: invalid argument: index (-1 bigint) must not be negative + +// TypeCheckError: +// main/make9.gno:5:28: invalid argument: index i (constant -1 of type int) must not be negative diff --git a/gnovm/tests/files/map0.gno b/gnovm/tests/files/map0.gno index 92ca5a2fe0d..a3ac1bc2250 100644 --- a/gnovm/tests/files/map0.gno +++ b/gnovm/tests/files/map0.gno @@ -10,3 +10,6 @@ func main() { // TypeCheckError: // main/map0.gno:7:3: duplicate key "hello" in map literal; main/map0.gno:4:2: declared and not used: m + +// Error: +// main/map0.gno:4:7-8:3: duplicate key ("hello" string) in map literal diff --git a/gnovm/tests/files/map31b.gno b/gnovm/tests/files/map31b.gno index c930ee5ba98..99195873bb7 100644 --- a/gnovm/tests/files/map31b.gno +++ b/gnovm/tests/files/map31b.gno @@ -7,8 +7,8 @@ var ( ) func main() { - tmpInt := &i - m[tmpInt] = 1 + tmpInt := &i // heapitem((3 int)) is dirty after this. + m[tmpInt] = 1 // m is also dirty. } // Realm: diff --git a/gnovm/tests/files/map43a.gno b/gnovm/tests/files/map43a.gno new file mode 100644 index 00000000000..bf28cd0162f --- /dev/null +++ b/gnovm/tests/files/map43a.gno @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + m := map[any]int{nil: 0, nil: 1, 3: 1} + fmt.Printf("%v", m) +} + +// Output: +// map[:1 3:1] diff --git a/gnovm/tests/files/maplit.gno b/gnovm/tests/files/maplit.gno new file mode 100644 index 00000000000..70914b18871 --- /dev/null +++ b/gnovm/tests/files/maplit.gno @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + a := map[int]int{0: 10, 0: 20} + fmt.Println(a) +} + +// Error: +// main/maplit.gno:6:7-32: duplicate key (0 int) in map literal + +// TypeCheckError: +// main/maplit.gno:6:26: duplicate key 0 in map literal diff --git a/gnovm/tests/files/maplit10.gno b/gnovm/tests/files/maplit10.gno new file mode 100644 index 00000000000..9661f9f23d1 --- /dev/null +++ b/gnovm/tests/files/maplit10.gno @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + a := map[[2]int]int{[2]int{1, 2}: 10, [2]int{3, 4}: 20} + fmt.Println(a) +} + +// Output: +// map[[1 2]:10 [3 4]:20] diff --git a/gnovm/tests/files/maplit11.gno b/gnovm/tests/files/maplit11.gno new file mode 100644 index 00000000000..c09441450a2 --- /dev/null +++ b/gnovm/tests/files/maplit11.gno @@ -0,0 +1,15 @@ +package main + +type S struct { + s []int +} + +func main() { + _ = map[S]int{S{[]int{1}}: 10} +} + +// Error: +// main/maplit11.gno:8:6-32: invalid map key type main.S + +// TypeCheckError: +// main/maplit11.gno:8:10: invalid map key type S diff --git a/gnovm/tests/files/maplit12.gno b/gnovm/tests/files/maplit12.gno new file mode 100644 index 00000000000..4635dddde6f --- /dev/null +++ b/gnovm/tests/files/maplit12.gno @@ -0,0 +1,11 @@ +package main + +func main() { + _ = map[[2][]int]int{[2][]int{{1}, {2}}: 10} +} + +// Error: +// main/maplit12.gno:4:6-46: invalid map key type [2][]int + +// TypeCheckError: +// main/maplit12.gno:4:10: invalid map key type [2][]int diff --git a/gnovm/tests/files/maplit13.gno b/gnovm/tests/files/maplit13.gno new file mode 100644 index 00000000000..a56f300df85 --- /dev/null +++ b/gnovm/tests/files/maplit13.gno @@ -0,0 +1,24 @@ +package main + +import "fmt" + +type Inner struct { + X int + Y int +} + +type Outer struct { + A Inner + B string +} + +func main() { + a := map[Outer]int{ + Outer{Inner{1, 2}, "a"}: 10, + Outer{Inner{3, 4}, "b"}: 20, + } + fmt.Println(a) +} + +// Output: +// map[{{1 2} a}:10 {{3 4} b}:20] diff --git a/gnovm/tests/files/maplit14.gno b/gnovm/tests/files/maplit14.gno new file mode 100644 index 00000000000..01b6f6e2a19 --- /dev/null +++ b/gnovm/tests/files/maplit14.gno @@ -0,0 +1,21 @@ +package main + +type Inner struct { + X int + Y []int +} + +type Outer struct { + A Inner + B string +} + +func main() { + _ = map[Outer]int{Outer{Inner{1, []int{2}}, "a"}: 10} +} + +// Error: +// main/maplit14.gno:14:6-55: invalid map key type main.Outer + +// TypeCheckError: +// main/maplit14.gno:14:10: invalid map key type Outer diff --git a/gnovm/tests/files/maplit2.gno b/gnovm/tests/files/maplit2.gno new file mode 100644 index 00000000000..3fef75e255c --- /dev/null +++ b/gnovm/tests/files/maplit2.gno @@ -0,0 +1,13 @@ +package main + +import "fmt" + +func main() { + i1 := 0 + i2 := 0 + a := map[int]int{i1: 10, i2: 20} + fmt.Println(a) +} + +// Output: +// map[0:20] diff --git a/gnovm/tests/files/maplit3.gno b/gnovm/tests/files/maplit3.gno new file mode 100644 index 00000000000..82aa514b82d --- /dev/null +++ b/gnovm/tests/files/maplit3.gno @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + i2 := 0 + a := map[int]int{0: 10, i2: 20} + fmt.Println(a) +} + +// Output: +// map[0:20] diff --git a/gnovm/tests/files/maplit4.gno b/gnovm/tests/files/maplit4.gno new file mode 100644 index 00000000000..04c064a677b --- /dev/null +++ b/gnovm/tests/files/maplit4.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func main() { + i3 := 0 + a := map[int]int{0: 10, 0: 11, i3: 20} + fmt.Println(a) +} + +// Error: +// main/maplit4.gno:7:7-40: duplicate key (0 int) in map literal + +// TypeCheckError: +// main/maplit4.gno:7:26: duplicate key 0 in map literal diff --git a/gnovm/tests/files/maplit5.gno b/gnovm/tests/files/maplit5.gno new file mode 100644 index 00000000000..0af6e994871 --- /dev/null +++ b/gnovm/tests/files/maplit5.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func main() { + i3 := false + a := map[bool]int{true: 10, true: 11, i3: 20} + fmt.Println(a) +} + +// Error: +// main/maplit5.gno:7:7-47: duplicate key (true bool) in map literal + +// TypeCheckError: +// main/maplit5.gno:7:30: duplicate key true in map literal diff --git a/gnovm/tests/files/maplit6.gno b/gnovm/tests/files/maplit6.gno new file mode 100644 index 00000000000..b81cf0b6671 --- /dev/null +++ b/gnovm/tests/files/maplit6.gno @@ -0,0 +1,13 @@ +package main + +import "fmt" + +func main() { + i1 := []int{1, 2} + i2 := []int{2, 3} + a := map[any]int{i1: 10, i2: 11, 3: 20} + fmt.Println(a) +} + +// Error: +// runtime error: slice type cannot be used as map key diff --git a/gnovm/tests/files/maplit7.gno b/gnovm/tests/files/maplit7.gno new file mode 100644 index 00000000000..f37f8bb63b5 --- /dev/null +++ b/gnovm/tests/files/maplit7.gno @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + a := map[any]int{[]int{1, 2}: 10, []int{2, 3}: 11, 3: 20} + fmt.Println(a) +} + +// Error: +// runtime error: slice type cannot be used as map key diff --git a/gnovm/tests/files/maplit8.gno b/gnovm/tests/files/maplit8.gno new file mode 100644 index 00000000000..e780bd86619 --- /dev/null +++ b/gnovm/tests/files/maplit8.gno @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + a := map[[]int]int{[]int{1, 2}: 10, []int{2, 3}: 11} + fmt.Println(a) +} + +// Error: +// main/maplit8.gno:6:7-54: invalid map key type []int + +// TypeCheckError: +// main/maplit8.gno:6:11: invalid map key type []int diff --git a/gnovm/tests/files/maplit9.gno b/gnovm/tests/files/maplit9.gno new file mode 100644 index 00000000000..a420b1488b2 --- /dev/null +++ b/gnovm/tests/files/maplit9.gno @@ -0,0 +1,11 @@ +package main + +func main() { + s1 := []int{1, 2} + s2 := []int{2, 3} + _ = map[*[]int]int{&s1: 10, &s2: 11} + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/panic3.gno b/gnovm/tests/files/panic3.gno index 2c7add03504..28ac3bc2364 100644 --- a/gnovm/tests/files/panic3.gno +++ b/gnovm/tests/files/panic3.gno @@ -6,9 +6,9 @@ func main() { } // Stacktrace: -// panic: nil pointer dereference +// panic: runtime error: nil pointer dereference // main() // main/panic3.gno:5 // Error: -// nil pointer dereference +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/panic3a.gno b/gnovm/tests/files/panic3a.gno index a8b926172e9..2bc4c4ce089 100644 --- a/gnovm/tests/files/panic3a.gno +++ b/gnovm/tests/files/panic3a.gno @@ -6,9 +6,9 @@ func main() { } // Stacktrace: -// panic: nil pointer dereference +// panic: runtime error: nil pointer dereference // main() // main/panic3a.gno:5 // Error: -// nil pointer dereference +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/panic3b.gno b/gnovm/tests/files/panic3b.gno index 85792011f1a..e5e1d8abeec 100644 --- a/gnovm/tests/files/panic3b.gno +++ b/gnovm/tests/files/panic3b.gno @@ -9,10 +9,10 @@ func main() { } // Error: -// division by zero +// runtime error: division by zero // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // foo() // main/panic3b.gno:5 // main() diff --git a/gnovm/tests/files/panic5.gno b/gnovm/tests/files/panic5.gno index 2dec22a2e3e..1d00a706aa6 100644 --- a/gnovm/tests/files/panic5.gno +++ b/gnovm/tests/files/panic5.gno @@ -9,9 +9,9 @@ func main() { } // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // main() // main/panic5.gno:6 // Error: -// division by zero +// runtime error: division by zero diff --git a/gnovm/tests/files/panic5a.gno b/gnovm/tests/files/panic5a.gno index 4e332de46ee..afe345a5a3d 100644 --- a/gnovm/tests/files/panic5a.gno +++ b/gnovm/tests/files/panic5a.gno @@ -8,9 +8,9 @@ func main() { } // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // main() // main/panic5a.gno:5 // Error: -// division by zero +// runtime error: division by zero diff --git a/gnovm/tests/files/panic5b.gno b/gnovm/tests/files/panic5b.gno index 43de52a888c..1ba20419e02 100644 --- a/gnovm/tests/files/panic5b.gno +++ b/gnovm/tests/files/panic5b.gno @@ -9,9 +9,9 @@ func main() { } // Error: -// division by zero +// runtime error: division by zero // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // main() // main/panic5b.gno:6 diff --git a/gnovm/tests/files/panic5c.gno b/gnovm/tests/files/panic5c.gno index 68667b276e6..adec22a43bb 100644 --- a/gnovm/tests/files/panic5c.gno +++ b/gnovm/tests/files/panic5c.gno @@ -6,12 +6,12 @@ func main() { } // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // main() // main/panic5c.gno:5 // Error: -// division by zero +// runtime error: division by zero // TypeCheckError: // main/panic5c.gno:5:2: declared and not used: b diff --git a/gnovm/tests/files/panic5d.gno b/gnovm/tests/files/panic5d.gno index fd55834fd26..66bf05ac1c4 100644 --- a/gnovm/tests/files/panic5d.gno +++ b/gnovm/tests/files/panic5d.gno @@ -8,9 +8,9 @@ func main() { } // Error: -// division by zero +// runtime error: division by zero // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // main() // main/panic5d.gno:5 diff --git a/gnovm/tests/files/panic6.gno b/gnovm/tests/files/panic6.gno index 937100f8708..2e276216bf5 100644 --- a/gnovm/tests/files/panic6.gno +++ b/gnovm/tests/files/panic6.gno @@ -20,9 +20,9 @@ func main() { } // Stacktrace: -// panic: nil pointer dereference +// panic: runtime error: nil pointer dereference // main() // main/panic6.gno:16 // Error: -// nil pointer dereference +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/panic7.gno b/gnovm/tests/files/panic7.gno index c82b4cdd1d9..fad910eadf5 100644 --- a/gnovm/tests/files/panic7.gno +++ b/gnovm/tests/files/panic7.gno @@ -6,9 +6,9 @@ func main() { } // Stacktrace: -// panic: nil pointer dereference +// panic: runtime error: nil pointer dereference // main() // main/panic7.gno:5 // Error: -// nil pointer dereference +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/panic8.gno b/gnovm/tests/files/panic8.gno index d767acaea3b..2497878b1d0 100644 --- a/gnovm/tests/files/panic8.gno +++ b/gnovm/tests/files/panic8.gno @@ -14,13 +14,13 @@ func main() { // world // Error: -// division by zero +// runtime error: division by zero // TypeCheckError: // main/panic8.gno:7:3: declared and not used: b // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // defer func(){ ... }() // main/panic8.gno:7 // main() diff --git a/gnovm/tests/files/ptr11.gno b/gnovm/tests/files/ptr11.gno index 98421562842..0f0b2ab4632 100644 --- a/gnovm/tests/files/ptr11.gno +++ b/gnovm/tests/files/ptr11.gno @@ -10,4 +10,4 @@ func main() { } // Error: -// nil pointer dereference +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/ptr11a.gno b/gnovm/tests/files/ptr11a.gno index d027934474a..876d90e5de5 100644 --- a/gnovm/tests/files/ptr11a.gno +++ b/gnovm/tests/files/ptr11a.gno @@ -20,4 +20,4 @@ func main() { } // Output: -// recovered nil pointer dereference +// recovered runtime error: nil pointer dereference diff --git a/gnovm/tests/files/ptr11b.gno b/gnovm/tests/files/ptr11b.gno index e4d1a401325..67c26feb086 100644 --- a/gnovm/tests/files/ptr11b.gno +++ b/gnovm/tests/files/ptr11b.gno @@ -14,4 +14,4 @@ func main() { } // Error: -// nil pointer dereference +// runtime error: nil pointer dereference diff --git a/gnovm/tests/files/ptr_array4.gno b/gnovm/tests/files/ptr_array4.gno index ef29439f562..2a9142c9fbf 100644 --- a/gnovm/tests/files/ptr_array4.gno +++ b/gnovm/tests/files/ptr_array4.gno @@ -12,4 +12,4 @@ func main() { } // Output: -// recovered nil pointer dereference +// recovered runtime error: nil pointer dereference diff --git a/gnovm/tests/files/ptr_array5.gno b/gnovm/tests/files/ptr_array5.gno index 84974180dae..a883421380c 100644 --- a/gnovm/tests/files/ptr_array5.gno +++ b/gnovm/tests/files/ptr_array5.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// recovered nil pointer dereference +// recovered runtime error: nil pointer dereference diff --git a/gnovm/tests/files/recover/recover3.gno b/gnovm/tests/files/recover/recover3.gno index f8c16e210f2..68a83249563 100644 --- a/gnovm/tests/files/recover/recover3.gno +++ b/gnovm/tests/files/recover/recover3.gno @@ -7,9 +7,9 @@ func main() { } // Error: -// division by zero +// runtime error: division by zero // Stacktrace: -// panic: division by zero +// panic: runtime error: division by zero // main() // main/recover3.gno:4 diff --git a/gnovm/tests/files/recover12.gno b/gnovm/tests/files/recover12.gno index 3c6429c6ba9..a343edc06f7 100644 --- a/gnovm/tests/files/recover12.gno +++ b/gnovm/tests/files/recover12.gno @@ -11,4 +11,4 @@ func main() { } // Output: -// recover: slice index out of bounds: 3 (len=3) +// recover: runtime error: slice index out of bounds: 3 (len=3) diff --git a/gnovm/tests/files/recover13.gno b/gnovm/tests/files/recover13.gno index 6b8bfaa754a..0c2307f3ce5 100644 --- a/gnovm/tests/files/recover13.gno +++ b/gnovm/tests/files/recover13.gno @@ -1,18 +1,15 @@ package main - func main() { - defer func() { - r := recover() - println("recover:", r) - }() - - arr := []int{1, 2, 3} - _ = arr[-1:] // Panics because of negative index + defer func() { + r := recover() + println("recover:", r) + }() + + arr := []int{1, 2, 3} + idx := -1 + _ = arr[idx:] // Panics because of negative index } // Output: -// recover: invalid slice index -1 (index must be non-negative) - -// TypeCheckError: -// main/recover13.gno:11:13: invalid argument: index -1 (constant of type int) must not be negative +// recover: runtime error: invalid slice index -1 (index must be non-negative) diff --git a/gnovm/tests/files/recover14.gno b/gnovm/tests/files/recover14.gno index 46ade005cff..67ed68fd7b8 100644 --- a/gnovm/tests/files/recover14.gno +++ b/gnovm/tests/files/recover14.gno @@ -7,8 +7,8 @@ func main() { }() x, y := 10, 0 - _ = x / y // Panics because of division by zero + _ = x / y // Panics because of runtime error: division by zero } // Output: -// recover: division by zero +// recover: runtime error: division by zero diff --git a/gnovm/tests/files/recover16.gno b/gnovm/tests/files/recover16.gno index 09dc22db9b7..adec0d3a09b 100644 --- a/gnovm/tests/files/recover16.gno +++ b/gnovm/tests/files/recover16.gno @@ -1,17 +1,15 @@ package main - func main() { - defer func() { - r := recover() - println("recover:", r) - }() - - _ = make([]int, -1) // Panics because of negative length + defer func() { + if r := recover(); r != nil { + println("recovered") + } + + }() + l := 2 + _ = make([]int, l, 1) } // Output: -// recover: len out of range - -// TypeCheckError: -// main/recover16.gno:10:21: invalid argument: index -1 (constant of type int) must not be negative +// recovered diff --git a/gnovm/tests/files/recover17.gno b/gnovm/tests/files/recover17.gno index 5496622bab3..6f0c9a43fdc 100644 --- a/gnovm/tests/files/recover17.gno +++ b/gnovm/tests/files/recover17.gno @@ -11,4 +11,4 @@ func main() { } // Output: -// recover: index out of range [10] with length 5 +// recover: runtime error: index out of range [10] with length 5 diff --git a/gnovm/tests/files/recover18.gno b/gnovm/tests/files/recover18.gno index b27a30a5840..e47ab8c2c75 100644 --- a/gnovm/tests/files/recover18.gno +++ b/gnovm/tests/files/recover18.gno @@ -11,4 +11,4 @@ func main() { } // Output: -// recover: uninitialized map index +// recover: runtime error: uninitialized map index diff --git a/gnovm/tests/files/recover19.gno b/gnovm/tests/files/recover19.gno index da4bc454984..098c467a9a6 100644 --- a/gnovm/tests/files/recover19.gno +++ b/gnovm/tests/files/recover19.gno @@ -11,4 +11,4 @@ func main() { } // Output: -// recover: nil pointer dereference +// recover: runtime error: nil pointer dereference diff --git a/gnovm/tests/files/recover20.gno b/gnovm/tests/files/recover20.gno index ffa4436c16b..bf2b402ee69 100644 --- a/gnovm/tests/files/recover20.gno +++ b/gnovm/tests/files/recover20.gno @@ -8,9 +8,9 @@ func main() { } // Error: -// nil pointer dereference +// runtime error: nil pointer dereference // Stacktrace: -// panic: nil pointer dereference +// panic: runtime error: nil pointer dereference // main() // main/recover20.gno:5 diff --git a/gnovm/tests/files/recover21.gno b/gnovm/tests/files/recover21.gno new file mode 100644 index 00000000000..fe32bec2e2c --- /dev/null +++ b/gnovm/tests/files/recover21.gno @@ -0,0 +1,17 @@ +package main + +import "fmt" + +func main() { + defer func() { + if r := recover(); r != nil { + fmt.Println("recovered: ", r) + } + }() + + v := -1 + _ = 1 << v +} + +// Output: +// recovered: runtime error: negative shift amount: (-1 int) diff --git a/gnovm/tests/files/recover22.gno b/gnovm/tests/files/recover22.gno new file mode 100644 index 00000000000..6fdafd91048 --- /dev/null +++ b/gnovm/tests/files/recover22.gno @@ -0,0 +1,15 @@ +package main + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + s := []int{1, 2, 3} + low := -1 + _ = s[low:2:3] +} + +// Output: +// recover: runtime error: invalid slice index -1 (index must be non-negative) diff --git a/gnovm/tests/files/recover23.gno b/gnovm/tests/files/recover23.gno new file mode 100644 index 00000000000..56dae3477f2 --- /dev/null +++ b/gnovm/tests/files/recover23.gno @@ -0,0 +1,15 @@ +package main + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + s := []int{1, 2, 3} + high := 5 + _ = s[0:high:3] +} + +// Output: +// recover: runtime error: invalid slice index 5 > 3 diff --git a/gnovm/tests/files/recover24.gno b/gnovm/tests/files/recover24.gno new file mode 100644 index 00000000000..caea1fcab84 --- /dev/null +++ b/gnovm/tests/files/recover24.gno @@ -0,0 +1,14 @@ +package main + +func main() { + defer func() { + r := recover() + println("recover:", r) + }() + + s := []int{1, 2, 3} + _ = s[0:1:10] +} + +// Output: +// recover: runtime error: slice bounds out of range [0:1:10] with capacity 3 diff --git a/gnovm/tests/files/recurse0.gno b/gnovm/tests/files/recurse0.gno index a6786bbaaed..88b97073ea5 100644 --- a/gnovm/tests/files/recurse0.gno +++ b/gnovm/tests/files/recurse0.gno @@ -5,8 +5,6 @@ type T struct { b []*T c map[string]T d map[string]*T - e chan T - f chan *T h *T i func(T) T j func(*T) *T @@ -18,8 +16,6 @@ type U struct { l []*T m map[string]T n map[string]*T - o chan T - p chan *T q *T r func(T) T s func(*T) *T @@ -33,5 +29,5 @@ func main() { } // Output: -// (struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(main.T) main.T),(nil func(*main.T) *main.T),(struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(main.T) main.T),(nil func(*main.T) *main.T)} main.U)} main.T) -// (struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(main.T) main.T),(nil func(*main.T) *main.T)} main.U) +// (struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil *main.T),(nil func(main.T) main.T),(nil func(*main.T) *main.T),(struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil *main.T),(nil func(main.T) main.T),(nil func(*main.T) *main.T)} main.U)} main.T) +// (struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil *main.T),(nil func(main.T) main.T),(nil func(*main.T) *main.T)} main.U) diff --git a/gnovm/tests/files/recursive10.gno b/gnovm/tests/files/recursive10.gno index 19789fdd57b..2cf16a55085 100644 --- a/gnovm/tests/files/recursive10.gno +++ b/gnovm/tests/files/recursive10.gno @@ -11,7 +11,7 @@ func main() { } // Error: -// main/recursive10.gno:3:1: loop in variable initialization: dependency trail [b B a A] circularly depends on b +// main/recursive10.gno:6:5: circular dependency: a -> A -> b -> B -> a // TypeCheckError: // main/recursive10.gno:6:5: initialization cycle for a; main/recursive10.gno:6:5: a refers to A; main/recursive10.gno:3:6: A refers to b; main/recursive10.gno:7:5: b refers to B; main/recursive10.gno:4:6: B refers to a diff --git a/gnovm/tests/files/recursive11.gno b/gnovm/tests/files/recursive11.gno index 756796f5f22..7650f511e94 100644 --- a/gnovm/tests/files/recursive11.gno +++ b/gnovm/tests/files/recursive11.gno @@ -7,7 +7,7 @@ func main() { } // Error: -// main/recursive11.gno:3:5: loop in variable initialization: dependency trail [B A] circularly depends on B +// main/recursive11.gno:3:5: circular dependency: A -> B -> A // TypeCheckError: // main/recursive11.gno:3:5: initialization cycle for A; main/recursive11.gno:3:5: A refers to B; main/recursive11.gno:4:5: B refers to A diff --git a/gnovm/tests/files/ref0.gno b/gnovm/tests/files/ref0.gno new file mode 100644 index 00000000000..45ca99f4d16 --- /dev/null +++ b/gnovm/tests/files/ref0.gno @@ -0,0 +1,13 @@ +package main + +// ref of a concrete int variable. +// xv.TV.T is int, which is correct. + +func main() { + var a int = 42 + b := &a + println(*b) +} + +// Output: +// 42 diff --git a/gnovm/tests/files/ref1.gno b/gnovm/tests/files/ref1.gno new file mode 100644 index 00000000000..83f02d1564f --- /dev/null +++ b/gnovm/tests/files/ref1.gno @@ -0,0 +1,15 @@ +package main + +// ref of a declared type variable. +// xv.TV.T is main.myint, which is correct. + +type myint int + +func main() { + var a myint = 42 + b := &a + println(*b) +} + +// Output: +// (42 main.myint) diff --git a/gnovm/tests/files/ref10.gno b/gnovm/tests/files/ref10.gno new file mode 100644 index 00000000000..e69dbabaf19 --- /dev/null +++ b/gnovm/tests/files/ref10.gno @@ -0,0 +1,17 @@ +package main + +import "fmt" + +// ref of a slice element that is an interface type. + +func main() { + s := []interface{}{1, "two", 3.0} + p := &s[0] + fmt.Printf("%T\n", p) + *p = "replaced" + fmt.Println(s[0]) +} + +// Output: +// *interface {} +// replaced diff --git a/gnovm/tests/files/ref11.gno b/gnovm/tests/files/ref11.gno new file mode 100644 index 00000000000..6af04ea1b23 --- /dev/null +++ b/gnovm/tests/files/ref11.gno @@ -0,0 +1,14 @@ +package main + +import "fmt" + +// nested pointer roundtrip: *&x where x is interface{}. + +func main() { + var i interface{} = 42 + v := *&i + fmt.Printf("%T %v\n", v, v) +} + +// Output: +// int 42 diff --git a/gnovm/tests/files/ref12.gno b/gnovm/tests/files/ref12.gno new file mode 100644 index 00000000000..b1a54b88f8b --- /dev/null +++ b/gnovm/tests/files/ref12.gno @@ -0,0 +1,18 @@ +package main + +import "fmt" + +// passing &interface_var to function expecting *interface{} (non-realm). + +func setIt(p *interface{}, val interface{}) { + *p = val +} + +func main() { + var i interface{} = 100 + setIt(&i, "changed") + fmt.Println(i) +} + +// Output: +// changed diff --git a/gnovm/tests/files/ref13.gno b/gnovm/tests/files/ref13.gno new file mode 100644 index 00000000000..59b6aafd195 --- /dev/null +++ b/gnovm/tests/files/ref13.gno @@ -0,0 +1,18 @@ +package main + +import "fmt" + +// ref of a nil named interface. + +type Stringer interface { + String() string +} + +func main() { + var x Stringer + p := &x + fmt.Printf("%T\n", p) +} + +// Output: +// *main.Stringer diff --git a/gnovm/tests/files/ref14.gno b/gnovm/tests/files/ref14.gno new file mode 100644 index 00000000000..bfde8cf168f --- /dev/null +++ b/gnovm/tests/files/ref14.gno @@ -0,0 +1,19 @@ +package main + +import "fmt" + +// ref of an array element that is an interface type. + +func main() { + var a [3]interface{} + a[0] = 42 + a[1] = "hello" + p := &a[1] + fmt.Printf("%T\n", p) + *p = 99 + fmt.Println(a[1]) +} + +// Output: +// *interface {} +// 99 diff --git a/gnovm/tests/files/ref15.gno b/gnovm/tests/files/ref15.gno new file mode 100644 index 00000000000..5703a4bd486 --- /dev/null +++ b/gnovm/tests/files/ref15.gno @@ -0,0 +1,16 @@ +package main + +import "fmt" + +// Deref of *interface{} when the interface holds nil. +// *p should be a nil interface{}, not a typed nil. + +func main() { + var i interface{} + p := &i + v := *p + fmt.Println(v == nil) +} + +// Output: +// true diff --git a/gnovm/tests/files/ref16.gno b/gnovm/tests/files/ref16.gno new file mode 100644 index 00000000000..3fd866921ea --- /dev/null +++ b/gnovm/tests/files/ref16.gno @@ -0,0 +1,18 @@ +package main + +import "fmt" + +// Deref of a converted pointer type. +// *((*Foo)(&Bar{})) should produce Foo, not Bar. + +type Foo struct{ X int } +type Bar struct{ X int } + +func main() { + b := Bar{X: 1} + v := *((*Foo)(&b)) + fmt.Printf("%T %v\n", v, v) +} + +// Output: +// main.Foo {1} diff --git a/gnovm/tests/files/ref17.gno b/gnovm/tests/files/ref17.gno new file mode 100644 index 00000000000..c8a140b0144 --- /dev/null +++ b/gnovm/tests/files/ref17.gno @@ -0,0 +1,17 @@ +package main + +import "fmt" + +// Deref of a converted pointer type with primitive underlying type. + +type Meters int +type Feet int + +func main() { + f := Feet(3) + v := *((*Meters)(&f)) + fmt.Printf("%T %v\n", v, v) +} + +// Output: +// main.Meters 3 diff --git a/gnovm/tests/files/ref18.gno b/gnovm/tests/files/ref18.gno new file mode 100644 index 00000000000..2f173a3c500 --- /dev/null +++ b/gnovm/tests/files/ref18.gno @@ -0,0 +1,16 @@ +package main + +import "fmt" + +// Interface holding a pointer value: &i should still be *interface{}. + +type S struct{ X int } + +func main() { + var i interface{} = &S{X: 1} + p := &i + fmt.Printf("%T\n", p) +} + +// Output: +// *interface {} diff --git a/gnovm/tests/files/ref19.gno b/gnovm/tests/files/ref19.gno new file mode 100644 index 00000000000..9ef3f58b9fc --- /dev/null +++ b/gnovm/tests/files/ref19.gno @@ -0,0 +1,16 @@ +package main + +import "fmt" + +// Type assertion through interface pointer. +// *p must produce a proper interface{} for the assertion to work. + +func main() { + var i interface{} = 42 + p := &i + v := (*p).(int) + fmt.Println(v) +} + +// Output: +// 42 diff --git a/gnovm/tests/files/ref2.gno b/gnovm/tests/files/ref2.gno new file mode 100644 index 00000000000..6a3c7b03f56 --- /dev/null +++ b/gnovm/tests/files/ref2.gno @@ -0,0 +1,19 @@ +package main + +// ref of a struct variable. +// xv.TV.T is main.MyStruct, which is correct. + +type MyStruct struct { + A int +} + +func main() { + var s MyStruct + s.A = 1 + p := &s + p.A = 2 + println(s) +} + +// Output: +// (struct{(2 int)} main.MyStruct) diff --git a/gnovm/tests/files/ref20.gno b/gnovm/tests/files/ref20.gno new file mode 100644 index 00000000000..dd671d06829 --- /dev/null +++ b/gnovm/tests/files/ref20.gno @@ -0,0 +1,23 @@ +package main + +import "fmt" + +// Deref of *Stringer preserves concrete type. + +type Stringer interface { + String() string +} + +type mystr struct{ s string } + +func (m mystr) String() string { return m.s } + +func main() { + var x Stringer = mystr{"hello"} + p := &x + v := *p + fmt.Println(v.String()) +} + +// Output: +// hello diff --git a/gnovm/tests/files/ref21.gno b/gnovm/tests/files/ref21.gno new file mode 100644 index 00000000000..4f1f4ee1ae4 --- /dev/null +++ b/gnovm/tests/files/ref21.gno @@ -0,0 +1,21 @@ +package main + +import "fmt" + +// Closure capturing &i where i is interface{}. +// Exercises heap-use path in PopAsPointer2. + +func main() { + var i interface{} = 42 + f := func() { + p := &i + fmt.Printf("%T\n", p) + *p = "hello" + } + f() + fmt.Println(i) +} + +// Output: +// *interface {} +// hello diff --git a/gnovm/tests/files/ref22.gno b/gnovm/tests/files/ref22.gno new file mode 100644 index 00000000000..8e43531ed2b --- /dev/null +++ b/gnovm/tests/files/ref22.gno @@ -0,0 +1,29 @@ +package main + +import "fmt" + +// Reassign different concrete type through *Stringer pointer. + +type Stringer interface { + String() string +} + +type A struct{} + +func (A) String() string { return "A" } + +type B struct{} + +func (B) String() string { return "B" } + +func main() { + var x Stringer = A{} + p := &x + fmt.Println((*p).String()) + *p = B{} + fmt.Println((*p).String()) +} + +// Output: +// A +// B diff --git a/gnovm/tests/files/ref23.gno b/gnovm/tests/files/ref23.gno new file mode 100644 index 00000000000..31f7d4e0355 --- /dev/null +++ b/gnovm/tests/files/ref23.gno @@ -0,0 +1,20 @@ +package main + +import "fmt" + +// ref of a []byte element (DataByteType path). +// The static type of s[i] is uint8, so &s[i] must be *uint8. +// Previously doOpRef had explicit DataByteType handling; now the +// static-type path (getTypeOf) already returns uint8. + +func main() { + s := []byte("hello") + p := &s[0] + fmt.Printf("%T\n", p) + *p = 'H' + fmt.Println(string(s)) +} + +// Output: +// *uint8 +// Hello diff --git a/gnovm/tests/files/ref24.gno b/gnovm/tests/files/ref24.gno new file mode 100644 index 00000000000..cdaff8c0f4d --- /dev/null +++ b/gnovm/tests/files/ref24.gno @@ -0,0 +1,23 @@ +package main + +import "fmt" + +// Auto-address for pointer receiver via selector expression. +// When t.PointerMethod() is called and t is addressable, the +// preprocessor synthesizes &t, setting ATTR_TYPEOF_VALUE on t +// before wrapping it in RefExpr. Exercises the setPreprocessed +// path in SelectorExpr TRANS_LEAVE. + +type Counter struct{ n int } + +func (c *Counter) Inc() { c.n++ } + +func main() { + var c Counter + c.Inc() + c.Inc() + fmt.Println(c.n) +} + +// Output: +// 2 diff --git a/gnovm/tests/files/ref3.gno b/gnovm/tests/files/ref3.gno new file mode 100644 index 00000000000..6a11d55280e --- /dev/null +++ b/gnovm/tests/files/ref3.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +// ref of a concrete type; use fmt to print the pointer type. +// This verifies the type of the pointer itself, not just the deref. + +func main() { + var a int = 1 + b := &a + fmt.Printf("%T\n", b) +} + +// Output: +// *int diff --git a/gnovm/tests/files/ref4.gno b/gnovm/tests/files/ref4.gno new file mode 100644 index 00000000000..34d44f1500d --- /dev/null +++ b/gnovm/tests/files/ref4.gno @@ -0,0 +1,16 @@ +package main + +import "fmt" + +// ref of a declared type; verify the pointer type string. + +type myint int + +func main() { + var a myint = 1 + b := &a + fmt.Printf("%T\n", b) +} + +// Output: +// *main.myint diff --git a/gnovm/tests/files/ref5.gno b/gnovm/tests/files/ref5.gno new file mode 100644 index 00000000000..e16ab6f1c0a --- /dev/null +++ b/gnovm/tests/files/ref5.gno @@ -0,0 +1,13 @@ +package main + +// ref of a nil interface{} variable, assign through pointer. + +func main() { + var i interface{} + p := &i + *p = 42 + println(i) +} + +// Output: +// 42 diff --git a/gnovm/tests/files/ref6.gno b/gnovm/tests/files/ref6.gno new file mode 100644 index 00000000000..7238e91d3c5 --- /dev/null +++ b/gnovm/tests/files/ref6.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +// ref of an interface{} variable holding a concrete value. +// In Go, &i where i is interface{} always gives *interface{}. + +func main() { + var i interface{} = 42 + p := &i + fmt.Printf("%T\n", p) +} + +// Output: +// *interface {} diff --git a/gnovm/tests/files/ref7.gno b/gnovm/tests/files/ref7.gno new file mode 100644 index 00000000000..9864536a6ab --- /dev/null +++ b/gnovm/tests/files/ref7.gno @@ -0,0 +1,17 @@ +package main + +import "fmt" + +// ref of an interface{} variable, then assign a different type through the pointer. +// In Go this is valid: *p = "hello" changes i from int to string. +// If the bug makes &i produce *int, then assigning a string would fail. + +func main() { + var i interface{} = 42 + p := &i + *p = "hello" + fmt.Println(i) +} + +// Output: +// hello diff --git a/gnovm/tests/files/ref8.gno b/gnovm/tests/files/ref8.gno new file mode 100644 index 00000000000..2ea9de14b32 --- /dev/null +++ b/gnovm/tests/files/ref8.gno @@ -0,0 +1,22 @@ +package main + +import "fmt" + +// ref of a named interface variable holding a concrete value. + +type Stringer interface { + String() string +} + +type mystr struct{ s string } + +func (m mystr) String() string { return m.s } + +func main() { + var x Stringer = mystr{"hello"} + p := &x + fmt.Printf("%T\n", p) +} + +// Output: +// *main.Stringer diff --git a/gnovm/tests/files/ref9.gno b/gnovm/tests/files/ref9.gno new file mode 100644 index 00000000000..86c9bf10e8a --- /dev/null +++ b/gnovm/tests/files/ref9.gno @@ -0,0 +1,21 @@ +package main + +import "fmt" + +// ref of a struct field that is an interface type. + +type S struct { + F interface{} +} + +func main() { + s := S{F: 42} + p := &s.F + fmt.Printf("%T\n", p) + *p = "hello" + fmt.Println(s.F) +} + +// Output: +// *interface {} +// hello diff --git a/gnovm/tests/files/slice5.gno b/gnovm/tests/files/slice5.gno index 5801d3c0c3e..0f17ce0f6d7 100644 --- a/gnovm/tests/files/slice5.gno +++ b/gnovm/tests/files/slice5.gno @@ -6,4 +6,4 @@ func main() { } // Error: -// slice index out of bounds: 6 (len=5) +// runtime error: slice index out of bounds: 6 (len=5) diff --git a/gnovm/tests/files/std13.gno b/gnovm/tests/files/std13.gno new file mode 100644 index 00000000000..3ff922eecda --- /dev/null +++ b/gnovm/tests/files/std13.gno @@ -0,0 +1,18 @@ +package main + +import "chain/runtime" + +func Register() { + runtime.AssertOriginCall() +} + +func main() { + fn := func() { + Register() + } + fn() + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/std14.gno b/gnovm/tests/files/std14.gno new file mode 100644 index 00000000000..794a27b871c --- /dev/null +++ b/gnovm/tests/files/std14.gno @@ -0,0 +1,47 @@ +package main + +import "chain/runtime" + +func Register() { + runtime.AssertOriginCall() +} + +func helper() { + Register() +} + +func main() { + // Case 1: closure wrapping Register — should NOT panic. + // Closures are transparent: main → Register → AssertOriginCall (3 call frames). + func() { + Register() + }() + println("closure call ok") + + // Case 2: nested closures wrapping Register — should NOT panic. + // All closures are transparent: main → Register → AssertOriginCall (3 call frames). + func() { + func() { + Register() + }() + }() + println("nested closure call ok") + + // Case 3: named function calling Register — SHOULD panic. + // main → helper → Register → AssertOriginCall (4 call frames > 3). + panicked := false + func() { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + helper() + }() + println("named func call panicked:", panicked) +} + +// Output: +// closure call ok +// nested closure call ok +// named func call panicked: true diff --git a/gnovm/tests/files/std15.gno b/gnovm/tests/files/std15.gno new file mode 100644 index 00000000000..65edfd1ada9 --- /dev/null +++ b/gnovm/tests/files/std15.gno @@ -0,0 +1,33 @@ +package main + +import "chain/runtime" + +func Register() { + runtime.AssertOriginCall() +} + +func main() { + // for-loop basic frame should be transparent. + for i := 0; i < 1; i++ { + Register() + } + println("for ok") + + // range basic frame should be transparent. + for range []int{1} { + Register() + } + println("range ok") + + // switch basic frame should be transparent. + switch 1 { + case 1: + Register() + } + println("switch ok") +} + +// Output: +// for ok +// range ok +// switch ok diff --git a/gnovm/tests/files/std16.gno b/gnovm/tests/files/std16.gno new file mode 100644 index 00000000000..64a4b32385e --- /dev/null +++ b/gnovm/tests/files/std16.gno @@ -0,0 +1,59 @@ +package main + +import "chain/runtime" + +func Register() { + runtime.AssertOriginCall() +} + +func callFunc(f func()) { + f() +} + +func helper() { + Register() +} + +func main() { + // Security case 1: closure passed as callback to a named function. + // main → callFunc → Register → AssertOriginCall (4 call frames > 3). + // The closure is transparent, but callFunc is a real call boundary — SHOULD panic. + panicked := false + func() { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + callFunc(func() { + Register() + }) + }() + println("callback to named func panicked:", panicked) + + // Security case 2: named function wrapped in closure. + // main → helper → Register → AssertOriginCall (4 call frames > 3). + // The outer closure is transparent, but helper is a real call — SHOULD panic. + panicked = false + func() { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + helper() + }() + println("named func in closure panicked:", panicked) + + // Sanity: direct closure call — should NOT panic. + // main → Register → AssertOriginCall (3 call frames). + func() { + Register() + }() + println("direct closure ok") +} + +// Output: +// callback to named func panicked: true +// named func in closure panicked: true +// direct closure ok diff --git a/gnovm/tests/files/types/cmp_array_c.gno b/gnovm/tests/files/types/cmp_array_c.gno index fdd62097f0f..fe0187f4a08 100644 --- a/gnovm/tests/files/types/cmp_array_c.gno +++ b/gnovm/tests/files/types/cmp_array_c.gno @@ -12,7 +12,7 @@ func main() { } // Error: -// main/cmp_array_c.gno:10:25-31: [2][]int is not comparable +// main/cmp_array_c.gno:10:25-31: [2][2][]int is not comparable // TypeCheckError: // main/cmp_array_c.gno:10:25: invalid operation: a == b ([2][2][]int cannot be compared); main/cmp_array_c.gno:11:25: invalid operation: a == c ([2][2][]int cannot be compared) diff --git a/gnovm/tests/files/types/cmp_struct_a.gno b/gnovm/tests/files/types/cmp_struct_a.gno index d3d9c8e48e3..4685a3480a0 100644 --- a/gnovm/tests/files/types/cmp_struct_a.gno +++ b/gnovm/tests/files/types/cmp_struct_a.gno @@ -17,7 +17,7 @@ func main() { } // Error: -// main/cmp_struct_a.gno:15:25-31: [2][]int is not comparable +// main/cmp_struct_a.gno:15:25-31: main.Matrix is not comparable // TypeCheckError: // main/cmp_struct_a.gno:15:25: invalid operation: a == b (struct containing [2][2][]int cannot be compared); main/cmp_struct_a.gno:16:25: invalid operation: a == c (struct containing [2][2][]int cannot be compared) diff --git a/gnovm/tests/files/types/varg_0.gno b/gnovm/tests/files/types/varg_0.gno new file mode 100644 index 00000000000..5a4b8f0c2de --- /dev/null +++ b/gnovm/tests/files/types/varg_0.gno @@ -0,0 +1,13 @@ +package main + +func main() { + var a uint = 1 + + b := make([]byte, 1.0< string) as type int in argument to make diff --git a/gnovm/tests/files/types/varg_2.gno b/gnovm/tests/files/types/varg_2.gno new file mode 100644 index 00000000000..8a73a76456d --- /dev/null +++ b/gnovm/tests/files/types/varg_2.gno @@ -0,0 +1,13 @@ +package main + +func main() { + b := make([]byte, "h") + + println(b) +} + +// Error: +// main/varg_2.gno:4:7-24: cannot use ("h" string) as type int in argument to make + +// TypeCheckError: +// main/varg_2.gno:4:20: cannot convert "h" (untyped string constant) to type int diff --git a/gnovm/tests/files/types/varg_3.gno b/gnovm/tests/files/types/varg_3.gno new file mode 100644 index 00000000000..8811e3f7ce3 --- /dev/null +++ b/gnovm/tests/files/types/varg_3.gno @@ -0,0 +1,14 @@ +package main + +func main() { + var i = "h" + b := make([]byte, i) + + println(b) +} + +// Error: +// main/varg_3.gno:5:7-22: invalid argument: index i (variable of type string) must be integer + +// TypeCheckError: +// main/varg_3.gno:5:20: invalid argument: index i (variable of type string) must be integer diff --git a/gnovm/tests/files/types/varg_4.gno b/gnovm/tests/files/types/varg_4.gno new file mode 100644 index 00000000000..6c3865aaf5a --- /dev/null +++ b/gnovm/tests/files/types/varg_4.gno @@ -0,0 +1,12 @@ +package main + +func main() { + + i := -2 + b := make([]byte, 1, i) + + println(b) +} + +// Error: +// runtime error: makeslice: cap out of range diff --git a/gnovm/tests/files/types/varg_5.gno b/gnovm/tests/files/types/varg_5.gno new file mode 100644 index 00000000000..74e6899be41 --- /dev/null +++ b/gnovm/tests/files/types/varg_5.gno @@ -0,0 +1,14 @@ +package main + +func bar(s ...int) { + var s1 []int + s1 = append(s1, s...) + println(s1) +} +func main() { + var a uint = 2 + bar(1.0< C -> E, B -> D -> F — two separate chains, no false cycle. +package main + +var ( + A, B = C, D + C = E + D = F + E = 10 + F = 20 +) + +func main() { + println(A, B) // 10 20 +} + +// Output: +// 10 20 diff --git a/gnovm/tests/files/var40.gno b/gnovm/tests/files/var40.gno new file mode 100644 index 00000000000..b15dabe0e1f --- /dev/null +++ b/gnovm/tests/files/var40.gno @@ -0,0 +1,17 @@ +// Three-variable multi-value decl where each depends on a separate chain. +// A gets D, B gets E, C gets F — D -> E -> F creates a linear dependency. +package main + +var ( + A, B, C = D, E, F + D = 1 + E = D + F = E +) + +func main() { + println(A, B, C) // 1 1 1 +} + +// Output: +// 1 1 1 diff --git a/gnovm/tests/files/var41.gno b/gnovm/tests/files/var41.gno new file mode 100644 index 00000000000..760eb6e9037 --- /dev/null +++ b/gnovm/tests/files/var41.gno @@ -0,0 +1,17 @@ +// Genuinely cyclic var declarations must still be rejected. +package main + +var ( + A = B + B = A +) + +func main() { + println(A, B) +} + +// Error: +// main/var41.gno:5:2-7: invalid recursive value: A -> B -> A + +// TypeCheckError: +// main/var41.gno:5:2: initialization cycle for A; main/var41.gno:5:2: A refers to B; main/var41.gno:6:2: B refers to A diff --git a/gnovm/tests/files/var42.gno b/gnovm/tests/files/var42.gno new file mode 100644 index 00000000000..5ad830d3816 --- /dev/null +++ b/gnovm/tests/files/var42.gno @@ -0,0 +1,14 @@ +// A var that references itself is a genuine cycle. +package main + +var A = A + +func main() { + println(A) +} + +// Error: +// main/var42.gno:4:5-10: invalid recursive value: A -> A + +// TypeCheckError: +// main/var42.gno:4:5: initialization cycle: A refers to itself diff --git a/gnovm/tests/files/var43.gno b/gnovm/tests/files/var43.gno new file mode 100644 index 00000000000..1d3d138c951 --- /dev/null +++ b/gnovm/tests/files/var43.gno @@ -0,0 +1,18 @@ +// Three-var cycle: A -> B -> C -> A must be rejected. +package main + +var ( + A = B + B = C + C = A +) + +func main() { + println(A) +} + +// Error: +// main/var43.gno:5:2-7: invalid recursive value: A -> B -> C -> A + +// TypeCheckError: +// main/var43.gno:5:2: initialization cycle for A; main/var43.gno:5:2: A refers to B; main/var43.gno:6:2: B refers to C; main/var43.gno:7:2: C refers to A diff --git a/gnovm/tests/files/var44.gno b/gnovm/tests/files/var44.gno new file mode 100644 index 00000000000..edc874856a4 --- /dev/null +++ b/gnovm/tests/files/var44.gno @@ -0,0 +1,16 @@ +// Multi-value decl where the two names swap — a genuine cycle. +package main + +var ( + A, B = B, A +) + +func main() { + println(A, B) +} + +// Error: +// main/var44.gno:2:1-10:2: invalid recursive value: A -> B -> A + +// TypeCheckError: +// main/var44.gno:5:2: initialization cycle for A; main/var44.gno:5:2: A refers to B; main/var44.gno:5:5: B refers to A diff --git a/gnovm/tests/files/var45.gno b/gnovm/tests/files/var45.gno new file mode 100644 index 00000000000..e6a1e09a5ac --- /dev/null +++ b/gnovm/tests/files/var45.gno @@ -0,0 +1,19 @@ +// Multi-value decl where one split name feeds back into the same decl. +// A gets C, but C depends on A — genuine cycle, not a false positive. +package main + +var ( + A, B = C, D + C = A + D = 2 +) + +func main() { + println(A, B, C) +} + +// Error: +// main/var45.gno:3:1-13:2: invalid recursive value: A -> C -> A + +// TypeCheckError: +// main/var45.gno:6:2: initialization cycle for A; main/var45.gno:6:2: A refers to C; main/var45.gno:7:2: C refers to A diff --git a/gnovm/tests/files/var_initorder.gno b/gnovm/tests/files/var_initorder.gno new file mode 100644 index 00000000000..5257bc653dc --- /dev/null +++ b/gnovm/tests/files/var_initorder.gno @@ -0,0 +1,21 @@ +package main + +// Order: +// B has no deps -> initialized first (incr->counter=1, B=1). +// A has no deps -> initialized second (incr->counter=2, A=2). +// C depends on B and A -> initialized last (C=B+A=3). + +var counter int + +func incr() int { counter++; return counter } + +var ( + C = B + A + B = incr() + A = incr() +) + +func main() { println(A, B, C) } + +// Output: +// 2 1 3 diff --git a/gnovm/tests/files/var_initorder10.gno b/gnovm/tests/files/var_initorder10.gno new file mode 100644 index 00000000000..856b4c9ec89 --- /dev/null +++ b/gnovm/tests/files/var_initorder10.gno @@ -0,0 +1,26 @@ +package main + +// Tests that method bodies are analysed for package-level variable dependencies. +// A = T{}.GetB() has no direct syntactic dependency on B, but GetB's body +// reads B. Compared to initorder9, this test ensures that a dependency on GetB +// does not recurse infinitely. +// Init order: T (type, no deps), B (no deps), A (deps T and B via GetB). + +type T struct{} + +func (T) GetB(i int) int { + if i < 3 { + return T{}.GetB(i + 1) + } + return B +} + +var ( + A = T{}.GetB(0) + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder11.gno b/gnovm/tests/files/var_initorder11.gno new file mode 100644 index 00000000000..12bdcb639ec --- /dev/null +++ b/gnovm/tests/files/var_initorder11.gno @@ -0,0 +1,23 @@ +package main + +// Tests that init order correctly handles transitive deps discovered through a +// pointer-receiver method body. A = (&T{}).Get() has no direct syntactic +// dependency on B, but Get's body reads B. This exercises the *PointerType +// branch in findDependentNames' SelectorExpr case: +// +// Contrast with var_initorder10.gno which uses a value receiver T{}.GetB(). +// Init order: T (type, no deps), B (no deps), A (deps T and B via *T.Get). + +type T struct{} + +func (t *T) Get() int { return B } + +var ( + A = (&T{}).Get() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder12.gno b/gnovm/tests/files/var_initorder12.gno new file mode 100644 index 00000000000..f22caf8929b --- /dev/null +++ b/gnovm/tests/files/var_initorder12.gno @@ -0,0 +1,19 @@ +package main + +// Tests that function call arguments are analysed as init-order constraints. +// A = double(B) passes B directly as an argument; without queuing CallExpr.Args, +// findDependentNames would not discover the dependency on B and could initialize +// A before B, producing double(0)=0 instead of double(21)=42. +// Init order: B (no deps), A (depends on B via argument expression). + +func double(x int) int { return x * 2 } + +var ( + A = double(B) + B = 21 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder13.gno b/gnovm/tests/files/var_initorder13.gno new file mode 100644 index 00000000000..14979c892b9 --- /dev/null +++ b/gnovm/tests/files/var_initorder13.gno @@ -0,0 +1,23 @@ +package main + +// Tests that function call arguments are analysed as init-order constraints. +// A = incr(B) passes B directly as an argument, but the dependency on B is +// created even if B is not used in the body of incr. +// Init order: B (no deps), A (depends on B via argument expression). + +var counter int + +func incr(arg int) int { + counter += 1 + return counter +} + +var ( + A = incr(B) + B = incr(0) +) + +func main() { println(A) } + +// Output: +// 2 diff --git a/gnovm/tests/files/var_initorder14.gno b/gnovm/tests/files/var_initorder14.gno new file mode 100644 index 00000000000..a5a7cd99c25 --- /dev/null +++ b/gnovm/tests/files/var_initorder14.gno @@ -0,0 +1,28 @@ +package main + +// Tests transitive dependency through chained method calls. +// A = T{}.MethodA(), MethodA calls MethodB, MethodB reads B. +// If findDependentNames only traces one level of method body, +// A's dependency on B (through MethodA→MethodB) might be missed, +// causing wrong init order. + +type T struct{} + +func (T) MethodA() int { + t := T{} + return t.MethodB() +} + +func (T) MethodB() int { + return B +} + +var ( + A = T{}.MethodA() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder15.gno b/gnovm/tests/files/var_initorder15.gno new file mode 100644 index 00000000000..73dc6474302 --- /dev/null +++ b/gnovm/tests/files/var_initorder15.gno @@ -0,0 +1,31 @@ +package main + +// Tests that init-order analysis through interface dispatch is not performed. +// A = Getter(T{}).GetB() converts T{} to the Getter interface before calling +// GetB; at that point the static receiver type is *InterfaceType, not +// *DeclaredType, so the dependency on T.GetB (and transitively on B) cannot +// be inferred. A is therefore treated as having no deps and is initialized +// before B, so A = B (zero) = 0. +// +// Note: the real Go compiler performs a more sophisticated analysis that can +// see through interface conversions in simple cases like this and produces 42. +// That behaviour is not required by the Go spec, which only mandates stepwise +// initialization; Gno's result of 0 is spec-compliant. + +type Getter interface { + GetB() int +} + +type T struct{} + +func (T) GetB() int { return B } + +var ( + A = Getter(T{}).GetB() + B = 42 +) + +func main() { println(A) } + +// Output: +// 0 diff --git a/gnovm/tests/files/var_initorder16.gno b/gnovm/tests/files/var_initorder16.gno new file mode 100644 index 00000000000..62646722208 --- /dev/null +++ b/gnovm/tests/files/var_initorder16.gno @@ -0,0 +1,30 @@ +package main + +// Tests that init-order analysis is not performed through interface dispatch, +// even when both variables call the same method via the same interface. +// Neither A nor B can have their dependency on T.Get (which reads B) inferred, +// because the static receiver is the Getter interface, not a *DeclaredType. +// Both are treated as ready immediately; they are initialized in declaration +// order (A first, then B), so A = B (zero) = 0. +// +// Note: the real Go compiler sees through the interface conversion and produces +// a different result. That behaviour is not required by the Go spec; Gno's +// result of 0 is spec-compliant. + +type Getter interface { + Get() int +} + +type T struct{} + +func (T) Get() int { return B } + +var ( + A = Getter(T{}).Get() + B = Getter(T{}).Get() +) + +func main() { println(A) } + +// Output: +// 0 diff --git a/gnovm/tests/files/var_initorder17.gno b/gnovm/tests/files/var_initorder17.gno new file mode 100644 index 00000000000..3e8b2d383e3 --- /dev/null +++ b/gnovm/tests/files/var_initorder17.gno @@ -0,0 +1,23 @@ +package main + +// Tests that a method call through a package-level receiver variable with a +// pointer receiver is correctly ordered. t.GetB() calls (*T).GetB which reads +// B, so A's dependency on B is inferred via the method body. +// Also checks that t (the receiver variable) is itself treated as a dep of A, +// and t has no deps, so init order is: T (type), t (no deps), B (no deps), A. + +type T struct{} + +func (*T) GetB() int { return B } + +var t T + +var ( + A = t.GetB() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder18.gno b/gnovm/tests/files/var_initorder18.gno new file mode 100644 index 00000000000..93508757862 --- /dev/null +++ b/gnovm/tests/files/var_initorder18.gno @@ -0,0 +1,17 @@ +package main + +// Tests that a self-referencing variable through its own function-literal +// initializer is detected as a circular dependency. B's init expression +// assigns to B, creating B -> B. + +var B = func() int { + B = 1 +}() + +func main() { println(B) } + +// Error: +// main/var_initorder18.gno:7:5: circular dependency: B -> B + +// TypeCheckError: +// main/var_initorder18.gno:9:1: missing return; main/var_initorder18.gno:7:5: initialization cycle: B refers to itself diff --git a/gnovm/tests/files/var_initorder19.gno b/gnovm/tests/files/var_initorder19.gno new file mode 100644 index 00000000000..b39f8e5bada --- /dev/null +++ b/gnovm/tests/files/var_initorder19.gno @@ -0,0 +1,24 @@ +package main + +// Tests that a dependency inside a function literal's body (single nesting +// level) is correctly attributed to the enclosing package-level ValueDecl. +// The inner `var local = B` is a local declaration; codaInitOrderDeps must +// walk past it and attribute the B reference to ValueDecl(A), not to the +// local ValueDecl. + +var A = func() int { + var ( + local = B + _ = 123 // just to avoid `fmt` simplifying this into :=. + ) + return local +}() + +var B = 42 + +func main() { + println(A) +} + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder2.gno b/gnovm/tests/files/var_initorder2.gno new file mode 100644 index 00000000000..74cf9f709ce --- /dev/null +++ b/gnovm/tests/files/var_initorder2.gno @@ -0,0 +1,43 @@ +package main + +// Tests init order with multiple vars all depending on the same function, +// which itself depends on a package-level var (transitive dep through function). +// Vars without unsatisfied deps are initialized in declaration order. +// Detailed order: +// - events: no deps, initialized first (zero value, no emit call). +// - B, A, C, G, D, E: each only calls emit(events), all ready after events. +// Initialized in declaration order: B, A, C, G, D, E. +// - Z: depends directly on A, B, C, D, E — all now satisfied. Z is placed +// earlier in declaration order than F, so Z runs next (emits "L"). +// - F: last remaining, initialized after Z. + +var events []string + +func emit(s string) string { events = append(events, s); return s } + +var ( + Z = A + "-" + B + "-" + C + "-" + emit("L") + D + "-" + E + B = emit("B") + A = emit("A") + C = emit("C") + G = emit("G") + D = emit("D") + E = emit("E") + F = emit("F") +) + +func main() { + for _, e := range events { + println(e) + } +} + +// Output: +// B +// A +// C +// G +// D +// E +// L +// F diff --git a/gnovm/tests/files/var_initorder20.gno b/gnovm/tests/files/var_initorder20.gno new file mode 100644 index 00000000000..dcdd686205f --- /dev/null +++ b/gnovm/tests/files/var_initorder20.gno @@ -0,0 +1,22 @@ +package main + +// Tests that init-order analysis correctly handles promoted methods from +// embedded structs. Outer embeds Inner, and Inner.GetB reads B. The call +// Outer{}.GetB() should be traced through to Inner.GetB's body, discovering +// the transitive dependency on B. + +type Inner struct{} + +func (Inner) GetB() int { return B } + +type Outer struct{ Inner } + +var ( + A = Outer{}.GetB() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder21.gno b/gnovm/tests/files/var_initorder21.gno new file mode 100644 index 00000000000..e35c2dcfc77 --- /dev/null +++ b/gnovm/tests/files/var_initorder21.gno @@ -0,0 +1,20 @@ +package main + +// Tests promoted pointer-receiver method through embedding. +// Outer embeds *Inner (pointer embed), Inner has a pointer receiver method. + +type Inner struct{} + +func (*Inner) GetB() int { return B } + +type Outer struct{ *Inner } + +var ( + A = Outer{Inner: &Inner{}}.GetB() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder22.gno b/gnovm/tests/files/var_initorder22.gno new file mode 100644 index 00000000000..e7b8222d680 --- /dev/null +++ b/gnovm/tests/files/var_initorder22.gno @@ -0,0 +1,17 @@ +package main + +// Tests that a circular dependency through a function-literal variable +// (ValueDecl, not FuncDecl) is correctly detected. Unlike a FuncDecl, +// a var holding a func literal participates in initialization ordering, +// so f -> B -> f is a real cycle. + +var f = func() int { return B } +var B = f() + +func main() {} + +// Error: +// main/var_initorder22.gno:8:5: circular dependency: f -> B -> f + +// TypeCheckError: +// main/var_initorder22.gno:8:5: initialization cycle for f; main/var_initorder22.gno:8:5: f refers to B; main/var_initorder22.gno:9:5: B refers to f diff --git a/gnovm/tests/files/var_initorder23.gno b/gnovm/tests/files/var_initorder23.gno new file mode 100644 index 00000000000..f854e50b135 --- /dev/null +++ b/gnovm/tests/files/var_initorder23.gno @@ -0,0 +1,21 @@ +package main + +// Tests that multiple blank-identifier declarations are initialized in +// source order and their side effects are visible to later declarations. +// Blank decls produce no names in fdeclared but still occupy their +// position in the pending list. + +var counter int + +func incr() int { counter++; return counter } + +var ( + _ = incr() // counter=1 + _ = incr() // counter=2 + A = counter // should be 2 +) + +func main() { println(A) } + +// Output: +// 2 diff --git a/gnovm/tests/files/var_initorder24.gno b/gnovm/tests/files/var_initorder24.gno new file mode 100644 index 00000000000..842a502caeb --- /dev/null +++ b/gnovm/tests/files/var_initorder24.gno @@ -0,0 +1,20 @@ +package main + +// Tests a 3-level transitive dependency chain through alternating +// FuncDecl and ValueDecl nodes: A -> f() -> B -> g() -> C. +// resolveEffectiveDeps collapses FuncDecl edges; Kahn's algorithm +// handles ValueDecl→ValueDecl transitivity. + +func f() int { return B } +func g() int { return C } + +var ( + A = f() + B = g() + C = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder25.gno b/gnovm/tests/files/var_initorder25.gno new file mode 100644 index 00000000000..34a3d3fd4c1 --- /dev/null +++ b/gnovm/tests/files/var_initorder25.gno @@ -0,0 +1,22 @@ +package main + +// Tests that a local variable shadowing a package-level name does NOT +// create a false dependency. f() declares a local B that shadows the +// package-level B; the NameExpr for the local B resolves to f's block, +// not the package block, so A should have no dependency on B. +// Init order: A and B are independent; A is declared first so it runs first. + +func f() int { + B := 99 + return B +} + +var ( + A = f() + B = 42 +) + +func main() { println(A, B) } + +// Output: +// 99 42 diff --git a/gnovm/tests/files/var_initorder26.gno b/gnovm/tests/files/var_initorder26.gno new file mode 100644 index 00000000000..b2ba80be74b --- /dev/null +++ b/gnovm/tests/files/var_initorder26.gno @@ -0,0 +1,16 @@ +package main + +// Tests that dependencies are correctly discovered through doubly-nested +// function literals. The NameExpr for B is two FuncLitExprs deep; +// addDependencyToTopDecl must walk past both to find ValueDecl(A). + +var A = func() int { + return func() int { return B }() +}() + +var B = 42 + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder3.gno b/gnovm/tests/files/var_initorder3.gno new file mode 100644 index 00000000000..4bc02dc5253 --- /dev/null +++ b/gnovm/tests/files/var_initorder3.gno @@ -0,0 +1,18 @@ +package main + +// Tests transitive dependency resolution through a chain of functions: +// A = f() depends on g() which depends on B. +// B must be initialized before A even though A is declared first. + +func f() int { return g() } +func g() int { return B } + +var ( + A = f() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder4.gno b/gnovm/tests/files/var_initorder4.gno new file mode 100644 index 00000000000..69a62acdd33 --- /dev/null +++ b/gnovm/tests/files/var_initorder4.gno @@ -0,0 +1,17 @@ +package main + +// Tests chained dependencies where the source order is the reverse of the +// initialization order. +// C depends on B, B depends on A; source order is C, B, A. +// Init order must be: A (no deps), B (deps A), C (deps B). + +var ( + C = B + 2 + B = A + 1 + A = 1 +) + +func main() { println(A, B, C) } + +// Output: +// 1 2 4 diff --git a/gnovm/tests/files/var_initorder5.gno b/gnovm/tests/files/var_initorder5.gno new file mode 100644 index 00000000000..596a9ed2cfe --- /dev/null +++ b/gnovm/tests/files/var_initorder5.gno @@ -0,0 +1,22 @@ +package main + +// Tests a single multi-name var spec where both names share the same +// initializer dependencies. A and B are declared by ONE ValueDecl so they +// are treated as a single initialization step; within the step values are +// evaluated left-to-right. +// Init order: counter (no deps), then {A,B} spec (deps counter via incr), +// then C (deps A and B). + +var counter int + +func incr() int { counter++; return counter } + +var ( + A, B = incr(), incr() + C = A + B +) + +func main() { println(A, B, C) } + +// Output: +// 1 2 3 diff --git a/gnovm/tests/files/var_initorder6.gno b/gnovm/tests/files/var_initorder6.gno new file mode 100644 index 00000000000..1f6d060d338 --- /dev/null +++ b/gnovm/tests/files/var_initorder6.gno @@ -0,0 +1,24 @@ +package main + +// Tests that a multi-name spec initialized by a multi-return function is +// treated as one atomic step. X and Y are ONE ValueDecl; Z must wait for +// both even though it could theoretically proceed after X alone. + +func pair() (int, int) { return 10 + incr(), 20 + incr() } + +var counter int + +func incr() int { + counter++ + return counter +} + +var ( + X, Y = pair() + Z = X + incr() +) + +func main() { println(X, Y, Z) } + +// Output: +// 11 22 14 diff --git a/gnovm/tests/files/var_initorder7.gno b/gnovm/tests/files/var_initorder7.gno new file mode 100644 index 00000000000..bdf4301d426 --- /dev/null +++ b/gnovm/tests/files/var_initorder7.gno @@ -0,0 +1,21 @@ +package main + +// Tests that a blank-identifier var spec (_) runs for its side effects in +// source order before a named var that shares the same dependencies. +// GetDeclNames() returns [] for _, so it is never registered in fdeclared, +// but it still occupies its source position in the pending list. +// Init order: counter, _ (side effect only), A. + +var counter int + +func incr() int { counter++; return counter } + +var ( + _ = incr() // runs first; counter becomes 1; contributes nothing to fdeclared + A = incr() // runs second; counter becomes 2; A = 2 +) + +func main() { println(A) } + +// Output: +// 2 diff --git a/gnovm/tests/files/var_initorder8.gno b/gnovm/tests/files/var_initorder8.gno new file mode 100644 index 00000000000..e2b41911212 --- /dev/null +++ b/gnovm/tests/files/var_initorder8.gno @@ -0,0 +1,23 @@ +package main + +// Tests that a method call through a package-level receiver variable is +// correctly ordered. The SelectorExpr `obj.Val()` makes A syntactically +// depend on `obj`, and the struct literal `T{val: B}` makes obj depend on B. +// So the transitive chain B -> obj -> A is inferred without needing to +// inspect the method body. +// Init order: T (type, no deps), B (no deps), obj (deps T and B), A (deps obj). + +type T struct{ val int } + +func (t T) Val() int { return t.val } + +var ( + B = 42 + obj = T{val: B} + A = obj.Val() +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder9.gno b/gnovm/tests/files/var_initorder9.gno new file mode 100644 index 00000000000..1116fd9b6cf --- /dev/null +++ b/gnovm/tests/files/var_initorder9.gno @@ -0,0 +1,22 @@ +package main + +// Tests that method bodies are analysed for package-level variable dependencies. +// A = T{}.GetB() has no direct syntactic dependency on B, but GetB's body +// reads B. The init-order analysis must trace into the method body (via +// selectorRecvTypeName + findDependentNames recursion) to discover that A +// transitively depends on B. +// Init order: T (type, no deps), B (no deps), A (deps T and B via GetB). + +type T struct{} + +func (T) GetB() int { return B } + +var ( + A = T{}.GetB() + B = 42 +) + +func main() { println(A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder_circ_multi.gno b/gnovm/tests/files/var_initorder_circ_multi.gno new file mode 100644 index 00000000000..7c88e0c8e09 --- /dev/null +++ b/gnovm/tests/files/var_initorder_circ_multi.gno @@ -0,0 +1,18 @@ +package main + +// var a depends on both B() and C(), both of which read a. +// The cycle error should always name B first (B < C lexicographically). + +var a = B() + C() + +func B() int { return a } + +func C() int { return a } + +func main() {} + +// Error: +// main/var_initorder_circ_multi.gno:6:5: circular dependency: a -> B -> a + +// TypeCheckError: +// main/var_initorder_circ_multi.gno:6:5: initialization cycle for a; main/var_initorder_circ_multi.gno:6:5: a refers to B; main/var_initorder_circ_multi.gno:8:6: B refers to a diff --git a/gnovm/tests/files/var_initorder_crossfn.gno b/gnovm/tests/files/var_initorder_crossfn.gno new file mode 100644 index 00000000000..64e73edae84 --- /dev/null +++ b/gnovm/tests/files/var_initorder_crossfn.gno @@ -0,0 +1,15 @@ +package main + +// Tests cross-file transitive dependency through a function. +// extern/initorder_crossfn: a.gno has A = GetB(), b.gno has B = 42 and +// func GetB() int { return B }. +// During preprocessing of b.gno, addDependencyToTopDecl records that GetB +// uses B, so A's ATTR_DECL_DEPS includes both GetB and (transitively) B. +// Init order within the package: B (no deps), then A (deps GetB and B). + +import "filetests/extern/initorder_crossfn" + +func main() { println(initorder_crossfn.A) } + +// Output: +// 42 diff --git a/gnovm/tests/files/var_initorder_xpkgmethod.gno b/gnovm/tests/files/var_initorder_xpkgmethod.gno new file mode 100644 index 00000000000..725640bff20 --- /dev/null +++ b/gnovm/tests/files/var_initorder_xpkgmethod.gno @@ -0,0 +1,21 @@ +package main + +// Tests that addDependencyToTopDecl does not record dependencies into method +// bodies defined in imported packages. initorder_xpkgmethod.T.GetB references +// that package's variable B; without the same-package guard (dt.PkgPath == +// ctxpn.PkgPath), the dep would be recorded with pn=main and resolveDeclDep +// would panic trying to find "B" in main's fileset. With the guard, the +// foreign method body is skipped. +// +// Also verifies that the intra-package init order of the imported package +// itself is correct: B must be initialised before A inside +// initorder_xpkgmethod (A = T{}.GetB() = 42). + +import "filetests/extern/initorder_xpkgmethod" + +func main() { + println(initorder_xpkgmethod.A) +} + +// Output: +// 42 diff --git a/gnovm/tests/files/zrealm_crossrealm10.gno b/gnovm/tests/files/zrealm_crossrealm10.gno index f4975bfd68a..1ff61657712 100644 --- a/gnovm/tests/files/zrealm_crossrealm10.gno +++ b/gnovm/tests/files/zrealm_crossrealm10.gno @@ -11,4 +11,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): SomeValue3<~VPBlock(3,5)>.Field +// cannot directly modify readonly tainted object (use a method or crossing function): SomeValue3<~VPBlock(3,5)>.Field diff --git a/gnovm/tests/files/zrealm_crossrealm2.gno b/gnovm/tests/files/zrealm_crossrealm2.gno index 13ca1f19142..fc7733df6ae 100644 --- a/gnovm/tests/files/zrealm_crossrealm2.gno +++ b/gnovm/tests/files/zrealm_crossrealm2.gno @@ -19,4 +19,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): t.Field +// cannot directly modify readonly tainted object (use a method or crossing function): t.Field diff --git a/gnovm/tests/files/zrealm_crossrealm25.gno b/gnovm/tests/files/zrealm_crossrealm25.gno index 27bc63c5ad2..51ec572c63b 100644 --- a/gnovm/tests/files/zrealm_crossrealm25.gno +++ b/gnovm/tests/files/zrealm_crossrealm25.gno @@ -25,4 +25,4 @@ func main(cur realm) { // s.A = 123 // Error: -// cannot directly modify readonly tainted object (w/o method): s<~VPBlock(1,1)>.A +// cannot directly modify readonly tainted object (use a method or crossing function): s<~VPBlock(1,1)>.A diff --git a/gnovm/tests/files/zrealm_crossrealm25b.gno b/gnovm/tests/files/zrealm_crossrealm25b.gno index e9a637f338c..9fe534ee101 100644 --- a/gnovm/tests/files/zrealm_crossrealm25b.gno +++ b/gnovm/tests/files/zrealm_crossrealm25b.gno @@ -30,4 +30,4 @@ func main(cur realm) { // s.A = 123 // Error: -// cannot directly modify readonly tainted object (w/o method): s<~VPBlock(3,1)>.A +// cannot directly modify readonly tainted object (use a method or crossing function): s<~VPBlock(3,1)>.A diff --git a/gnovm/tests/files/zrealm_crossrealm25c.gno b/gnovm/tests/files/zrealm_crossrealm25c.gno index fb1ce73760b..259d6843132 100644 --- a/gnovm/tests/files/zrealm_crossrealm25c.gno +++ b/gnovm/tests/files/zrealm_crossrealm25c.gno @@ -34,4 +34,4 @@ func main(cur realm) { // s_g.A = 123 // Error: -// cannot directly modify readonly tainted object (w/o method): s_g<~VPBlock(3,1)>.A +// cannot directly modify readonly tainted object (use a method or crossing function): s_g<~VPBlock(3,1)>.A diff --git a/gnovm/tests/files/zrealm_crossrealm26.gno b/gnovm/tests/files/zrealm_crossrealm26.gno index e10c93ba418..c635f934a6e 100644 --- a/gnovm/tests/files/zrealm_crossrealm26.gno +++ b/gnovm/tests/files/zrealm_crossrealm26.gno @@ -17,4 +17,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): s<~VPBlock(1,1)>.A +// cannot directly modify readonly tainted object (use a method or crossing function): s<~VPBlock(1,1)>.A diff --git a/gnovm/tests/files/zrealm_crossrealm27.gno b/gnovm/tests/files/zrealm_crossrealm27.gno index 8323f05814b..1b68cd11140 100644 --- a/gnovm/tests/files/zrealm_crossrealm27.gno +++ b/gnovm/tests/files/zrealm_crossrealm27.gno @@ -23,7 +23,7 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): s<~VPBlock(3,1)>.A +// cannot directly modify readonly tainted object (use a method or crossing function): s<~VPBlock(3,1)>.A // Preprocessed: // file{ package crossrealm; import crossrealm_b gno.land/r/tests/vm/crossrealm_b; type Struct (const-type gno.land/r/crossrealm.Struct); var s *(typeval{gno.land/r/crossrealm.Struct}); func init.2() { s2 := &((const-type gno.land/r/crossrealm.Struct){A: (const (100 int))}); (const (ref(gno.land/r/tests/vm/crossrealm_b) package{})).SetObject((const (undefined)), func func(){ (const (println func(...interface {})))(&(s2<~VPBlock(1,0)>.A)) }>); s<~VPBlock(3,1)> = s2<~VPBlock(1,0)> }; func main(cur (const-type .uverse.realm)) { s<~VPBlock(3,1)>.A = (const (123 int)); (const (println func(...interface {})))(s<~VPBlock(3,1)>) } } diff --git a/gnovm/tests/files/zrealm_crossrealm3.gno b/gnovm/tests/files/zrealm_crossrealm3.gno index e91d4fee79f..63229c100d7 100644 --- a/gnovm/tests/files/zrealm_crossrealm3.gno +++ b/gnovm/tests/files/zrealm_crossrealm3.gno @@ -13,7 +13,8 @@ func init() { } func main(cur realm) { - // NOTE: it is also valid to modify it using an external realm function. + // NOTE: Modifying an external-realm object via a non-crossing method is + // allowed by soft crossing. somevalue.Modify() println(somevalue) } diff --git a/gnovm/tests/files/zrealm_crossrealm30.gno b/gnovm/tests/files/zrealm_crossrealm30.gno index 7be9656c453..4d4eaa9b3d8 100644 --- a/gnovm/tests/files/zrealm_crossrealm30.gno +++ b/gnovm/tests/files/zrealm_crossrealm30.gno @@ -39,4 +39,4 @@ func main() { } // Error: -// cannot directly modify readonly tainted object (w/o method): sa.A +// cannot directly modify readonly tainted object (use a method or crossing function): sa.A diff --git a/gnovm/tests/files/zrealm_crossrealm30b.gno b/gnovm/tests/files/zrealm_crossrealm30b.gno index 58b99ae6eb0..4206202e394 100644 --- a/gnovm/tests/files/zrealm_crossrealm30b.gno +++ b/gnovm/tests/files/zrealm_crossrealm30b.gno @@ -40,4 +40,4 @@ func main() { } // Error: -// cannot directly modify readonly tainted object (w/o method): sa.A +// cannot directly modify readonly tainted object (use a method or crossing function): sa.A diff --git a/gnovm/tests/files/zrealm_crossrealm30c.gno b/gnovm/tests/files/zrealm_crossrealm30c.gno index c67139fd70a..90830f0c063 100644 --- a/gnovm/tests/files/zrealm_crossrealm30c.gno +++ b/gnovm/tests/files/zrealm_crossrealm30c.gno @@ -45,4 +45,4 @@ func main() { } // Error: -// cannot directly modify readonly tainted object (w/o method): sa.A +// cannot directly modify readonly tainted object (use a method or crossing function): sa.A diff --git a/gnovm/tests/files/zrealm_crossrealm33.gno b/gnovm/tests/files/zrealm_crossrealm33.gno index 2783ce3ed35..71a2a8d420e 100644 --- a/gnovm/tests/files/zrealm_crossrealm33.gno +++ b/gnovm/tests/files/zrealm_crossrealm33.gno @@ -13,4 +13,4 @@ func main(cur realm) { // Output: // Error: -// cannot modify external-realm or non-realm object +// cannot directly modify readonly tainted object (use a method or crossing function): n<~VPBlock(3,10)> diff --git a/gnovm/tests/files/zrealm_crossrealm35.gno b/gnovm/tests/files/zrealm_crossrealm35.gno new file mode 100644 index 00000000000..ea0bf06ba79 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm35.gno @@ -0,0 +1,59 @@ +// PKGPATH: gno.land/r/crossrealm +package crossrealm + +// Verifies that cross-realm assign+recover does NOT corrupt state. +// +// crossrealm_d has a package-level counter (initially 100) with both +// a non-crossing setter (SetCounter) and a crossing setter +// (SetCounterCrossing). This test calls the non-crossing setter from +// another realm, recovers the panic, then checks that the state is +// unchanged. +// +// Impact without the fix: the non-crossing SetCounter(0) would land +// in memory before DidUpdate panics. After recover(), the counter is +// 0 in memory but 100 in persistence. A subsequent crossing call like +// DoubleCounter would read 0 and produce 0 instead of 200 — the +// realm acts on state that was never committed. +// +// The fix: PopAsPointer checks readonly BEFORE the assignment, so +// the counter never changes in memory. + +import ( + "gno.land/r/tests/vm/crossrealm_d" +) + +func main() { + // 1. Initial state. + println("counter:", crossrealm_d.GetCounter(cross)) // 100 + + // 2. Non-crossing set → panics, recover catches it. + func() { + defer func() { + r := recover() + if r != nil { + println("panic caught:", r.(string)) + } + }() + crossrealm_d.SetCounter(0) + }() + + // 3. Counter must still be 100. + println("counter after set+recover:", crossrealm_d.GetCounter(cross)) + + // 4. A crossing call that depends on counter. + // Without the fix: counter=0 in memory → DoubleCounter returns 0. + // With the fix: counter=100 → DoubleCounter returns 200. + result := crossrealm_d.DoubleCounter(cross) + println("double:", result) + + // 5. Verify the crossing setter works correctly for comparison. + crossrealm_d.SetCounterCrossing(cross, 50) + println("after crossing set:", crossrealm_d.GetCounter(cross)) // 50 +} + +// Output: +// counter: 100 +// panic caught: cannot directly modify readonly tainted object (use a method or crossing function): counter<~VPBlock(3,0)> +// counter after set+recover: 100 +// double: 200 +// after crossing set: 50 diff --git a/gnovm/tests/files/zrealm_crossrealm36.gno b/gnovm/tests/files/zrealm_crossrealm36.gno new file mode 100644 index 00000000000..492f103cecc --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm36.gno @@ -0,0 +1,25 @@ +// PKGPATH: gno.land/r/crossrealm +package crossrealm + +// Negative control: without recover(), the cross-realm readonly +// check properly aborts the entire transaction. + +import ( + "gno.land/r/tests/vm/crossrealm_d" +) + +func main() { + println("counter:", crossrealm_d.GetCounter(cross)) + + // No recover — the cross-realm panic aborts everything. + crossrealm_d.SetCounter(0) + + // This line is never reached. + println("counter after:", crossrealm_d.GetCounter(cross)) +} + +// Output: +// counter: 100 + +// Error: +// cannot directly modify readonly tainted object (use a method or crossing function): counter<~VPBlock(3,0)> diff --git a/gnovm/tests/files/zrealm_crossrealm37.gno b/gnovm/tests/files/zrealm_crossrealm37.gno new file mode 100644 index 00000000000..10778f05592 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm37.gno @@ -0,0 +1,47 @@ +// PKGPATH: gno.land/r/crossrealm +package crossrealm + +import ( + "chain/runtime" + + "gno.land/r/tests/vm/crossrealm_e" +) + +func main() { + println("owner:", crossrealm_e.GetOwner()) + attacker := runtime.CurrentRealm().Address() + println("attacker: ", attacker) + + func() { + defer func() { + r := recover() + if r != nil { + println("exception caught: ", r.(string)) + } + }() + crossrealm_e.SetOwner(runtime.CurrentRealm().Address()) + }() + + owner := crossrealm_e.GetOwner() + println("owner == attacker: ", owner == attacker) + + crossrealm_e.TransferToken(cross) +} + +// before fix: +// owner: g1dao_address_here +// attacker: g1h2y7mn4d8w5ed08kqt8sdd7tp4j96eahyn6yan +// exception caught: cannot modify external-realm or non-realm object +// owner == attacker: true +// ===send token to: g1h2y7mn4d8w5ed08kqt8sdd7tp4j96eahyn6yan + +// after fix: + +// Output: +// owner: g1dao_address_here +// attacker: g1h2y7mn4d8w5ed08kqt8sdd7tp4j96eahyn6yan +// exception caught: cannot directly modify readonly tainted object (use a method or crossing function): owner<~VPBlock(3,1)> +// owner == attacker: false + +// Error: +// unauthorized diff --git a/gnovm/tests/files/zrealm_crossrealm4.gno b/gnovm/tests/files/zrealm_crossrealm4.gno index 40502eb71e0..5cdcbcf4419 100644 --- a/gnovm/tests/files/zrealm_crossrealm4.gno +++ b/gnovm/tests/files/zrealm_crossrealm4.gno @@ -13,7 +13,8 @@ func init() { } func main(cur realm) { - // NOTE: it is valid to modify it using the external realm function. + // NOTE: Modifying an external-realm object via a non-crossing method is + // allowed by soft crossing. somevalue.Modify() println(somevalue) } diff --git a/gnovm/tests/files/zrealm_crossrealm5.gno b/gnovm/tests/files/zrealm_crossrealm5.gno index 256e012d533..89e04a378db 100644 --- a/gnovm/tests/files/zrealm_crossrealm5.gno +++ b/gnovm/tests/files/zrealm_crossrealm5.gno @@ -19,4 +19,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): somevalue<~VPBlock(3,0)>.Field +// cannot directly modify readonly tainted object (use a method or crossing function): somevalue<~VPBlock(3,0)>.Field diff --git a/gnovm/tests/files/zrealm_crossrealm7.gno b/gnovm/tests/files/zrealm_crossrealm7.gno index 183b99950d9..43c83207351 100644 --- a/gnovm/tests/files/zrealm_crossrealm7.gno +++ b/gnovm/tests/files/zrealm_crossrealm7.gno @@ -11,4 +11,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): somevalue1<~VPBlock(3,3)>.Field +// cannot directly modify readonly tainted object (use a method or crossing function): somevalue1<~VPBlock(3,3)>.Field diff --git a/gnovm/tests/files/zrealm_crossrealm8.gno b/gnovm/tests/files/zrealm_crossrealm8.gno index d373c3a2991..d69e5edf6be 100644 --- a/gnovm/tests/files/zrealm_crossrealm8.gno +++ b/gnovm/tests/files/zrealm_crossrealm8.gno @@ -11,4 +11,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): SomeValue2<~VPBlock(3,4)>.Field +// cannot directly modify readonly tainted object (use a method or crossing function): SomeValue2<~VPBlock(3,4)>.Field diff --git a/gnovm/tests/files/zrealm_crossrealm9.gno b/gnovm/tests/files/zrealm_crossrealm9.gno index 379dc4ec668..e1b60495503 100644 --- a/gnovm/tests/files/zrealm_crossrealm9.gno +++ b/gnovm/tests/files/zrealm_crossrealm9.gno @@ -11,4 +11,4 @@ func main(cur realm) { } // Error: -// cannot directly modify readonly tainted object (w/o method): (const (ref(gno.land/p/demo/tests) package{})).SomeValue2.Field +// cannot directly modify readonly tainted object (use a method or crossing function): (const (ref(gno.land/p/demo/tests) package{})).SomeValue2.Field diff --git a/gnovm/tests/files/zrealm_databyte0.gno b/gnovm/tests/files/zrealm_databyte0.gno new file mode 100644 index 00000000000..038aca0e196 --- /dev/null +++ b/gnovm/tests/files/zrealm_databyte0.gno @@ -0,0 +1,33 @@ +// PKGPATH: gno.land/r/test +package test + +// Verify that plain byte-index assignment (bs[i] = v) on a Data-backed []byte +// realm variable marks the backing ArrayValue dirty via DidUpdate, so the +// mutation persists across transactions. +var bs = []byte("aabb") + +func main(cur realm) { + bs[0] = 'A' + bs[1] = 'B' + println(string(bs)) +} + +// Output: +// ABbb + +// Realm: +// finalizerealm["gno.land/r/test"] +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:4](5)= +// @@ -1,10 +1,10 @@ +// { +// - "Data": "YWFiYg==", +// + "Data": "QUJiYg==", +// "List": null, +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:4", +// "LastObjectSize": "181", +// - "ModTime": "0", +// + "ModTime": "6", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3", +// "RefCount": "1" +// } diff --git a/gnovm/tests/files/zrealm_map2.gno b/gnovm/tests/files/zrealm_map2.gno index 456ae8cae6e..ab68a33962c 100644 --- a/gnovm/tests/files/zrealm_map2.gno +++ b/gnovm/tests/files/zrealm_map2.gno @@ -30,6 +30,6 @@ func main(cur realm) { } // Output: -// caught panic: cannot directly modify readonly tainted object (w/o method): rm<~VPBlock(1,0)>[(const ("attacker" string))] +// caught panic: cannot directly modify readonly tainted object (use a method or crossing function): rm<~VPBlock(1,0)>[(const ("attacker" string))] // len(m) = 1 // attacker key exists: false diff --git a/gnovm/tests/files/zrealm_map3.gno b/gnovm/tests/files/zrealm_map3.gno index bce84849748..cd9aa2b7110 100644 --- a/gnovm/tests/files/zrealm_map3.gno +++ b/gnovm/tests/files/zrealm_map3.gno @@ -44,7 +44,7 @@ func main(cur realm) { } // Output: -// assign panic: cannot directly modify readonly tainted object (w/o method): rm<~VPBlock(1,0)>[(const ("attacker" string))] +// assign panic: cannot directly modify readonly tainted object (use a method or crossing function): rm<~VPBlock(1,0)>[(const ("attacker" string))] // delete panic: cannot delete from readonly tainted map // admin exists: true // user1: 500 diff --git a/gnovm/tests/files/zrealm_ref0.gno b/gnovm/tests/files/zrealm_ref0.gno new file mode 100644 index 00000000000..5a69da8ab84 --- /dev/null +++ b/gnovm/tests/files/zrealm_ref0.gno @@ -0,0 +1,20 @@ +// PKGPATH: gno.land/r/test +package test + +// Realm filetest: ref of a concrete type in a realm. +// This works correctly since xv.TV.T matches the static type. + +var x int + +func init() { + x = 1 +} + +func main(cur realm) { + p := &x + *p = 2 + println(x) +} + +// Output: +// 2 diff --git a/gnovm/tests/files/zrealm_ref1.gno b/gnovm/tests/files/zrealm_ref1.gno new file mode 100644 index 00000000000..86132992d7a --- /dev/null +++ b/gnovm/tests/files/zrealm_ref1.gno @@ -0,0 +1,24 @@ +// PKGPATH: gno.land/r/test +package test + +// Realm filetest: ref of a struct field via pointer in a realm. +// This works correctly since the struct type is concrete. + +type MyStruct struct { + A int +} + +var s MyStruct + +func init() { + s.A = 10 +} + +func main(cur realm) { + p := &s + p.A = 20 + println(s) +} + +// Output: +// (struct{(20 int)} gno.land/r/test.MyStruct) diff --git a/gnovm/tests/files/zrealm_ref2.gno b/gnovm/tests/files/zrealm_ref2.gno new file mode 100644 index 00000000000..428444816ea --- /dev/null +++ b/gnovm/tests/files/zrealm_ref2.gno @@ -0,0 +1,20 @@ +// PKGPATH: gno.land/r/test +package test + +import "fmt" + +// Realm filetest: ref of an interface{} var holding a concrete value. + +var i interface{} + +func init() { + i = 42 +} + +func main(cur realm) { + p := &i + fmt.Printf("%T\n", p) +} + +// Output: +// *interface {} diff --git a/gnovm/tests/files/zrealm_ref3.gno b/gnovm/tests/files/zrealm_ref3.gno new file mode 100644 index 00000000000..8efcf4a450e --- /dev/null +++ b/gnovm/tests/files/zrealm_ref3.gno @@ -0,0 +1,19 @@ +// PKGPATH: gno.land/r/test +package test + +// Realm filetest: ref of an interface{} var, mutation through pointer. + +var i interface{} + +func init() { + i = 42 +} + +func main(cur realm) { + p := &i + *p = "hello" + println(i) +} + +// Output: +// hello diff --git a/gnovm/tests/files/zrealm_ref4.gno b/gnovm/tests/files/zrealm_ref4.gno new file mode 100644 index 00000000000..c706dc1de31 --- /dev/null +++ b/gnovm/tests/files/zrealm_ref4.gno @@ -0,0 +1,16 @@ +// PKGPATH: gno.land/r/test +package test + +import "fmt" + +// Realm filetest: ref of a nil interface{} var. + +var i interface{} + +func main(cur realm) { + p := &i + fmt.Printf("%T\n", p) +} + +// Output: +// *interface {} diff --git a/gnovm/tests/files/zrealm_ref5.gno b/gnovm/tests/files/zrealm_ref5.gno new file mode 100644 index 00000000000..d737c29349e --- /dev/null +++ b/gnovm/tests/files/zrealm_ref5.gno @@ -0,0 +1,26 @@ +// PKGPATH: gno.land/r/test +package test + +import "fmt" + +// Realm filetest: function takes *interface{} parameter. +// Tests that the pointer type is checked correctly +// when passed to a function expecting *interface{}. + +var i interface{} + +func init() { + i = 100 +} + +func setViaPtr(p *interface{}, val interface{}) { + *p = val +} + +func main(cur realm) { + setViaPtr(&i, "changed") + fmt.Println(i) +} + +// Output: +// changed diff --git a/gnovm/tests/files/zrealm_ref6.gno b/gnovm/tests/files/zrealm_ref6.gno new file mode 100644 index 00000000000..6ebd9dbafe3 --- /dev/null +++ b/gnovm/tests/files/zrealm_ref6.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/test +package test + +import "fmt" + +// Realm filetest: ref of a struct field that is an interface type. + +type S struct { + F interface{} +} + +var s S + +func init() { + s.F = 42 +} + +func main(cur realm) { + p := &s.F + fmt.Printf("%T\n", p) + *p = "hello" + fmt.Println(s.F) +} + +// Output: +// *interface {} +// hello diff --git a/gnovm/tests/files/zrealm_ref7.gno b/gnovm/tests/files/zrealm_ref7.gno new file mode 100644 index 00000000000..c9cb5760348 --- /dev/null +++ b/gnovm/tests/files/zrealm_ref7.gno @@ -0,0 +1,23 @@ +// PKGPATH: gno.land/r/test +package test + +import "fmt" + +// Realm filetest: ref of a slice element that is an interface type. + +var s []interface{} + +func init() { + s = []interface{}{1, "two", 3.0} +} + +func main(cur realm) { + p := &s[0] + fmt.Printf("%T\n", p) + *p = "replaced" + fmt.Println(s[0]) +} + +// Output: +// *interface {} +// replaced diff --git a/gnovm/tests/files/zrealm_ref8.gno b/gnovm/tests/files/zrealm_ref8.gno new file mode 100644 index 00000000000..2588386f41d --- /dev/null +++ b/gnovm/tests/files/zrealm_ref8.gno @@ -0,0 +1,50 @@ +// PKGPATH: gno.land/r/test +package test + +// Realm filetest: pointer-receiver method call on a package-level +// value type via auto-address (&x).Method(), both direct and +// inside a closure callback. Mirrors the boards2 pattern where +// gFlaggingThresholds.Set() is called inside crossingFn(func(){...}). + +type Tree struct { + size int +} + +func (t *Tree) Set(k string, v int) { + t.size += v +} + +func (t *Tree) Size() int { + return t.size +} + +var gTree Tree + +func withCallback(fn func()) { + fn() +} + +func init() { + // Direct auto-address: (&gTree).Set(...) + gTree.Set("a", 1) + + // Auto-address inside closure callback (like crossingFn pattern) + withCallback(func() { + gTree.Set("b", 2) + }) +} + +func main(cur realm) { + // Auto-address after realm persistence + gTree.Set("c", 3) + + // Also inside closure after persistence + withCallback(func() { + gTree.Set("d", 4) + }) + + println(gTree.Size()) +} + +// Output: +// 10 diff --git a/gnovm/tests/files/zrealm_ref9.gno b/gnovm/tests/files/zrealm_ref9.gno new file mode 100644 index 00000000000..109bb4c87a2 --- /dev/null +++ b/gnovm/tests/files/zrealm_ref9.gno @@ -0,0 +1,33 @@ +// PKGPATH: gno.land/r/test +package test + +// Realm filetest: direct pointer-receiver method call on +// package-level value type (no closure). Tests that auto-address +// (&gTree).Set() works after realm persistence. + +type Tree struct { + size int +} + +func (t *Tree) Set(k string, v int) { + t.size += v +} + +func (t *Tree) Size() int { + return t.size +} + +var gTree Tree + +func init() { + gTree.Set("a", 1) + gTree.Set("b", 2) +} + +func main(cur realm) { + gTree.Set("c", 3) + println(gTree.Size()) +} + +// Output: +// 6 diff --git a/gnovm/tests/stdlibs/chain/runtime/testing_runtime.go b/gnovm/tests/stdlibs/chain/runtime/testing_runtime.go index 4951c43ddd2..ec93776857d 100644 --- a/gnovm/tests/stdlibs/chain/runtime/testing_runtime.go +++ b/gnovm/tests/stdlibs/chain/runtime/testing_runtime.go @@ -40,25 +40,30 @@ func typedString(s gno.StringValue) gno.TypedValue { func isOriginCall(m *gno.Machine) bool { tname := m.Frames[0].Func.Name + // Count only actual function call frames (excludes closures and + // control-flow basic frames like for/range/switch). + callFrames := m.NumCallFrames() switch tname { case "main": // test is a _filetest + // Non-closure frames expected: // 0. main // 1. $RealmFuncName - // 2. td.IsOriginCall - return len(m.Frames) == 3 + // 2. runtime.AssertOriginCall + return callFrames == 3 case "RunTest": // test is a _test + // Non-closure frames expected: // 0. testing.RunTest // 1. tRunner // 2. $TestFuncName // 3. $RealmFuncName - // 4. std.IsOriginCall - return len(m.Frames) == 5 + // 4. runtime.AssertOriginCall + return callFrames == 5 } // support init() in _filetest // XXX do we need to distinguish from 'runtest'/_test? // XXX pretty hacky even if not. if strings.HasPrefix(string(tname), "init.") { - return len(m.Frames) == 3 + return callFrames == 3 } panic("unable to determine if test is a _test or a _filetest") } diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go index 6f2a855b7db..12a5739225a 100644 --- a/gnovm/tests/stdlibs/generated.go +++ b/gnovm/tests/stdlibs/generated.go @@ -10,6 +10,7 @@ import ( testlibs_chain_runtime "github.com/gnolang/gno/gnovm/tests/stdlibs/chain/runtime" testlibs_fmt "github.com/gnolang/gno/gnovm/tests/stdlibs/fmt" testlibs_os "github.com/gnolang/gno/gnovm/tests/stdlibs/os" + testlibs_runtime "github.com/gnolang/gno/gnovm/tests/stdlibs/runtime" testlibs_testing "github.com/gnolang/gno/gnovm/tests/stdlibs/testing" testlibs_unicode "github.com/gnolang/gno/gnovm/tests/stdlibs/unicode" ) @@ -365,6 +366,38 @@ var nativeFuncs = [...]NativeFunc{ p0) }, }, + { + "runtime", + "GC", + []gno.FieldTypeExpr{}, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + testlibs_runtime.GC( + m, + ) + }, + }, + { + "runtime", + "MemStats", + []gno.FieldTypeExpr{}, + []gno.FieldTypeExpr{ + {NameExpr: *gno.Nx("r0"), Type: gno.X("string")}, + }, + true, + func(m *gno.Machine) { + r0 := testlibs_runtime.MemStats( + m, + ) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, { "testing", "getContext", diff --git a/gnovm/stdlibs/runtime/runtime.gno b/gnovm/tests/stdlibs/runtime/runtime.gno similarity index 100% rename from gnovm/stdlibs/runtime/runtime.gno rename to gnovm/tests/stdlibs/runtime/runtime.gno diff --git a/gnovm/stdlibs/runtime/runtime.go b/gnovm/tests/stdlibs/runtime/runtime.go similarity index 100% rename from gnovm/stdlibs/runtime/runtime.go rename to gnovm/tests/stdlibs/runtime/runtime.go diff --git a/misc/deployments/gnoland-1/.gitignore b/misc/deployments/gnoland-1/.gitignore new file mode 100644 index 00000000000..5fcebe2d84a --- /dev/null +++ b/misc/deployments/gnoland-1/.gitignore @@ -0,0 +1 @@ +genesis-work/ diff --git a/misc/deployments/gnoland-1/README.md b/misc/deployments/gnoland-1/README.md new file mode 100644 index 00000000000..68dc9a11610 --- /dev/null +++ b/misc/deployments/gnoland-1/README.md @@ -0,0 +1,99 @@ +# gnoland-1 — Hard Fork of gnoland1 + +`gnoland-1` is the upgraded successor to `gnoland1`. It is produced via a +coordinated hard fork at a governance-approved halt height. + +## Chain ID change + +| Old | New | +|----------|------------| +| gnoland1 | gnoland-1 | + +The hyphen was added to make the chain ID upgrade-compatible — `gnoland1` +could not support chain upgrades that preserve the chain ID cleanly because +the naming convention conflated chain identity with version. `gnoland-1` is +the permanent base name; future upgrades increment a suffix on a sub-release +tag (e.g., `chain/gnoland-1.1`). + +## What changed + +This hard fork bundles the following upgrades in one shot: + +- **`r/sys/params` halt height** (gnolang/gno#5368): GovDAO can now vote to + halt the chain at a specific block and enforce a minimum binary version on + restart. _(awaiting merge)_ +- **`r/gnops/valopers` fee = 0**: Registration fee was set to 0 via a GovDAO + transaction on gnoland1; preserved in genesis replay. No code change needed. +- **Namereg GovDAO whitelist** (gnolang/gno#5293): Namereg now checks GovDAO + membership before allowing name registration. ✅ _merged_ +- **GovDAO scripts** (gnolang/gno#5375): Updated scripts for the new chain. ✅ _merged_ + +**Not confirmed for this hard fork** (need explicit sign-off from Jae): +- Gas parameter updates (gnolang/gno#5291, #5289, #5274) + +## Upgrade workflow + +Approach: **Scenario A — genesis tx-replay with InitialHeight preservation**. +All historical txs from gnoland1 are exported with their original block heights +and timestamps, then assembled into the genesis for gnoland-1. The new chain +starts at `initial_height = halt_height + 1`, preserving height continuity. + +``` +gnoland1 (running) + │ + ├── [gnoland1.2] Operators rolling-update with halt_height config + │ + ├── Chain halts at GovDAO-approved height + │ + ├── Each validator runs migrate-from-gnoland1.sh ← NOT YET IMPLEMENTED + │ - tx-archive exports all txs with block height + timestamp + │ - genesis-assemble produces genesis.json for gnoland-1 + │ (chain_id=gnoland-1, initial_height=halt+1, original_chain_id=gnoland1) + │ + ├── Validators compare genesis.json SHA-256 + │ (must all match before anyone restarts) + │ + └── Validators restart with new binary + new genesis + chain-id: gnoland-1, starts at height halt+1 +``` + +## ⚠️ Migration script not yet written + +**The migration script (`migrate-from-gnoland1.sh`) is the critical missing +piece.** Until it exists and has been tested on a dry-run on test12, the +hard fork cannot happen. + +Blockers: +- `tx-archive genesis-assemble` command (companion to gnolang/gno#5411) +- `tx-archive` offline export from block store (no live node required) +- Jae's tm2 `GenesisDoc.InitialHeight` port (hard blocker for gnolang/gno#5411) +- test12 dry-run: full halt → export → genesis-assemble → restart + +See the TODO block inside `migrate-from-gnoland1.sh` for details. + +Dry-run target: test12 (see gnoland1/govdao-scripts/ for tooling). + +## GovDAO scripts + +The govdao scripts in `govdao-scripts/` are identical to those in +`../gnoland1/govdao-scripts/` but default to `CHAIN_ID=gnoland-1`. + +All scripts default to `GNOKEY_NAME=moul`, `CHAIN_ID=gnoland-1`, and +`REMOTE=https://rpc.gno.land:443`. Override via env vars. + +```bash +./govdao-scripts/add-validator-from-valopers.sh ADDR +./govdao-scripts/add-validator.sh ADDR PUBKEY [POWER] +./govdao-scripts/rm-validator.sh ADDR +./govdao-scripts/unrestrict-account.sh ADDR [ADDR...] +``` + +## Config + +Copy `config.toml` and edit the `# Change me` fields: + +```shell +mkdir -p gnoland-data/config +cp config.toml gnoland-data/config/config.toml +grep -n "Change me" gnoland-data/config/config.toml +``` diff --git a/misc/deployments/gnoland-1/config.toml b/misc/deployments/gnoland-1/config.toml new file mode 100644 index 00000000000..9d07079c679 --- /dev/null +++ b/misc/deployments/gnoland-1/config.toml @@ -0,0 +1,256 @@ +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# Database backend: pebbledb | goleveldb | boltdb +#* pebbledb (github.com/cockroachdb/pebble) +# - pure go +# - stable +#* goleveldb (github.com/syndtr/goleveldb) +# - pure go +# - stable +# - use goleveldb build tag +#* boltdb (uses etcd's fork of bolt - go.etcd.io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) +db_backend = "pebbledb" + +# Database directory +db_dir = "db" + +# If this node is many blocks behind the tip of the chain, FastSync +# allows them to catchup quickly by downloading blocks in parallel +# and verifying their commits +fast_sync = true +home = "" + +# A custom human readable name for this node +moniker = "" # TODO: Change me! + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "secrets/node_key.json" + +# TCP or UNIX socket address for the profiling server to listen on +prof_laddr = "" + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the Tendermint binary +proxy_app = "tcp://127.0.0.1:26658" + +##### app settings ##### +[application] + +# Lowest gas prices accepted by a validator +min_gas_prices = "" + +# State pruning strategy [everything, nothing, syncable] +prune_strategy = "syncable" + +##### consensus configuration options ##### +[consensus] + +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" +home = "" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "10ms" # Do NOT change me, leave me at 10ms! +peer_query_maj23_sleep_duration = "2s" + +# Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) +skip_timeout_commit = false +timeout_commit = "3s" # Do NOT change me, leave me at 3s! +timeout_precommit = "1s" +timeout_precommit_delta = "500ms" +timeout_prevote = "1s" +timeout_prevote_delta = "500ms" +timeout_propose = "3s" +timeout_propose_delta = "500ms" +wal_file = "wal/cs.wal/wal" + +##### private validator configuration options ##### +[consensus.priv_validator] +home = "" + +# Path to the JSON file containing the private key to use for signing using a local signer +local_signer = "priv_validator_key.json" + +# Path to the JSON file containing the last validator state to prevent double-signing +sign_state = "priv_validator_state.json" + +# Configuration for the remote signer client +[consensus.priv_validator.remote_signer] + +# Maximum number of retries to dial the remote signer. If set to -1, will retry indefinitely +dial_max_retries = -1 + +# Interval between retries to dial the remote signer +dial_retry_interval = "5s" + +# Timeout to dial the remote signer +dial_timeout = "5s" + +# Timeout for requests to the remote signer +request_timeout = "5s" + +# Address of the remote signer to dial (UNIX or TCP). If set, the local signer is disabled +server_address = "" + +# List of authorized public keys for the remote signer (only for TCP). If empty, all keys are authorized +tcp_authorized_keys = [] + +# Keep alive period for the remote signer connection (only for TCP) +tcp_keep_alive_period = "2s" + +##### mempool configuration options ##### +[mempool] +broadcast = true + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache_size = 10000 +home = "" + +# Limit the total size of all txs in the mempool. +# This only accounts for raw transactions (e.g. given 1MB transactions and +# max_txs_bytes=5MB, mempool will only accept 5 transactions). +max_pending_txs_bytes = 1073741824 # ~1GB +recheck = true + +# Maximum number of transactions in the mempool +size = 10000 # Advised value is 10000 +wal_dir = "" + +##### peer to peer configuration options ##### +[p2p] + +# Address to advertise to peers for them to dial +# If empty, will use the same port as the laddr, +# and will introspect on the listener or use UPnP +# to figure out the address. +external_address = "" # TODO: Change me! + +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "10ms" # Do NOT change me, leave me at 10ms! +home = "" + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26656" + +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 40 # Advised value is 40 + +# Maximum size of a message packet payload, in bytes +max_packet_msg_payload_size = 1024 + +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "" # Change me: update with gnoland-1 peer addresses after the hard fork + +# Set true to enable the peer-exchange reactor +pex = true + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "" + +# Rate at which packets can be received, in bytes/second +recv_rate = 5120000 + +# Comma separated list of seed nodes to connect to +seeds = "" # Change me: update with gnoland-1 seed addresses after the hard fork + +# Rate at which packets can be sent, in bytes/second +send_rate = 5120000 + +##### rpc server configuration options ##### +[rpc] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", "OPTIONS"] + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = ["*"] + +# TCP or UNIX socket address for the gRPC server to listen on +# NOTE: This server only supports /broadcast_tx_commit +grpc_laddr = "" + +# Maximum number of simultaneous connections. +# Does not include RPC (HTTP&WebSocket) connections. See max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +grpc_max_open_connections = 900 +home = "" + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://0.0.0.0:26657" # Please use a reverse proxy! + +# Maximum size of request body, in bytes +max_body_bytes = 1000000 + +# Maximum size of request header, in bytes +max_header_bytes = 1048576 + +# Maximum number of simultaneous connections (including WebSocket). +# Does not include gRPC connections. See grpc_max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max_open_connections = 900 + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/tendermint/issues/3435 +timeout_broadcast_tx_commit = "10s" + +# The path to a file containing certificate that is used to create the HTTPS server. +# Might be either absolute path or path related to tendermint's config directory. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. +tls_cert_file = "" + +# The path to a file containing matching private key that is used to create the HTTPS server. +# Might be either absolute path or path related to tendermint's config directory. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. +tls_key_file = "" + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = false + +##### node telemetry ##### +[telemetry] +# the endpoint to export metrics to, like a local OpenTelemetry collector +exporter_endpoint = "" # Change me to the OTEL endpoint! +meter_name = "gnoland-1" +metrics_enabled = false # Advised to be `true` + +# the ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service), in Prometheus this is transformed into the label 'exported_instance +service_instance_id = "gno-node-1" # Change me! + +# in Prometheus this is transformed into the label 'exported_job' +service_name = "gno.land" +traces_enabled = false + +##### event store ##### +[tx_event_store] + +# Type of event store +event_store_type = "none" + +# Event store parameters +[tx_event_store.event_store_params] diff --git a/misc/deployments/gnoland-1/generate-genesis.sh b/misc/deployments/gnoland-1/generate-genesis.sh new file mode 100755 index 00000000000..77bf970bb23 --- /dev/null +++ b/misc/deployments/gnoland-1/generate-genesis.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Generate gnoland-1 hardfork genesis. +# +# Wraps `gnogenesis fork generate` with gnoland-1 chain IDs hardcoded. +# +# Env vars: +# SOURCE source to fetch state from (default: production RPC) +# http://... RPC of a running or recently-halted node +# /path/to/dir local node data directory (stopped node) +# /path/to/*.json exported genesis +# HALT_HEIGHT block height at which gnoland1 was halted +# (empty = auto-detect from source) +# PV_KEY path to the new validator's priv_validator_key.json. +# When set, a valset-reset migration tx is built and +# appended at the end of replay (updates r/sys/validators/v2 +# to match the new GenesisDoc.Validators). Leave empty to +# skip migrations. +# CALLER govDAO T1 address that runs the migration MsgRun +# (default: g1manfred47...) +# +# Usage: +# ./generate-genesis.sh +# SOURCE=http://rpc.gno.land:26657 ./generate-genesis.sh +# HALT_HEIGHT=704052 ./generate-genesis.sh +# PV_KEY=./my-valkey.json ./generate-genesis.sh + +set -euo pipefail + +CHAIN_ID="gnoland-1" +ORIGINAL_CHAIN_ID="gnoland1" + +SOURCE="${SOURCE:-http://rpc.gno.land:26657}" +HALT_HEIGHT="${HALT_HEIGHT:-}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +GNOGENESIS_DIR="$REPO_ROOT/contribs/gnogenesis" +OUTPUT="$SCRIPT_DIR/genesis.json" + +# Build the gnogenesis binary if not already available. +if command -v gnogenesis >/dev/null 2>&1; then + BIN="gnogenesis" +else + BIN="$SCRIPT_DIR/genesis-work/bin/gnogenesis" + if [[ ! -x "$BIN" ]]; then + printf "Building gnogenesis...\n" + mkdir -p "$(dirname "$BIN")" + go build -C "$GNOGENESIS_DIR" -o "$BIN" . + fi +fi + +CMD_ARGS=( + fork generate + --source "$SOURCE" + --chain-id "$CHAIN_ID" + --original-chain-id "$ORIGINAL_CHAIN_ID" + --output "$OUTPUT" +) +[[ -n "$HALT_HEIGHT" ]] && CMD_ARGS+=(--halt-height "$HALT_HEIGHT") + +# Build the post-replay migration jsonl if a new-valset priv_validator_key +# is provided. This appends a govDAO proposal tx (MsgRun) at the end of +# appState.Txs that resets r/sys/validators/v2 to match the new +# GenesisDoc.Validators — reconciling the in-gno side with the tm2 side. +if [[ -n "${PV_KEY:-}" ]]; then + MIG_JSONL="$SCRIPT_DIR/migrations/migrations.jsonl" + printf "Building migrations (PV_KEY=%s)...\n" "$PV_KEY" + CALLER="${CALLER:-g1manfred47kzduec920z88wfr64ylksmdcedlf5}" \ + PV_KEY="$PV_KEY" \ + OUT_JSONL="$MIG_JSONL" \ + CHAIN_ID="$CHAIN_ID" \ + REPO_ROOT="$REPO_ROOT" \ + "$SCRIPT_DIR/migrations/build.sh" + CMD_ARGS+=(--migration-tx "$MIG_JSONL") +fi + +"$BIN" "${CMD_ARGS[@]}" + +# Print sha256 for cross-validator coordination. +if [[ -f "$OUTPUT" ]]; then + if command -v sha256sum >/dev/null 2>&1; then + SHA256=$(sha256sum "$OUTPUT" | cut -d' ' -f1) + elif command -v shasum >/dev/null 2>&1; then + SHA256=$(shasum -a 256 "$OUTPUT" | cut -d' ' -f1) + fi + printf "\nsha256: %s\n" "${SHA256:-}" +fi diff --git a/misc/deployments/gnoland-1/govdao-scripts/add-validator-from-valopers.sh b/misc/deployments/gnoland-1/govdao-scripts/add-validator-from-valopers.sh new file mode 100755 index 00000000000..31928940ece --- /dev/null +++ b/misc/deployments/gnoland-1/govdao-scripts/add-validator-from-valopers.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Add a validator from the r/gnops/valopers registry via govDAO proposal. +# +# Uses r/gnops/valopers/proposal.NewValidatorProposalRequest to look up the +# valoper profile on-chain and create a governance proposal, then votes YES +# and executes it immediately. +# +# Usage: +# ./add-validator-from-valopers.sh
+# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: gnoland-1) +# REMOTE - RPC endpoint (default: https://rpc.betanet.testnets.gno.land:443) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland-1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +if [ $# -lt 1 ]; then + echo "Usage: $0
" + echo "" + echo "Looks up the valoper profile from r/gnops/valopers and creates a" + echo "govDAO proposal to add them to the validator set, votes YES, and" + echo "executes it." + echo "" + echo "The valoper must have registered at r/gnops/valopers first." + echo "" + echo "Example:" + echo " $0 g1abc...xyz" + exit 1 +fi + +ADDR="$1" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/add_from_valopers.gno" < [voting_power] +# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: gnoland-1) +# REMOTE - RPC endpoint (default: https://rpc.betanet.testnets.gno.land:443) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland-1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +if [ $# -lt 2 ]; then + echo "Usage: $0
[voting_power]" + echo "" + echo "Example:" + echo " $0 g1abc...xyz gpub1pggj7... 1" + exit 1 +fi + +ADDR="$1" +PUB_KEY="$2" +POWER="${3:-1}" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/add_validator.gno" < +# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: gnoland-1) +# REMOTE - RPC endpoint (default: https://rpc.betanet.testnets.gno.land:443) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland-1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +if [ $# -lt 1 ]; then + echo "Usage: $0
" + echo "" + echo "Example:" + echo " $0 g1abc...xyz" + exit 1 +fi + +ADDR="$1" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/rm_validator.gno" <&2 + exit 1 +fi + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland-1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +# Build address list for the Gno code. +ADDR_ARGS="" +for addr in "$@"; do + ADDR_ARGS="${ADDR_ARGS} address(\"${addr}\"), +" +done + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/unrestrict.gno" < +# app_state.txs: full tx array with metadata preserved +# - Height AND timestamp are preserved (Jae's correctness requirement). +# - Validators independently run this script and compare genesis SHA-256 +# before restarting. +# +# Usage: +# ./migrate-from-gnoland1.sh --data-dir [--halt-height N] +# +# The script writes genesis.json (gnoland-1) to the current directory. +# +# Prerequisites: +# - gnoland1 must be fully halted at the governance-approved halt height +# - tx-archive (with genesis-assemble subcommand) must be installed +# See: https://github.com/gnolang/tx-archive +# - The new gnoland binary (>= gnoland-1) must be in PATH +# +# Dependencies (PRs that must be merged in the new binary): +# - #5334: halt_height config field (MERGED) +# - #5293: namereg GovDAO whitelist (MERGED) +# - #5375: new govdao-scripts (MERGED) +# - #5368: GovDAO-based halt height via r/sys/params (awaiting merge) +# - #5411: genesis replay with OriginalChainID (awaiting merge) +# - #5390: GnoTxMetadata block_height + chain_id extension (awaiting merge) +set -eo pipefail + +# ============================================================================= +# TODO: IMPLEMENT THIS MIGRATION SCRIPT +# +# This is the critical piece that makes the gnoland1 → gnoland-1 hard fork +# possible. Until it is written AND dry-run on test12, the hard fork CANNOT happen. +# +# The decisions below have been made (as of 2026-04-09): +# ✅ Scenario A chosen: genesis tx-replay with InitialHeight preservation +# ✅ Scenario B (height reset) deprioritized +# ✅ Chain ID: gnoland1 → gnoland-1 (one-time rename, agreed) +# ✅ Height + timestamp preservation required (Jae's correctness requirement) +# ✅ #5334, #5293, #5375 merged +# ⏳ #5368 (GovDAO halt) awaiting reviews +# ⏳ #5411 (genesis replay), #5390 (tx metadata) awaiting merge +# ⏳ #5377 (--migrate flag) may be superseded by #5411 approach +# +# Implementation steps: +# +# 1. HALT VERIFICATION +# Check the data dir to confirm gnoland1 stopped at the expected height. +# Read the committed block height from the blockstore and compare against +# the halt_height that was voted on via GovDAO (#5368). +# +# 2. TX EXPORT +# Run tx-archive backup against the halted gnoland1 data dir to produce +# a JSONL file with all successful txs, each including: +# - timestamp: Unix seconds of the block that included the tx +# - block_height: block number the tx ran at (#5390) +# - chain_id: "gnoland1" (#5390) +# Command (once tx-archive supports --data-dir mode): +# tx-archive backup \ +# --data-dir "$DATA_DIR" \ +# --output txs.jsonl +# +# 3. GENESIS ASSEMBLY +# Run tx-archive genesis-assemble to produce genesis.json: +# tx-archive genesis-assemble \ +# --input txs.jsonl \ +# --chain-id gnoland-1 \ +# --original-chain-id gnoland1 \ +# --initial-height $((HALT_HEIGHT + 1)) \ +# --output genesis.json +# The genesis will have: +# chain_id: "gnoland-1" +# initial_height: halt_height + 1 (Jae's InitialHeight tm2 port required) +# app_state.txs: full tx array with metadata +# app_state.original_chain_id: "gnoland1" (for sig verification) +# +# 4. VERIFICATION +# All validators independently run this script and compare: +# sha256sum genesis.json +# Hashes MUST match before anyone restarts. +# Optionally run gnoupgrade statediff for before/after comparison. +# +# 5. RESTART COORDINATION +# Validators restart with the new binary and the new genesis.json. +# The new binary must be >= chain/gnoland-1 tag. +# +# BLOCKERS (must be resolved before implementing): +# [ ] tx-archive genesis-assemble command (companion to #5411) +# [ ] tx-archive --data-dir mode for offline export from block store +# [ ] Jae's tm2 GenesisDoc.InitialHeight field (hard blocker for #5411) +# [ ] test12 dry-run: validate full halt → export → genesis-assemble → restart +# +# RELATED: +# Issue #5374: chain upgrade strategy meta-issue +# PR #5411: genesis replay mechanism (main hardfork PR) +# PR #5390: GnoTxMetadata block_height + chain_id extension +# PR #5368: GovDAO-based halt height via r/sys/params +# PR #5377: in-place block-replay --migrate flag (may complement #5411) +# PR #5369: gnoupgrade toolkit (replay/statediff/healthcheck) +# ============================================================================= + +echo "ERROR: migrate-from-gnoland1.sh is not yet implemented." +echo "" +echo "Blockers:" +echo " - tx-archive genesis-assemble command (companion to gnolang/gno#5411)" +echo " - Jae's tm2 GenesisDoc.InitialHeight field" +echo " - test12 dry-run of the full halt → export → genesis-assemble → restart flow" +echo "" +echo "Track progress at: https://github.com/gnolang/gno/issues/5374" +exit 1 diff --git a/misc/deployments/gnoland-1/migrations/01_reset_valset.gno.tmpl b/misc/deployments/gnoland-1/migrations/01_reset_valset.gno.tmpl new file mode 100644 index 00000000000..cfbaa8b0f03 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/01_reset_valset.gno.tmpl @@ -0,0 +1,44 @@ +// 01_reset_valset.gno.tmpl — MsgRun body that runs AT THE END of the +// hardfork replay (post-history) to update r/sys/validators/v2 so the +// in-gno valset matches the new GenesisDoc.Validators. +// +// Why: tm2 consensus uses GenesisDoc.Validators (which `gnogenesis fork` +// rewrites in the output genesis), but r/sys/validators/v2 still holds +// the ORIGINAL gnoland1 valset from govdao_prop1. Without this migration, +// queries against the realm return stale data and any govDAO proposal +// that touches the valset via the realm would be working against a ghost +// of the pre-fork world. +// +// Placeholders (replaced by migrations/build.sh before signing): +// OLD_VALIDATORS_GO placeholder — zero-voting-power entries for each pre-fork validator +// NEW_VALIDATORS_GO placeholder — full entries for the post-fork validator(s) +// +// Caller: a govDAO T1 member (see build.sh CALLER env). Sig verify is +// skipped at genesis-replay via --skip-genesis-sig-verification. +package main + +import ( + "gno.land/p/sys/validators" + "gno.land/r/gov/dao" + valr "gno.land/r/sys/validators/v2" +) + +func main() { + req := valr.NewPropRequest( + func() []validators.Validator { + return []validators.Validator{ + // ---- remove pre-fork validators (voting_power=0 = remove) ---- + {{OLD_VALIDATORS_GO}} + // ---- add post-fork validators ---- + {{NEW_VALIDATORS_GO}} + } + }, + "Hardfork: reset validator set", + "Reconcile r/sys/validators/v2 with the new GenesisDoc.Validators "+ + "after the gnoland1 → gnoland-1 hardfork.", + ) + + id := dao.MustCreateProposal(cross, req) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: id}) + dao.ExecuteProposal(cross, id) +} diff --git a/misc/deployments/gnoland-1/migrations/02_add_t1_member.gno.tmpl b/misc/deployments/gnoland-1/migrations/02_add_t1_member.gno.tmpl new file mode 100644 index 00000000000..fb63cf532a9 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/02_add_t1_member.gno.tmpl @@ -0,0 +1,32 @@ +// 02_add_t1_member.gno.tmpl — run AT THE END of the hardfork replay. +// +// Step 1 of the T1 rotation: the sole pre-fork T1 member (manfred) invites +// NEW_T1_ADDR into T1 via a govDAO proposal. Because manfred is the only +// T1 at this point, his single YES vote == 100% supermajority and the +// proposal executes immediately. +// +// Placeholders (replaced by migrations/build.sh before signing): +// NEW_T1_ADDR placeholder — new T1 member's bech32 address +// PORTFOLIO placeholder — human-readable justification (required) +// +// Caller: sole T1 member (manfred) — patched into msg[0].caller by build.sh. +package main + +import ( + "gno.land/r/gov/dao" + "gno.land/r/gov/dao/v3/impl" + "gno.land/r/gov/dao/v3/memberstore" +) + +func main() { + req := impl.NewAddMemberRequest( + cross, + address("{{NEW_T1_ADDR}}"), + memberstore.T1, + `{{PORTFOLIO}}`, + ) + + id := dao.MustCreateProposal(cross, req) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: id}) + dao.ExecuteProposal(cross, id) +} diff --git a/misc/deployments/gnoland-1/migrations/03_withdraw_manfred_propose.gno.tmpl b/misc/deployments/gnoland-1/migrations/03_withdraw_manfred_propose.gno.tmpl new file mode 100644 index 00000000000..c76e5ce4ae8 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/03_withdraw_manfred_propose.gno.tmpl @@ -0,0 +1,33 @@ +// 03_withdraw_manfred_propose.gno.tmpl — run AT THE END of the hardfork replay. +// +// Step 2 of the T1 rotation: manfred proposes his own withdrawal from T1 and +// votes YES. The memberstore now has two T1 members (manfred + NEW_T1_ADDR), +// so manfred's single vote is only 50% — below the 66.66% supermajority, +// so the proposal stays OPEN at the end of this tx. +// +// Step 3 (04_withdraw_manfred_execute.gno.tmpl) is run in a separate tx by +// NEW_T1_ADDR to cast the second YES vote and execute. +// +// Placeholders (replaced by migrations/build.sh before signing): +// OLD_T1_ADDR placeholder — address being withdrawn (manfred) +// WITHDRAW_REASON placeholder — required reason string for T1 removals +// +// Caller: manfred (OLD_T1_ADDR) — patched into msg[0].caller by build.sh. +package main + +import ( + "gno.land/r/gov/dao" + "gno.land/r/gov/dao/v3/impl" +) + +func main() { + req := impl.NewWithdrawMemberRequest( + cross, + address("{{OLD_T1_ADDR}}"), + `{{WITHDRAW_REASON}}`, + ) + + id := dao.MustCreateProposal(cross, req) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: id}) + // No ExecuteProposal here: supermajority not reached yet (50%). +} diff --git a/misc/deployments/gnoland-1/migrations/04_withdraw_manfred_execute.gno.tmpl b/misc/deployments/gnoland-1/migrations/04_withdraw_manfred_execute.gno.tmpl new file mode 100644 index 00000000000..05d265de2c9 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/04_withdraw_manfred_execute.gno.tmpl @@ -0,0 +1,61 @@ +// 04_withdraw_manfred_execute.gno.tmpl — run AT THE END of the hardfork replay. +// +// Step 3 (final) of the T1 rotation: NEW_T1_ADDR votes YES on the open +// WithdrawMember proposal created in step 2 and executes it. With both T1 +// members voting YES, supermajority is 100% and manfred is removed. +// +// Finding the pid: +// The proposal was created by the PREVIOUS migration tx (signed as +// manfred). It's the highest-pid proposal whose author == OLD_T1_ADDR +// and title == "Member Withdrawal Proposal". We iterate from pid=0 +// upward to find it — cheap, since the chain has only a handful of +// proposals at genesis time. +// +// Placeholders (replaced by migrations/build.sh before signing): +// OLD_T1_ADDR placeholder — address being withdrawn (manfred) +// +// Caller: NEW_T1_ADDR (the freshly-added T1 member) — patched into +// msg[0].caller by build.sh. +package main + +import ( + "gno.land/r/gov/dao" +) + +const ( + oldT1Addr = address("{{OLD_T1_ADDR}}") + withdrawalTitle = "Member Withdrawal Proposal" +) + +func main() { + pid, ok := findWithdrawProposal() + if !ok { + panic("could not locate the WithdrawMember proposal authored by " + oldT1Addr.String()) + } + + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: pid}) + dao.ExecuteProposal(cross, pid) +} + +// findWithdrawProposal walks the proposal tree and returns the highest-pid +// proposal whose author matches OLD_T1_ADDR and whose title matches the +// WithdrawMember one. Stops on the first GetProposal error (= pid not +// allocated yet). +func findWithdrawProposal() (dao.ProposalID, bool) { + var ( + found bool + best dao.ProposalID + ) + for i := int64(0); ; i++ { + pid := dao.ProposalID(i) + p, err := dao.GetProposal(cross, pid) + if err != nil || p == nil { + break + } + if p.Author() == oldT1Addr && p.Title() == withdrawalTitle { + best = pid + found = true + } + } + return best, found +} diff --git a/misc/deployments/gnoland-1/migrations/05_disable_sysnames.gno.tmpl b/misc/deployments/gnoland-1/migrations/05_disable_sysnames.gno.tmpl new file mode 100644 index 00000000000..4cc22d2d001 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/05_disable_sysnames.gno.tmpl @@ -0,0 +1,27 @@ +// 05_disable_sysnames.gno.tmpl — MsgRun body that temporarily disables the +// r/sys/names namespace-permission check via a govDAO proposal. +// +// Why: gnoland1's r/sys/names.enabled is `true` at halt height, which means +// only addresses whose bech32 matches a namespace can deploy under that +// namespace. The `sys` namespace can never match any real address, so +// step 06 (addpkg r/sys/validators/v3) would fail with "unauthorized user". +// Setting the VM param `vm:p:sysnames_pkgpath` to "" makes +// checkNamespacePermission skip the check (keeper.go: sysNamesPkg == "" → +// return nil). Step 07 restores the path so the check is re-enabled. +// +// Caller: current govDAO sole T1 (manfred pre-rotation, or $T1_CALLER +// post-rotation). Sig verify is skipped at genesis-replay via +// --skip-genesis-sig-verification. +package main + +import ( + "gno.land/r/gov/dao" + "gno.land/r/sys/params" +) + +func main() { + r := params.NewSysParamStringPropRequest("vm", "p", "sysnames_pkgpath", "") + id := dao.MustCreateProposal(cross, r) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: id}) + dao.ExecuteProposal(cross, id) +} diff --git a/misc/deployments/gnoland-1/migrations/07_restore_sysnames.gno.tmpl b/misc/deployments/gnoland-1/migrations/07_restore_sysnames.gno.tmpl new file mode 100644 index 00000000000..3aa13692b3d --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/07_restore_sysnames.gno.tmpl @@ -0,0 +1,21 @@ +// 07_restore_sysnames.gno.tmpl — MsgRun body that re-enables the r/sys/names +// namespace-permission check after step 06 (addpkg r/sys/validators/v3). +// +// Pairs with 05_disable_sysnames: after v3 is deployed under r/sys/*, we +// restore the default `vm:p:sysnames_pkgpath = "gno.land/r/sys/names"` so +// subsequent addpkgs continue to enforce namespace authz. +// +// Caller: current govDAO sole T1 (same as step 05). +package main + +import ( + "gno.land/r/gov/dao" + "gno.land/r/sys/params" +) + +func main() { + r := params.NewSysParamStringPropRequest("vm", "p", "sysnames_pkgpath", "gno.land/r/sys/names") + id := dao.MustCreateProposal(cross, r) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: id}) + dao.ExecuteProposal(cross, id) +} diff --git a/misc/deployments/gnoland-1/migrations/08_set_valset_realm.gno.tmpl b/misc/deployments/gnoland-1/migrations/08_set_valset_realm.gno.tmpl new file mode 100644 index 00000000000..02a30907e27 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/08_set_valset_realm.gno.tmpl @@ -0,0 +1,34 @@ +// 08_set_valset_realm.gno.tmpl — MsgRun body that sets the VM param +// `vm:p:valset_realm_path` to the v3 realm path. +// +// Why: the mainnet chain state was initialized before v3 existed, so the +// ValsetRealmPath field on vm.Params isn't populated — queries return "". +// EndBlocker reads this param to know which realm owns valset state: +// +// valsetRealm := vm.ValsetRealmDefault // "gno.land/r/sys/validators/v3" +// prmk.GetString(ctx, vm.ValsetRealmParamPath, &valsetRealm) +// +// GetString overwrites the default with whatever is stored, including "". +// Without this step, EndBlocker reads valsetRealm="" and checks params at +// `vm::new_updates_available` instead of `vm:gno.land/r/sys/validators/v3:*`, +// so updates written by r/sys/validators/v3 never propagate to tm2 consensus. +// +// Pairs with step 06 (addpkg r/sys/validators/v3). Must run after step 07 +// restores the namespace check, since the govDAO proposal itself doesn't need +// namespace authz but any later r/sys/* changes should go through the check. +// +// Caller: current govDAO sole T1 ($T1_CALLER — manfred pre-rotation, or +// NEW_T1_ADDR after step 04's rotation). +package main + +import ( + "gno.land/r/gov/dao" + "gno.land/r/sys/params" +) + +func main() { + r := params.NewSysParamStringPropRequest("vm", "p", "valset_realm_path", "gno.land/r/sys/validators/v3") + id := dao.MustCreateProposal(cross, r) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: id}) + dao.ExecuteProposal(cross, id) +} diff --git a/misc/deployments/gnoland-1/migrations/build.sh b/misc/deployments/gnoland-1/migrations/build.sh new file mode 100755 index 00000000000..e36bfab4571 --- /dev/null +++ b/misc/deployments/gnoland-1/migrations/build.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +# Synthesise the gnoland-1 hardfork migration jsonl from templates. +# +# Output: $OUT_JSONL (one amino-JSON TxWithMetadata per line). +# Passed to `gnogenesis fork generate --migration-tx $OUT_JSONL`. +# +# What it does +# ============ +# 1. Valset reset — fills 01_reset_valset.gno.tmpl with: +# - OLD_VALIDATORS_GO = voting_power=0 entries for all INITIAL_VALSET +# entries of gnoland1 (removes them from +# r/sys/validators/v2) +# - NEW_VALIDATORS_GO = the single post-fork validator described by +# $NEW_VALSET_JSON (produced by hf-glue +# init-node.sh, or by a manual list for a +# coordinated hardfork) +# Signed by $CALLER (sole T1, manfred). +# +# 2. T1 rotation (optional, enabled when $NEW_T1_ADDR is set). 3 additional +# txs in the jsonl: +# - 02 AddMember(NEW_T1_ADDR, T1) — manfred proposes, votes, executes +# (100% supermajority as sole T1) +# - 03 WithdrawMember(manfred) — manfred proposes + votes YES +# (50% of 2 T1s, not executed yet) +# - 04 (caller=NEW_T1_ADDR) finds the open Withdraw proposal, votes YES, +# executes (100% with both voting YES) +# +# 3. Wraps each rendered .gno body in a MsgRun tx signed by a local ephemeral +# key; the tx's `caller` field is patched to the appropriate T1 member so +# --skip-genesis-sig-verification at replay uses that as OriginCaller. +# 4. Emits one `{tx: {...}}` per migration into $OUT_JSONL. +# +# Env +# === +# CALLER govDAO T1 member address for the valset-reset + add-member +# + withdraw-propose txs (required) +# default: g1manfred47kzduec920z88wfr64ylksmdcedlf5 +# RPC_URL source-chain RPC (required). Queried once via +# abci_query vm/qeval to derive OLD_ADDRS from the +# *current* r/sys/validators/v2 state, so the valset +# reset only attempts to remove validators that actually +# exist at fork time. When unset, falls back to the +# hardcoded gnoland1 INITIAL_VALSET below (may be stale +# — will panic at replay if the source chain removed any +# of them via historical govDAO proposals). +# default: (unset — uses hardcoded fallback) +# NEW_T1_ADDR address to install as the sole T1 member of the +# post-fork govDAO. When set, three extra migration txs +# are appended (see 2. above). When empty, only the +# valset reset is emitted. +# T1_PORTFOLIO human-readable portfolio/justification attached to the +# AddMember proposal (required when NEW_T1_ADDR is set). +# default: "post-hardfork T1 rotation" +# T1_WITHDRAW_REASON reason string attached to the WithdrawMember proposal +# (r/gov/dao requires this for T1 removals). +# default: "replaced by NEW_T1_ADDR as part of hardfork" +# NEW_VALSET_JSON path to JSON with new validators, format: +# [{"address": "g1...", "pub_key": "gpub1...", +# "voting_power": 10, "name": "hf-local"}, ...] +# default: synthesised from a priv_validator_key.json if +# $PV_KEY is set. +# PV_KEY alternate: path to a priv_validator_key.json; if set +# and $NEW_VALSET_JSON is empty, a single-validator set +# is derived from it (power=10, name=hf-local). +# OUT_JSONL output path (default: ./migrations.jsonl) +# GNOKEY_BIN gnokey binary (auto-built if missing) +# REPO_ROOT repo root (auto-detected) +# +# Example (valset-only) +# ===================== +# CALLER=g1manfred47kzduec920z88wfr64ylksmdcedlf5 \ +# PV_KEY=/path/to/priv_validator_key.json \ +# ./build.sh +# +# Example (valset + T1 rotation) +# ============================== +# CALLER=g1manfred47kzduec920z88wfr64ylksmdcedlf5 \ +# PV_KEY=/path/to/priv_validator_key.json \ +# NEW_T1_ADDR=g1yournewcontrolleraddresshere \ +# T1_PORTFOLIO="Core dev, handing over from moul" \ +# ./build.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}" +OUT_JSONL="${OUT_JSONL:-$SCRIPT_DIR/migrations.jsonl}" +CALLER="${CALLER:-g1manfred47kzduec920z88wfr64ylksmdcedlf5}" +CHAIN_ID="${CHAIN_ID:-gnoland-1}" + +NEW_T1_ADDR="${NEW_T1_ADDR:-}" +T1_PORTFOLIO="${T1_PORTFOLIO:-post-hardfork T1 rotation}" +T1_WITHDRAW_REASON="${T1_WITHDRAW_REASON:-replaced by NEW_T1_ADDR as part of hardfork}" + +# Hardcoded fallback: gnoland1 INITIAL_VALSET (mirrors misc/deployments/ +# gnoland1/gen-genesis.sh). Used only when $RPC_URL is unset. Likely stale at +# any real fork point because govDAO proposals may have added/removed valset +# entries post-genesis — prefer RPC-derived OLD_ADDRS whenever possible. +FALLBACK_OLD_ADDRS=( + "g1vta7dwp4guuhkfzksenfcheky4xf9hue8mgne4" + "g1d5hh9fw3l00gugfzafskaxqlmsyvxfaj6l2q60" + "g1uhv7wr7nku89se3t7v8fpquc7n5sf8rfkywxpc" + "g10jdd8vlgydfypynrk23ul90jnsg5twrtvmcmh4" + "g1eueypc9w524ctda3y0kwd4jruw5p4zqpjna0jq" + "g1kn7p0wqumvqlcqzhkwnavkhf0z4qnr73ltwsae" + "g10j90aqjv6uju3dksq8m08s6u47x59glkdxqzm2" +) + +# ---- resolve OLD_ADDRS from live r/sys/validators/v2, or fall back ---- +# abci_query vm/qeval returns the amino-printed slice of +# gno.land/p/sys/validators.Validator values. We don't parse the full amino +# pretty-print — we just regex-extract every bech32 g1 address from the +# decoded payload. This is safe because the bech32 data-charset for gpub1 +# pubkeys excludes the digit `1`, so the sequence `g1` only occurs at the +# start of address values, never inside a pubkey. +query_current_valset_addrs() { + local rpc="$1" + local data_b64 resp data + data_b64=$(printf '%s' 'gno.land/r/sys/validators/v2.GetValidators()' | openssl base64 -A) + resp=$(curl -fsS -X POST -H 'Content-Type: application/json' \ + -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"abci_query\",\"params\":{\"path\":\"vm/qeval\",\"data\":\"$data_b64\"}}" \ + "$rpc") || return 1 + data=$(jq -r '.result.response.ResponseBase.Data // empty' <<<"$resp") + [[ -n "$data" ]] || return 1 + printf '%s' "$data" | openssl base64 -d -A | grep -oE 'g1[0-9a-z]{38}' +} + +OLD_ADDRS=() +if [[ -n "${RPC_URL:-}" ]]; then + while IFS= read -r addr; do + [[ -n "$addr" ]] && OLD_ADDRS+=("$addr") + done < <(query_current_valset_addrs "$RPC_URL") + if [[ ${#OLD_ADDRS[@]} -eq 0 ]]; then + echo "ERROR: failed to derive OLD_ADDRS from RPC $RPC_URL (empty response or no g1 addresses)" >&2 + exit 1 + fi + echo " valset source: RPC $RPC_URL (${#OLD_ADDRS[@]} validator(s))" +else + OLD_ADDRS=("${FALLBACK_OLD_ADDRS[@]}") + echo " valset source: FALLBACK (hardcoded INITIAL_VALSET, ${#OLD_ADDRS[@]} validator(s))" + echo " WARNING: no RPC_URL set — valset reset may panic if source chain removed any of these." >&2 +fi + +# ---- resolve GNOKEY_BIN ---- +if [[ -z "${GNOKEY_BIN:-}" ]]; then + if command -v gnokey >/dev/null 2>&1; then + GNOKEY_BIN="gnokey" + else + GNOKEY_BIN="$REPO_ROOT/contribs/gnogenesis/genesis-work/bin/gnokey" + [[ -x "$GNOKEY_BIN" ]] || { + mkdir -p "$(dirname "$GNOKEY_BIN")" + go build -C "$REPO_ROOT/gno.land/cmd/gnokey" -o "$GNOKEY_BIN" . + } + fi +fi + +# ---- assemble NEW_VALSET_JSON from PV_KEY if needed ---- +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +if [[ -z "${NEW_VALSET_JSON:-}" ]]; then + : "${PV_KEY:?either NEW_VALSET_JSON or PV_KEY is required}" + # r/sys/validators/v2 wants the bech32 (gpub1...) pubkey — priv_validator_key.json + # stores the raw base64 under pub_key.value. Use `gnoland secrets get` to convert. + SECRETS_DIR="$(dirname "$PV_KEY")" + BECH_PUBKEY="$(go run -C "$REPO_ROOT" ./gno.land/cmd/gnoland secrets get validator_key.pub_key --raw -data-dir "$SECRETS_DIR" | tail -n 1 | tr -d '[:space:]')" + [[ "$BECH_PUBKEY" == gpub1* ]] || { + echo "ERROR: failed to derive bech32 pubkey from $PV_KEY (got: $BECH_PUBKEY)" >&2 + exit 1 + } + ADDR="$(jq -r '.address' "$PV_KEY")" + NEW_VALSET_JSON="$WORK/new_valset.json" + jq -n --arg addr "$ADDR" --arg pub "$BECH_PUBKEY" '[{ + address: $addr, + pub_key: $pub, + voting_power: 10, + name: "hf-local" + }]' >"$NEW_VALSET_JSON" +fi + +# ---- set up ephemeral signing key (used for all migration txs) ---- +GK_HOME="$WORK/gnokey-home" +mkdir -p "$GK_HOME" +EPHEMERAL_MNEMONIC="source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" +# stdin order for `gnokey add --recover --insecure-password-stdin`: +# 1. mnemonic +# 2. passphrase (empty line = no passphrase, skips confirm prompt) +printf '%s\n\n' "$EPHEMERAL_MNEMONIC" | + "$GNOKEY_BIN" add --recover --insecure-password-stdin --home "$GK_HOME" ephemeral >/dev/null + +# ---- helper: render .gno file from template, wrap into a signed tx, patch caller ---- +# Args: +# Prints the resulting {tx: {...}} line on stdout. +render_tx() { + local gno_path="$1" caller="$2" + local tx_json="$WORK/$(basename "$gno_path" .gno).tx.json" + + "$GNOKEY_BIN" maketx run \ + --gas-wanted 100000000 \ + --gas-fee 1ugnot \ + --chainid "$CHAIN_ID" \ + --home "$GK_HOME" \ + ephemeral \ + "$gno_path" >"$tx_json" + + # Patch the caller field so the MsgRun executes as $caller (not ephemeral). + jq --arg caller "$caller" '.msg[0].caller = $caller' "$tx_json" >"$tx_json.patched" + mv "$tx_json.patched" "$tx_json" + + # Sign (bogus sig, skipped at replay — but the tx format requires one). + echo "" | "$GNOKEY_BIN" sign \ + --tx-path "$tx_json" \ + --chainid "$CHAIN_ID" \ + --account-number 0 \ + --account-sequence 0 \ + --home "$GK_HOME" \ + --insecure-password-stdin \ + ephemeral >/dev/null + + # Wrap as {tx: {...}} — TxWithMetadata accepts this with empty metadata; + # BlockHeight is forced to 0 by gnogenesis readMigrationTxs. + jq -c '{tx: .}' "$tx_json" +} + +# ---- helper: render a template with placeholder=value pairs ---- +# Args: [ ...] +render_template() { + local tmpl="$1" out="$2" + shift 2 + cp "$tmpl" "$out" + local spec name val + for spec in "$@"; do + name="${spec%%=*}" + val="${spec#*=}" + # awk-based substitution (BSD sed can't reliably handle newlines + special chars in replacement). + PH_NAME="$name" PH_VAL="$val" awk ' + BEGIN { name = ENVIRON["PH_NAME"]; val = ENVIRON["PH_VAL"] } + { gsub("\\{\\{" name "\\}\\}", val); print } + ' "$out" >"$out.tmp" + mv "$out.tmp" "$out" + done +} + +# ---- 1. valset reset tx (caller=manfred) ---- +OLD_GO="" +for a in "${OLD_ADDRS[@]}"; do + OLD_GO+="{Address: \"$a\", VotingPower: 0},"$'\n\t\t\t\t' +done + +NEW_GO=$(jq -r '.[] | "{Address: \"\(.address)\", PubKey: \"\(.pub_key)\", VotingPower: \(.voting_power)},"' "$NEW_VALSET_JSON" | awk 'BEGIN{ORS="\n\t\t\t\t"}{print}') + +RENDERED_01="$WORK/01_reset_valset.gno" +render_template "$SCRIPT_DIR/01_reset_valset.gno.tmpl" "$RENDERED_01" \ + "OLD_VALIDATORS_GO=$OLD_GO" "NEW_VALIDATORS_GO=$NEW_GO" + +: >"$OUT_JSONL" +render_tx "$RENDERED_01" "$CALLER" >>"$OUT_JSONL" +printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_01")" "$CALLER" + +# ---- 2-4. T1 rotation (optional) ---- +if [[ -n "$NEW_T1_ADDR" ]]; then + # Basic sanity checks on NEW_T1_ADDR. + [[ "$NEW_T1_ADDR" =~ ^g1[0-9a-z]{38}$ ]] || { + echo "ERROR: NEW_T1_ADDR does not look like a valid bech32 address: $NEW_T1_ADDR" >&2 + exit 1 + } + [[ "$NEW_T1_ADDR" != "$CALLER" ]] || { + echo "ERROR: NEW_T1_ADDR must differ from CALLER (no-op rotation)" >&2 + exit 1 + } + + # 02 — manfred adds NEW_T1_ADDR as T1 (100% supermajority, passes). + RENDERED_02="$WORK/02_add_t1_member.gno" + render_template "$SCRIPT_DIR/02_add_t1_member.gno.tmpl" "$RENDERED_02" \ + "NEW_T1_ADDR=$NEW_T1_ADDR" "PORTFOLIO=$T1_PORTFOLIO" + render_tx "$RENDERED_02" "$CALLER" >>"$OUT_JSONL" + printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_02")" "$CALLER" + + # 03 — manfred proposes his own withdrawal + votes YES (50%, not executed). + RENDERED_03="$WORK/03_withdraw_manfred_propose.gno" + render_template "$SCRIPT_DIR/03_withdraw_manfred_propose.gno.tmpl" "$RENDERED_03" \ + "OLD_T1_ADDR=$CALLER" "WITHDRAW_REASON=$T1_WITHDRAW_REASON" + render_tx "$RENDERED_03" "$CALLER" >>"$OUT_JSONL" + printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_03")" "$CALLER" + + # 04 — NEW_T1_ADDR votes YES on the open withdraw prop + executes. + RENDERED_04="$WORK/04_withdraw_manfred_execute.gno" + render_template "$SCRIPT_DIR/04_withdraw_manfred_execute.gno.tmpl" "$RENDERED_04" \ + "OLD_T1_ADDR=$CALLER" + render_tx "$RENDERED_04" "$NEW_T1_ADDR" >>"$OUT_JSONL" + printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_04")" "$NEW_T1_ADDR" +fi + +# ---- 5-7. deploy r/sys/validators/v3 ---- +# v3 introduces the params-keeper-driven valset flow (see PR #5485). Mainnet +# never had it, so a fresh addpkg is needed post-fork. gnoland1's r/sys/names +# namespace check is enabled at halt height, so a direct addpkg under +# r/sys/* returns "unauthorized user". Strategy: wrap the addpkg with a +# temporary VM-param flip — steps 05/07 disable/restore the namespace check +# via govDAO proposals (the only authorized path to set vm:p:sysnames_pkgpath +# is `r/sys/params.NewSysParamStringPropRequest`). +V3_PKGDIR="${V3_PKGDIR:-$REPO_ROOT/examples/gno.land/r/sys/validators/v3}" +[[ -d "$V3_PKGDIR" ]] || { + echo "ERROR: v3 pkgdir not found: $V3_PKGDIR" >&2 + exit 1 +} + +# Current sole T1 member at this point in the migration sequence. If T1 +# rotation ran (steps 02-04), manfred is no longer T1; NEW_T1_ADDR is. The +# govDAO proposals in 05/07 need supermajority from the current T1. +if [[ -n "$NEW_T1_ADDR" ]]; then + T1_CALLER="$NEW_T1_ADDR" +else + T1_CALLER="$CALLER" +fi + +# 05 — disable namespace check (govDAO proposal, caller=T1_CALLER). +RENDERED_05="$WORK/05_disable_sysnames.gno" +cp "$SCRIPT_DIR/05_disable_sysnames.gno.tmpl" "$RENDERED_05" +render_tx "$RENDERED_05" "$T1_CALLER" >>"$OUT_JSONL" +printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_05")" "$T1_CALLER" + +# 06 — addpkg r/sys/validators/v3 (MsgAddPackage, creator=manfred; sig-skip +# applies since this is a genesis-mode migration tx). +RENDERED_06="$WORK/06_addpkg_validators_v3.tx.json" +"$GNOKEY_BIN" maketx addpkg \ + --gas-wanted 100000000 \ + --gas-fee 1ugnot \ + --pkgpath "gno.land/r/sys/validators/v3" \ + --pkgdir "$V3_PKGDIR" \ + --chainid "$CHAIN_ID" \ + --home "$GK_HOME" \ + ephemeral >"$RENDERED_06" + +# MsgAddPackage uses `creator` (not `caller`). Patch to manfred so the +# addpkg runs as him under --skip-genesis-sig-verification. +jq --arg creator "$CALLER" '.msg[0].creator = $creator' "$RENDERED_06" >"$RENDERED_06.patched" +mv "$RENDERED_06.patched" "$RENDERED_06" + +echo "" | "$GNOKEY_BIN" sign \ + --tx-path "$RENDERED_06" \ + --chainid "$CHAIN_ID" \ + --account-number 0 \ + --account-sequence 0 \ + --home "$GK_HOME" \ + --insecure-password-stdin \ + ephemeral >/dev/null + +jq -c '{tx: .}' "$RENDERED_06" >>"$OUT_JSONL" +printf ' migration: %-38s caller=%s\n' "06_addpkg_validators_v3" "$CALLER" + +# 07 — restore namespace check (govDAO proposal, caller=T1_CALLER). +RENDERED_07="$WORK/07_restore_sysnames.gno" +cp "$SCRIPT_DIR/07_restore_sysnames.gno.tmpl" "$RENDERED_07" +render_tx "$RENDERED_07" "$T1_CALLER" >>"$OUT_JSONL" +printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_07")" "$T1_CALLER" + +# 08 — point vm:p:valset_realm_path at the v3 realm (govDAO proposal, +# caller=T1_CALLER). Without this, EndBlocker reads valsetRealm="" from +# pre-v3 mainnet state and never picks up the updates r/sys/validators/v3 +# writes to its params. +RENDERED_08="$WORK/08_set_valset_realm.gno" +cp "$SCRIPT_DIR/08_set_valset_realm.gno.tmpl" "$RENDERED_08" +render_tx "$RENDERED_08" "$T1_CALLER" >>"$OUT_JSONL" +printf ' migration: %-38s caller=%s\n' "$(basename "$RENDERED_08")" "$T1_CALLER" + +printf ' written: %s\n' "$OUT_JSONL" diff --git a/misc/deployments/gnoland1/govdao-scripts/README.md b/misc/deployments/gnoland1/govdao-scripts/README.md index e63c01c5049..88f969de206 100644 --- a/misc/deployments/gnoland1/govdao-scripts/README.md +++ b/misc/deployments/gnoland1/govdao-scripts/README.md @@ -10,4 +10,7 @@ All scripts default to `GNOKEY_NAME=moul`, `CHAIN_ID=gnoland1`, and `REMOTE=http ./rm-validator.sh ADDR # remove a validator ./extend-govdao-t1.sh # add 6 T1 members to govDAO (one-time bootstrap) ./unrestrict-account.sh ADDR [ADDR...] # allow address(es) to transfer ugnot +./restrict-account.sh ADDR [ADDR...] # re-restrict account(s) from transferring ugnot +./set-cla.sh URL # set/update CLA document via govDAO proposal +./set-valoper-minfee.sh AMOUNT # update valoper registration minimum fee ``` diff --git a/misc/deployments/gnoland1/govdao-scripts/restrict-account.sh b/misc/deployments/gnoland1/govdao-scripts/restrict-account.sh new file mode 100755 index 00000000000..7b7528f761b --- /dev/null +++ b/misc/deployments/gnoland1/govdao-scripts/restrict-account.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Re-restrict an account so it can no longer transfer ugnot when bank is locked. +# +# Usage: +# ./restrict-account.sh ADDR [ADDR...] +# +# Example: +# ./restrict-account.sh g1abc...123 +# ./restrict-account.sh g1abc...123 g1def...456 +# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: gnoland1) +# REMOTE - RPC endpoint (default: https://rpc.betanet.testnets.gno.land:443) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +if [ $# -eq 0 ]; then + echo "Usage: $0 ADDR [ADDR...]" >&2 + exit 1 +fi + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +# Build address list for the Gno code. +ADDR_ARGS="" +for addr in "$@"; do + ADDR_ARGS="${ADDR_ARGS} address(\"${addr}\"), +" +done + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/restrict.gno" <&2 + echo " $0 \"\" # disable CLA enforcement" >&2 + exit 1 +fi + +CLA_URL="$1" + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | cut -d' ' -f1 + else + echo "Error: no sha256 tool found (install coreutils or perl)" >&2 + return 1 + fi +} + +if [ -z "$CLA_URL" ]; then + CLA_HASH="" + echo "Disabling CLA enforcement" +else + echo "Fetching CLA from: $CLA_URL" + wget -q -O "$TMPDIR/cla.md" "$CLA_URL" + CLA_HASH=$(sha256_file "$TMPDIR/cla.md") + echo " sha256: $CLA_HASH" +fi + +cat >"$TMPDIR/set_cla.gno" < +# ./set-valoper-minfee.sh 0 # disable registration fee +# ./set-valoper-minfee.sh 20000000 # set to 20 GNOT +# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: gnoland1) +# REMOTE - RPC endpoint (default: https://rpc.betanet.testnets.gno.land:443) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +if [ $# -ne 1 ]; then + echo "Usage: $0 " >&2 + echo " $0 0 # disable registration fee" >&2 + echo " $0 20000000 # set to 20 GNOT" >&2 + exit 1 +fi + +MIN_FEE="$1" + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-gnoland1}" +REMOTE="${REMOTE:-https://rpc.betanet.testnets.gno.land:443}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/set_minfee.gno" < - - - -## [Latest Blogposts](/r/gnoland/blog) - -- [The More You Gno 17](/r/gnoland/blog:p/monthly-dev-17) -- [The More You Gno 16](/r/gnoland/blog:p/monthly-dev-16) -- [Student Contributor Program: Cohort 4 Wrap-up](/r/gnoland/blog:p/scp-cohort-4) -- [Gnomes at EthCC '25](/r/gnoland/blog:p/ethcc2025) - -||| - -## [Latest Events](/r/devrels/events) - -- [Gno Seoul - KBW Edition](https://luma.com/5b4h90tb) -- [GopherCon UK 2025](https://www.gophercon.co.uk/) -- [Web3 Kamp 2025](https://www.web3kamp.org/) -- [EthCC 8](https://ethcc.io/) -- [Welcome to Gno.land @ c-base Berlin](https://lu.ma/3yf84hab) - -||| - -## [Hall of Realms](/r/leon/hor) - -- [r/agherasie/forms](/r/agherasie/forms) -- [r/gfanton/gnomaze](/r/gfanton/gnomaze) -- [r/jjoptimist/home](/r/jjoptimist/home) -- [r/leon/derive](/r/leon/derive) -- [r/leon/home](/r/leon/home) diff --git a/misc/deployments/home-alias/pages/about.md b/misc/deployments/home-alias/pages/about.md new file mode 100644 index 00000000000..9975f4d8056 --- /dev/null +++ b/misc/deployments/home-alias/pages/about.md @@ -0,0 +1,28 @@ +# Gno.land Is A Platform To Write Smart Contracts In Gno + +Gno.land is a next-generation smart contract platform using Gno, an interpreted +version of the general-purpose Go programming language. On Gno.land, smart +contracts can be uploaded on-chain only by publishing their full source code, +making it trivial to verify the contract or fork it into an improved version. +With a system to publish reusable code libraries on-chain, Gno.land serves as +the "GitHub" of the ecosystem, with realms built using fully transparent, +auditable code that anyone can inspect and reuse. + +Gno.land addresses many pressing issues in the blockchain space, starting with +the ease of use and intuitiveness of smart contract platforms. Developers can +write smart contracts without having to learn a new language that's exclusive to +a single ecosystem or limited by design. Go developers can easily port their +existing web apps to Gno.land or build new ones from scratch, making web3 vastly +more accessible. + +Secured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority +consensus mechanism, Gno.land prioritizes fairness and merit, rewarding the +people most active on the platform. PoC restructures the financial incentives +that often corrupt blockchain projects, opting instead to reward contributors +for their work based on expertise, commitment, and alignment. + +One of our inspirations for Gno.land is the gospels, which built a system of +moral code that lasted thousands of years. By observing a minimal production +implementation, Gno.land's design will endure over time and serve as a reference +for future generations with censorship-resistant tools that improve their +understanding of the world. diff --git a/misc/deployments/home-alias/pages/contribute.md b/misc/deployments/home-alias/pages/contribute.md new file mode 100644 index 00000000000..26645a35252 --- /dev/null +++ b/misc/deployments/home-alias/pages/contribute.md @@ -0,0 +1,109 @@ +# Contributor Ecosystem: Call for Contributions + +Gno.land puts at the center of its identity the contributors that help to create +and shape the project into what it is; incentivizing those who contribute the +most and help advance its vision. Eventually, contributions will be incentivized +directly on-chain; in the meantime, this page serves to illustrate our current +off-chain initiatives. + +Gno.land is still in full-steam development. For now, we're looking for the +earliest of adopters; curious to explore a new way to build smart contracts and +eager to make an impact. Joining Gno.land's development now means you can help +to shape the base of its development ecosystem, which will pave the way for the +next generation of blockchain programming. + +As an open-source project, we welcome all contributions. On this page you can +find some pointers on where to get started; as well as some incentives for the +most valuable and important contributions. + +## Where to get started + +If you are interested in contributing to Gno.land, you can jump in on our +[GitHub monorepo](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md) +\- where most development happens. + +### Resources for contributors + +- [Write Gno in the browser](https://play.gno.land) +- [Read about the Gno Language](/gnolang) +- [Visit the official documentation](https://docs.gno.land) +- [Efficient local development for Gno](https://docs.gno.land/builders/local-dev-with-gnodev) +- [Get testnet GNOTs](https://faucet.gno.land) +- [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) + +## Gno Bounties + +> **Note: The Gno Bounty Program is currently paused until further notice.** + +Two reasons prompted this pause: + +- **Focus on Mainnet.** The team is fully focused on the Gno.land Beta + Mainnet launch — the highest priority — as well as the critical post-beta + work required to deliver a complete Mainnet. +- **Decline in submission quality.** The bounty program was created to + encourage innovative, creative, and technical contributions that + meaningfully advance Gno.land. However, with the rise of generative AI, + the overall quality of submissions has declined significantly. A large + portion of entries are clearly AI-generated; others, though less obviously + so, are often inaccurate upon review due to overreliance on AI. Reviewing + and validating these submissions has become increasingly time-consuming, + diverting focus from development work. + +Community contributions remain strongly encouraged, particularly: + +- Tooling and infrastructure projects +- Application-focused development +- Longer-term, high-quality initiatives + +These types of contributions are better aligned with Gno's goals than +short-term bounties, and are very welcome provided they follow the +[contributing guidelines](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md). +Sustained, high-quality contributions will be rewarded over time via the +Gno.land GovDAO. + +## Gno.land Grants + +The Gno.land grants program is to encourage and support the growth of the +Gno.land contributor community, and build out the usability of the platform and +smart contract library. The program provides financial resources to contributors +to explore the Gno tech stack, and build dApps, tooling, infrastructure, +products, and smart contract libraries in Gno.land. + +For more details on Gno.land grants, suggested topics, and how to apply, visit +our grants [repository](https://github.com/gnolang/grants). + +## Join Game of Realms + +Game of Realms is the overarching contributor network of gnomes, currently +running off-chain, and will eventually transition on-chain. At this stage, a +Game of Realms contribution is comprised of high-impact contributions +identified as +['notable contributions'](https://github.com/gnolang/game-of-realms/tree/main/contributors). + +These contributions are not linked to immediate financial rewards, but are +notable in nature, in the sense they are a challenge, make a significant +addition to the project, and require persistence, with minimal feedback loops +from the core team. + +The selection of a notable contribution or the sum of contributions that equal +'notable' is based on the impact it has on the development of the project. For +now, it is focused on code contributions, and will evolve over time. The Gno +development teams will initially qualify and evaluate notable contributions, and +vote off-chain on adding them to the 'notable contributions' folder on GitHub. + +You can always contribute to the project, and all contributions will be noticed. +Contributing now is a way to build your personal contributor profile in Gno.land +early on in the ecosystem, and signal your commitment to the project, the +community, and its future. + +There are a variety of ways to make your contributions count: + +- Core code contributions +- Realm and pure package development +- Validator tooling +- Developer tooling +- Tutorials and documentation + +To start, we recommend you create a PR in the Game of Realms +[repository](https://github.com/gnolang/game-of-realms) to create your profile +page for all your contributions. diff --git a/misc/deployments/home-alias/pages/ecosystem.md b/misc/deployments/home-alias/pages/ecosystem.md new file mode 100644 index 00000000000..b5e5f151005 --- /dev/null +++ b/misc/deployments/home-alias/pages/ecosystem.md @@ -0,0 +1,39 @@ +# Discover gno.land Ecosystem Projects & Initiatives + +### [Gno Playground](https://play.gno.land) + +Gno Playground is a simple web interface that lets you write, test, and +experiment with your Gno code to improve your understanding of the Gno language. +You can share your code, run unit tests, deploy your realms and packages, and +execute functions in your code using the repo. + +Visit the playground at [play.gno.land](https://play.gno.land)! + +### [Gnoscan](https://gnoscan.io) + +Developed by the Onbloc team, Gnoscan is gno.land's blockchain explorer. Anyone +can use Gnoscan to easily find information that resides on the gno.land +blockchain, such as wallet addresses, TX hashes, blocks, and contracts. Gnoscan +makes our on-chain data easy to read and intuitive to discover. + +Explore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)! + +### Adena + +Adena is a user-friendly non-custodial wallet for gno.land. Open-source and +developed by Onbloc, Adena allows gnomes to interact easily with the chain. With +an emphasis on UX, Adena is built to handle millions of realms and tokens with a +high-quality interface, support for NFTs and custom tokens, and seamless +integration. Install Adena via the [official website](https://www.adena.app/) + +### GnoSwap + +GnoSwap is currently under development and led by the Onbloc team. GnoSwap will +be the first DEX on gno.land and is an automated market maker (AMM) protocol +written in Gno that allows for permissionless token exchanges on the platform. + +### Gno Native Kit + +[Gno Native Kit](https://github.com/gnolang/gnonative) is a framework that +allows developers to build and port gno.land (d)apps written in the (d)app's +native language. diff --git a/misc/deployments/home-alias/pages/events.md b/misc/deployments/home-alias/pages/events.md new file mode 100644 index 00000000000..7a3aac5a6f3 --- /dev/null +++ b/misc/deployments/home-alias/pages/events.md @@ -0,0 +1,205 @@ +# gno.land events + +Below is a list of all gno.land events, including in progress, upcoming, and past ones. + +--- + +## Past events + +### Gno Seoul - KBW Edition +Join us for an easy night in Seoul during KBW 2025! + +**Location:** Seoul, South Korea +**Starts:** 25 Sep 2025, 06:00 PM UTC+9 +**Ends:** 25 Sep 2025, 10:00 PM UTC+9 + +### GopherCon UK 2025 +Join us at our booth and listen to our workshop during GopherCon UK 2025! + +**Location:** London, United Kingdom +**Starts:** 13 Aug 2025, 12:00 AM UTC+2 +**Ends:** 15 Aug 2025, 12:00 AM UTC+2 + +### Web3 Kamp 2025 +We're sponsoring Web3 Kamp in Serbia, onboarding Web3 juniors to Gno.land. + +**Location:** Petnica, Serbia +**Starts:** 31 Jul 2025, 12:00 AM UTC+2 +**Ends:** 10 Aug 2025, 12:00 AM UTC+2 + +### EthCC 8 +Join us at our booth during EthCC 8 + +**Location:** Cannes, France +**Starts:** 30 Jun 2025, 12:00 AM UTC+2 +**Ends:** 03 Jul 2025, 12:00 AM UTC+2 + +### Welcome to Gno.land @ c-base Berlin +Join our meetup at the famous c-base hideout in Berlin! + +**Location:** Berlin, Germany +**Starts:** 15 Jun 2025, 06:00 PM UTC+2 +**Ends:** 15 Jun 2025, 08:30 PM UTC+2 + +### Writing dApps in Go +Join our talk at ETH Belgrade #3! + +**Location:** Belgrade, Serbia +**Starts:** 04 Jun 2025, 06:00 PM UTC+2 +**Ends:** 04 Jun 2025, 07:00 PM UTC+2 + +### Writing Smart Contracts in Go +Learn how to write smart contracts in Go! + +**Location:** Belgrade, Serbia +**Starts:** 10 Apr 2025, 06:00 PM UTC+2 +**Ends:** 10 Apr 2025, 10:00 PM UTC+2 + +### DAOs in Gno.land +Come hear about Gno.land DAOs at the ETH Belgrade Community Meetup #19 + +**Location:** Belgrade, Serbia +**Starts:** 05 Mar 2025, 06:00 PM UTC+2 +**Ends:** 05 Mar 2025, 09:00 PM UTC+2 + +### FOSDEM 2025 +Listen to @moul's lightning talk at FOSDEM. + +**Location:** Brussels, Belgium +**Starts:** 01 Feb 2025, 08:00 AM UTC+2 +**Ends:** 02 Feb 2025, 06:00 PM UTC+2 + +### BUIDL with Cosmos Tooling +Join us at our side event during Web3 Summit! + +**Location:** Berlin, Germany +**Starts:** 20 Aug 2024, 05:00 PM UTC+2 +**Ends:** 20 Aug 2024, 10:00 PM UTC+2 + +### Web3 Kamp 2024 +Workshop: "Exploring Web3 Ecosystems - Building a dapp in Go" + +**Location:** Petnica, Serbia +**Starts:** 01 Aug 2024, 10:00 AM UTC+2 +**Ends:** 09 Aug 2024, 05:00 PM UTC+2 + +### Nebular Summit 2024 +Join our workshop! + +**Location:** Brussels, Belgium +**Starts:** 12 Jul 2024, 10:00 AM UTC+2 +**Ends:** 13 Jul 2024, 06:00 PM UTC+2 + +### GopherCon US 2024 +Come meet us at our booth! + +**Location:** Chicago, US +**Starts:** 07 Jul 2024, 10:00 AM UTC-6 +**Ends:** 10 Jul 2024, 06:00 PM UTC-6 + +### GopherCon EU 2024 +Come meet us at our booth! + +**Location:** Berlin, Germany +**Starts:** 17 Jun 2024, 10:00 AM UTC+2 +**Ends:** 20 Jun 2024, 10:00 AM UTC+2 + +### Gno @ Golang Serbia +Join the meetup! + +**Location:** Belgrade, Serbia +**Starts:** 23 May 2024, 06:00 PM UTC+2 +**Ends:** 23 May 2024, 10:00 PM UTC+2 + +### Intro to Gno Tokyo +Join the meetup! + +**Location:** Shinjuku City, Tokyo, Japan +**Starts:** 11 Apr 2024, 06:30 PM UTC+9 +**Ends:** 11 Apr 2024, 10:00 PM UTC+9 + +### Go to Gno Seoul +Join the workshop! + +**Location:** Seoul, South Korea +**Starts:** 23 Mar 2024, 10:00 AM UTC+9 +**Ends:** 23 Mar 2024, 06:00 PM UTC+9 + +### GopherCon US 2023 +Come meet us at our booth! + +**Location:** San Diego, US +**Starts:** 26 Sep 2023, 10:00 AM UTC-7 +**Ends:** 29 Sep 2023, 06:00 PM UTC-7 + +### GopherCon EU 2023 +Come meet us at our booth! + +**Location:** Berlin, Germany +**Starts:** 26 Jul 2023, 10:00 AM UTC+2 +**Ends:** 29 Jul 2023, 06:00 PM UTC+2 + +### Nebular Summit gno.land for Developers + +**Location:** Paris, France +**Starts:** 24 Jul 2023, 10:00 AM UTC+2 +**Ends:** 25 Jul 2023, 06:00 PM UTC+2 + +### EthCC 2023 +Come meet us at our booth! + +**Location:** Paris, France +**Starts:** 17 Jul 2023, 10:00 AM UTC+2 +**Ends:** 20 Jul 2023, 06:00 PM UTC+2 + +### BUIDL Asia +Proof of Contribution in gno.land + +**Location:** Seoul, South Korea +**Starts:** 06 Jun 2023, 10:00 AM UTC+9 +**Ends:** 07 Jun 2023, 06:00 PM UTC+9 + +### ETH Seoul +The Evolution of Smart Contracts: A Journey into gno.land + +**Location:** Seoul, South Korea +**Starts:** 02 Jun 2023, 10:00 AM UTC+9 +**Ends:** 04 Jun 2023, 06:00 PM UTC+9 + +### Game Developer Conference +Side Event: Web3 Gaming Apps Powered by Gno + +**Location:** San Francisco, US +**Starts:** 23 Mar 2023, 10:00 AM UTC-7 +**Ends:** 23 Mar 2023, 06:00 PM UTC-7 + +### EthDenver 2023 +Side Event: Discover gno.land + +**Location:** Denver, US +**Starts:** 24 Feb 2023, 10:00 AM UTC-6 +**Ends:** 05 Mar 2023, 10:00 AM UTC-6 + +### Istanbul Blockchain Week + +**Location:** Istanbul, Turkey +**Starts:** 14 Nov 2022, 10:00 AM UTC+3 +**Ends:** 17 Nov 2022, 06:00 PM UTC+3 + +### Web Summit Buckle Up and Build with Cosmos + +**Location:** Lisbon, Portugal +**Starts:** 01 Nov 2022, 09:00 AM UTC+1 +**Ends:** 04 Nov 2022, 06:00 PM UTC+1 + +### Cosmoverse + +**Location:** Medellin, Colombia +**Starts:** 26 Sep 2022, 09:00 AM UTC-5 +**Ends:** 28 Sep 2022, 06:00 PM UTC-5 + +### Berlin Blockchain Week Buckle Up and Build with Cosmos + +**Location:** Berlin, Germany +**Starts:** 11 Sep 2022, 09:00 AM UTC+2 +**Ends:** 18 Sep 2022, 06:00 PM UTC+2 diff --git a/misc/deployments/home-alias/pages/gnolang.md b/misc/deployments/home-alias/pages/gnolang.md new file mode 100644 index 00000000000..8b6cd6ee728 --- /dev/null +++ b/misc/deployments/home-alias/pages/gnolang.md @@ -0,0 +1,82 @@ +# About the Gno, the Language for Gno.land + +[Gno](https://github.com/gnolang/gno) is an interpretation of the widely-used Go +(Golang) programming language for blockchain created by Cosmos co-founder Jae +Kwon in 2022 to mark a new era in smart contracting. Gno is ~99% identical to +Go, so Go programmers can start coding in Gno right away, with a minimal +learning curve. For example, Gno comes with blockchain-specific standard +libraries, but any code that doesn't use blockchain-specific logic can run in Go +with minimal processing. Libraries that don't make sense in the blockchain +context, such as network or operating-system access, are not available in Gno. +Otherwise, Gno loads and uses many standard libraries that power Go, so most of +the parsing of the source code is the same. + +Under the hood, the Gno code is parsed into an abstract syntax tree (AST) and +the AST itself is used in the interpreter, rather than bytecode as in many +virtual machines such as Java, Python, or Wasm. This makes even the GnoVM +accessible to any Go programmer. The novel design of the intuitive GnoVM +interpreter allows Gno to freeze and resume the program by persisting and +loading the entire memory state. Gno is deterministic, auto-persisted, and +auto-Merkle-ized, allowing (smart contract) programs to be succinct, as the +programmer doesn't have to serialize and deserialize objects to persist them +into a database (unlike programming applications with the Cosmos SDK). + +## How Gno Differs from Go + +The composable nature of Go/Gno allows for type-checked interactions between +contracts, making Gno.land safer and more powerful, as well as operationally +cheaper and faster. Smart contracts on Gno.land are light, simple, more focused, +and easily interoperable—a network of interconnected contracts rather than +siloed monoliths that limit interactions with other contracts. + +## Gno Inherits Go's Built-in Security Features + +Go supports secure programming through exported/non-exported fields, enabling a +"least-authority" design. It is easy to create objects and APIs that expose only +what should be accessible to callers while hiding what should not be simply by +the capitalization of letters, thus allowing a succinct representation of secure +logic that can be called by multiple users. + +Another major advantage of Go is that the language comes with an ecosystem of +great tooling, like the compiler and third-party tools that statically analyze +code. Gno inherits these advantages from Go directly to create a smart contract +programming language that provides embedding, composability, type-check safety, +and garbage collection, helping developers to write secure code relying on the +compiler, parser, and interpreter to give warning alerts for common mistakes. + +## Gno vs Solidity + +The most widely-adopted smart contract language today is Ethereum's +EVM-compatible Solidity. With bytecode built from the ground up and Turing +complete, Solidity opened up a world of possibilities for decentralized +applications (dApps) and there are currently more than 10 million contracts +deployed on Ethereum. However, Solidity provides limited tooling and its EVM has +a stack limit and computational inefficiencies. + +Solidity is designed for one purpose only (writing smart contracts) and is bound +by the limitations of the EVM. In addition, developers have to learn several +languages if they want to understand the whole stack or work across different +ecosystems. Gno aspires to exceed Solidity on multiple fronts (and other smart +contract languages like CosmWasm or Substrate) as every part of the stack is +written in Gno. It's easy for developers to understand the entire system just by +studying a relatively small code base. + +## Gno Is Essential for the Wider Adoption of Web3 + +Gno makes imports as easy as they are in web2 with runtime-based imports for +seamless dependency flow comprehension, and support for complex structs, beyond +primitive types. Gno is ultimately cost-effective as dependencies are loaded +once, enabling remote function calls as local, and providing automatic and +independent per-realm state persistence. + +Using Gno, developers can rapidly accelerate application development and adopt a +modular structure by reusing and reassembling existing modules without building +from scratch. They can embed one structure inside another in an intuitive way +while preserving localism, and the language specification is simple, +successfully balancing practicality and minimalism. + +The Go language is so well designed that the Gno smart contract system will +become the new gold standard for smart contract development and other blockchain +applications. As a programming language that is universally adopted, secure, +composable, and complete, Gno is essential for the broader adoption of web3 and +its sustainable growth. diff --git a/misc/deployments/home-alias/pages/license.md b/misc/deployments/home-alias/pages/license.md new file mode 100644 index 00000000000..233269c4266 --- /dev/null +++ b/misc/deployments/home-alias/pages/license.md @@ -0,0 +1,635 @@ +# Gno Network General Public License +Copyright (C) 2024 NewTendermint, LLC + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNO Network General Public License as published by +NewTendermint, LLC, either version 4 of the License, or (at your option) any +later version published by NewTendermint, LLC. + +This program is distributed in the hope that it will be useful, but is provided +as-is and WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNO Network +General Public License for more details. + +You should have received a copy of the GNO Network General Public License along +with this program. If not, see . + +Attached below are the terms of the GNO Network General Public License, Version +4 (a fork of the GNU Affero General Public License 3). + +## Additional Terms + +### Strong Attribution + +If any of your user interfaces, such as websites and mobile applications, serve +as the primary point of entry to a platform or blockchain that 1) offers users +the ability to upload their own smart contracts to the platform or blockchain, +and 2) leverages any Covered Work (including the GNO virtual machine) to run +those smart contracts on the platform or blockchain ("Applicable Work"), then +the Applicable Work must prominently link to (1) gno.land or (2) any other URL +designated by NewTendermint, LLC that has not been rejected by the governance of +the first chain known as gno.land, provided that the identity of the first chain +is not ambiguous. In the event the identity of the first chain is ambiguous, +then NewTendermint, LLC's designation shall control. Such link must appear +conspicuously in the header or footer of the Applicable Work, such that all +users may learn of gno.land or the URL designated by NewTendermint, LLC. + +This additional attribution requirement shall remain in effect for (1) 7 years +from the date of publication of the Applicable Work, or (2) 7 years from the +date of publication of the Covered Work (including republication of new +versions), whichever is later, but no later than 12 years after the application +of this strong attribution requirement to the publication of the Applicable +Work. For purposes of this Strong Attribution requirement, Covered Work shall +mean any work that is licensed under the GNO Network General Public License, +Version 4 or later, by NewTendermint, LLC. + + +# GNO NETWORK GENERAL PUBLIC LICENSE + +Version 4, 7 May 2024 + +Modified from the GNU AFFERO GENERAL PUBLIC LICENSE. GNU is not affiliated with +GNO or NewTendermint, LLC. Copyright (C) 2022 NewTendermint, LLC. + +## Preamble + +The GNO Network General Public License is a free, copyleft license for software +and other kinds of works, specifically designed to ensure cooperation with the +community in the case of network server software. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, our General +Public Licenses are intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for them if you wish), that you +receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can do +these things. + +Developers that use our General Public Licenses protect your rights with two +steps: (1) assert copyright on the software, and (2) offer you this License +which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in +alternate versions of the program, if they receive widespread use, become +available for other developers to incorporate. Many developers of free software +are heartened and encouraged by the resulting cooperation. However, in the case +of software used on network servers, this result may fail to come about. The GNU +General Public License permits making a modified version and letting the public +access it on a server without ever releasing its source code to the public. + +The GNO Network General Public License is designed specifically to ensure that, +in such cases, the modified source code becomes available to the community. It +requires the operator of a network server to provide the source code of the +modified version running there to the users of that server. Therefore, public +use of a modified version, on a publicly accessible server, gives the public +access to the source code of the modified version. + +The precise terms and conditions for copying, distribution and modification +follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 4 of the GNO Network General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. +Each licensee is addressed as "you". "Licensees" and "recipients" may be +individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a +fashion requiring copyright permission, other than the making of an exact copy. +The resulting work is called a "modified version" of the earlier work or a work +"based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to +make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent +that it includes a convenient and prominently visible feature that (1) displays +an appropriate copyright notice, and (2) tells the user that there is no +warranty for the work (except to the extent that warranties are provided), that +licensees may convey the work under this License, and how to view a copy of this +License. If the interface presents a list of user commands or options, such as +a menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than the +work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only to +enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code +form. A "Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, or an +object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in +performing those activities but which are not part of the work. For example, +Corresponding Source includes interface definition files associated with source +files for the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and other +parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on +the Program, and are irrevocable provided the stated conditions are met. This +License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License only +if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or running +the covered works for you must do so exclusively on your behalf, under your +direction and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure under +any applicable law fulfilling obligations under article 11 of the WIPO copyright +treaty adopted on 20 December 1996, or similar laws prohibiting or restricting +circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the covered +work, and you disclaim any intention to limit operation or modification of the +work as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, +in any medium, provided that you conspicuously and appropriately publish on each +copy an appropriate copyright notice; keep intact all notices stating that this +License and any non-permissive terms added in accord with section 7 apply to the +code; keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may +offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it +from the Program, in the form of source code under the terms of section 4, +provided that you also meet all of these conditions: + +- a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. This +requirement modifies the requirement in section 4 to "keep intact all notices". +- c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This License will +therefore apply, along with any applicable section 7 additional terms, to the +whole of the work, and all its parts, regardless of how they are packaged. This +License gives no permission to license the work in any other way, but it does +not invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive interfaces +that do not display Appropriate Legal Notices, your work need not make them do +so. + +A compilation of a covered work with other separate and independent works, which +are not by their nature extensions of the covered work, and which are not +combined with it such as to form a larger program, in or on a volume of a +storage or distribution medium, is called an "aggregate" if the compilation and +its resulting copyright are not used to limit the access or legal rights of the +compilation's users beyond what the individual works permit. Inclusion of a +covered work in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 +and 5, provided that you also convey the machine-readable Corresponding Source +under the terms of this License, in one of these ways: + +- a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the Corresponding +Source fixed on a durable physical medium customarily used for software +interchange. +- b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a written offer, +valid for at least three years and valid for as long as you offer spare parts or +customer support for that product model, to give anyone who possesses the object +code either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium +customarily used for software interchange, for a price no more than your +reasonable cost of physically performing this conveying of source, or (2) access +to copy the Corresponding Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This alternative is allowed +only occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. +- d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the Corresponding +Source in the same way through the same place at no further charge. You need +not require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the +Corresponding Source may be on a different server (operated by you or a third +party) that supports equivalent copying facilities, provided you maintain clear +directions next to the object code saying where to find the Corresponding +Source. Regardless of what server hosts the Corresponding Source, you remain +obligated to ensure that it is available for as long as needed to satisfy these +requirements. +- e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding Source of the +work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall be +resolved in favor of coverage. For a particular product received by a +particular user, "normally used" refers to a typical or common use of that class +of product, regardless of the status of the particular user or of the way in +which the particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the product has +substantial commercial, industrial or non-consumer uses, unless such uses +represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified version +of its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented or +interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part of a +transaction in which the right of possession and use of the User Product is +transferred to the recipient in perpetuity or for a fixed term (regardless of +how the transaction is characterized), the Corresponding Source conveyed under +this section must be accompanied by the Installation Information. But this +requirement does not apply if neither you nor any third party retains the +ability to install modified object code on the User Product (for example, the +work has been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates for a +work that has been modified or installed by the recipient, or for the User +Product in which it has been modified or installed. Access to a network may be +denied when the modification itself materially and adversely affects the +operation of the network or violates the rules and protocols for communication +across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require no +special password or key for unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License by +making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they were +included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may +be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal Notices +displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in reasonable ways +as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or +- e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of it) with +contractual assumptions of liability to the recipient, for any liability that +these contractual assumptions directly impose on those licensors and authors; or +- g) Requiring strong attribution such as notices on any user interfaces +that run or convey any covered work, such as a prominent link to a URL on the +header of a website, such that all users of the covered work may become aware of +the notice, for a period no longer than 20 years. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License along +with a term that is a further restriction, you may remove that term. If a +license document contains a further restriction but permits relicensing or +conveying under this License, you may add to a covered work material governed by +the terms of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply to +those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements +apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including any +patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated (a) provisionally, unless and until +the copyright holder explicitly and finally terminates your license, and (b) +permanently, if the copyright holder fails to notify you of the violation by +some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of violation +of this License (for any work) from that copyright holder, and you cure the +violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of +parties who have received copies or rights from you under this License. If your +rights have been terminated and not permanently reinstated, you do not qualify +to receive new licenses for the same material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of +the Program. Ancillary propagation of a covered work occurring solely as a +consequence of using peer-to-peer transmission to receive a copy likewise does +not require acceptance. However, nothing other than this License grants you +permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or +propagating a covered work, you indicate your acceptance of this License to do +so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a +license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance by +third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work +results from an entity transaction, each party to that transaction who receives +a copy of the work also receives whatever licenses to the work the party's +predecessor in interest had or could give under the previous paragraph, plus a +right to possession of the Corresponding Source of the work from the predecessor +in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under this +License, and you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed by +making, using, selling, offering for sale, or importing the Program or any +portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of +the Program or a work on which the Program is based. The work thus licensed is +called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, +that would be infringed by some manner, permitted by this License, of making, +using, or selling its contributor version, but do not include claims that would +be infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents of +its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement or +commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of +charge and under the terms of this License, through a publicly available network +server or other readily accessible means, then you must either (1) cause the +Corresponding Source to be so available, or (2) arrange to deprive yourself of +the benefit of the patent license for this particular work, or (3) arrange, in a +manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual +knowledge that, but for the patent license, your conveying the covered work in a +country, or your recipient's use of the covered work in a country, would +infringe one or more identifiable patents in that country that you have reason +to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a +patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients of +the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of +its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with a +third party that is in the business of distributing software, under which you +make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you (or +copies made from those copies), or (b) primarily for and in connection with +specific products or compilations that contain the covered work, unless you +entered into that arrangement, or that patent license was granted, prior to 28 +March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to you +under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot convey a covered work so +as to simultaneously satisfy your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way you +could satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, +your modified version must prominently offer all users interacting with it +remotely through a computer network (if your version supports such interaction) +an opportunity to receive the Corresponding Source of your version by providing +access to the Corresponding Source from a network server at no charge, through +some standard or customary means of facilitating copying of software. This +Corresponding Source shall include the Corresponding Source for any work covered +by version 3 of the GNU General Public License that is incorporated pursuant to +the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link +or combine any covered work with a work licensed under version 3 of the GNU +General Public License into a single combined work, and to convey the resulting +work. The terms of this License will continue to apply to the part which is the +covered work, but the work with which it is combined will remain governed by +version 3 of the GNU General Public License. + +### 14. Revised Versions of this License. + +NewTendermint LLC may publish revised and/or new versions of the GNO Network +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNO Network General Public License "or +any later version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by +the Gno Software Foundation. If the Program does not specify a version number +of the GNO Network General Public License, you may choose any version ever +published by NewTendermint LLC. + +If the Program specifies that a proxy can decide which future versions of the +GNO Network General Public License can be used, that proxy's public statement of +acceptance of a version permanently authorizes you to choose that version for +the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright holder +as a result of your choosing to follow a later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption of +liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use +to the public, the best way to achieve this is to make it free software which +everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion of +warranty; and each file should have at least the "copyright" line and a pointer +to where the full notice is found. + + +Copyright (C) + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNO Network General Public License as published by +NewTendermint LLC, either version 4 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNO Network General Public License for more +details. + +You should have received a copy of the GNO Network General Public License along +with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, +you should also make sure that it provides a way for users to get its source. +For example, if your program is a web application, its interface could display a +"Source" link that leads users to an archive of the code. There are many ways +you could offer source, and different solutions will be better for different +programs; see section 13 for the specific requirements. diff --git a/misc/deployments/home-alias/pages/links.md b/misc/deployments/home-alias/pages/links.md new file mode 100644 index 00000000000..cb0355f48df --- /dev/null +++ b/misc/deployments/home-alias/pages/links.md @@ -0,0 +1,16 @@ +# Gno.land Linker + +Below are Gno.land's most important links. + +#### [Gno GitHub, the project's main repository](https://github.com/gnolang/gno) +#### [Getting Started with Gno](https://github.com/gnolang/getting-started/) +#### [Gno.land Official Docs](https://docs.gno.land/) +#### [Gnoverse Github, a community organization for Gnomes](https://github.com/gnoverse) +#### [Gno.land Workshops, a repo for Gno resources](https://github.com/gnolang/workshops) +#### [Gno.land Students Program](https://github.com/gnolang/student-contributors-program/) +#### [Gno.land Contributor page](https://gno.land/contribute) +#### [Gno.land Discord](https://discord.gg/gnoland) +#### [Gno.land X](https://x.com/_gnoland) +#### [Gno.land YouTube](https://www.youtube.com/@_gnoland) +#### [Gno.land Playground, an online editor for exploring Gno.land](https://play.gno.land) +#### [GnoScan, a Gno.land block explorer](https://gnoscan.io/) diff --git a/misc/deployments/home-alias/pages/partners.md b/misc/deployments/home-alias/pages/partners.md new file mode 100644 index 00000000000..4c5a4e07cad --- /dev/null +++ b/misc/deployments/home-alias/pages/partners.md @@ -0,0 +1,11 @@ +# Partnerships + +### Fund and Grants Program + +Are you a builder, tinkerer, or researcher? If you're looking to create awesome +dApps, tooling, infrastructure, or smart contract libraries on Gno.land, you can +apply for a grant. The Gno.land Ecosystem Fund and Grants program provides +financial contributions for individuals and teams to innovate on the platform. + +Read more about our Funds and Grants program +[here](https://github.com/gnolang/grants). diff --git a/misc/deployments/home-alias/pages/start.md b/misc/deployments/home-alias/pages/start.md new file mode 100644 index 00000000000..bc5f60d7460 --- /dev/null +++ b/misc/deployments/home-alias/pages/start.md @@ -0,0 +1,7 @@ +# Getting Started with Gno + +## Getting Started with Gno + +Visit the +[getting-started](https://github.com/gnolang/getting-started) +repo to try Gno in 5 minutes! diff --git a/misc/deployments/test13.gno.land/govdao-scripts/add-validator.sh b/misc/deployments/test13.gno.land/govdao-scripts/add-validator.sh new file mode 100755 index 00000000000..44e90b167e5 --- /dev/null +++ b/misc/deployments/test13.gno.land/govdao-scripts/add-validator.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Add a validator to test-13 via govDAO proposal, using r/sys/validators/v3. +# +# v3 propagates valset changes through the VM params keeper (instead of events + +# VM query-back in v2), so EndBlocker picks up updates directly from params. +# +# Usage: +# ./add-validator.sh
[voting_power] +# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: test-13) +# REMOTE - RPC endpoint (default: http://127.0.0.1:26657) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-test-13}" +REMOTE="${REMOTE:-http://127.0.0.1:26657}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +if [ $# -lt 2 ]; then + echo "Usage: $0
[voting_power]" + echo "" + echo "Example:" + echo " $0 g1abc...xyz gpub1pggj7... 1" + exit 1 +fi + +ADDR="$1" +PUB_KEY="$2" +POWER="${3:-1}" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/add_validator.gno" < [pubkey] +# +# • voting_power=0 → REMOVE the validator; pubkey field is ignored if +# present and may be omitted. +# • voting_power>0 → ADD a new validator; pubkey is required. +# +# Blank lines and lines starting with '#' are ignored. To update an +# existing validator's power in the same batch, add a remove line +# (power=0) followed by an add line at the new power. +# +# Usage: +# ./batch-change.sh +# +# Environment (same defaults as add-validator.sh): +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: test-13) +# REMOTE - RPC endpoint (default: http://127.0.0.1:26657) +# GAS_WANTED - gas limit (default: 100000000 — batches use more gas) +# GAS_FEE - gas fee (default: 2000000ugnot) +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-test-13}" +REMOTE="${REMOTE:-http://127.0.0.1:26657}" +GAS_WANTED="${GAS_WANTED:-100000000}" +GAS_FEE="${GAS_FEE:-2000000ugnot}" + +if [ $# -lt 1 ]; then + echo "Usage: $0 " + echo "" + echo "changes.txt format (one per line):" + echo "
[pubkey]" + echo "" + echo " power=0 → remove (pubkey ignored)" + echo " power>0 → add (pubkey required)" + exit 1 +fi + +CHANGES_FILE="$1" +[[ -f "$CHANGES_FILE" ]] || { + echo "error: changes file not found: $CHANGES_FILE" >&2 + exit 1 +} + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +# Render each non-comment, non-blank line into a Go literal inside the +# validator slice. Power=0 entries omit PubKey; power>0 entries require it. +entries="" +preview="" +lineno=0 +while IFS= read -r raw || [[ -n "$raw" ]]; do + lineno=$((lineno + 1)) + line="$(printf '%s' "$raw" | sed 's/[[:space:]]\+$//')" + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + + # shellcheck disable=SC2206 + fields=($line) + addr="${fields[0]:-}" + power="${fields[1]:-}" + pub="${fields[2]:-}" + + if [[ -z "$addr" || -z "$power" ]]; then + echo "error: line $lineno: expected '
[pubkey]', got '$raw'" >&2 + exit 1 + fi + if ! [[ "$power" =~ ^[0-9]+$ ]]; then + echo "error: line $lineno: power must be a non-negative integer (got '$power')" >&2 + exit 1 + fi + + # Sanity-check addresses and pubkeys so malformed entries can't inject + # into the generated Go — only allow the bech32 character set. + if ! [[ "$addr" =~ ^g1[0-9a-z]+$ ]]; then + echo "error: line $lineno: address '$addr' is not a valid g1 bech32" >&2 + exit 1 + fi + + if [[ "$power" == "0" ]]; then + entries+="$(printf '\n\t\t\t{Address: address("%s"), VotingPower: 0},' "$addr")" + preview+=$(printf '\n - remove %s' "$addr") + else + if [[ -z "$pub" ]]; then + echo "error: line $lineno: add requires a pubkey (power=$power for $addr has no pubkey)" >&2 + exit 1 + fi + if ! [[ "$pub" =~ ^gpub1[0-9a-z]+$ ]]; then + echo "error: line $lineno: pubkey '$pub' is not a valid gpub1 bech32" >&2 + exit 1 + fi + entries+="$(printf '\n\t\t\t{Address: address("%s"), PubKey: "%s", VotingPower: %s},' "$addr" "$pub" "$power")" + preview+=$(printf '\n - add %s (power %s)' "$addr" "$power") + fi +done <"$CHANGES_FILE" + +if [[ -z "$entries" ]]; then + echo "error: no changes found in $CHANGES_FILE (blank or comments only)" >&2 + exit 1 +fi + +cat >"$TMPDIR/batch_change.gno" < +# +# Environment (same defaults as add-validator.sh): +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: test-13) +# REMOTE - RPC endpoint (default: http://127.0.0.1:26657) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-test-13}" +REMOTE="${REMOTE:-http://127.0.0.1:26657}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +if [ $# -lt 3 ]; then + echo "Usage: $0
" + echo "" + echo "Example:" + echo " $0 g1abc...xyz gpub1pggj7... 5" + exit 1 +fi + +ADDR="$1" +PUB_KEY="$2" +POWER="$3" + +if ! [[ "$POWER" =~ ^[1-9][0-9]*$ ]]; then + echo "error: new_voting_power must be a positive integer (use rm-validator.sh for removal)" >&2 + exit 1 +fi + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/change_power.gno" < +# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: test-13) +# REMOTE - RPC endpoint (default: http://127.0.0.1:26657) +# GAS_WANTED - gas limit (default: 50000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +# +# Preconditions: +# • The target address MUST currently be a validator in v3. v3's PoA +# backend panics ("proposed validator must be part of the set already") +# when asked to remove a non-member; the whole proposal aborts and no +# valset change applies. Use add-validator.sh first if unsure. +set -eo pipefail + +GNOKEY_NAME="${GNOKEY_NAME:-moul}" +CHAIN_ID="${CHAIN_ID:-test-13}" +REMOTE="${REMOTE:-http://127.0.0.1:26657}" +GAS_WANTED="${GAS_WANTED:-50000000}" +GAS_FEE="${GAS_FEE:-1000000ugnot}" + +if [ $# -lt 1 ]; then + echo "Usage: $0
" + echo "" + echo "Example:" + echo " $0 g1abc...xyz" + exit 1 +fi + +ADDR="$1" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/rm_validator.gno" < ' (matches +# gno-cluster's INITIAL_VALSET output). Mutually exclusive with VALIDATOR_ADDR. +VALIDATOR_LIST ?= +# Only used by `make fetch-from-dir` (alternative local-source flow). +NODE_DIR ?= +TXS_JSONL ?= +# Extra realm patches applied at hardfork-assembly time (see scripts/migrate.sh). +# Space-separated PKGPATH=SRCDIR entries. Set to empty to disable. +# (migrate.sh always patches r/sys/params from examples/; use this for more.) +PATCH_REALMS ?= +# Optional: rotate the govDAO sole T1 member to a different address as part +# of the post-replay migration (see scripts/migrate.sh step 5 and +# misc/deployments/gnoland-1/migrations/build.sh). Empty = no rotation. +NEW_T1_ADDR ?= +T1_PORTFOLIO ?= +T1_WITHDRAW_REASON ?= + +export SOURCE RPC_URL ORIGINAL_CHAIN_ID CHAIN_ID HALT_HEIGHT VALIDATOR_NAME VALIDATOR_ADDR VALIDATOR_PUBKEY VALIDATOR_LIST NODE_DIR TXS_JSONL PATCH_REALMS NEW_T1_ADDR T1_PORTFOLIO T1_WITHDRAW_REASON + +# ---- paths ----------------------------------------------------------------- +HERE := $(abspath .) +REPO := $(abspath ../..) +OUT := $(HERE)/out + +.DEFAULT_GOAL := help + +.PHONY: help +help: ## show this help + @printf "misc/hf-glue — experimental hardfork testbed\n\n" + @awk 'BEGIN {FS=":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +.PHONY: gen-local-genesis +gen-local-genesis: ## rebuild gnoland1 base genesis locally (runs misc/deployments/gnoland1/gen-genesis.sh); output at out/source-genesis.json + @mkdir -p $(OUT) + @OUT=$(OUT) REPO=$(REPO) $(HERE)/scripts/gen-local-genesis.sh + +.PHONY: fetch migrate +fetch: migrate ## alias for `make migrate` (kept for back-compat) + +migrate: ## run scripts/migrate.sh — declarative hardfork recipe (fetch + patch + assemble) + @mkdir -p $(OUT) + @SOURCE=$(SOURCE) \ + RPC_URL=$(RPC_URL) \ + ORIGINAL_CHAIN_ID=$(ORIGINAL_CHAIN_ID) \ + CHAIN_ID=$(CHAIN_ID) \ + HALT_HEIGHT=$(HALT_HEIGHT) \ + PATCH_REALMS='$(PATCH_REALMS)' \ + OUT=$(OUT) REPO=$(REPO) \ + $(HERE)/scripts/migrate.sh + +.PHONY: genesis +genesis: ## produce out/genesis.json only — single val (VALIDATOR_ADDR+VALIDATOR_PUBKEY) OR multi val (VALIDATOR_LIST) + @test -n "$(HALT_HEIGHT)" || { echo "HALT_HEIGHT required"; exit 1; } + @if [ -n "$(VALIDATOR_LIST)" ] && [ -n "$(VALIDATOR_ADDR)$(VALIDATOR_PUBKEY)" ]; then \ + echo "VALIDATOR_LIST is mutually exclusive with VALIDATOR_ADDR/VALIDATOR_PUBKEY"; exit 1; fi + @if [ -z "$(VALIDATOR_LIST)" ]; then \ + test -n "$(VALIDATOR_ADDR)" || { echo "VALIDATOR_ADDR required (or set VALIDATOR_LIST)"; exit 1; }; \ + test -n "$(VALIDATOR_PUBKEY)" || { echo "VALIDATOR_PUBKEY required (or set VALIDATOR_LIST)"; exit 1; }; \ + else \ + test -f "$(VALIDATOR_LIST)" || { echo "VALIDATOR_LIST path not found: $(VALIDATOR_LIST)"; exit 1; }; \ + fi + @mkdir -p $(OUT) + @SOURCE=$(SOURCE) \ + RPC_URL=$(RPC_URL) \ + ORIGINAL_CHAIN_ID=$(ORIGINAL_CHAIN_ID) \ + CHAIN_ID=$(CHAIN_ID) \ + HALT_HEIGHT=$(HALT_HEIGHT) \ + VALIDATOR_ADDR=$(VALIDATOR_ADDR) \ + VALIDATOR_PUBKEY=$(VALIDATOR_PUBKEY) \ + VALIDATOR_LIST=$(VALIDATOR_LIST) \ + VALIDATOR_NAME=$(VALIDATOR_NAME) \ + PATCH_REALMS='$(PATCH_REALMS)' \ + OUT=$(OUT) REPO=$(REPO) \ + $(HERE)/scripts/migrate.sh + @if [ -n "$(VALIDATOR_LIST)" ]; then \ + go run -C $(REPO)/misc/hf-glue/fixvalidator . \ + --valset-list $(VALIDATOR_LIST) \ + --genesis $(OUT)/genesis.json ; \ + else \ + go run -C $(REPO)/misc/hf-glue/fixvalidator . \ + --address $(VALIDATOR_ADDR) --pubkey $(VALIDATOR_PUBKEY) \ + --genesis $(OUT)/genesis.json \ + --name $(VALIDATOR_NAME) --power 10 ; \ + fi + @echo "" + @echo "Genesis ready: $(OUT)/genesis.json" + @echo " SHA256: $$(shasum -a 256 $(OUT)/genesis.json | cut -d' ' -f1)" + @echo " tx count: $$(jq '.app_state.txs | length' $(OUT)/genesis.json)" + @echo " chain_id: $$(jq -r '.chain_id' $(OUT)/genesis.json)" + @echo " valset: $$(jq '.validators | length' $(OUT)/genesis.json) validator(s)" + +.PHONY: fetch-from-dir +fetch-from-dir: ## (alt) build hardfork genesis from a local gnoland data dir ($NODE_DIR, $TXS_JSONL, $HALT_HEIGHT required) + @mkdir -p $(OUT) + @NODE_DIR=$(NODE_DIR) TXS_JSONL=$(TXS_JSONL) \ + ORIGINAL_CHAIN_ID=$(ORIGINAL_CHAIN_ID) \ + CHAIN_ID=$(CHAIN_ID) \ + HALT_HEIGHT=$(HALT_HEIGHT) \ + OUT=$(OUT) REPO=$(REPO) \ + $(HERE)/scripts/fetch-from-dir.sh + +.PHONY: init +init: ## generate single-validator secrets and patch out/genesis.json + @mkdir -p $(OUT) + @VALIDATOR_NAME=$(VALIDATOR_NAME) \ + OUT=$(OUT) REPO=$(REPO) \ + $(HERE)/scripts/init-node.sh + +.PHONY: up +up: init ## start the gnoland node in docker (init is idempotent and re-stages genesis if changed) + docker compose up -d --build + @echo "" + @echo "Node RPC: http://localhost:26657" + @echo "Chain ID: $(CHAIN_ID)" + @echo "Tail logs: make logs" + +.PHONY: down +down: ## stop the node (keep state) + docker compose down + +.PHONY: reset-db +reset-db: down ## wipe node db + WAL + last-signed state, keep genesis + keys + rm -rf $(OUT)/gnoland-home/db $(OUT)/gnoland-home/wal $(OUT)/gnoland-home/data + printf '{"height":"0","round":"0","step":0}\n' > $(OUT)/gnoland-home/secrets/priv_validator_state.json + @echo "db wiped; next 'make up' will re-replay genesis from scratch" + +.PHONY: logs +logs: ## tail node logs + docker compose logs -f --tail=200 + +.PHONY: status +status: ## show node /status via RPC + @curl -s http://localhost:26657/status | jq . || true + +.PHONY: reset +reset: down ## stop and wipe ALL generated state (genesis, keys, db) + rm -rf $(OUT) + @echo "out/ removed." + +.PHONY: smoketest +smoketest: ## run 'gnogenesis fork test' in-memory against out/genesis.json + @test -f $(OUT)/genesis.json || { echo "missing out/genesis.json — run 'make fetch' first"; exit 1; } + cd $(REPO)/contribs/gnogenesis && go run . fork test --genesis $(OUT)/genesis.json --verbose + +.PHONY: assert-migrations +assert-migrations: ## assert live node's post-replay state matches migration intent (overrides: REMOTE= EXPECTED_T1= EXPECTED_VALSET_ADDRS=) + @REMOTE="$${REMOTE:-http://localhost:26657}" \ + EXPECTED_T1="$${EXPECTED_T1:-$(NEW_T1_ADDR)}" \ + EXPECTED_VALSET_ADDRS="$${EXPECTED_VALSET_ADDRS:-$(VALIDATOR_ADDR)}" \ + $(HERE)/scripts/assert-migrations.sh + +.PHONY: verify-txs-jsonl +verify-txs-jsonl: ## compare out/source/txs.jsonl against source-chain RPC (cardinality + spot-check at random heights with txs) + @test -n "$(HALT_HEIGHT)" || { echo "HALT_HEIGHT required"; exit 1; } + @RPC_URL="$(RPC_URL)" \ + HALT_HEIGHT=$(HALT_HEIGHT) \ + OUT=$(OUT) \ + $(HERE)/scripts/verify-txs-jsonl.sh + +.PHONY: state-diff +state-diff: ## diff realm render output source-chain@halt_height vs replay (overrides: SOURCE_RPC= REPLAY_RPC= REALMS= REPLAY_HEIGHT=); writes out/STATE-DIFF.md + @test -n "$(HALT_HEIGHT)" || { echo "HALT_HEIGHT required"; exit 1; } + @SOURCE_RPC="$${SOURCE_RPC:-$(RPC_URL)}" \ + REPLAY_RPC="$${REPLAY_RPC:-http://localhost:26657}" \ + HALT_HEIGHT=$(HALT_HEIGHT) \ + REALMS="$${REALMS:-}" \ + OUT=$(OUT) \ + $(HERE)/scripts/state-diff.sh + +.PHONY: audit-balances +audit-balances: ## diff per-signer ugnot balance source-chain@halt_height vs replay (overrides: SOURCE_RPC= REPLAY_RPC= SIGNER_LIMIT=); writes out/BALANCE-AUDIT.md + @test -n "$(HALT_HEIGHT)" || { echo "HALT_HEIGHT required"; exit 1; } + @SOURCE_RPC="$${SOURCE_RPC:-$(RPC_URL)}" \ + REPLAY_RPC="$${REPLAY_RPC:-http://localhost:26657}" \ + HALT_HEIGHT=$(HALT_HEIGHT) \ + SIGNER_LIMIT="$${SIGNER_LIMIT:-200}" \ + OUT=$(OUT) \ + $(HERE)/scripts/audit-balances.sh + +.PHONY: verify-reproducibility +verify-reproducibility: ## build genesis twice in isolated OUT dirs and assert identical SHA256 (forwards all `make genesis` envs) + @test -n "$(HALT_HEIGHT)" || { echo "HALT_HEIGHT required"; exit 1; } + @SHARED_TXS="$${SHARED_TXS:-$(OUT)/source/txs.jsonl}" \ + OUT=$(OUT) \ + $(HERE)/scripts/verify-reproducibility.sh + +.PHONY: audit-realm-imports +audit-realm-imports: ## flag addpkgs whose import paths no longer resolve against the current stdlib+examples tree; writes out/REALM-IMPORTS-AUDIT.md + @REPO=$(REPO) OUT=$(OUT) \ + $(HERE)/scripts/audit-realm-imports.sh + +.PHONY: compare-gas-modes +compare-gas-modes: ## run smoketest twice with gas_replay_mode=strict vs source and diff failure counts; writes out/GAS-MODES-COMPARE.md + @test -f $(OUT)/genesis.json || { echo "missing out/genesis.json — run 'make genesis' first"; exit 1; } + @REPO=$(REPO) OUT=$(OUT) \ + $(HERE)/scripts/compare-gas-modes.sh + +.PHONY: replay-log +replay-log: ## run in-process genesis replay, tee full log to out/replay.log + @$(HERE)/scripts/replay-log.sh + +.PHONY: report-replay +report-replay: ## generate out/REPLAY-REPORT.md from out/replay.log + @$(HERE)/scripts/report-replay.sh + +.PHONY: check-state +check-state: ## probe running node, compare to gno.land, write out/STATE-REPORT.md + @$(HERE)/scripts/check-state.sh + +.PHONY: reports +reports: replay-log report-replay check-state ## full reporting pipeline (replay + compare + write) + @echo "" + @echo "Reports:" + @echo " $(OUT)/replay.log" + @echo " $(OUT)/REPLAY-REPORT.md" + @echo " $(OUT)/STATE-REPORT.md" diff --git a/misc/hf-glue/README.md b/misc/hf-glue/README.md new file mode 100644 index 00000000000..c38509cf8d8 --- /dev/null +++ b/misc/hf-glue/README.md @@ -0,0 +1,216 @@ +# misc/hf-glue — HIGHLY EXPERIMENTAL hardfork testbed + +> ⚠️ **DO NOT MERGE. DO NOT USE IN PRODUCTION.** ⚠️ +> +> Throwaway integration harness for the hardfork-replay mechanism. +> Depends on (now split into) these PRs: +> +> - [#5511](https://github.com/gnolang/gno/pull/5511) `feat/genesis-replay-upgrade3` — replay engine: `PastChainIDs`, `GnoTxMetadata.{BlockHeight,ChainID,Failed,SignerInfo}`, `InitialHeight` +> - [#5540](https://github.com/gnolang/gno/pull/5540) hardfork-replay improvements — `tm2/sdk` `InitialHeight > 1` fixes, genesis-mode `PastChainIDs[0]` override, `hardfork --patch-realm` +> - [#5533](https://github.com/gnolang/gno/pull/5533) `contribs/tx-archive` — hardfork-replay metadata, `SignerInfo` brute-force resolver, progress log, gas-replay report +> - [#5376](https://github.com/gnolang/gno/pull/5376) gnoland-1 chain config +> - [#5368](https://github.com/gnolang/gno/pull/5368) govDAO halt-height — fuels the `--patch-realm` demo +> +> Findings land in the PRs above, **not** on this branch. + +## What this gives you + +One command pulls a full source chain and replays it into a single-validator +fork that runs in Docker, serves RPC + gnoweb, and can optionally ship realm +upgrades inside the fork (via `--patch-realm`). + +``` +┌──────────────┐ 1) base genesis ┌───────────────────────────────┐ +│ GitHub │ ─────────────────►│ │ +│ release │ │ misc/hardfork: │ 3) hardfork +│ gnoland1.0 │ │ assemble genesis.json │ ───────────►┌──────────┐ +└──────────────┘ │ + PastChainIDs │ │ Docker │ + │ + InitialHeight │ │ gnoland │ +┌──────────────┐ 2) historical │ + SignerInfo per tx │ │ (single │ +│ rpc.gno.land │ ─ txs (batched) ─►│ + single local validator │ │ validator│ +│ (gnoland1) │ contribs/ │ + optional --patch-realm │ │ + gnoweb)│ +└──────────────┘ tx-archive └───────────────────────────────┘ └──────────┘ + │ + ▼ + http://localhost:26657 (RPC) + http://localhost:8888 (gnoweb) +``` + +End-to-end tested against gnoland1 (halt @ 704052): 2 637 historical txs, 192 MB +output genesis, **0 / 2715 tx failures** on replay, 1:1 render parity vs. prod +gno.land for sampled realms. + +## Requirements + +- Go 1.24+ (for building `hardfork` / `tx-archive` locally) +- Docker + `docker compose` +- `jq`, `curl`, `bash` + +## Quick start + +```bash +cd misc/hf-glue + +# 1. Pull base genesis (GitHub release by default) + all historical txs from RPC +# → out/source/config/genesis.json + out/source/txs.jsonl +# Then assemble the hardfork genesis → out/genesis.json +make fetch # ~12 min on full gnoland1 + +# 2. Generate a fresh single-validator identity and patch the genesis to use it. +# Also writes a config.toml binding RPC + p2p to 0.0.0.0. +make init + +# 3. Start the node + gnoweb in Docker. +make up + +# RPC at http://localhost:26657 +# gnoweb at http://localhost:8888 + +# Tail logs +make logs + +# Post txs against the fork from another terminal +gnokey maketx ... -remote http://localhost:26657 -chainid gnoland-1 + +# Stop but keep state +make down + +# Wipe db + WAL + last-signed state (keeps genesis + keys — lets you re-replay) +make reset-db + +# Nuclear reset (wipe everything, including out/) +make reset +``` + +### Picking a different source chain + +Everything is env-driven: + +```bash +SOURCE=http://rpc.test11.testnets.gno.land:443 \ +RPC_URL=http://rpc.test11.testnets.gno.land:443 \ +ORIGINAL_CHAIN_ID=test11 \ +CHAIN_ID=test11-hf \ +make fetch init up +``` + +| Variable | Default | Meaning | +|---|---|---| +| `SOURCE` | `https://github.com/gnolang/gno/releases/download/chain/gnoland1.0/genesis.json` | Base genesis. Direct `.json` URL, RPC endpoint (`/genesis` is fetched + unwrapped), or local file. | +| `RPC_URL` | `https://rpc.gno.land` | RPC endpoint used by `tx-archive` to pull historical blocks. | +| `ORIGINAL_CHAIN_ID` | `gnoland1` | Source chain ID — goes into `PastChainIDs` so historical tx sigs verify. | +| `CHAIN_ID` | `gnoland-1` | New chain ID after the fork. | +| `HALT_HEIGHT` | *(auto)* | Block to stop pulling at. Empty = RPC's current latest at start time. | +| `VALIDATOR_NAME` | `hf-glue-local` | Name baked into the single-validator entry in the genesis. | +| `PATCH_REALMS` | `gno.land/r/sys/params=$REPO/examples/gno.land/r/sys/params` | Space-separated `PKGPATH=SRCDIR` entries. Rewrites matching genesis-mode addpkg txs in-memory with files from the given dir — lets you deliver realm upgrades as part of the fork. Empty to disable. | +| `NODE_DIR`, `TXS_JSONL` | *(unset)* | Only used by `make fetch-from-dir` — local data dir alternative to the RPC pull. | + +## Delivering a realm upgrade inside the fork + +Default `PATCH_REALMS` swaps `r/sys/params` with the repo's examples copy. After +merging [#5368](https://github.com/gnolang/gno/pull/5368), that copy gains +`halt.gno` with `NewSetHaltRequest` — so the fork boots with the govDAO halt +mechanism available, without the realm ever having been redeployed on-chain: + +``` +$ curl -sG "http://localhost:26657/abci_query?path=%22vm%2Fqfile%22" \ + --data-urlencode "data=gno.land/r/sys/params" \ + | jq -r '.result.response.ResponseBase.Data' | base64 -d +fee_collector.gno +gnomod.toml +halt.gno ← added by --patch-realm +params.gno +unlock.gno +``` + +The source genesis on disk stays pristine — patches are applied only during +hardfork assembly, in memory. + +## Make targets + +| target | what | +|---|---| +| `make fetch` | Pull base genesis + all blocks, assemble `out/genesis.json` | +| `make fetch-from-dir` | Alt: assemble from a local gnoland data dir (requires `NODE_DIR` + `TXS_JSONL`) | +| `make init` | Generate validator secrets + `config.toml` + patch genesis to single validator | +| `make up` | Docker compose up (gnoland + gnoweb) | +| `make down` | Stop containers, keep state | +| `make logs` | Tail `gnoland` logs | +| `make status` | Print `/status` JSON | +| `make reset-db` | Wipe DB + WAL + last-signed state (lets you re-replay without nuking keys) | +| `make reset` | Nuclear — wipe all of `out/` | +| `make smoketest` | In-memory replay via `hardfork test --verbose` (no Docker, no persisted state) | +| `make replay-log` | Same as smoketest, tee full log to `out/replay.log` + summary | +| `make report-replay` | Build categorized `out/REPLAY-REPORT.md` from replay log | +| `make check-state` | Compare running node vs `gno.land` prod, write `out/STATE-REPORT.md` | +| `make reports` | Full pipeline: `replay-log` + `report-replay` + `check-state` | +| `make gen-local-genesis` | (Alternative) Rebuild gnoland1 base genesis locally via `misc/deployments/gnoland1/gen-genesis.sh` instead of downloading | + +## Files + +| path | purpose | +|---|---| +| `Makefile` | Entrypoint targets above | +| `docker-compose.yml` | `gnoland` + `gnoweb` services, Dockerfile `target=all` image | +| `scripts/fetch.sh` | 3-stage: download genesis, run `tx-archive backup`, assemble with `hardfork genesis` | +| `scripts/fetch-from-dir.sh` | Local-dir alternative (no RPC) | +| `scripts/init-node.sh` | `gnoland secrets init` + `config init` + rewrite validator via `fixvalidator` | +| `scripts/gen-local-genesis.sh` | Calls `misc/deployments/gnoland1/gen-genesis.sh` if you want to rebuild rather than download | +| `scripts/replay-log.sh` | In-process replay + log capture | +| `scripts/report-replay.sh` | Build `REPLAY-REPORT.md` from log | +| `scripts/check-state.sh` | Local vs prod comparison report | +| `fixvalidator/` | Tiny Go helper that overwrites the genesis validator set with a single local key | +| `out/` | *(gitignored)* all generated artifacts — genesis, secrets, node data, reports | + +## Status (halt @ 704052, full gnoland1 chain) + +What's been validated end-to-end on this branch: + +- [x] Account numbers / sequences preserved via `SignerInfo` brute-force resolver +- [x] Historical tx signatures verify via `PastChainIDs` allowlist +- [x] `InitialHeight > 1` handled across consensus, state, store, SDK (`BaseApp.validateHeight`, `BaseApp.Info`, `saveState`) +- [x] First block produced at `InitialHeight` exactly (704053) +- [x] Node restarts from persisted state cleanly +- [x] Genesis-mode + historical tx replay: **0 / 2715 failures** (one unrelated `r/sys/txfees` storage-deposit failure not caused by the replay itself) +- [x] Chain-ID switch `gnoland1` → `gnoland-1` verified on `/status` + on every historical-tx sig verification +- [x] Realm parity vs. prod: `r/sys/names`, `r/sys/users`, `r/gov/dao` (+`:proposals`), `r/gnoland/blog`, `r/gnoland/coins`, `r/gnoland/wugnot` all ✅ +- [x] Manfred's account: `account_num=3096261`, `sequence=31` — matches production exactly +- [x] Delivering a realm upgrade as part of the fork via `--patch-realm` (demo: `r/sys/params` gains `halt.gno` from [#5368](https://github.com/gnolang/gno/pull/5368)) +- [x] `contribs/tx-archive` pulls the full chain in ~12 min with progress logging + +Things the testbed does **not** cover: + +- Multi-validator scenarios (we run a single local validator) +- `--skip-failing-genesis-txs` is still enabled; a few gnogenesis txs with + `msg.Creator ≠ signing key` fail the pubkey-address check and are skipped. + Production gnoland1 uses the same flag for the same reason. +- Parameter-set preservation (e.g. `valoper` gas-fee=0) is not explicitly + asserted — relies on `app_state` carrying over. + +## Relation to `hardfork test` + +`hardfork test` (in `misc/hardfork`) does an in-memory smoke-test — node runs, +replays in RAM, exits. Perfect for CI. **This testbed is the opposite**: +persistent disk state, real Docker node, keeps running, accepts txs, exposes +gnoweb — meant for a human to poke at. + +## Reproducing end-to-end + +```bash +cd misc/hf-glue +make fetch && make init && make up + +# Wait ~30s for replay to finish and the first block to commit. +curl -s http://localhost:26657/status | jq '.result.sync_info.latest_block_height' +# → 704053+ + +# Verify the patched realm +curl -sG "http://localhost:26657/abci_query?path=%22vm%2Fqfile%22" \ + --data-urlencode "data=gno.land/r/sys/params" \ + | jq -r '.result.response.ResponseBase.Data' | base64 -d | grep halt.gno +# → halt.gno + +# Generate reports +make reports +ls -la out/*.md +``` diff --git a/misc/hf-glue/docker-compose.yml b/misc/hf-glue/docker-compose.yml new file mode 100644 index 00000000000..92455c224c5 --- /dev/null +++ b/misc/hf-glue/docker-compose.yml @@ -0,0 +1,55 @@ +# misc/hf-glue — HIGHLY EXPERIMENTAL hardfork testbed. +# Runs a single-validator gnoland node replaying a hardforked genesis. +# +# Prereqs (run from misc/hf-glue): +# make fetch init +# Then: +# make up + +services: + gnoland: + container_name: hf-glue-node + image: hf-glue-gnoland:latest + build: + context: ../.. + dockerfile: Dockerfile + target: all + restart: unless-stopped + command: + - start + - --lazy=true + # Genesis-mode txs (BlockHeight==0) from gnogenesis are signed by the + # deployer key while msg.Creator is a different address (manfred, etc). + # The ante handler's pubkey-address check rejects these. Production + # gnoland1 uses the same flag; historical txs (BlockHeight>0) are still + # signature-verified via PastChainIDs. + - --skip-genesis-sig-verification + - --skip-failing-genesis-txs + - --data-dir=/gnoroot/gnoland-data + - --genesis=/gnoroot/gnoland-data/genesis.json + environment: + GNOROOT: /gnoroot + volumes: + - ./out/gnoland-home:/gnoroot/gnoland-data + entrypoint: ["/usr/bin/gnoland"] + ports: + - "26656:26656" # p2p + - "26657:26657" # rpc + stop_grace_period: 30s + + gnoweb: + container_name: hf-glue-gnoweb + image: hf-glue-gnoland:latest + depends_on: + - gnoland + restart: unless-stopped + command: + - -remote=http://gnoland:26657 + - -chainid=${CHAIN_ID:-gnoland-1} + - -help-chainid=${CHAIN_ID:-gnoland-1} + - -help-remote=http://127.0.0.1:26657 + - -bind=0.0.0.0:8888 + entrypoint: ["/usr/bin/gnoweb"] + ports: + - "8888:8888" + stop_grace_period: 10s diff --git a/misc/hf-glue/fixvalidator/main.go b/misc/hf-glue/fixvalidator/main.go new file mode 100644 index 00000000000..da31cb51b74 --- /dev/null +++ b/misc/hf-glue/fixvalidator/main.go @@ -0,0 +1,222 @@ +// fixvalidator rewrites the validator set in a gnoland genesis.json. +// Input modes (mutually exclusive): +// - priv_validator_key.json path (single validator) +// - bech32 address + pubkey pair (single validator, key-less environments) +// - valset-list file (multi-validator, gno-cluster-style lines) +// +// Usage: +// +// fixvalidator --priv-key --genesis [--name NAME] [--power N] +// fixvalidator --address g1... --pubkey gpub1... --genesis [--name NAME] [--power N] +// fixvalidator --valset-list --genesis +// +// valset-list format: one validator per line, three whitespace-separated +// fields — " ". Blank lines and lines starting with '#' +// are ignored. Addresses are derived from pubkeys. This matches gno-cluster's +// INITIAL_VALSET output (strip the `INITIAL_VALSET=(` wrapper and quotes +// before feeding it in). +// +// This is testbed glue (misc/hf-glue). Not intended to be installed. +package main + +import ( + "bufio" + "encoding/json" + "flag" + "fmt" + "io" + "os" + "strconv" + "strings" + + _ "github.com/gnolang/gno/gno.land/pkg/gnoland" // register GnoGenesisState amino type + "github.com/gnolang/gno/tm2/pkg/amino" + signer "github.com/gnolang/gno/tm2/pkg/bft/privval/signer/local" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" +) + +func main() { + var ( + privPath string + addrStr string + pubkeyStr string + valsetList string + genesisPath string + name string + power int64 + emitJSON bool + ) + + flag.StringVar(&privPath, "priv-key", "", "path to priv_validator_key.json (single-validator mode)") + flag.StringVar(&addrStr, "address", "", "validator bech32 address g1... (requires --pubkey; single-validator mode)") + flag.StringVar(&pubkeyStr, "pubkey", "", "validator bech32 pubkey gpub1... (requires --address)") + flag.StringVar(&valsetList, "valset-list", "", "path to valset-list file (multi-validator mode; ' ' per line)") + flag.StringVar(&genesisPath, "genesis", "", "path to genesis.json to rewrite in place (omit with --emit-json)") + flag.StringVar(&name, "name", "hf-glue-local", "validator name (single-validator mode only)") + flag.Int64Var(&power, "power", 10, "validator voting power (single-validator mode only)") + flag.BoolVar(&emitJSON, "emit-json", false, "print the resolved valset as hf-glue's NEW_VALSET_JSON format to stdout and exit (no --genesis needed)") + flag.Parse() + + if !emitJSON && genesisPath == "" { + fmt.Fprintln(os.Stderr, "--genesis is required (or use --emit-json)") + os.Exit(2) + } + + validators, err := resolveValidators(privPath, addrStr, pubkeyStr, valsetList, name, power) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(2) + } + + if emitJSON { + if err := emitNewValsetJSON(os.Stdout, validators); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } + return + } + + if err := run(genesisPath, validators); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } +} + +// emitNewValsetJSON writes the validator set as a JSON array in the same +// shape hf-glue's migrate.sh and build.sh expect for NEW_VALSET_JSON — +// {address, pub_key, voting_power, name}. +func emitNewValsetJSON(w io.Writer, validators []bft.GenesisValidator) error { + type entry struct { + Address string `json:"address"` + PubKey string `json:"pub_key"` + VotingPower int64 `json:"voting_power"` + Name string `json:"name"` + } + entries := make([]entry, len(validators)) + for i, v := range validators { + entries[i] = entry{ + Address: v.Address.String(), + PubKey: v.PubKey.String(), + VotingPower: v.Power, + Name: v.Name, + } + } + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(entries) +} + +// resolveValidators chooses the input mode and returns the new valset. Modes +// are mutually exclusive; priv-key > address+pubkey > valset-list in precedence. +func resolveValidators(privPath, addrStr, pubkeyStr, valsetList, name string, power int64) ([]bft.GenesisValidator, error) { + switch { + case privPath != "": + pv, err := signer.LoadFileKey(privPath) + if err != nil { + return nil, fmt.Errorf("load priv key: %w", err) + } + return []bft.GenesisValidator{{Address: pv.Address, PubKey: pv.PubKey, Power: power, Name: name}}, nil + + case addrStr != "" || pubkeyStr != "": + if addrStr == "" || pubkeyStr == "" { + return nil, fmt.Errorf("--address and --pubkey must be provided together") + } + address, err := crypto.AddressFromBech32(addrStr) + if err != nil { + return nil, fmt.Errorf("parse address %q: %w", addrStr, err) + } + pubkey, err := crypto.PubKeyFromBech32(pubkeyStr) + if err != nil { + return nil, fmt.Errorf("parse pubkey %q: %w", pubkeyStr, err) + } + if derived := pubkey.Address(); address != derived { + return nil, fmt.Errorf("--address %s does not match --pubkey (derives %s)", address, derived) + } + return []bft.GenesisValidator{{Address: address, PubKey: pubkey, Power: power, Name: name}}, nil + + case valsetList != "": + f, err := os.Open(valsetList) + if err != nil { + return nil, fmt.Errorf("open valset-list %q: %w", valsetList, err) + } + defer f.Close() + return parseValsetList(f) + + default: + return nil, fmt.Errorf("one of --priv-key, --address/--pubkey, or --valset-list is required") + } +} + +// parseValsetList reads the multi-validator list format. Format per line: +// +// +// +// Blank lines and lines whose first non-space character is '#' are ignored. +// Pubkey addresses are derived via crypto.PubKey.Address(). +func parseValsetList(r io.Reader) ([]bft.GenesisValidator, error) { + var out []bft.GenesisValidator + scanner := bufio.NewScanner(r) + lineNo := 0 + for scanner.Scan() { + lineNo++ + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + fields := strings.Fields(line) + if len(fields) != 3 { + return nil, fmt.Errorf("line %d: want 3 fields ' ', got %d", lineNo, len(fields)) + } + name, powerStr, pubkeyStr := fields[0], fields[1], fields[2] + power, err := strconv.ParseInt(powerStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("line %d: invalid power %q: %w", lineNo, powerStr, err) + } + pubkey, err := crypto.PubKeyFromBech32(pubkeyStr) + if err != nil { + return nil, fmt.Errorf("line %d: invalid pubkey %q: %w", lineNo, pubkeyStr, err) + } + out = append(out, bft.GenesisValidator{ + Address: pubkey.Address(), + PubKey: pubkey, + Power: power, + Name: name, + }) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan valset-list: %w", err) + } + if len(out) == 0 { + return nil, fmt.Errorf("valset-list is empty") + } + return out, nil +} + +func run(genesisPath string, validators []bft.GenesisValidator) error { + genDoc, err := bft.GenesisDocFromFile(genesisPath) + if err != nil { + return fmt.Errorf("load genesis: %w", err) + } + + oldCount := len(genDoc.Validators) + genDoc.Validators = validators + + if err := genDoc.ValidateAndComplete(); err != nil { + return fmt.Errorf("validate genesis after rewrite: %w", err) + } + + data, err := amino.MarshalJSONIndent(genDoc, "", " ") + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + if err := os.WriteFile(genesisPath, data, 0o644); err != nil { + return fmt.Errorf("write genesis: %w", err) + } + + fmt.Printf("replaced %d validator(s) with %d new validator(s):\n", oldCount, len(validators)) + for i, v := range validators { + fmt.Printf(" [%d] %s (%s), power=%d, name=%q\n", i, v.Address, v.PubKey, v.Power, v.Name) + } + return nil +} diff --git a/misc/hf-glue/fixvalidator/main_test.go b/misc/hf-glue/fixvalidator/main_test.go new file mode 100644 index 00000000000..cffe31d7293 --- /dev/null +++ b/misc/hf-glue/fixvalidator/main_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "strings" + "testing" +) + +func TestParseValsetList(t *testing.T) { + // Three distinct pubkeys so Address() derivations are distinct too. Values + // are real bech32 pubkeys from gnoland ed25519 keys; they're not + // round-tripped here, just parsed. + in := ` +# comment line, ignored +node-1 1 gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0wau58zgeg7g9z5hn9k9p4emkjckpnfxhg5h30s7h08yza4dffwxqc8fqd + +node-2 5 gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0wau58zgeg7g9z5hn9k9p4emkjckpnfxhg5h30s7h08yza4dffwxqc8fqd +node-3 10 gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0wau58zgeg7g9z5hn9k9p4emkjckpnfxhg5h30s7h08yza4dffwxqc8fqd +` + got, err := parseValsetList(strings.NewReader(in)) + if err != nil { + t.Fatalf("parseValsetList: unexpected error: %v", err) + } + if len(got) != 3 { + t.Fatalf("want 3 validators, got %d", len(got)) + } + wantNames := []string{"node-1", "node-2", "node-3"} + wantPowers := []int64{1, 5, 10} + for i, v := range got { + if v.Name != wantNames[i] { + t.Errorf("[%d] Name = %q, want %q", i, v.Name, wantNames[i]) + } + if v.Power != wantPowers[i] { + t.Errorf("[%d] Power = %d, want %d", i, v.Power, wantPowers[i]) + } + if v.PubKey == nil { + t.Errorf("[%d] PubKey is nil", i) + continue + } + if v.Address != v.PubKey.Address() { + t.Errorf("[%d] Address %s != derived %s", i, v.Address, v.PubKey.Address()) + } + } +} + +func TestParseValsetList_BadLines(t *testing.T) { + cases := map[string]string{ + "too-few-fields": "node-1 1\n", + "bad-power": "node-1 NaN gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0wau58zgeg7g9z5hn9k9p4emkjckpnfxhg5h30s7h08yza4dffwxqc8fqd\n", + "bad-pubkey": "node-1 1 not-a-pubkey\n", + "empty": "\n\n# just comments\n", + } + for name, in := range cases { + t.Run(name, func(t *testing.T) { + if _, err := parseValsetList(strings.NewReader(in)); err == nil { + t.Fatalf("expected error, got nil") + } + }) + } +} diff --git a/misc/hf-glue/scripts/assert-migrations.sh b/misc/hf-glue/scripts/assert-migrations.sh new file mode 100755 index 00000000000..229b9941a61 --- /dev/null +++ b/misc/hf-glue/scripts/assert-migrations.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# assert-migrations.sh — verify that the hf-glue post-replay state matches +# the intent of every migration step (see +# misc/deployments/gnoland-1/migrations/). Run against a live node after +# genesis replay has completed. +# +# Why: with --skip-failing-genesis-txs, any migration that silently panics is +# absorbed without aborting the chain, so a defective rc can boot and look +# fine while leaving the T1 rotation, v3 deploy, or param flips unapplied. +# This script is the positive check that catches silent migration misfires. +# +# Env +# === +# REMOTE RPC endpoint to query (default http://localhost:26657) +# EXPECTED_T1 bech32 address expected to be the sole post-rotation +# T1 member. When rotation is configured, this is the +# value of NEW_T1_ADDR used at genesis build time. +# Defaults to gnoland-1's production T1 (g1aeddlft…), +# override when building from a different CALLER. +# EXPECTED_VALSET_ADDRS Space-separated list of bech32 g1 addresses that +# should appear in r/sys/validators/v2 after +# migration 01. Leave empty to skip the v2-valset +# check. +# GNOKEY_BIN gnokey binary (default: gnokey on $PATH) +# +# Exit status +# =========== +# 0 — all assertions passed +# 1 — one or more assertions failed (details printed to stdout) +# 2 — prerequisite error (bad tool, RPC unreachable, etc.) +set -euo pipefail + +REMOTE="${REMOTE:-http://localhost:26657}" +EXPECTED_T1="${EXPECTED_T1:-g1aeddlftlfk27ret5rf750d7w5dume3kcsm8r8m}" +EXPECTED_VALSET_ADDRS="${EXPECTED_VALSET_ADDRS:-}" +GNOKEY_BIN="${GNOKEY_BIN:-gnokey}" + +# Manfred is the pre-rotation sole T1 on gnoland1; compared against +# $EXPECTED_T1 to decide whether to assert manfred was withdrawn. +readonly MANFRED="g1manfred47kzduec920z88wfr64ylksmdcedlf5" + +command -v "$GNOKEY_BIN" >/dev/null 2>&1 || { + echo "gnokey not found on PATH (set GNOKEY_BIN=...)" >&2 + exit 2 +} + +fail=0 +pass=0 + +# ---- Helpers + +# Extracts the `data:` line from a `gnokey query` response, stripping the +# leading `data: ` prefix. Single-line extraction — if the response body spans +# multiple lines, only the first is returned. +extract_data() { + awk 'NR==FNR{next}/^data:/{sub(/^data: /,""); print; exit}' /dev/null "$@" +} + +query_param() { + local key="$1" + "$GNOKEY_BIN" query -remote "$REMOTE" "params/$key" 2>/dev/null | + awk '/^data:/{sub(/^data: /,""); print; exit}' +} + +query_qeval() { + local expr="$1" + "$GNOKEY_BIN" query -remote "$REMOTE" "vm/qeval" --data "$expr" 2>/dev/null | + awk '/^data:/{sub(/^data: /,""); print; exit}' +} + +query_qfuncs_raw() { + local pkg="$1" + "$GNOKEY_BIN" query -remote "$REMOTE" "vm/qfuncs" --data "$pkg" 2>/dev/null +} + +check_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$actual" == "$expected" ]]; then + printf ' [OK] %s\n' "$desc" + pass=$((pass + 1)) + else + printf ' [FAIL] %s\n want=%s\n got =%s\n' "$desc" "$expected" "$actual" + fail=$((fail + 1)) + fi +} + +check_contains() { + local desc="$1" needle="$2" haystack="$3" + if [[ "$haystack" == *"$needle"* ]]; then + printf ' [OK] %s\n' "$desc" + pass=$((pass + 1)) + else + printf ' [FAIL] %s\n want substring=%s\n in=%s\n' "$desc" "$needle" "$haystack" + fail=$((fail + 1)) + fi +} + +check_not_contains() { + local desc="$1" needle="$2" haystack="$3" + if [[ "$haystack" != *"$needle"* ]]; then + printf ' [OK] %s\n' "$desc" + pass=$((pass + 1)) + else + printf ' [FAIL] %s\n did NOT want substring=%s\n in=%s\n' "$desc" "$needle" "$haystack" + fail=$((fail + 1)) + fi +} + +echo "━━━ assert-migrations against $REMOTE ━━━" +echo " expected T1: $EXPECTED_T1" +[[ -n "$EXPECTED_VALSET_ADDRS" ]] && echo " expected v2: $EXPECTED_VALSET_ADDRS" +echo + +# ---- Sanity: node reachable +if ! "$GNOKEY_BIN" query -remote "$REMOTE" ".app/version" >/dev/null 2>&1; then + echo " RPC unreachable at $REMOTE" >&2 + exit 2 +fi + +# ---- Migration 05 + 07 (sysnames namespace check disable/restore) +# Step 05 sets the vm param to "", step 07 restores it. If 06 (addpkg v3) +# fails silently before 07 runs, the path stays empty and the authz check +# is permanently off — exactly the silent-failure case this guards against. +check_eq 'migration 05+07: vm:p:sysnames_pkgpath restored to r/sys/names' \ + '"gno.land/r/sys/names"' \ + "$(query_param 'vm:p:sysnames_pkgpath')" + +# r/sys/names internal flag stayed true through the restore (we never touched +# .enabled, only the VM param pointing at the pkg). +check_eq 'r/sys/names.IsEnabled() still true after migration' \ + '(true bool)' \ + "$(query_qeval 'gno.land/r/sys/names.IsEnabled()')" + +# ---- Migration 06 (addpkg r/sys/validators/v3) +v3funcs="$(query_qfuncs_raw 'gno.land/r/sys/validators/v3')" +check_contains 'migration 06: v3 realm deployed (NewValsetChangeExecutor exported)' \ + 'NewValsetChangeExecutor' "$v3funcs" +check_contains 'migration 06: v3 realm deployed (GetValidators exported)' \ + 'GetValidators' "$v3funcs" + +# ---- Migration 08 (valset_realm_path points at v3) +check_eq 'migration 08: vm:p:valset_realm_path = v3' \ + '"gno.land/r/sys/validators/v3"' \ + "$(query_param 'vm:p:valset_realm_path')" + +# ---- v3 pending-update drain +# If new_updates_available=true, EndBlocker hasn't consumed the last proposal +# yet — either the chain isn't producing blocks or the param path is wrong. +# valset_prev reflects the last applied valset; no strict assertion because +# it legitimately evolves as add-validator proposals land. Accept empty as +# equivalent to false: v3's init.gno only seeds valset_prev, so this key is +# unset at fresh boot until the first change proposal writes it. +nua="$(query_param 'vm:gno.land/r/sys/validators/v3:new_updates_available')" +if [[ "$nua" == "false" || -z "$nua" ]]; then + printf ' [OK] v3: no pending valset update unconsumed by EndBlocker\n' + pass=$((pass + 1)) +else + printf ' [FAIL] v3: no pending valset update unconsumed by EndBlocker\n got =%s\n' "$nua" + fail=$((fail + 1)) +fi + +# ---- Migration 01 (v2 valset swapped) — informational only +# v2 is vestigial after PR #5485: EndBlocker no longer reads from it. The +# reset_valset migration is cosmetic and is known to partial-apply when a +# removed-validator exists in the batch (the whole proposal panics on the +# first missing entry and leaves subsequent removes + the new-validator add +# unapplied). Surface the current state for visual inspection but do not +# fail the check — consensus is driven by GenesisDoc.Validators and v3. +if [[ -n "$EXPECTED_VALSET_ADDRS" ]]; then + v2out="$(query_qeval 'gno.land/r/sys/validators/v2.GetValidators()')" + for addr in $EXPECTED_VALSET_ADDRS; do + if [[ "$v2out" == *"$addr"* ]]; then + printf ' [OK] migration 01 (informational): r/sys/validators/v2 has %s\n' "$addr" + pass=$((pass + 1)) + else + printf ' [WARN] migration 01 (informational): r/sys/validators/v2 missing %s (v2 is dead code post-PR#5485)\n' "$addr" + fi + done +fi + +# ---- Migration 02-04 (T1 rotation) +# memberstore.Get() has an ACL that rejects calls from qeval's empty-realm +# context, so membership is read from the public render endpoints: +# /r/gov/dao/v3/memberstore — tier summary ("Tier T1 contains N members") +# /r/gov/dao/v3/memberstore:members — tabular member list with T1/T2/T3 rows +summary="$("$GNOKEY_BIN" query -remote "$REMOTE" vm/qrender --data 'gno.land/r/gov/dao/v3/memberstore:' 2>/dev/null)" +members="$("$GNOKEY_BIN" query -remote "$REMOTE" vm/qrender --data 'gno.land/r/gov/dao/v3/memberstore:members' 2>/dev/null)" + +t1_count="$(printf '%s\n' "$summary" | grep -oE 'Tier T1 contains [0-9]+ members' | grep -oE '[0-9]+' | head -1)" +check_eq 'migration 02-04: govDAO T1 tier size = 1' '1' "${t1_count:-}" + +check_contains "migration 02-04: $EXPECTED_T1 is T1 member" \ + "| $EXPECTED_T1 |" "$members" + +if [[ "$EXPECTED_T1" != "$MANFRED" ]]; then + check_not_contains 'migration 03-04: manfred withdrawn from T1' \ + "| $MANFRED |" "$members" +fi + +echo +printf 'Results: %d ok, %d fail\n' "$pass" "$fail" +exit "$fail" diff --git a/misc/hf-glue/scripts/audit-balances.sh b/misc/hf-glue/scripts/audit-balances.sh new file mode 100755 index 00000000000..b740b759bda --- /dev/null +++ b/misc/hf-glue/scripts/audit-balances.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# audit-balances.sh — compare every historical tx signer's balance between +# the source chain at halt_height and our replay, to surface accounts that +# diverged (mostly: accounts with balance on mainnet that evaporated during +# replay because of post-mainnet storage-deposit semantics). +# +# Output +# ====== +# out/BALANCE-AUDIT.md — markdown table of every diverged signer with +# mainnet balance, replay balance, and the ugnot delta. Rows sorted by +# descending delta so the largest divergences appear first. The existing +# hf_topup_balance pipeline in migrate.sh consumes a flat address list; +# this audit's output is the raw data for choosing which addresses to +# add to that pipeline. +# +# Why +# === +# Under --skip-failing-genesis-txs we observe ~2580 InsufficientFunds +# failures at replay. Most come from historical signers whose balance on +# mainnet was intact right up to halt_height, but which get drained in +# replay by new storage-deposit charges that didn't exist on the source +# chain. Without this audit, a production launch silently accepts those +# balance drops — user txs that once worked would keep working if we +# compensated. +# +# Approach +# ======== +# 1. Walk out/source/txs.jsonl, extract (signer_address, tx_count). Pull +# signer_info[0].address from the tx metadata; tx-archive produces one +# address per tx, multi-sig not currently modeled here. +# 2. For each unique signer: query auth/accounts at mainnet@halt_height +# and at the replay node. Parse coins → ugnot amount. +# 3. Emit the diff. No heuristics on criticality — that's a human call. +# +# Env +# === +# SOURCE_RPC source chain RPC (default https://rpc.gno.land) +# REPLAY_RPC replay node RPC (default http://localhost:26657) +# HALT_HEIGHT source-chain height to query balances at (required) +# TXS_JSONL path to cached txs.jsonl (default $OUT/source/txs.jsonl) +# OUT misc/hf-glue/out (auto-resolved) +# SIGNER_LIMIT unique signers to audit (default 200; use 0 for all). +# Auditing all can be thousands of RPC calls. +# GNOKEY_BIN gnokey binary (default: gnokey on $PATH) +# +# Exit status +# =========== +# 0 — audit completed (even if divergences found — this is informational) +# 2 — prerequisite error +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="${OUT:-$(cd "$SCRIPT_DIR/.." && pwd)/out}" +mkdir -p "$OUT" + +SOURCE_RPC="${SOURCE_RPC:-https://rpc.gno.land}" +REPLAY_RPC="${REPLAY_RPC:-http://localhost:26657}" +TXS_JSONL="${TXS_JSONL:-$OUT/source/txs.jsonl}" +SIGNER_LIMIT="${SIGNER_LIMIT:-200}" +GNOKEY_BIN="${GNOKEY_BIN:-gnokey}" + +: "${HALT_HEIGHT:?HALT_HEIGHT is required}" + +command -v "$GNOKEY_BIN" >/dev/null 2>&1 || { + echo "gnokey not found" >&2 + exit 2 +} +command -v jq >/dev/null 2>&1 || { + echo "jq not found" >&2 + exit 2 +} +[[ -f "$TXS_JSONL" ]] || { + echo "txs.jsonl not found at $TXS_JSONL" >&2 + exit 2 +} + +REPORT="$OUT/BALANCE-AUDIT.md" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# ---- Helpers + +# bank/balances/ returns a one-line data response: +# data: "ugnot,denom2,..." +# We parse only ugnot. Using bank/balances instead of auth/accounts because +# the latter multi-line JSON is annoying to parse from gnokey's text output. +query_balance_ugnot() { + local rpc="$1" addr="$2" height="${3:-0}" + local args=(-remote "$rpc") + [[ "$height" -gt 0 ]] && args+=(-height "$height") + + local data + data="$("$GNOKEY_BIN" query "${args[@]}" "bank/balances/$addr" 2>/dev/null | + awk '/^data:/{sub(/^data: /,""); print; exit}')" + # data is a quoted string: "754954090ugnot,123foo" (or "" for empty). + # Strip surrounding quotes, then extract ugnot amount. + data="${data#\"}" + data="${data%\"}" + if [[ -z "$data" ]]; then + echo 0 + return + fi + local ugnot + ugnot="$(printf '%s' "$data" | + tr ',' '\n' | + awk '/ugnot$/ { + sub(/ugnot$/,"") + print + exit + }')" + echo "${ugnot:-0}" +} + +# Extract unique signer addresses from txs.jsonl, newest-first by their +# occurrence order. BSD awk: no `length()` on arrays with gawk-style +# semantics, but we sort by count after. +extract_signers() { + jq -r '.metadata.signer_info[0].address // empty' <"$TXS_JSONL" | + sort | uniq -c | sort -rn | + awk '{ print $2 "\t" $1 }' +} + +# ---- Extract signer set +echo "━━━ audit-balances ━━━" +echo " source $SOURCE_RPC @ height=$HALT_HEIGHT" +echo " replay $REPLAY_RPC @ current tip" +echo " txs $TXS_JSONL" +echo " limit $SIGNER_LIMIT unique signers (0 = all)" +echo + +extract_signers >"$WORK/signers.tsv" +total_signers="$(wc -l <"$WORK/signers.tsv" | tr -d ' ')" +echo " found $total_signers unique signers in txs.jsonl" + +if [[ "$SIGNER_LIMIT" -gt 0 ]]; then + head -n "$SIGNER_LIMIT" "$WORK/signers.tsv" >"$WORK/audit.tsv" +else + cp "$WORK/signers.tsv" "$WORK/audit.tsv" +fi +audit_count="$(wc -l <"$WORK/audit.tsv" | tr -d ' ')" +echo " auditing $audit_count" +echo + +# ---- Per-signer balance query +: >"$WORK/rows.tsv" +i=0 +while IFS=$'\t' read -r addr tx_count; do + i=$((i + 1)) + printf ' [%d/%d] %s ...\r' "$i" "$audit_count" "$addr" + + src_bal="$(query_balance_ugnot "$SOURCE_RPC" "$addr" "$HALT_HEIGHT")" + rep_bal="$(query_balance_ugnot "$REPLAY_RPC" "$addr" 0)" + delta=$((src_bal - rep_bal)) + + printf '%s\t%s\t%s\t%s\t%s\n' "$addr" "$tx_count" "$src_bal" "$rep_bal" "$delta" \ + >>"$WORK/rows.tsv" +done <"$WORK/audit.tsv" +echo + +# ---- Build report +# Sort by |delta| descending so the most diverged accounts are first. +sort -t$'\t' -k5 -rn "$WORK/rows.tsv" >"$WORK/rows.sorted.tsv" + +{ + echo "# Balance audit" + echo "" + echo "_Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)_" + echo "" + echo "- **Source**: \`$SOURCE_RPC\` at \`height=$HALT_HEIGHT\`" + echo "- **Replay**: \`$REPLAY_RPC\` at current tip" + echo "- **Audited**: $audit_count of $total_signers unique signers from \`txs.jsonl\`" + echo "" + echo "All amounts in ugnot. \`delta = source − replay\`; positive means the" + echo "account had more on mainnet than we have post-replay — a candidate" + echo "for \`hf_topup_balance\`. Zero delta means the replay matches source" + echo "balance exactly." + echo "" + echo "| Address | tx count on source | mainnet @ halt | replay @ tip | delta |" + echo "|---------|-------------------:|---------------:|-------------:|------:|" + + while IFS=$'\t' read -r addr tx_count src_bal rep_bal delta; do + [[ -z "$addr" ]] && continue + printf '| `%s` | %s | %s | %s | %s |\n' \ + "$addr" "$tx_count" "$src_bal" "$rep_bal" "$delta" + done <"$WORK/rows.sorted.tsv" + + echo "" + echo "---" + echo "" + echo "## Summary" + diverged="$(awk -F'\t' '$5 != 0 { c++ } END { print c+0 }' "$WORK/rows.sorted.tsv")" + positive="$(awk -F'\t' '$5 > 0 { c++ } END { print c+0 }' "$WORK/rows.sorted.tsv")" + sum_positive="$(awk -F'\t' '$5 > 0 { s += $5 } END { print s+0 }' "$WORK/rows.sorted.tsv")" + echo "" + echo "- Accounts audited: **$audit_count**" + echo "- Diverged (delta ≠ 0): **$diverged**" + echo "- Replay short of source (delta > 0): **$positive** accounts, total **$sum_positive ugnot**" + echo "" + if [[ "$SIGNER_LIMIT" -gt 0 && "$total_signers" -gt "$audit_count" ]]; then + echo "_Partial audit: $((total_signers - audit_count)) signer(s) not audited._" + echo "_Set SIGNER_LIMIT=0 to audit all signers._" + echo "" + fi +} >"$REPORT" + +echo "Report: $REPORT" +echo +printf 'Summary: audited=%d diverged=%d\n' "$audit_count" "$diverged" diff --git a/misc/hf-glue/scripts/audit-realm-imports.sh b/misc/hf-glue/scripts/audit-realm-imports.sh new file mode 100755 index 00000000000..79128781fbc --- /dev/null +++ b/misc/hf-glue/scripts/audit-realm-imports.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# audit-realm-imports.sh — scan every addpkg tx in out/source/txs.jsonl for +# imports that no longer resolve on the current gno checkout, so we catch +# realms that were valid on the source chain but now reference removed, +# renamed, or moved packages after the hardfork. +# +# Why +# === +# Historical addpkg txs pin imports to whatever stdlib + examples were +# available when they were signed on mainnet. Between then and now, gno +# has moved quickly: stdlib paths have been renamed (chain/runtime was +# std.* before, versioned packages like gno.land/p/nt/ufmt/v0 split out, +# some realms deleted entirely). After hardfork replay, a historical +# realm may deserialise fine but fail the first time someone calls into +# it because its imports no longer resolve. This audit surfaces them +# before they surface as user-facing errors. +# +# What it reports +# =============== +# • imports that no longer exist anywhere in the current tree +# • per-import count across all realms importing it (bigger blast +# radius first) +# • a summary tally of broken vs intact realms +# +# Env +# === +# TXS_JSONL default $OUT/source/txs.jsonl (historical post-genesis +# addpkg txs) +# SOURCE_GENESIS default $OUT/source/config/genesis.json (base genesis- +# mode addpkg txs). Most realms on mainnet ship through +# this path, not through historical txs, so omitting it +# would miss the bulk of the addpkg surface. +# REPO gno repo root (default: two dirs up from this script) +# OUT misc/hf-glue/out (auto-resolved) +# +# Exit +# ==== +# 0 — no broken imports found +# 1 — at least one realm imports a missing path +# 2 — prerequisite error +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HERE="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO="${REPO:-$(cd "$HERE/../.." && pwd)}" +OUT="${OUT:-$HERE/out}" +TXS_JSONL="${TXS_JSONL:-$OUT/source/txs.jsonl}" +SOURCE_GENESIS="${SOURCE_GENESIS:-$OUT/source/config/genesis.json}" + +command -v jq >/dev/null 2>&1 || { + echo "jq not found" >&2 + exit 2 +} +[[ -f "$SOURCE_GENESIS" ]] || { + echo "source genesis not found at $SOURCE_GENESIS" >&2 + exit 2 +} +[[ -f "$TXS_JSONL" ]] || { + echo "txs.jsonl not found at $TXS_JSONL" >&2 + exit 2 +} + +REPORT="$OUT/REALM-IMPORTS-AUDIT.md" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# ---- Build an inventory of pkgpaths that resolve on the current tree. +# Any path that exists as a directory under gnovm/stdlibs or +# examples// is considered resolvable. +build_known_paths() { + # stdlib: top-level dirs under gnovm/stdlibs/ with gno files. + # Path = the relative dir (e.g. "chain/params"). + find "$REPO/gnovm/stdlibs" -type f -name '*.gno' 2>/dev/null | + sed -e "s|^$REPO/gnovm/stdlibs/||" -e 's|/[^/]*$||' | + sort -u + + # examples: paths are "gno.land/..." derived from the directory tree. + find "$REPO/examples" -type f -name '*.gno' 2>/dev/null | + sed -e "s|^$REPO/examples/||" -e 's|/[^/]*$||' | + sort -u +} + +build_known_paths >"$WORK/known.tsv" +known_count="$(wc -l <"$WORK/known.tsv" | tr -d ' ')" + +# ---- Extract every (pkgpath, imported_path) edge from addpkg txs. +# Each addpkg tx: .tx.msg[i]."@type" == "/vm.m_addpkg", msg.package.path + +# msg.package.files[*].body. Walk the bodies, pull `import "X"` lines +# (simple single-import form — does not currently handle grouped imports +# that span multiple lines; see caveat below). +# +# Source set: (a) historical txs in txs.jsonl (post-genesis addpkg calls), +# (b) genesis-mode addpkg txs in source_genesis.app_state.txs (the bulk — +# gnoland1 deployed most realms here before block 1). Both streams use the +# same amino shape so a single jq extract works on each. +jq -r ' + select(.tx.msg != null) | + .tx.msg[] | + select(.["@type"] == "/vm.m_addpkg") | + .package as $p | + $p.files[] | + select(.name | endswith(".gno") and (endswith("_test.gno") | not) and (endswith("_filetest.gno") | not)) | + [.name, $p.path, .body] | @tsv +' "$TXS_JSONL" >"$WORK/files.tsv" 2>/dev/null || true + +jq -r ' + .app_state.txs[]? | + select(.tx.msg != null) | + .tx.msg[] | + select(.["@type"] == "/vm.m_addpkg") | + .package as $p | + $p.files[] | + select(.name | endswith(".gno") and (endswith("_test.gno") | not) and (endswith("_filetest.gno") | not)) | + [.name, $p.path, .body] | @tsv +' "$SOURCE_GENESIS" >>"$WORK/files.tsv" 2>/dev/null || true + +# Caveat: this parser handles two real-world styles: +# import "path" +# import ( +# "path1" +# "path2" +# ) +# Single-line block imports (import ("x"; "y")) are not gno-canonical +# and are ignored. +# BSD awk (macOS default) doesn't support match()'s 3-arg array form; we +# extract quoted paths with a simple regex + substr using RSTART/RLENGTH, +# which works on both gawk and BSD awk. +awk -F'\t' ' + function extract_quoted(line, start, len, s) { + # returns the first "quoted" substring in line, or "" if none + if (match(line, /"[^"]+"/)) { + s = substr(line, RSTART + 1, RLENGTH - 2) + return s + } + return "" + } + BEGIN { edges = 0 } + { + pkgpath = $2 + # File body comes in as a literal \n-separated string; split on \n. + n = split($3, lines, /\\n/) + in_block = 0 + for (i = 1; i <= n; i++) { + line = lines[i] + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + if (in_block) { + if (line == ")") { in_block = 0; continue } + sub(/[[:space:]]*\/\/.*$/, "", line) + q = extract_quoted(line) + if (q != "") { print pkgpath "\t" q; edges++ } + continue + } + if (line ~ /^import[[:space:]]+\(/) { in_block = 1; continue } + if (line ~ /^import[[:space:]]+/) { + q = extract_quoted(line) + if (q != "") { print pkgpath "\t" q; edges++ } + } + } + } +' "$WORK/files.tsv" | sort -u >"$WORK/edges.tsv" + +total_edges="$(wc -l <"$WORK/edges.tsv" | tr -d ' ')" + +# ---- For each imported path, decide if it resolves. +# Strip "gno.land/" prefix for examples lookup; stdlib paths have no prefix. +: >"$WORK/missing.tsv" +while IFS=$'\t' read -r pkgpath import; do + [[ -z "$import" ]] && continue + # Local tests sometimes import "." or relative; skip those conservatively. + case "$import" in + . | ./* | \.\./*) continue ;; + esac + # Match against known list. + if grep -qxF "$import" "$WORK/known.tsv"; then + continue + fi + printf '%s\t%s\n' "$import" "$pkgpath" >>"$WORK/missing.tsv" +done <"$WORK/edges.tsv" + +missing_edges="$(wc -l <"$WORK/missing.tsv" | tr -d ' ')" + +# ---- Tally: how many distinct missing imports, and which realms hit them. +cut -f1 "$WORK/missing.tsv" | sort | uniq -c | sort -rn >"$WORK/missing-by-import.tsv" +missing_imports="$(wc -l <"$WORK/missing-by-import.tsv" | tr -d ' ')" +affected_realms="$(cut -f2 "$WORK/missing.tsv" | sort -u | wc -l | tr -d ' ')" + +# ---- Emit report +{ + echo "# Realm-imports audit" + echo "" + echo "_Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)_" + echo "" + echo "- **Source txs**: \`$TXS_JSONL\`" + echo "- **Tree checked**: \`$REPO/gnovm/stdlibs\` + \`$REPO/examples\`" + echo "- **Known paths**: $known_count" + echo "- **Edges scanned**: $total_edges (unique \`(realm, import)\` pairs)" + echo "" + if [[ "$missing_edges" -eq 0 ]]; then + echo "## ✅ All historical imports resolve against the current tree" + echo "" + echo "Every realm deployed historically still imports packages that" + echo "exist in this branch. No post-fork import dangling." + else + echo "## ❌ $missing_imports distinct missing import(s) affecting $affected_realms realm(s)" + echo "" + echo "Each row below is an import path that no realm-importing-it can" + echo "resolve on this branch. If a user calls into an affected realm" + echo "post-fork, it will fail at load time." + echo "" + echo "| Import path | Realms affected |" + echo "|-------------|----------------:|" + while read -r count path; do + printf '| \`%s\` | %s |\n' "$path" "$count" + done <"$WORK/missing-by-import.tsv" + echo "" + echo "### Realms importing missing paths" + echo "" + cut -f2 "$WORK/missing.tsv" | sort -u | while read -r realm; do + printf -- '- \`%s\`\n' "$realm" + done + fi +} >"$REPORT" + +echo "Report: $REPORT" +echo +printf 'Edges=%d Missing=%d (imports=%d, realms=%d)\n' \ + "$total_edges" "$missing_edges" "$missing_imports" "$affected_realms" + +exit $((missing_edges > 0 ? 1 : 0)) diff --git a/misc/hf-glue/scripts/check-state.sh b/misc/hf-glue/scripts/check-state.sh new file mode 100755 index 00000000000..25b4925b769 --- /dev/null +++ b/misc/hf-glue/scripts/check-state.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# Probe the running hardfork node, compare against live gno.land, +# write a STATE-REPORT.md with findings. +# +# Usage: ./scripts/check-state.sh [address] +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$HERE/out" +REPORT="$OUT/STATE-REPORT.md" +mkdir -p "$OUT" + +LOCAL_RPC="${LOCAL_RPC:-http://127.0.0.1:26657}" +LOCAL_WEB="${LOCAL_WEB:-http://127.0.0.1:8888}" +PROD_RPC="${PROD_RPC:-https://rpc.gno.land}" +PROD_WEB="${PROD_WEB:-https://gno.land}" +ADDRESS="${1:-g1manfred47kzduec920z88wfr64ylksmdcedlf5}" + +b64() { printf '%s' "$1" | base64 | tr -d '\n'; } + +# Query vm/qrender: prints one of +# "OK:" +# "ERR:" +# "UNREACHABLE" +qrender() { + local rpc="$1" path="$2" + local data resp + data=$(b64 "$path") + resp=$(curl -sS --max-time 10 "${rpc}/abci_query?path=%22vm%2Fqrender%22&data=${data}" 2>/dev/null || echo "") + if [[ -z "$resp" ]]; then + echo "UNREACHABLE" + return + fi + local err + err=$(printf '%s' "$resp" | jq -r '.result.response.ResponseBase.Error."@type" // empty' 2>/dev/null || echo "") + if [[ -n "$err" ]]; then + echo "ERR:$err" + return + fi + local data_out + data_out=$(printf '%s' "$resp" | jq -r '.result.response.ResponseBase.Data // empty' 2>/dev/null || echo "") + local preview + preview=$(printf '%s' "$data_out" | tr -d '\n' | head -c 80) + echo "OK:${preview}" +} + +# Query chain status as JSON +chain_status() { + local rpc="$1" + curl -sS --max-time 5 "${rpc}/status" 2>/dev/null | + jq -c '{chain_id: .result.node_info.network, latest_block: .result.sync_info.latest_block_height, catching_up: .result.sync_info.catching_up}' \ + 2>/dev/null || echo '"unreachable"' +} + +# Query account balance via auth/accounts +account_info() { + local rpc="$1" addr="$2" + local resp data + resp=$(curl -sS --max-time 10 "${rpc}/abci_query?path=%22auth%2Faccounts%2F${addr}%22" 2>/dev/null || echo "") + data=$(printf '%s' "$resp" | jq -r '.result.response.ResponseBase.Data // empty' 2>/dev/null || echo "") + if [[ -z "$data" || "$data" == "null" ]]; then + echo "(no account)" + return + fi + printf '%s' "$data" | base64 -d 2>/dev/null | jq -c '.BaseAccount | {coins, account_number, sequence}' 2>/dev/null || echo "(decode failed)" +} + +# ---- start report ---------------------------------------------------------- +{ + echo "# Hardfork State Report" + echo "" + echo "_Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)_" + echo "" + echo "- **Local**: $LOCAL_RPC (gnoweb: $LOCAL_WEB)" + echo "- **Prod**: $PROD_RPC (gnoweb: $PROD_WEB)" + echo "- **Address under test**: \`$ADDRESS\`" + echo "" + echo "## Chain status" + echo "" + echo "| Chain | Status |" + echo "|-------|--------|" + echo "| Local | \`$(chain_status "$LOCAL_RPC")\` |" + echo "| Prod | \`$(chain_status "$PROD_RPC")\` |" + echo "" + echo "## Expected realms (should exist after hardfork)" + echo "" + echo "| Realm | Local | Prod |" + echo "|-------|-------|------|" + for realm in \ + "gno.land/r/sys/params:" \ + "gno.land/r/sys/names:" \ + "gno.land/r/sys/users:" \ + "gno.land/r/gov/dao:" \ + "gno.land/r/gov/dao:proposals" \ + "gno.land/r/gnoland/home:" \ + "gno.land/r/gnoland/blog:" \ + "gno.land/r/gnoland/valopers:" \ + "gno.land/r/gnoland/coins:" \ + "gno.land/r/gnoland/wugnot:"; do + l=$(qrender "$LOCAL_RPC" "$realm") + p=$(qrender "$PROD_RPC" "$realm") + # collapse to status icon + lt="❌ $l" + [[ $l == OK:* ]] && lt="✅" + pt="❌ $p" + [[ $p == OK:* ]] && pt="✅" + echo "| \`$realm\` | $lt | $pt |" + done + echo "" + echo "## Bank balance — \`$ADDRESS\`" + echo "" + echo "| Chain | auth/accounts | r/gnoland/coins:balances |" + echo "|-------|---------------|--------------------------|" + la=$(account_info "$LOCAL_RPC" "$ADDRESS") + pa=$(account_info "$PROD_RPC" "$ADDRESS") + lc=$(qrender "$LOCAL_RPC" "gno.land/r/gnoland/coins:balances?address=${ADDRESS}&coin" | head -c 120) + pc=$(qrender "$PROD_RPC" "gno.land/r/gnoland/coins:balances?address=${ADDRESS}&coin" | head -c 120) + echo "| Local | \`$la\` | \`$lc\` |" + echo "| Prod | \`$pa\` | \`$pc\` |" + echo "" + echo "## Gas / consensus params" + echo "" + echo "### Local consensus" + echo '```json' + curl -sS --max-time 5 "$LOCAL_RPC/consensus_params" 2>/dev/null | + jq '.result.consensus_params.Block' 2>/dev/null || echo "(unreachable)" + echo '```' + echo "" + echo "### Prod consensus" + echo '```json' + curl -sS --max-time 5 "$PROD_RPC/consensus_params" 2>/dev/null | + jq '.result.consensus_params.Block' 2>/dev/null || echo "(unreachable)" + echo '```' + echo "" + echo "### Local gas price" + echo '```json' + curl -sS --max-time 10 "$LOCAL_RPC/abci_query?path=%22auth%2Fgasprice%22" 2>/dev/null | + jq -r '.result.response.ResponseBase.Data // empty' | + base64 -d 2>/dev/null | + jq '.' 2>/dev/null || echo "(no data)" + echo '```' + echo "" + echo "### Prod gas price" + echo '```json' + curl -sS --max-time 10 "$PROD_RPC/abci_query?path=%22auth%2Fgasprice%22" 2>/dev/null | + jq -r '.result.response.ResponseBase.Data // empty' | + base64 -d 2>/dev/null | + jq '.' 2>/dev/null || echo "(no data)" + echo '```' + echo "" + # Surface synthetic balance top-ups (from hf_topup_balance) so a + # reviewer looking at STATE-REPORT.md sees the full picture: any + # address appearing here has a local/prod delta that is expected and + # documented, not a replay divergence. + if [[ -f "$OUT/TOPUP-REPORT.md" ]]; then + echo "## ⚠ Synthetic state modifications" + echo "" + echo "The hardfork replay ran against a genesis where the following" + echo "balances were synthetically increased by \`hf_topup_balance\`." + echo "These are NOT replay divergences — see \`out/TOPUP-REPORT.md\`" + echo "for the full audit trail with reasons." + echo "" + echo "| Address | Local balance | Prod balance | Reason |" + echo "|---------|--------------:|-------------:|--------|" + # Parse each table row from TOPUP-REPORT.md and probe both chains. + # Row shape: | `addr` | before | after | +delta | reason | + while IFS='|' read -r _ addr_cell _ _ _ reason_cell _; do + addr=$(printf '%s' "$addr_cell" | tr -d ' `') + [[ "$addr" =~ ^g1[0-9a-z]+$ ]] || continue + reason=$(printf '%s' "$reason_cell" | sed -e 's/^ *//' -e 's/ *$//') + la=$(account_info "$LOCAL_RPC" "$addr") + pa=$(account_info "$PROD_RPC" "$addr") + lc=$(printf '%s' "$la" | jq -r '.coins // "(n/a)"' 2>/dev/null || echo "(n/a)") + pc=$(printf '%s' "$pa" | jq -r '.coins // "(n/a)"' 2>/dev/null || echo "(n/a)") + echo "| \`$addr\` | $lc | $pc | $reason |" + done <"$OUT/TOPUP-REPORT.md" + echo "" + fi + echo "## Visual comparison (open these side-by-side)" + echo "" + echo "| Page | Local | Prod |" + echo "|------|-------|------|" + for p in "r/gov/dao" "r/gnoland/blog" "r/gnoland/home" "r/sys/params" "r/gnoland/coins:balances?address=${ADDRESS}&coin"; do + echo "| \`${p}\` | [$LOCAL_WEB/$p]($LOCAL_WEB/$p) | [$PROD_WEB/$p]($PROD_WEB/$p) |" + done + echo "" +} >"$REPORT" + +echo "Report written to: $REPORT" +echo "" +cat "$REPORT" diff --git a/misc/hf-glue/scripts/compare-gas-modes.sh b/misc/hf-glue/scripts/compare-gas-modes.sh new file mode 100755 index 00000000000..dc42d1ca4a6 --- /dev/null +++ b/misc/hf-glue/scripts/compare-gas-modes.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# compare-gas-modes.sh — run the in-memory smoketest twice against the +# current genesis, once with GasReplayMode="strict" (new VM gas meter +# applied to historical txs) and once with "source" (bypass the gas meter +# for historical txs, preserving source-chain outcomes), and report the +# difference in replay failure counts. +# +# Why +# === +# Most of the 2580 InsufficientFundsError failures we observe on rc6 are +# cascades from post-mainnet storage-deposit charges that didn't exist +# on the source chain. "source" mode is designed for exactly this: it +# bypasses the new gas meter for historical txs so their outcomes match +# what the source chain recorded. Before recommending "source" for a +# production launch we need concrete numbers on how many failures it +# actually eliminates vs "strict". +# +# gnogenesis fork generate currently writes gas_replay_mode="" (which +# behaves as "strict"). This script patches the generated genesis.json +# to toggle the field, runs the smoketest on each variant, and diffs the +# reported failure count. It doesn't modify the authoritative genesis. +# +# Env +# === +# OUT misc/hf-glue/out (auto-resolved) +# REPO gno repo root (auto-resolved) +# GENESIS path to the already-built genesis (default $OUT/genesis.json) +# +# Exit +# ==== +# 0 — both smoketests ran, report written +# 2 — prerequisite error (missing genesis, jq, etc.) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HERE="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO="${REPO:-$(cd "$HERE/../.." && pwd)}" +OUT="${OUT:-$HERE/out}" +GENESIS="${GENESIS:-$OUT/genesis.json}" + +command -v jq >/dev/null 2>&1 || { + echo "jq not found" >&2 + exit 2 +} +[[ -f "$GENESIS" ]] || { + echo "genesis not found at $GENESIS" >&2 + exit 2 +} + +REPORT="$OUT/GAS-MODES-COMPARE.md" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +echo "━━━ compare-gas-modes ━━━" +echo " genesis $GENESIS" +echo + +run_smoketest() { + local mode="$1" gen="$2" log="$3" + echo " running smoketest with gas_replay_mode=\"$mode\"" + # gnogenesis fork test exits non-zero when failures > 0 (which is the + # common case with --skip-failing-genesis-txs absorbing noise), so we + # tolerate that here and extract the count from the output. + (cd "$REPO/contribs/gnogenesis" && go run . fork test --genesis "$gen") >"$log" 2>&1 || true +} + +extract_failures() { + local log="$1" + grep -oE 'Failures:[[:space:]]+[0-9]+' "$log" | head -1 | grep -oE '[0-9]+' +} + +extract_ok() { + local log="$1" + grep -oE 'Txs processed:[[:space:]]+[0-9]+[[:space:]]*/[[:space:]]*[0-9]+' "$log" | head -1 +} + +# Variant A — strict (empty string; app.go treats "" and "strict" as same) +jq '.app_state.gas_replay_mode = "strict"' "$GENESIS" >"$WORK/strict.json" +run_smoketest strict "$WORK/strict.json" "$WORK/strict.log" + +# Variant B — source +jq '.app_state.gas_replay_mode = "source"' "$GENESIS" >"$WORK/source.json" +run_smoketest source "$WORK/source.json" "$WORK/source.log" + +s_fail="$(extract_failures "$WORK/strict.log")" +s_tx="$(extract_ok "$WORK/strict.log")" +u_fail="$(extract_failures "$WORK/source.log")" +u_tx="$(extract_ok "$WORK/source.log")" + +delta=$((${s_fail:-0} - ${u_fail:-0})) + +{ + echo "# Gas replay mode comparison" + echo "" + echo "_Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)_" + echo "" + echo "- **Genesis**: \`$GENESIS\`" + echo "" + echo "| Mode | Txs processed | Failures |" + echo "|--------|----------------------------------------|---------:|" + echo "| strict | ${s_tx:-} | ${s_fail:-?} |" + echo "| source | ${u_tx:-} | ${u_fail:-?} |" + echo "" + echo "**Delta (strict − source): ${delta}** failures eliminated by bypassing the new-VM gas meter for historical txs." + echo "" + echo "Interpretation" + echo "--------------" + echo "" + echo "- \`strict\` (default) applies the current VM's gas meter to every" + echo " tx, including historical ones. Txs that consumed N gas on the" + echo " source chain may consume >N on this branch (new opcodes," + echo " storage-deposit metering) and fail under the original fee cap." + echo "- \`source\` skips gas metering for txs with \`metadata.block_height > 0\`" + echo " (historical) and records \`metadata.gas_used\` from the source" + echo " chain in the response. Result: historical txs match source" + echo " outcomes regardless of VM gas drift." + echo "" + echo "Launch posture" + echo "--------------" + echo "" + if [[ "$delta" -gt 0 ]]; then + echo "\`source\` mode eliminated ${delta} failure(s). If the eliminated" + echo "failures are InsufficientFundsError (fee-cap shortfalls) or" + echo "gas-related, it's preserving user interactions that would" + echo "otherwise be lost. Trade-off: any genuinely-broken historical" + echo "tx (VM panic unrelated to gas) is still skipped, and the new" + echo "VM's gas meter is bypassed for history so post-fork chains" + echo "can't rely on that metering for replay-time audits." + else + echo "\`source\` mode eliminated **zero** failures on this genesis." + echo "The 2580-ish InsufficientFundsError replay failures we observe" + echo "fire at the ante handler (\`DeductFees\`) BEFORE the gas meter" + echo "is consulted — DeductFees checks the signer's static GasFee" + echo "against their live balance, and a signer drained by" + echo "post-mainnet storage-deposit charges has 0 ugnot regardless" + echo "of which gas-replay mode the VM would use afterwards." + echo "" + echo "Recovering these failures cannot be done through gas-replay" + echo "mode. The available levers are:" + echo " 1. \`hf_topup_balance\` for each diverged signer (see" + echo " \`make audit-balances\`)." + echo " 2. Patching \`DeductFees\` itself to skip historical txs" + echo " under a SkipFeeDeduction context key — bigger semantic" + echo " change, upstream-only." + echo "" + echo "For test13-rc the default \`strict\`/empty mode is fine; \`source\`" + echo "adds no value on top and makes the chain harder to audit." + fi +} >"$REPORT" + +echo +echo "Report: $REPORT" +echo "strict fail=$s_fail | source fail=$u_fail | delta=$delta" diff --git a/misc/hf-glue/scripts/fetch-from-dir.sh b/misc/hf-glue/scripts/fetch-from-dir.sh new file mode 100755 index 00000000000..1219aa91c78 --- /dev/null +++ b/misc/hf-glue/scripts/fetch-from-dir.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Alternative to fetch.sh that pulls source-chain state from a LOCAL gnoland +# node data directory instead of hitting RPC endpoints. +# +# Use this when you have a locally-synced gnoland1 node — replaying its +# blockstore.db is far faster (and offline) than pulling blocks via RPC. +# +# Expected layout of $NODE_DIR (matches `gnoland start --data-dir` output): +# $NODE_DIR/ +# config/genesis.json +# db/ +# blockstore.db/ ← historical txs live here +# state.db/ +# ... +# +# Currently the misc/hardfork tool's dirSource reads genesis.json + an optional +# txs.jsonl. Reading blockstore.db directly is not yet implemented, so this +# script first runs tx-archive against a locally-spawned RPC (or — future — +# reads the block store directly once misc/hardfork/source_dir.go grows that +# support). For now we assume the caller also provides a txs.jsonl alongside +# the genesis, or has run an RPC locally. +# +# Inputs (env): +# NODE_DIR path to a gnoland-data dir +# TXS_JSONL optional — path to a pre-exported txs.jsonl +# (if absent, an error is raised — see TODO below) +# ORIGINAL_CHAIN_ID source chain ID +# CHAIN_ID new chain ID +# HALT_HEIGHT required for local mode (we can't auto-detect without RPC) +# OUT output directory (absolute) +# REPO repo root (absolute) +set -euo pipefail + +: "${NODE_DIR:?NODE_DIR is required (path to a gnoland data directory)}" +: "${ORIGINAL_CHAIN_ID:?ORIGINAL_CHAIN_ID is required}" +: "${CHAIN_ID:?CHAIN_ID is required}" +: "${HALT_HEIGHT:?HALT_HEIGHT is required when using local source}" +: "${OUT:?OUT is required}" +: "${REPO:?REPO is required}" + +GENESIS="$OUT/genesis.json" +STAGE="$OUT/source" +STAGE_GEN="$STAGE/config/genesis.json" +STAGE_TXS="$STAGE/txs.jsonl" + +echo "── fetch hardfork genesis (local dir) ────────────────────────" +echo " node dir: $NODE_DIR" +echo " original chain id: $ORIGINAL_CHAIN_ID" +echo " new chain id: $CHAIN_ID" +echo " halt height: $HALT_HEIGHT" +echo " output: $GENESIS" +echo "" + +mkdir -p "$STAGE/config" + +# ---- genesis.json from the local node ---- +SRC_GEN="$NODE_DIR/config/genesis.json" +if [[ ! -f "$SRC_GEN" ]]; then + SRC_GEN="$NODE_DIR/genesis.json" +fi +if [[ ! -f "$SRC_GEN" ]]; then + echo "ERROR: genesis.json not found under $NODE_DIR" >&2 + exit 1 +fi +echo "[1/3] using base genesis: $SRC_GEN" +cp "$SRC_GEN" "$STAGE_GEN" + +# ---- txs.jsonl ---- +if [[ -n "${TXS_JSONL:-}" ]]; then + if [[ ! -f "$TXS_JSONL" ]]; then + echo "ERROR: TXS_JSONL=$TXS_JSONL does not exist" >&2 + exit 1 + fi + echo "[2/3] using provided txs.jsonl: $TXS_JSONL" + cp "$TXS_JSONL" "$STAGE_TXS" +else + # TODO: once misc/hardfork/source_dir.go reads blockstore.db directly, + # point dirSource at $NODE_DIR/db/blockstore.db and skip this step. + echo "ERROR: no TXS_JSONL provided." >&2 + echo " Either (a) pass TXS_JSONL=/path/to/txs.jsonl, or" >&2 + echo " (b) run 'contribs/tx-archive backup' against a local RPC on this node," >&2 + echo " (c) wait for misc/hardfork to grow blockstore.db support (open issue)." >&2 + exit 1 +fi + +# ---- assemble ---- +echo "" +echo "[3/3] assembling hardfork genesis..." +cd "$REPO/misc/hardfork" + +ARGS=( + genesis + --source "$STAGE" + --chain-id "$CHAIN_ID" + --original-chain-id "$ORIGINAL_CHAIN_ID" + --halt-height "$HALT_HEIGHT" + --output "$GENESIS" +) +go run . "${ARGS[@]}" + +echo "" +if command -v sha256sum >/dev/null 2>&1; then + echo "sha256: $(sha256sum "$GENESIS" | cut -d' ' -f1)" +elif command -v shasum >/dev/null 2>&1; then + echo "sha256: $(shasum -a 256 "$GENESIS" | cut -d' ' -f1)" +fi +echo "done — genesis written to $GENESIS" diff --git a/misc/hf-glue/scripts/fetch-txs-chunked.sh b/misc/hf-glue/scripts/fetch-txs-chunked.sh new file mode 100755 index 00000000000..35d6ed2d469 --- /dev/null +++ b/misc/hf-glue/scripts/fetch-txs-chunked.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Fetch historical txs from an RPC in chunks with retries, concatenate into +# out/source/txs.jsonl. Use when the RPC is flaky and tx-archive's single +# run can't complete — tx-archive has no resume, so chunking is the only way +# to make progress on unreliable connections. +# +# Inputs (env): +# RPC_URL (default https://rpc.gno.land) +# HALT_HEIGHT (required) +# OUT (default misc/hf-glue/out) +# REPO (required — repo root, to locate contribs/tx-archive) +# CHUNK (default 20000 blocks per fetch) +# MAX_RETRIES (default 8) +set -euo pipefail + +: "${REPO:?REPO is required}" +: "${HALT_HEIGHT:?HALT_HEIGHT is required}" +RPC_URL="${RPC_URL:-https://rpc.gno.land}" +OUT="${OUT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/out}" +CHUNK="${CHUNK:-20000}" +MAX_RETRIES="${MAX_RETRIES:-8}" + +STAGE_DIR="$OUT/source" +FINAL="$STAGE_DIR/txs.jsonl" +CHUNK_DIR="$STAGE_DIR/txs-chunks" +mkdir -p "$CHUNK_DIR" + +echo "── chunked RPC fetch ──────────────────────────────────────────" +echo " rpc $RPC_URL" +echo " range 1..$HALT_HEIGHT" +echo " chunk size $CHUNK blocks" +echo " chunk dir $CHUNK_DIR" +echo "" + +fetch_chunk() { + local from="$1" to="$2" out_path="$3" + local attempt=1 + while ((attempt <= MAX_RETRIES)); do + echo " [$from..$to] attempt $attempt/$MAX_RETRIES" + if (cd "$REPO/contribs/tx-archive" && go run ./cmd backup \ + -remote "$RPC_URL" \ + -from-block "$from" \ + -to-block "$to" \ + -batch 100 \ + -output-path "$out_path" \ + -overwrite); then + return 0 + fi + echo " [$from..$to] failed, sleeping $((attempt * 5))s" + sleep $((attempt * 5)) + ((attempt++)) + done + return 1 +} + +from=1 +while ((from <= HALT_HEIGHT)); do + to=$((from + CHUNK - 1)) + ((to > HALT_HEIGHT)) && to=$HALT_HEIGHT + chunk_file="$CHUNK_DIR/${from}-${to}.jsonl" + if [[ -s "$chunk_file" ]] || [[ -f "$chunk_file.done" ]]; then + echo " [$from..$to] cached ($(wc -l <"$chunk_file" 2>/dev/null | tr -d ' ') txs)" + else + if ! fetch_chunk "$from" "$to" "$chunk_file"; then + echo "ERROR: chunk $from..$to failed after $MAX_RETRIES attempts" >&2 + exit 1 + fi + touch "$chunk_file.done" + fi + from=$((to + 1)) +done + +echo "" +echo "── assembling final txs.jsonl ─────────────────────────────────" +: >"$FINAL" +for f in $(ls "$CHUNK_DIR"/*.jsonl | sort -t- -k1 -n); do + cat "$f" >>"$FINAL" +done +echo " wrote $FINAL" +echo " total txs: $(wc -l <"$FINAL" | tr -d ' ')" diff --git a/misc/hf-glue/scripts/gen-local-genesis.sh b/misc/hf-glue/scripts/gen-local-genesis.sh new file mode 100755 index 00000000000..ccb8d970e5d --- /dev/null +++ b/misc/hf-glue/scripts/gen-local-genesis.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Rebuild the gnoland1 base genesis locally via +# misc/deployments/gnoland1/gen-genesis.sh, then stage it at +# out/source-genesis.json for the hardfork tool to consume as a file source. +# +# Inputs (env): +# OUT output directory (absolute) +# REPO repo root (absolute) +set -euo pipefail + +: "${OUT:?OUT is required}" +: "${REPO:?REPO is required}" + +GENESIS_SRC="$REPO/misc/deployments/gnoland1/genesis.json" +OUT_FILE="$OUT/source-genesis.json" + +echo "── rebuild gnoland1 genesis locally ────────────────────────" +echo " this runs misc/deployments/gnoland1/gen-genesis.sh (takes a few minutes)" +echo " output will be staged at: $OUT_FILE" +echo "" + +mkdir -p "$OUT" + +# Reuse a pre-existing build if present to shave time on reruns. +EXTRA=() +if [[ -d "$REPO/misc/deployments/gnoland1/genesis-work/bin" ]]; then + EXTRA+=(--no-install) +fi + +( cd "$REPO/misc/deployments/gnoland1" && ./gen-genesis.sh "${EXTRA[@]}" ) + +if [[ ! -f "$GENESIS_SRC" ]]; then + echo "ERROR: expected $GENESIS_SRC after gen-genesis.sh but it does not exist" >&2 + exit 1 +fi + +cp "$GENESIS_SRC" "$OUT_FILE" +echo "" +echo "done — source genesis at $OUT_FILE" +echo "" +echo "Next:" +echo " SOURCE=$OUT_FILE make fetch init up" diff --git a/misc/hf-glue/scripts/init-node.sh b/misc/hf-glue/scripts/init-node.sh new file mode 100755 index 00000000000..017682e8ed4 --- /dev/null +++ b/misc/hf-glue/scripts/init-node.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Initialise gnoland-home for the testbed: +# - run `gnoland secrets init` to generate the single validator identity +# - rewrite validators in out/genesis.json so it contains ONLY that key +# +# The docker container mounts $OUT/gnoland-home/ as its --data-dir, so the +# node boots with the key generated here. +# +# Inputs (env): +# VALIDATOR_NAME name baked into the genesis validator entry +# OUT output directory (absolute) +# REPO repo root (absolute) +set -euo pipefail + +: "${VALIDATOR_NAME:?VALIDATOR_NAME is required}" +: "${OUT:?OUT is required}" +: "${REPO:?REPO is required}" + +GENESIS="$OUT/genesis.json" +HOME_DIR="$OUT/gnoland-home" +SECRETS_DIR="$HOME_DIR/secrets" +PV_KEY="$SECRETS_DIR/priv_validator_key.json" + +if [[ ! -f "$GENESIS" ]]; then + echo "missing $GENESIS — run 'make migrate' first" >&2 + exit 1 +fi + +echo "── init single-validator node ───────────────────────────────" +mkdir -p "$HOME_DIR" + +# ---- 1. generate validator secrets if not already present ---- +if [[ -f "$PV_KEY" ]]; then + echo " secrets already present at $SECRETS_DIR — reusing" +else + echo " generating secrets in $SECRETS_DIR" + mkdir -p "$SECRETS_DIR" + go run -C "$REPO" ./gno.land/cmd/gnoland secrets init --data-dir "$SECRETS_DIR" +fi + +# ---- 2. rewrite validator set in the genesis to a single entry ---- +echo "" +echo " rewriting validator set in genesis..." +go run -C "$REPO/misc/hf-glue/fixvalidator" . \ + --priv-key "$PV_KEY" \ + --genesis "$GENESIS" \ + --name "$VALIDATOR_NAME" \ + --power 10 + +# ---- 3. write config.toml so RPC binds to 0.0.0.0 (accessible from host) ---- +CONFIG_DIR="$HOME_DIR/config" +CONFIG_FILE="$CONFIG_DIR/config.toml" +mkdir -p "$CONFIG_DIR" +if [[ -f "$CONFIG_FILE" ]]; then + echo " config already present at $CONFIG_FILE — reusing" +else + go run -C "$REPO" ./gno.land/cmd/gnoland config init -config-path "$CONFIG_FILE" + if command -v sed >/dev/null 2>&1; then + sed -i.bak 's|tcp://127.0.0.1:26657|tcp://0.0.0.0:26657|' "$CONFIG_FILE" + sed -i.bak 's|tcp://127.0.0.1:26656|tcp://0.0.0.0:26656|' "$CONFIG_FILE" + rm -f "$CONFIG_FILE.bak" + fi + echo " config written to $CONFIG_FILE" +fi + +# ---- 4. stage genesis.json next to the node data ---- +cp "$GENESIS" "$HOME_DIR/genesis.json" + +echo "" +echo "done — node home ready at $HOME_DIR" diff --git a/misc/hf-glue/scripts/lib/hf.sh b/misc/hf-glue/scripts/lib/hf.sh new file mode 100755 index 00000000000..80681b485e8 --- /dev/null +++ b/misc/hf-glue/scripts/lib/hf.sh @@ -0,0 +1,361 @@ +#!/usr/bin/env bash +# misc/hf-glue/scripts/lib/hf.sh — helpers used by migrate.sh (and friends). +# +# The intent is that migrate.sh reads like a config: each line describes a +# piece of the migration (where to get the genesis, where to get the txs, +# what to patch, etc). Plumbing lives here. +# +# Env set by the caller's Makefile: +# OUT, REPO absolute paths +# ORIGINAL_CHAIN_ID, CHAIN_ID +# HALT_HEIGHT (may be empty — auto-detected from RPC by hf_fetch_txs_via_rpc) + +set -euo pipefail + +# ---- state ---------------------------------------------------------------- +# Filled by the hf_* functions. Read at the end by hf_assemble. +_HF_STAGE="" # staging dir (gnoland-data layout) +_HF_STAGE_GEN="" # path to base genesis.json +_HF_STAGE_TXS="" # path to historical txs.jsonl (empty until step 2) +_HF_PATCHES=() # list of "pkgpath=srcdir" entries for --patch-realm +_HF_OVERLAYS=() # overlay tx files (pre-history, not yet supported) +_HF_MIGRATIONS=() # migration tx files (post-history, not yet supported) +_HF_TOPUPS=() # balance top-ups: "addr=amount=reason" (post-assemble) + +# ---- presentation --------------------------------------------------------- +hf_banner() { + printf '\n\033[1;36m━━━ %s ━━━\033[0m\n' "$*" +} + +hf_kv() { + printf " %-22s \033[36m%s\033[0m\n" "$1" "$2" +} + +hf_die() { + printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2 + exit 1 +} + +hf_warn() { + printf '\033[1;31m⚠ WARN:\033[0m %s\n' "$*" >&2 +} + +# ---- setup ---------------------------------------------------------------- +# hf_init — must be the first call. Prints a header, creates the staging dir. +hf_init() { + : "${OUT:?OUT is required}" + : "${REPO:?REPO is required}" + : "${ORIGINAL_CHAIN_ID:?ORIGINAL_CHAIN_ID is required}" + : "${CHAIN_ID:?CHAIN_ID is required}" + + _HF_STAGE="$OUT/source" + _HF_STAGE_GEN="$_HF_STAGE/config/genesis.json" + _HF_STAGE_TXS="$_HF_STAGE/txs.jsonl" + mkdir -p "$_HF_STAGE/config" + + hf_banner "hardfork migration" + hf_kv "original chain id" "$ORIGINAL_CHAIN_ID" + hf_kv "new chain id" "$CHAIN_ID" + hf_kv "halt height" "${HALT_HEIGHT:-}" + hf_kv "output genesis" "$OUT/genesis.json" + hf_kv "staging dir" "$_HF_STAGE" + echo "" +} + +# ---- step 1: base genesis ------------------------------------------------- +# hf_fetch_genesis_from_url URL +# Direct .json asset (e.g. GitHub release). +hf_fetch_genesis_from_url() { + local url="$1" + hf_banner "step 1 — base genesis (URL)" + if [[ -f "$_HF_STAGE_GEN" ]]; then + hf_kv "cached" "$(_hf_size "$_HF_STAGE_GEN") bytes" + return 0 + fi + hf_kv "url" "$url" + curl -fSL --retry 3 --retry-delay 5 --max-time 600 --progress-bar \ + -o "$_HF_STAGE_GEN" "$url" + hf_kv "size" "$(_hf_size "$_HF_STAGE_GEN") bytes" +} + +# hf_fetch_genesis_from_rpc RPC_URL +# Fetches ${RPC_URL}/genesis and unwraps the JSON-RPC envelope. +hf_fetch_genesis_from_rpc() { + local rpc="$1" + hf_banner "step 1 — base genesis (RPC)" + if [[ -f "$_HF_STAGE_GEN" ]]; then + hf_kv "cached" "$(_hf_size "$_HF_STAGE_GEN") bytes" + return 0 + fi + local env="${rpc%/}/genesis" + hf_kv "url" "$env" + curl -fSL --retry 3 --retry-delay 5 --max-time 600 --progress-bar \ + -o "$_HF_STAGE/envelope.json" "$env" + jq -c '.result.genesis' <"$_HF_STAGE/envelope.json" >"$_HF_STAGE_GEN" + rm -f "$_HF_STAGE/envelope.json" + hf_kv "size" "$(_hf_size "$_HF_STAGE_GEN") bytes" +} + +# hf_fetch_genesis_from_file PATH +# Local file copy. +hf_fetch_genesis_from_file() { + local src="$1" + hf_banner "step 1 — base genesis (file)" + [[ -f "$src" ]] || hf_die "genesis file not found: $src" + if [[ -f "$_HF_STAGE_GEN" ]]; then + hf_kv "cached" "$(_hf_size "$_HF_STAGE_GEN") bytes" + return 0 + fi + hf_kv "from" "$src" + cp "$src" "$_HF_STAGE_GEN" + hf_kv "size" "$(_hf_size "$_HF_STAGE_GEN") bytes" +} + +# ---- step 2: historical txs ----------------------------------------------- +# hf_fetch_txs_via_rpc RPC_URL +# Uses contribs/tx-archive with batching. Auto-detects HALT_HEIGHT from +# the RPC's /status if HALT_HEIGHT is empty. +hf_fetch_txs_via_rpc() { + local rpc="$1" + hf_banner "step 2 — historical txs (RPC)" + if [[ -z "${HALT_HEIGHT:-}" ]]; then + HALT_HEIGHT=$(curl -fsS --max-time 30 "${rpc%/}/status" | + jq -r '.result.sync_info.latest_block_height') + hf_kv "halt (auto)" "$HALT_HEIGHT" + else + hf_kv "halt" "$HALT_HEIGHT" + fi + if [[ -f "$_HF_STAGE_TXS" ]]; then + hf_kv "cached" "$(wc -l <"$_HF_STAGE_TXS" | tr -d ' ') txs" + return 0 + fi + hf_kv "rpc" "$rpc" + hf_kv "range" "1..$HALT_HEIGHT" + (cd "$REPO/contribs/tx-archive" && go run ./cmd backup \ + -remote "$rpc" \ + -from-block 1 \ + -to-block "$HALT_HEIGHT" \ + -batch 1000 \ + -output-path "$_HF_STAGE_TXS" \ + -overwrite) + hf_kv "total" "$(wc -l <"$_HF_STAGE_TXS" | tr -d ' ') txs" +} + +# hf_fetch_txs_from_jsonl PATH +# Copy a pre-exported txs.jsonl. Still requires HALT_HEIGHT. +hf_fetch_txs_from_jsonl() { + local src="$1" + hf_banner "step 2 — historical txs (jsonl)" + [[ -f "$src" ]] || hf_die "txs.jsonl not found: $src" + : "${HALT_HEIGHT:?HALT_HEIGHT is required when pulling txs from a file}" + hf_kv "from" "$src" + cp "$src" "$_HF_STAGE_TXS" + hf_kv "total" "$(wc -l <"$_HF_STAGE_TXS" | tr -d ' ') txs" +} + +# hf_skip_txs +# No historical txs at all (genesis-only hardfork). +hf_skip_txs() { + hf_banner "step 2 — historical txs (none)" + : "${HALT_HEIGHT:?HALT_HEIGHT is required when skipping tx pull}" + hf_kv "halt" "$HALT_HEIGHT" + : >"$_HF_STAGE_TXS" +} + +# ---- patches + overlays --------------------------------------------------- +# hf_patch_addpkg PKGPATH SRCDIR +# Rewrites the genesis-mode addpkg tx for PKGPATH in-place with the +# *.gno + gnomod.toml files from SRCDIR. Source genesis on disk stays +# untouched — the patch is applied in memory during hf_assemble. +hf_patch_addpkg() { + local pkg="$1" src="$2" + [[ -d "$src" ]] || hf_die "patch srcdir not found: $src" + _HF_PATCHES+=("$pkg=$src") +} + +# hf_overlay_txs PATH +# Future: inject extra genesis-mode txs BEFORE historical tx replay +# (post-genesis-mode, pre-history). Not yet plumbed in misc/hardfork — +# hf_assemble will refuse if any overlay was requested. +hf_overlay_txs() { + local src="$1" + _HF_OVERLAYS+=("$src") +} + +# hf_migration_tx PATH +# Inject a migration tx jsonl that runs AFTER historical replay +# (e.g. to update r/sys/validators/v2 to the new valset, to reset +# chain params, etc). "Reproduce history, then mutate". +# +# Each jsonl line is an amino-JSON TxWithMetadata; BlockHeight is +# forced to 0 at replay (genesis-mode execution). +# +# Valset-swap note: gnoland1 seeds its valset via govdao_prop1.gno +# at genesis. A hardfork inherits that state, so r/sys/validators/v2 +# still lists the original 7 validators even though tm2 consensus is +# driven by GenesisDoc.Validators (which `gnogenesis fork` rewrites). +# The migration tx reconciles the two sides. +hf_migration_tx() { + local src="$1" + [[ -f "$src" ]] || hf_die "migration tx jsonl not found: $src" + _HF_MIGRATIONS+=("$src") +} + +# hf_topup_balance ADDR AMOUNT [REASON] +# Add coins to ADDR in the assembled genesis balances. Used when a +# replay under newer code exposes an account that can't cover invariants +# that didn't exist when the original tx was signed. +# +# Concrete case: master's storage-deposit code (added after gnoland1 +# launched) locks a deposit from msg.Creator for every addpkg. A +# creator that deploys many realms in a row runs out of ugnot mid- +# replay. The original txs were signed with max_deposit="" (the field +# didn't exist yet) and can't be retroactively modified without +# invalidating signatures (though we currently run with +# --skip-genesis-sig-verification, leaving the payload stable is the +# honest move). Top up the creator instead. +# +# The top-up is applied as a post-process on $OUT/genesis.json after +# gnogenesis fork generate completes. Balances in the source genesis +# on disk stay untouched. +hf_topup_balance() { + local addr="$1" amount="$2" reason="${3:-unspecified}" + [[ -n "$addr" && -n "$amount" ]] || hf_die "hf_topup_balance: addr and amount are required" + _HF_TOPUPS+=("$addr=$amount=$reason") +} + +# ---- step 3: assemble ----------------------------------------------------- +# hf_assemble +# Runs `gnogenesis fork generate` against the staged source dir, +# applying any accumulated --patch-realm and --migration-tx entries. +hf_assemble() { + hf_banner "step 3 — assemble hardfork genesis" + : "${HALT_HEIGHT:?HALT_HEIGHT must be set (auto-detected earlier, or pass explicitly)}" + + if [[ ${#_HF_OVERLAYS[@]} -gt 0 ]]; then + hf_die "hf_overlay_txs is not supported by gnogenesis fork yet (${#_HF_OVERLAYS[@]} requested)" + fi + + local args=( + fork generate + --source "$_HF_STAGE" + --chain-id "$CHAIN_ID" + --original-chain-id "$ORIGINAL_CHAIN_ID" + --halt-height "$HALT_HEIGHT" + --output "$OUT/genesis.json" + ) + local p + for p in "${_HF_PATCHES[@]:-}"; do + [[ -z "$p" ]] && continue + hf_kv "patch" "$p" + args+=(--patch-realm "$p") + done + local m + for m in "${_HF_MIGRATIONS[@]:-}"; do + [[ -z "$m" ]] && continue + hf_kv "migration" "$m" + args+=(--migration-tx "$m") + done + + (cd "$REPO/contribs/gnogenesis" && go run . "${args[@]}") + + _hf_apply_topups + + echo "" + if command -v sha256sum >/dev/null 2>&1; then + hf_kv "sha256" "$(sha256sum "$OUT/genesis.json" | cut -d' ' -f1)" + elif command -v shasum >/dev/null 2>&1; then + hf_kv "sha256" "$(shasum -a 256 "$OUT/genesis.json" | cut -d' ' -f1)" + fi + hf_kv "output" "$OUT/genesis.json" +} + +# ---- internal ------------------------------------------------------------- +_hf_size() { wc -c <"$1" | tr -d ' '; } + +# _hf_apply_topups +# Apply _HF_TOPUPS to $OUT/genesis.json in-place. Each entry adds the +# given amount to the target address's balance (creating the balance +# line if absent). Python is used because app_state.balances is a +# multi-million-entry list on mainnet-scale snapshots. +_hf_apply_topups() { + [[ ${#_HF_TOPUPS[@]} -gt 0 ]] || return 0 + hf_banner "post-assemble — balance top-ups" + local t + for t in "${_HF_TOPUPS[@]}"; do + hf_kv "topup" "$t" + done + + GENESIS="$OUT/genesis.json" TOPUP_REPORT="$OUT/TOPUP-REPORT.md" \ + python3 - "${_HF_TOPUPS[@]}" <<'PY' +import datetime +import json +import os +import re +import sys + +path = os.environ["GENESIS"] +report = os.environ["TOPUP_REPORT"] + +with open(path) as f: + g = json.load(f) + +bals = g.setdefault("app_state", {}).setdefault("balances", []) +entry_re = re.compile(r"^(g1[0-9a-z]+)=([0-9]+)([a-zA-Z]+)$") +coin_re = re.compile(r"^([0-9]+)([a-zA-Z]+)$") + +idx = {} +for i, line in enumerate(bals): + m = entry_re.match(line) + if m: + idx[(m.group(1), m.group(3))] = i + +applied = [] +for raw in sys.argv[1:]: + addr, amount, reason = raw.split("=", 2) + m = coin_re.match(amount) + if not m: + sys.exit(f"hf_topup_balance: invalid amount: {amount}") + n, denom = int(m.group(1)), m.group(2) + key = (addr, denom) + if key in idx: + old = entry_re.match(bals[idx[key]]) + prev = int(old.group(2)) + new_amt = prev + n + bals[idx[key]] = f"{addr}={new_amt}{denom}" + applied.append((addr, prev, new_amt, denom, reason)) + else: + bals.append(f"{addr}={n}{denom}") + applied.append((addr, 0, n, denom, reason)) + +with open(path, "w") as f: + json.dump(g, f, indent=2) + +# Audit trail: persistent report of every synthetic balance change. +with open(report, "w") as f: + f.write("# Synthetic balance top-ups\n\n") + f.write( + f"_Generated {datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds')}_\n\n" + ) + f.write( + "> ⚠ **These balances were injected into the hardfork genesis after\n" + "> `gnogenesis fork generate` ran.** The replay that follows is no\n" + "> longer a faithful reproduction of the source chain — divergence\n" + "> against prod is expected for the addresses listed below.\n" + ">\n" + "> Each entry documents the reason the top-up was needed. Prefer\n" + "> fixing the underlying issue (e.g. a VM bypass for genesis-mode\n" + "> txs predating a feature) over accumulating top-ups.\n\n" + ) + f.write("| Address | Before | After | Δ | Reason |\n") + f.write("|---------|-------:|------:|---:|--------|\n") + for addr, before, after, denom, reason in applied: + delta = after - before + f.write( + f"| `{addr}` | {before}{denom} | {after}{denom} | +{delta}{denom} | {reason} |\n" + ) +PY + + hf_kv "report" "$OUT/TOPUP-REPORT.md" + hf_warn "${#_HF_TOPUPS[@]} synthetic balance top-up(s) applied — see $OUT/TOPUP-REPORT.md" +} diff --git a/misc/hf-glue/scripts/migrate.sh b/misc/hf-glue/scripts/migrate.sh new file mode 100755 index 00000000000..2b39a4890b3 --- /dev/null +++ b/misc/hf-glue/scripts/migrate.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# misc/hf-glue/scripts/migrate.sh +# +# Declarative hardfork migration — configured here, plumbed in lib/hf.sh. +# Defaults target gnoland1 → gnoland-1. Override by exporting any of +# SOURCE / RPC_URL / CHAIN_ID / ORIGINAL_CHAIN_ID / HALT_HEIGHT / PATCH_REALMS +# before running. +# +# Think of this file as a config that happens to be executable. Each hf_* +# call below is one line of intent; add / remove / reorder them to describe +# a different migration. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/hf.sh +source "$SCRIPT_DIR/lib/hf.sh" + +hf_init + +# ------------------------------------------------------------------------- +# 1) Where to get the BASE GENESIS +# ------------------------------------------------------------------------- +# Pick one. $SOURCE from the Makefile decides which branch runs. +: "${SOURCE:=https://github.com/gnolang/gno/releases/download/chain/gnoland1.0/genesis.json}" +case "$SOURCE" in +*.json | */genesis.json) hf_fetch_genesis_from_url "$SOURCE" ;; +http://* | https://*) hf_fetch_genesis_from_rpc "$SOURCE" ;; +*) hf_fetch_genesis_from_file "$SOURCE" ;; +esac + +# ------------------------------------------------------------------------- +# 2) Where to get the HISTORICAL TXS +# ------------------------------------------------------------------------- +: "${RPC_URL:=https://rpc.gno.land}" +hf_fetch_txs_via_rpc "$RPC_URL" +# Alternatives: +# hf_fetch_txs_from_jsonl /path/to/txs.jsonl +# hf_skip_txs + +# ------------------------------------------------------------------------- +# 3) REALM PATCHES (ride along the hardfork) +# ------------------------------------------------------------------------- +# Swap r/sys/params with the repo's current examples copy. After merging +# #5368 that copy has halt.gno (NewSetHaltRequest), so the forked chain +# boots with the govDAO halt mechanism available. +hf_patch_addpkg "gno.land/r/sys/params" "$REPO/examples/gno.land/r/sys/params" + +# Extra patches from $PATCH_REALMS (space-separated PKGPATH=SRCDIR). +for spec in ${PATCH_REALMS:-}; do + [[ -z "$spec" ]] && continue + hf_patch_addpkg "${spec%%=*}" "${spec#*=}" +done + +# ------------------------------------------------------------------------- +# 3b) BALANCE TOP-UPS (pre-replay synthetic seeding) +# ------------------------------------------------------------------------- +# Last-resort escape hatch when a genesis-mode tx would fail under newer +# code because an invariant that didn't exist when the tx was signed +# cannot be covered from the replay's starting state. +# +# This diverges the replayed state from the source chain — use only when +# the alternative is an incomplete replay. Every top-up is written to +# out/TOPUP-REPORT.md and surfaced in out/STATE-REPORT.md so the synthetic +# change is visible in the audit trail, not just in the fetch console. +# +# Current case (r/sys/txfees / tx_index=76): +# Under master's storage-deposit logic (added after gnoland1 launched), +# every addpkg *locks* a deposit from msg.Creator proportional to realm +# size. The creator g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l deploys 7 +# r/sys/* realms in a row and exhausts its 58.7 Mugnot balance at the +# 8th (r/sys/txfees). Since all 85 genesis-mode addpkg txs were signed +# with max_deposit="" (the field didn't exist on gnoland1) and the +# realm code on master is identical to gnoland1's, no cherry-pick fixes +# this — the gap is purely between old txs and new SDK semantics. +# Top up the creator so the locks fit. +hf_topup_balance "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l" "1000000000ugnot" \ + "storage-deposit headroom for r/sys/* genesis-mode deploys" + +# ------------------------------------------------------------------------- +# 4) OVERLAY TXS (pre-history, not yet supported) +# ------------------------------------------------------------------------- +# Future: inject extra txs between genesis-mode and historical replay. +# hf_overlay_txs "$SCRIPT_DIR/../overlays/20260417_add_moderator.jsonl" + +# ------------------------------------------------------------------------- +# 5) MIGRATION TXS (post-history) +# ------------------------------------------------------------------------- +# These run AFTER historical replay — "reproduce history, then mutate". +# +# Valset swap: gnoland1 seeds its valset via govdao_prop1.gno, so the +# post-fork r/sys/validators/v2 still lists the *original* 7 validators +# even though tm2 consensus is driven by GenesisDoc.Validators (which +# `gnogenesis fork` rewrites to our local validator via fixvalidator). +# The migration below reconciles the two: it wipes the 7 originals and +# registers the new valset via a govDAO proposal signed as manfred +# (T1 member) under --skip-genesis-sig-verification. +# +# Delegates to misc/deployments/gnoland-1/migrations/build.sh, which +# renders the template with the local priv_validator_key.json and +# produces a signed jsonl under $OUT/migrations.jsonl. +PV_KEY_DEFAULT="$OUT/gnoland-home/secrets/priv_validator_key.json" +PV_KEY="${PV_KEY:-$PV_KEY_DEFAULT}" +VALIDATOR_ADDR="${VALIDATOR_ADDR:-}" +VALIDATOR_PUBKEY="${VALIDATOR_PUBKEY:-}" +VALIDATOR_LIST="${VALIDATOR_LIST:-}" + +MIG_VALSET_SOURCE="" +MIG_VALSET_JSON="" +if [[ -f "$PV_KEY" ]]; then + MIG_VALSET_SOURCE="pv_key=$PV_KEY" +elif [[ -n "$VALIDATOR_LIST" ]]; then + # Build a multi-validator NEW_VALSET_JSON from a " " + # list file (one entry per line). Addresses are derived by fixvalidator, + # but build.sh needs explicit addresses in the JSON — so we shell out to + # gnokey / fixvalidator's parsing logic via an inline jq + awk pipeline. + MIG_VALSET_SOURCE="valset-list=$VALIDATOR_LIST" + MIG_VALSET_JSON="$OUT/new_valset.json" + go run -C "$REPO/misc/hf-glue/fixvalidator" . --valset-list "$VALIDATOR_LIST" --emit-json >"$MIG_VALSET_JSON" +elif [[ -n "$VALIDATOR_ADDR" && -n "$VALIDATOR_PUBKEY" ]]; then + # Build a NEW_VALSET_JSON from raw strings (no priv_validator_key.json + # needed). build.sh skips its PV_KEY path when NEW_VALSET_JSON is set. + MIG_VALSET_SOURCE="addr+pubkey (manual)" + MIG_VALSET_JSON="$OUT/new_valset.json" + jq -n --arg addr "$VALIDATOR_ADDR" --arg pub "$VALIDATOR_PUBKEY" --arg name "${VALIDATOR_NAME:-hf-local}" '[{ + address: $addr, + pub_key: $pub, + voting_power: 10, + name: $name + }]' >"$MIG_VALSET_JSON" +fi + +if [[ -n "$MIG_VALSET_SOURCE" ]]; then + hf_banner "step 5 — post-replay migration (valset swap${NEW_T1_ADDR:+ + T1 rotation})" + hf_kv "valset source" "$MIG_VALSET_SOURCE" + [[ -n "${NEW_T1_ADDR:-}" ]] && hf_kv "new T1 addr" "$NEW_T1_ADDR" + MIG_JSONL="$OUT/migrations.jsonl" + CALLER="${CALLER:-g1manfred47kzduec920z88wfr64ylksmdcedlf5}" \ + PV_KEY="$PV_KEY" \ + NEW_VALSET_JSON="$MIG_VALSET_JSON" \ + RPC_URL="$RPC_URL" \ + NEW_T1_ADDR="${NEW_T1_ADDR:-}" \ + T1_PORTFOLIO="${T1_PORTFOLIO:-}" \ + T1_WITHDRAW_REASON="${T1_WITHDRAW_REASON:-}" \ + OUT_JSONL="$MIG_JSONL" \ + CHAIN_ID="$CHAIN_ID" \ + REPO_ROOT="$REPO" \ + bash "$REPO/misc/deployments/gnoland-1/migrations/build.sh" + hf_migration_tx "$MIG_JSONL" +else + hf_banner "step 5 — post-replay migration (skipped)" + hf_kv "reason" "no $PV_KEY, VALIDATOR_LIST, or VALIDATOR_ADDR/VALIDATOR_PUBKEY set" +fi + +# ------------------------------------------------------------------------- +# 6) ASSEMBLE the hardfork genesis +# ------------------------------------------------------------------------- +hf_assemble diff --git a/misc/hf-glue/scripts/replay-log.sh b/misc/hf-glue/scripts/replay-log.sh new file mode 100755 index 00000000000..d6856279c1a --- /dev/null +++ b/misc/hf-glue/scripts/replay-log.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Run an in-process genesis replay via `hardfork test --verbose` and capture +# the per-tx log for analysis. Exits cleanly after replay completes (no docker, +# no persistent state). +# +# Usage: ./scripts/replay-log.sh [path/to/genesis.json] +# +# Output: out/replay.log (full log) + stdout summary +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO="$(cd "$HERE/../.." && pwd)" +OUT="$HERE/out" + +GENESIS="${1:-$OUT/genesis.json}" +LOG="$OUT/replay.log" + +if [[ ! -f "$GENESIS" ]]; then + echo "missing $GENESIS — run 'make fetch' first" >&2 + exit 1 +fi + +mkdir -p "$OUT" + +echo "── genesis replay smoke-test ────────────────────────────────" +echo " genesis: $GENESIS" +echo " log: $LOG" +echo "" + +cd "$REPO/contribs/gnogenesis" +go run . fork test \ + --genesis "$GENESIS" \ + --verbose \ + --timeout 30m 2>&1 | tee "$LOG" + +echo "" +echo "── summary ──────────────────────────────────────────────────" +echo "" +printf " OK txs: %d\n" "$(grep -c '^ \[OK\]' "$LOG" || true)" +printf " FAIL txs: %d\n" "$(grep -c '^ \[FAIL\]' "$LOG" || true)" +echo "" +echo " Unique failure reasons:" +grep -oE 'error=[^"]+' "$LOG" 2>/dev/null \ + | sed -E 's/error=//; s/\\n.*//' \ + | sort | uniq -c | sort -rn | head -20 | sed 's/^/ /' \ + || true +echo "" +echo " Failed packages (addpkg):" +grep '^ \[FAIL\]' "$LOG" 2>/dev/null \ + | grep -oE 'gno\.land/[pr]/[a-zA-Z0-9/_-]+' \ + | sort -u | head -30 | sed 's/^/ /' || true +echo "" +echo "Full log: $LOG" diff --git a/misc/hf-glue/scripts/report-replay.sh b/misc/hf-glue/scripts/report-replay.sh new file mode 100755 index 00000000000..ac0e9944c7f --- /dev/null +++ b/misc/hf-glue/scripts/report-replay.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# Produce a structured REPORT.md from a replay log. +# +# The goal: separate root-cause failures (sig mismatch, missing param, etc) +# from cascade failures (import errors from deps that failed earlier), so we +# can decide what to fix upstream vs ignore for testing. +# +# Usage: ./scripts/report-replay.sh [path/to/replay.log] +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$HERE/out" +LOG="${1:-$OUT/replay.log}" +REPORT="$OUT/REPLAY-REPORT.md" + +if [[ ! -f "$LOG" ]]; then + echo "missing $LOG — run 'make replay-log' first" >&2 + exit 1 +fi + +mkdir -p "$OUT" + +total_ok=$(grep -c '^ \[OK\]' "$LOG" || true) +total_fail=$(grep -c '^ \[FAIL\]' "$LOG" || true) +total=$((total_ok + total_fail)) + +# Extract failure lines. Each [FAIL] line contains the error text inline. +# Bucket by error kind. +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT +grep '^ \[FAIL\]' "$LOG" > "$tmp" || true + +# --- bucketing ---------------------------------------------------------------- +pubkey_mismatch=$(grep -c 'PubKey does not match' "$tmp" || true) +chain_id_err=$(grep -c 'signature verification failed' "$tmp" || true) +cascade_import=$(grep -c 'could not import' "$tmp" || true) +type_check=$(grep -c 'type check errors' "$tmp" || true) +insufficient_funds=$(grep -cE '(insufficient|out of gas|insufficient funds)' "$tmp" || true) +other=$((total_fail - pubkey_mismatch - cascade_import - type_check - insufficient_funds)) +[[ $other -lt 0 ]] && other=0 + +# --- distinct root-cause (non-cascade) packages ------------------------------ +# Cascade = "could not import gno.land/..." — the package itself is fine, +# its dep failed earlier. Surface the DEPS that are missing instead. +missing_imports=$(grep -oE 'could not import gno\.land/[^ \"\\]+' "$tmp" \ + | sort | uniq -c | sort -rn | head -20 || true) + +# --- packages that failed with a non-import reason (potential root causes) ---- +# Grab the addpkg package path from fail lines that do NOT mention "could not import". +root_cause_fails=$(grep -v 'could not import' "$tmp" \ + | grep -oE 'gno\.land/[pr]/[a-zA-Z0-9/_.-]+' \ + | sort -u | head -30 || true) + +# --- write report ------------------------------------------------------------ +{ + echo "# Genesis Replay Report" + echo "" + echo "_Generated $(date -u +%Y-%m-%dT%H:%M:%SZ) from ${LOG}_" + echo "" + echo "## Summary" + echo "" + echo "| Metric | Count |" + echo "|--------|------:|" + echo "| Total txs | $total |" + echo "| ✅ OK | $total_ok |" + echo "| ❌ Failed | $total_fail |" + echo "" + echo "## Failure categories" + echo "" + echo "| Category | Count | Kind |" + echo "|----------|------:|------|" + echo "| PubKey does not match signer address | $pubkey_mismatch | **root cause** — genesis signature mismatch |" + echo "| Signature verification failed (chain-id) | $chain_id_err | **root cause** — chain-id leak during sig verify |" + echo "| Type check — \`could not import\` | $cascade_import | **cascade** — dep package failed earlier |" + echo "| Type check — other | $type_check | investigate |" + echo "| Insufficient funds / out of gas | $insufficient_funds | investigate |" + echo "| Other | $other | investigate |" + echo "" + echo "## Root-cause failures" + echo "" + echo "These are failures NOT caused by a missing import. If any of these are" + echo "library packages, they cause a downstream cascade." + echo "" + echo '```' + if [[ -z "$root_cause_fails" ]]; then + echo "(none detected)" + else + echo "$root_cause_fails" + fi + echo '```' + echo "" + echo "## Cascade — missing imports" + echo "" + echo "Each line is a package that downstream txs tried to import but wasn't" + echo "deployed. If it appears here, either (a) its deploy tx failed as a" + echo "root cause, or (b) it's not in the genesis at all." + echo "" + echo '```' + if [[ -z "$missing_imports" ]]; then + echo "(none)" + else + echo "$missing_imports" + fi + echo '```' + echo "" + echo "## First 10 failure log lines (for context)" + echo "" + echo '```' + head -10 "$tmp" | sed 's/\\n/\n /g' + echo '```' + echo "" + echo "## Recommendation" + echo "" + if [[ $pubkey_mismatch -gt 0 ]]; then + echo "- **Fix pubkey mismatch first** ($pubkey_mismatch tx). The gnoland1 genesis" + echo " carries signatures whose pubkey doesn't derive to the signer address." + echo " Either the source genesis is malformed, or our ante-handler is" + echo " reading the wrong pubkey during hardfork replay." + fi + if [[ $chain_id_err -gt 0 ]]; then + echo "- **Chain-id leak** ($chain_id_err tx). Genesis-mode txs are being verified" + echo " against the new chain id instead of the original. Check the" + echo " \`PastChainIDs\` handling in \`loadAppState\`." + fi + if [[ $cascade_import -gt $((total_fail / 2)) ]]; then + echo "- **Most failures are cascade.** Fixing the root causes above will" + echo " likely drop the failure count dramatically." + fi + echo "" +} > "$REPORT" + +echo "report written to: $REPORT" +echo "" +cat "$REPORT" | head -60 diff --git a/misc/hf-glue/scripts/state-diff.sh b/misc/hf-glue/scripts/state-diff.sh new file mode 100755 index 00000000000..6a0f192558d --- /dev/null +++ b/misc/hf-glue/scripts/state-diff.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# state-diff.sh — diff realm render output between the source chain at +# halt_height and our post-replay node. Catches silent state divergence +# introduced by the 2605 "Unable to deliver genesis tx" failures that +# --skip-failing-genesis-txs absorbs. +# +# Approach +# ======== +# For each realm in REALMS (one pkgpath[:subpath] per line), fetch +# vm/qrender output from both sides: +# source chain: `gnokey query -remote $SOURCE_RPC -height $HALT_HEIGHT` +# replay: `gnokey query -remote $REPLAY_RPC` +# Normalize out obviously-ephemeral bits (absolute paths, wall clock, +# SVG blobs, current block height, etc.), then byte-compare. Emit a +# STATE-DIFF.md with per-realm pass/fail; for failing realms include the +# first N lines of the unified diff. +# +# What this can't catch +# ===================== +# A realm's Render function only surfaces what it chooses to. State +# stored in maps the render ignores, private fields, etc., are invisible. +# For those realms, add a companion qeval check in a follow-up. +# +# Env +# === +# SOURCE_RPC source chain RPC (default https://rpc.gno.land) +# REPLAY_RPC post-replay node RPC (default http://localhost:26657) +# HALT_HEIGHT source-chain height to query (required; same value used +# at genesis build time) +# REALMS newline-separated pkgpath[:subpath] list; defaults to +# the canonical set (valset v2, govDAO, memberstore, +# users, home, blog, sys/params, sys/names) +# OUT misc/hf-glue/out (auto-resolved) +# DIFF_CONTEXT lines of context per failing diff (default 20) +# GNOKEY_BIN gnokey binary (default: gnokey on $PATH) +# +# Exit status +# =========== +# 0 — every realm matches after normalization +# 1 — at least one realm diverges (see STATE-DIFF.md) +# 2 — prerequisite error +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="${OUT:-$(cd "$SCRIPT_DIR/.." && pwd)/out}" +mkdir -p "$OUT" + +SOURCE_RPC="${SOURCE_RPC:-https://rpc.gno.land}" +REPLAY_RPC="${REPLAY_RPC:-http://localhost:26657}" +DIFF_CONTEXT="${DIFF_CONTEXT:-20}" +GNOKEY_BIN="${GNOKEY_BIN:-gnokey}" + +: "${HALT_HEIGHT:?HALT_HEIGHT is required (pin the source-chain height to query)}" + +# The replay node can be queried at historical heights (0 = current tip). +# Production recipe: run this script against a FRESH post-replay node +# before any on-chain activity can drift the state — the default tip +# matches initial_height in that case. Override REPLAY_HEIGHT to pin a +# specific historical height; requires the node to still retain state at +# that height (no pruning). +REPLAY_HEIGHT="${REPLAY_HEIGHT:-0}" + +# Default realm set. Each entry is a pkgpath with optional :subpath passed +# to vm/qrender. Extend via REALMS env (newline-separated) for ad-hoc runs. +REALMS="${REALMS:-$( + cat <<'EOF' +gno.land/r/sys/validators/v2: +gno.land/r/sys/names: +gno.land/r/sys/params: +gno.land/r/gov/dao: +gno.land/r/gov/dao/v3/memberstore:members +gno.land/r/gnoland/home: +gno.land/r/gnoland/blog: +gno.land/r/gnoland/users: +EOF +)}" + +command -v "$GNOKEY_BIN" >/dev/null 2>&1 || { + echo "gnokey not found on PATH (set GNOKEY_BIN=...)" >&2 + exit 2 +} +command -v diff >/dev/null 2>&1 || { + echo "diff not found on PATH" >&2 + exit 2 +} + +REPORT="$OUT/STATE-DIFF.md" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# ---- Normalization +# Ephemeral render bits that differ between mainnet and our replay even for +# logically-equivalent state: +# • embedded SVG charts (base64 blobs with sizes in px → drop wholesale) +# • absolute URLs pointing at the chain's own rpc/web (host differs) +# • wall-clock timestamps ("Generated at ...") +# • "height" / "block" markers that reference the chain's current tip +# • trailing whitespace +# The goal is to preserve semantically-meaningful text (addresses, names, +# ids, counts, titles, descriptions) and drop everything transient. +normalize() { + sed \ + -e '/data:image\/svg+xml;base64,/d' \ + -e 's|https://[^[:space:])]*||g' \ + -e 's|http://[^[:space:])]*||g' \ + -e 's|rpc\.gno\.land||g' \ + -e 's|latest_block_height[^[:space:]]*||g' \ + -e 's|height=[0-9]*|height=|g' \ + -e 's|Generated [^\\n]*|Generated