diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 477a2639cc1..72d9d51a0ec 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -9,6 +9,8 @@ on: # any change to gnovm code can make examples fail - gnovm/** - examples/** + - misc/deployments/** + - contribs/gnogenesis/** workflow_dispatch: inputs: debug: @@ -67,6 +69,28 @@ jobs: modulepath: "examples" go-version: "1.23.x" + #deployments: + # name: Validate deployments + # runs-on: ubuntu-latest + # timeout-minutes: 15 + # steps: + # - uses: actions/checkout@v5 + # - uses: actions/setup-go@v5 + # with: + # go-version: "1.23.x" + # - name: Run deployment generate & test + # run: | + # for dir in misc/deployments/*/; do + # if [ -f "$dir/Makefile" ] && make -C "$dir" -n generate >/dev/null 2>&1; then + # echo "=== $dir ===" + # make -C "$dir" generate + # if make -C "$dir" -n test >/dev/null 2>&1; then + # make -C "$dir" test + # fi + # make -C "$dir" clean + # fi + # done + mod-tidy: strategy: fail-fast: false diff --git a/examples/gno.land/r/gov/dao/v3/loader/loader.gno b/examples/gno.land/r/gov/dao/v3/loader/loader.gno index b82f6adba79..6e14649ae44 100644 --- a/examples/gno.land/r/gov/dao/v3/loader/loader.gno +++ b/examples/gno.land/r/gov/dao/v3/loader/loader.gno @@ -1,3 +1,23 @@ +// loader.gno initialises the govDAO v3 implementation and tier structure. +// +// It intentionally does NOT add any members or set AllowedDAOs. When the +// allowedDAOs list in the DAO proxy is empty, InAllowedDAOs() returns true +// for any caller (see r/gov/dao/proxy.gno), which lets a subsequent MsgRun +// bootstrap the member set and then lock things down. +// +// Bootstrap flow (official network genesis or local dev): +// +// 1. All packages — including this loader — are deployed via MsgAddPackage. +// The loader sets up tier entries and the DAO implementation. +// 2. A MsgRun executes a setup script (e.g. govdao_prop1.gno) which: +// a. Adds a temporary deployer as T1 member (for supermajority). +// b. Creates a governance proposal to register validators, votes YES, +// and executes it. +// c. Adds the real govDAO members directly via memberstore.Get(). +// d. Removes the temporary deployer. +// e. Calls dao.UpdateImpl to set AllowedDAOs, locking down access. +// +// See misc/deployments/ for concrete genesis generation examples. package loader import ( @@ -6,41 +26,15 @@ import ( "gno.land/r/gov/dao/v3/memberstore" ) -// this is only executed when loaded into genesis func init() { + // Create tier entries in the members tree (required before any SetMember). memberstore.Get().SetTier(memberstore.T1) - memberstore.Get().SetMember(memberstore.T1, address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"), &memberstore.Member{InvitationPoints: 3}) // Jae - memberstore.Get().SetMember(memberstore.T1, address("g1manfred47kzduec920z88wfr64ylksmdcedlf5"), &memberstore.Member{InvitationPoints: 3}) // Manfred - memberstore.Get().SetMember(memberstore.T1, address("g1e6gxg5tvc55mwsn7t7dymmlasratv7mkv0rap2"), &memberstore.Member{InvitationPoints: 3}) // Milos - memberstore.Get().SetMember(memberstore.T1, address("g18amm3fc00t43dcxsys6udug0czyvqt9e7p23rd"), &memberstore.Member{InvitationPoints: 3}) // Marc - memberstore.Get().SetMember(memberstore.T1, address("g1mx4pum9976th863jgry4sdjzfwu03qan5w2v9j"), &memberstore.Member{InvitationPoints: 3}) // Ray - memberstore.Get().SetMember(memberstore.T1, address("g1m0rgan0rla00ygmdmp55f5m0unvsvknluyg2a4"), &memberstore.Member{InvitationPoints: 3}) // Morgan - memberstore.Get().SetMember(memberstore.T1, address("g1ker4vvggvsyatexxn3hkthp2hu80pkhrwmuczr"), &memberstore.Member{InvitationPoints: 3}) // Sergio - memberstore.Get().SetMember(memberstore.T1, address("g1aeddlftlfk27ret5rf750d7w5dume3kcsm8r8m"), &memberstore.Member{InvitationPoints: 3}) // Antoine - memberstore.Get().SetMember(memberstore.T1, address("g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun"), &memberstore.Member{InvitationPoints: 3}) // Jerónimo - memberstore.Get().SetMember(memberstore.T1, address("g1lckl8j2g3jyyuq6fx7pke3uz4kemht7lw4fg5l"), &memberstore.Member{InvitationPoints: 3}) // Danny - memberstore.Get().SetMember(memberstore.T1, address("g1778y2yphxs2wpuaflsy5y9qwcd4gttn4g5yjx5"), &memberstore.Member{InvitationPoints: 3}) // Michelle - memberstore.Get().SetMember(memberstore.T1, address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5"), &memberstore.Member{InvitationPoints: 3}) // Leon - memberstore.Get().SetTier(memberstore.T2) - memberstore.Get().SetMember(memberstore.T2, address("g1jazghxvvgz3egnr2fc8uf72z4g0l03596y9ls7"), &memberstore.Member{InvitationPoints: 2}) // Nemanja - memberstore.Get().SetMember(memberstore.T2, address("g1dfr24yhk5ztwtqn2a36m8f6ud8cx5hww4dkjfl"), &memberstore.Member{InvitationPoints: 2}) // Antonio - memberstore.Get().SetMember(memberstore.T2, address("g12vx7dn3dqq89mz550zwunvg4qw6epq73d9csay"), &memberstore.Member{InvitationPoints: 2}) // Dongwon - memberstore.Get().SetMember(memberstore.T2, address("g17n4y745s08awwq4e0a38lagsgtntna0749tnxe"), &memberstore.Member{InvitationPoints: 2}) // Jinwoo - memberstore.Get().SetMember(memberstore.T2, address("g1mpkp5lm8lwpm0pym4388836d009zfe4maxlqsq"), &memberstore.Member{InvitationPoints: 2}) // Alexis - memberstore.Get().SetMember(memberstore.T2, address("g197q5e9v00vuz256ly7fq7v3ekaun5cr7wmjgfh"), &memberstore.Member{InvitationPoints: 2}) // Salvo - memberstore.Get().SetTier(memberstore.T3) - memberstore.Get().SetMember(memberstore.T3, address("g1qynsu9dwj9lq0m5fkje7jh6qy3md80ztqnshhm"), &memberstore.Member{InvitationPoints: 1}) // Rémi - memberstore.Get().SetMember(memberstore.T3, address("g1mq7g0jszdmn4qdpc9tq94w0gyex37su892n80m"), &memberstore.Member{InvitationPoints: 1}) // Alan - memberstore.Get().SetMember(memberstore.T3, address("g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a"), &memberstore.Member{InvitationPoints: 1}) // Norman - memberstore.Get().SetMember(memberstore.T3, address("g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr"), &memberstore.Member{InvitationPoints: 1}) // Albert - memberstore.Get().SetMember(memberstore.T3, address("g1ckae7tc5sez8ul3ssne75sk4muwgttp6ks2ky9"), &memberstore.Member{InvitationPoints: 1}) // ByeongJun - memberstore.Get().SetMember(memberstore.T3, address("g127l4gkhk0emwsx5tmxe96sp86c05h8vg5tufzq"), &memberstore.Member{InvitationPoints: 1}) // Maxwell - memberstore.Get().SetMember(memberstore.T3, address("g19p3yzr3cuhzqa02j0ce6kzvyjqfzwemw3vam0x"), &memberstore.Member{InvitationPoints: 1}) // Guilhem + // Set the DAO implementation. AllowedDAOs is intentionally left empty + // so that the genesis MsgRun can manipulate the memberstore directly. dao.UpdateImpl(cross, dao.UpdateRequest{ - DAO: impl.GetInstance(), - AllowedDAOs: []string{"gno.land/r/gov/dao/v3/impl"}, + DAO: impl.GetInstance(), }) } diff --git a/examples/gno.land/r/sys/names/verifier.gno b/examples/gno.land/r/sys/names/verifier.gno index bf615b46745..0f075b7a0b5 100644 --- a/examples/gno.land/r/sys/names/verifier.gno +++ b/examples/gno.land/r/sys/names/verifier.gno @@ -1,23 +1,16 @@ -// Package names provides functionality for checking of package deployments -// by users registered in r/sys/users are done to proper namespaces. +// Package names enforces namespace permissions for package deployment. +// Only address-prefix (PA) namespaces are allowed. package names -import ( - "strings" - - "gno.land/p/nt/ownable" - - "gno.land/r/sys/users" -) +import "gno.land/p/nt/ownable/v0" var ( - Ownable = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // dropped in genesis via Enable. XXX We should switch to something better once the GovDAO situation is stabilized. - + Ownable = ownable.NewWithAddressByPrevious("g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p") // genesis deployer — dropped in genesis via Enable. enabled = false ) -// IsAuthorizedAddressForNamespace ensures that the given address has ownership of the given name. -// A user's name found in r/sys/users is equivalent to their namespace. +// IsAuthorizedAddressForNamespace checks if the given address can deploy to the given namespace. +// Only the address's own PA namespace is permitted. func IsAuthorizedAddressForNamespace(address_XXX address, namespace string) bool { return verifier(enabled, address_XXX, namespace) } @@ -26,7 +19,7 @@ func IsAuthorizedAddressForNamespace(address_XXX address, namespace string) bool // 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.DropOwnershipByPrevious(); err != nil { + if err := Ownable.DropOwnership(); err != nil { panic(err) } enabled = true @@ -36,32 +29,18 @@ func IsEnabled() bool { return enabled } -// verifier checks the store to see that the -// user has properly registered a given name/namespace. -// This function considers as valid an `address` that matches the `namespace` (PA namespaces) -func verifier(enabled bool, address_XXX address, namespace string) bool { - if !enabled { +// verifier checks namespace deployment permissions. +// An address matching the namespace is the only allowed case. +func verifier(isEnabled bool, address_XXX address, namespace string) bool { + if !isEnabled { return true // only in pre-genesis cases } - if strings.TrimSpace(address_XXX.String()) == "" || strings.TrimSpace(namespace) == "" { + if namespace == "" || !address_XXX.IsValid() { return false } // Allow user with their own address as namespace - // This enables pseudo-anon namespaces // ie gno.land/{p,r}/{ADDRESS}/** - if address_XXX.String() == namespace { - return true - } - - // Can be a registered namespace or an alias - userData, _ := users.ResolveName(namespace) - if userData == nil || userData.IsDeleted() { - return false - } - - /// XXX: add check for r/sys/teams down the line - - return userData.Addr() == address_XXX + return address_XXX.String() == namespace } diff --git a/examples/gno.land/r/sys/names/verifier_test.gno b/examples/gno.land/r/sys/names/verifier_test.gno index 35805d4bc52..8b55bb53e8c 100644 --- a/examples/gno.land/r/sys/names/verifier_test.gno +++ b/examples/gno.land/r/sys/names/verifier_test.gno @@ -3,46 +3,40 @@ package names import ( "testing" - "gno.land/p/nt/ownable" - "gno.land/p/nt/testutils" - "gno.land/p/nt/uassert" - "gno.land/p/nt/urequire" - "gno.land/r/sys/users" + "gno.land/p/nt/ownable/v0" + "gno.land/p/nt/testutils/v0" + "gno.land/p/nt/uassert/v0" ) var alice = testutils.TestAddress("alice") func TestDefaultVerifier(t *testing.T) { - // Check disabled, any case is true + // Disabled: any case is true uassert.True(t, verifier(false, alice, alice.String())) uassert.True(t, verifier(false, "", alice.String())) uassert.True(t, verifier(false, alice, "somerandomusername")) - // Check enabled - // username + addr mismatch - uassert.False(t, verifier(true, alice, "notregistered")) - // PA namespace check + // Enabled: PA namespace check uassert.True(t, verifier(true, alice, alice.String())) - // Empty name/address - uassert.False(t, verifier(true, address(""), "")) - - // Register proper username - testing.SetRealm(testing.NewCodeRealm("gno.land/r/gnoland/users/v1")) // authorized write - urequire.NoError(t, users.RegisterUser(cross, "alice", alice)) + // Enabled: non-PA namespaces denied + uassert.False(t, verifier(true, alice, "notregistered")) + uassert.False(t, verifier(true, alice, "alice")) - // Proper namespace - uassert.True(t, verifier(true, alice, "alice")) + // Enabled: empty name/address + uassert.False(t, verifier(true, address(""), "")) + uassert.False(t, verifier(true, alice, "")) + uassert.False(t, verifier(true, address(""), "something")) } func TestEnable(t *testing.T) { - testing.SetRealm(testing.NewUserRealm("g1manfred47kzduec920z88wfr64ylksmdcedlf5")) + testing.SetRealm(testing.NewUserRealm("g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p")) uassert.NotPanics(t, func() { Enable(cross) }) - // Confirm enable drops ownerships + // Confirm enable drops ownership uassert.Equal(t, Ownable.Owner().String(), "") uassert.AbortsWithMessage(t, ownable.ErrUnauthorized.Error(), func() { Enable(cross) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 448e80c9a54..dba40f94fc6 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -55,8 +55,9 @@ type startCfg struct { dataDir string lazyInit bool - logLevel string - logFormat string + logLevel string + logFormat string + earlyStart bool } func newStartCmd(io commands.IO) *commands.Command { @@ -163,6 +164,13 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { false, "flag indicating if lazy init is enabled. Generates the node secrets, configuration, and genesis.json", ) + + fs.BoolVar( + &c.earlyStart, + "x-early-start", + false, + "[experimental] start RPC and P2P before genesis time, deferring only consensus", + ) } func execStart(ctx context.Context, c *startCfg, io commands.IO) error { @@ -262,7 +270,11 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { } // Create a default node, with the given setup - gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger) + opts := []node.Option{} + if c.earlyStart { + opts = append(opts, node.WithEarlyStart()) + } + gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger, opts...) if err != nil { return fmt.Errorf("unable to create the Gnoland node, %w", err) } diff --git a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar index 28624b7c95d..d0dde24e05b 100644 --- a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar +++ b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar @@ -1,10 +1,9 @@ loadpkg gno.land/r/sys/names -loadpkg gno.land/r/gnoland/users/v1 adduser admin adduser gui -patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $admin_user_addr # use our custom admin +patchpkg "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p" $admin_user_addr # use our custom admin gnoland start @@ -37,47 +36,30 @@ gnokey query vm/qeval --data "gno.land/r/sys/names.IsEnabled()" stdout 'true bool' -# Try to add a pkg an with unregistered user +# Try to add a pkg with a user on someone else's address as namespace # gui addpkg -> gno.land/r//one ! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/one -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test gui stderr 'is not authorized to deploy packages to namespace' -# Try to add a pkg with an unregistered user, on their own address as namespace +# Try to add a pkg on own address as namespace (PA namespace) # gui addpkg -> gno.land/r//one gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$gui_user_addr/one -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test gui stdout 'OK!' -## Test unregistered namespace +## Test non-PA namespace -# Call addpkg with admin user on gui namespace +# Call addpkg with admin user on arbitrary namespace # admin addpkg -> gno.land/r/guiland/one -# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. ! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test admin stderr 'is not authorized to deploy packages to namespace' -## Test registered namespace - -# test gui register namespace -# gui call -> gnoland/users/v1.Register -gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 -func Register -send "1000000ugnot" -gas-fee 1000000ugnot -gas-wanted 13000000 -broadcast -chainid=tendermint_test -args 'guigui123' gui -stdout 'OK!' - -# Test gui publishing on guigui123/one -# gui addpkg -> gno.land/r/guigui123/one -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guigui123/one -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test gui -stdout 'OK!' - -# Test admin publishing on guigui123/two -# admin addpkg -> gno.land/r/guigui123/two -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guigui123/two -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test admin -stderr 'is not authorized to deploy packages to namespace' - -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/random!name/mypkg -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test gui +# Invalid package path characters +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/random!name/mypkg -gas-fee 1000000ugnot -gas-wanted 550000 -broadcast -chainid=tendermint_test gui stderr 'but got "gno.land/r/random!name/mypkg"' # Test that publishing to filetests is prohibited -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guigui123/filetests -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test gui -stderr 'but got "gno.land/r/guigui123/filetests" ending in filetests' +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$gui_user_addr/filetests -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test gui +stderr 'ending in filetests' -- gnomod.toml -- module = "gno.land/r/mypkg" diff --git a/gno.land/pkg/integration/testdata/user_journey.txtar b/gno.land/pkg/integration/testdata/user_journey.txtar index 2cf90ff295f..bcc1e9430f3 100644 --- a/gno.land/pkg/integration/testdata/user_journey.txtar +++ b/gno.land/pkg/integration/testdata/user_journey.txtar @@ -1,14 +1,17 @@ # This test simulates a user journey in the Gno ecosystem, including # adding packages, using well-known Realms and interracting with other users. -loadpkg gno.land/r/gnoland/users/v1 loadpkg gno.land/r/demo/defi/grc20factory loadpkg gno.land/r/demo/disperse loadpkg gno.land/r/gnoland/wugnot loadpkg gno.land/r/archive/boards +loadpkg gno.land/r/sys/users/init loadpkg gno.land/r/sys/names +# Bootstrap the init controller for user registration +genesiscall gno.land/r/sys/users/init Bootstrap + # Override admin address in r/sys/names with test1 address -patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $test1_user_addr +patchpkg "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p" $test1_user_addr # Add 3 users with different balances adduser user1 1ugnot @@ -141,8 +144,10 @@ stdout '220 int64' # Users discuss on Boards # ########################### -# user3 register as gnoland/user -gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 -func Register -args user333 -gas-fee 1000000ugnot -gas-wanted 14000000 -send 1000000ugnot -broadcast -chainid=tendermint_test user3 +# Register users for boards (boards requires registered users for board creation) +gnokey maketx call -pkgpath gno.land/r/sys/users/init -func RegisterUser -args "user333" -args "$user3_user_addr" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test user3 +stdout 'OK!' +gnokey maketx call -pkgpath gno.land/r/sys/users/init -func RegisterUser -args "user222" -args "$user2_user_addr" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test user2 stdout 'OK!' # user3 creates a new board and a thread @@ -155,10 +160,6 @@ stdout 'OK!' ! gnokey maketx call -pkgpath gno.land/r/archive/boards -func CreateReply -args 1 -args 1 -args 1 -args 'Hello!' -gas-fee 1000000ugnot -gas-wanted 14000000 -broadcast -chainid=tendermint_test user1 stderr 'please register, otherwise minimum fee 100000000 is required if anonymous' -# user2 registers as gnoland/user -gnokey maketx call -pkgpath gno.land/r/gnoland/users/v1 -func Register -args user222 -gas-fee 1000000ugnot -gas-wanted 14000000 -send 1000000ugnot -broadcast -chainid=tendermint_test user2 -stdout 'OK!' - # user2 posts a reply to the thread gnokey maketx call -pkgpath gno.land/r/archive/boards -func CreateReply -args 1 -args 1 -args 1 -args 'Hey hey hey!' -gas-fee 1000000ugnot -gas-wanted 14000000 -broadcast -chainid=tendermint_test user2 stdout 'OK!' @@ -175,36 +176,36 @@ stdout 'OK!' # Add package / home # ###################### -# Enable `sys/names` to deploy packages to user namespace +# Enable `sys/names` to enforce namespace restrictions gnokey maketx call -pkgpath gno.land/r/sys/names -func Enable -gas-fee 100000ugnot -gas-wanted 1500000 -broadcast -chainid tendermint_test test1 stdout 'OK!' -# user2 publishes a custom home package to its namespace -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/user222/home -gas-fee 1000000ugnot -gas-wanted 14000000 -broadcast -chainid=tendermint_test user2 +# user2 publishes a custom home package to its address namespace +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$user2_user_addr/home -gas-fee 1000000ugnot -gas-wanted 14000000 -broadcast -chainid=tendermint_test user2 stdout 'OK!' # Render user2's home package -gnokey query vm/qrender --data "gno.land/r/user222/home:" +gnokey query vm/qrender --data "gno.land/r/$user2_user_addr/home:" stdout 'My awesome home package with admin: '${user2_user_addr} # user2's transfer ownership to user1 -gnokey maketx call -pkgpath gno.land/r/user222/home -func TransferOwnership -args "$user1_user_addr" -send 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 14000000 -broadcast -chainid=tendermint_test user2 +gnokey maketx call -pkgpath gno.land/r/$user2_user_addr/home -func TransferOwnership -args "$user1_user_addr" -send 100000000ugnot -gas-fee 1000000ugnot -gas-wanted 14000000 -broadcast -chainid=tendermint_test user2 stdout 'OK!' # Wait for https://github.com/gnolang/gno/pull/4278 to be merged # # Render user2's home package again, the output should reflect admin change -# gnokey query vm/qrender --data "gno.land/r/user222/home:" +# gnokey query vm/qrender --data "gno.land/r/$user2_user_addr/home:" # stdout 'My awesome home package with admin: '${user1_user_addr} -- gnomod.toml -- -module = "gno.land/r/user222/home" +module = "gno.land/r/home" gno = "0.9" -- home.gno -- package home -import "gno.land/p/nt/ownable" +import "gno.land/p/nt/ownable/v0" var admin = ownable.NewWithOrigin() diff --git a/gnovm/cmd/gno/tool.go b/gnovm/cmd/gno/tool.go index d0d566221bd..ba7475ee675 100644 --- a/gnovm/cmd/gno/tool.go +++ b/gnovm/cmd/gno/tool.go @@ -29,6 +29,7 @@ func newToolCmd(io commands.IO) *commands.Command { // ast // publish/release // render -- call render()? + newDeplistCmd(io), newTranspileCmd(io), // "vm" -- starts an in-memory chain that can be interacted with? ) diff --git a/gnovm/cmd/gno/tool_deplist.go b/gnovm/cmd/gno/tool_deplist.go new file mode 100644 index 00000000000..d341bae7296 --- /dev/null +++ b/gnovm/cmd/gno/tool_deplist.go @@ -0,0 +1,186 @@ +package main + +import ( + "context" + "flag" + "fmt" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type deplistCfg struct { + json bool + testDep bool +} + +func newDeplistCmd(io commands.IO) *commands.Command { + cfg := &deplistCfg{} + return commands.NewCommand( + commands.Metadata{ + Name: "deplist", + ShortUsage: "gno tool deplist [flags] [...]", + ShortHelp: "list dependencies in topological order", + LongHelp: "Deplist resolves transitive dependencies for the given packages and prints them in topological order (dependencies first).", + }, + cfg, + func(_ context.Context, args []string) error { + return execDeplist(cfg, args, io) + }, + ) +} + +func (c *deplistCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar(&c.json, "json", false, "output in JSON format") + fs.BoolVar(&c.testDep, "test-dep", false, "include test dependencies") +} + +func execDeplist(cfg *deplistCfg, args []string, io commands.IO) error { + if len(args) == 0 { + return flag.ErrHelp + } + + loadCfg := packages.LoadConfig{ + Fetcher: testPackageFetcher, + Deps: true, + Test: cfg.testDep, + Out: io.Err(), + } + + pkgs, err := packages.Load(loadCfg, args...) + if err != nil { + return err + } + + if cfg.testDep { + // Iteratively promote dep packages to patterns so their test + // dependencies are loaded, mirroring two-pass `go list -deps -test`. + modCacheDir := filepath.Clean(gnomod.ModCachePath()) + patternFor := func(p *packages.Package) string { + if gnolang.IsStdlib(p.ImportPath) || strings.HasPrefix(filepath.Clean(p.Dir), modCacheDir) { + return p.ImportPath + } + return p.Dir + } + + promoted := make(map[string]struct{}, len(pkgs)) + known := make(map[string]struct{}, len(pkgs)) + for _, p := range pkgs { + known[p.Dir] = struct{}{} + if len(p.Match) != 0 { + promoted[p.Dir] = struct{}{} + } + } + + for i := 0; i < len(pkgs); i++ { + p := pkgs[i] + if gnolang.IsStdlib(p.ImportPath) { + continue + } + if _, ok := promoted[p.Dir]; ok { + continue + } + promoted[p.Dir] = struct{}{} + + more, err := packages.Load(loadCfg, patternFor(p)) + if err != nil { + return err + } + for _, q := range more { + if _, ok := known[q.Dir]; !ok { + known[q.Dir] = struct{}{} + pkgs = append(pkgs, q) + } + } + } + } + + // Filter out stdlibs — they're built into the VM and not deployed. + var userPkgs packages.PkgList + for _, pkg := range pkgs { + if gnolang.IsStdlib(pkg.ImportPath) { + continue + } + userPkgs = append(userPkgs, pkg) + } + + // Topological sort by source imports only. Test deps may form cycles + // (e.g. avl_test → uassert → avl) which is fine — they expand the + // package set but don't affect deployment order. + sorted, err := sortSkipMissing(userPkgs) + if err != nil { + return err + } + + var out []*packages.Package + for _, pkg := range sorted { + if !pkg.Ignore { + out = append(out, pkg) + } + } + + if cfg.json { + lw := newJsonListWriter(io.Out()) + for _, pkg := range out { + if err := lw.write(pkg); err != nil { + return err + } + } + return nil + } + + for _, pkg := range out { + fmt.Fprintln(io.Out(), pkg.Dir) + } + return nil +} + +// sortSkipMissing is a topological sort that silently skips imports +// not present in the package list (e.g. stdlibs filtered out earlier). +func sortSkipMissing(pkgs packages.PkgList) ([]*packages.Package, error) { + byPath := make(map[string]*packages.Package, len(pkgs)) + for _, p := range pkgs { + byPath[p.ImportPath] = p + } + + visited := make(map[string]struct{}) + onStack := make(map[string]bool) + var sorted []*packages.Package + + var visit func(pkg *packages.Package) error + visit = func(pkg *packages.Package) error { + if onStack[pkg.ImportPath] { + return fmt.Errorf("cycle detected: %s", pkg.ImportPath) + } + if _, ok := visited[pkg.ImportPath]; ok { + return nil + } + visited[pkg.ImportPath] = struct{}{} + onStack[pkg.ImportPath] = true + + for _, imp := range pkg.Imports[packages.FileKindPackageSource] { + dep, ok := byPath[imp] + if !ok { + continue // stdlib or other non-user package + } + if err := visit(dep); err != nil { + return err + } + } + + onStack[pkg.ImportPath] = false + sorted = append(sorted, pkg) + return nil + } + + for _, p := range pkgs { + if err := visit(p); err != nil { + return nil, err + } + } + return sorted, nil +} diff --git a/misc/deployments/gnoland1/.gitignore b/misc/deployments/gnoland1/.gitignore new file mode 100644 index 00000000000..63eeeaa081b --- /dev/null +++ b/misc/deployments/gnoland1/.gitignore @@ -0,0 +1,5 @@ +genesis-work/ +genesis.json +genesis-redacted.json +genesis_txs.jsonl +airdrop_balances.txt.gz diff --git a/misc/deployments/gnoland1/Makefile b/misc/deployments/gnoland1/Makefile new file mode 100644 index 00000000000..d485df8767c --- /dev/null +++ b/misc/deployments/gnoland1/Makefile @@ -0,0 +1,40 @@ +REPO_ROOT := $(shell cd ../../.. && pwd) +GENESIS_SHA256 := 97bb99f918d46da375226c9749ba01b5ed1774e0954e225c5f48a9997e7620c6 + +.PHONY: help generate generate-txs test clean + +help: ## Show available targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-16s %s\n", $$1, $$2}' + +generate: genesis.json ## Generate genesis.json and verify checksum + @echo "Verifying genesis.json checksum..." + @shasum -a 256 genesis.json + @echo '$(GENESIS_SHA256) genesis.json' | shasum -a 256 -c + +genesis.json: gen-genesis.sh + ./gen-genesis.sh + +generate-txs: genesis_txs.jsonl ## Generate genesis transactions only + +genesis_txs.jsonl: gen-genesis.sh + ./gen-genesis.sh --txs-only + +test: genesis.json ## Verify genesis syntax and boot a node + @test -s genesis.json || (echo "FAIL: genesis.json is empty" && exit 1) + @gnogenesis verify -genesis-path genesis.json + @# Boot a node to validate genesis block execution (not just syntax). + @echo "Booting node to validate genesis block..." + @DATA=$$(mktemp -d) && \ + timeout 30 gnoland start \ + -lazy \ + -genesis genesis.json \ + -skip-genesis-sig-verification \ + -data-dir "$$DATA" \ + -genesis-balances-file /dev/null \ + -genesis-txs-file /dev/null \ + 2>&1 | grep -q "ABCI Handshake App Info" && \ + rm -rf "$$DATA" && \ + echo "OK: genesis.json is valid (node booted successfully)" + +clean: ## Remove generated files + rm -rf genesis.json packages.gen.txt genesis_txs.jsonl genesis-work diff --git a/misc/deployments/gnoland1/README.md b/misc/deployments/gnoland1/README.md new file mode 100644 index 00000000000..8c900c72c9b --- /dev/null +++ b/misc/deployments/gnoland1/README.md @@ -0,0 +1,63 @@ +# gnoland1 Validator Setup + +Basic, tested instructions for joining the `gnoland1` network as a validator. Advanced operators may adapt these steps to their own infrastructure (Docker, systemd, etc.) at their discretion. + +## 1. Generate the Genesis + +Every validator must produce the same `genesis.json`. Run from this directory: + +```shell +make generate +``` + +This generates the genesis and verifies the sha256 checksum automatically. + +## 2. Build & Install the Node + +From the **repository root** (`gno/`): + +```shell +make install.gnoland install.gnokey +``` + +This installs `gnoland` and `gnokey` to your `$GOPATH/bin`. + +## 3. Initialize Secrets and Configure + +```shell +gnoland secrets init +``` + +For remote signing via `gnokms`, see the [gnokms documentation](../../../contribs/gnokms/README.md). + +Copy the provided config and edit the `# TODO` fields: + +```shell +mkdir -p gnoland-data/config +cp config.toml gnoland-data/config/config.toml +grep -n TODO gnoland-data/config/config.toml # shows what to change +``` + +## 4. Start the Node + +```shell +gnoland start \ + --skip-genesis-sig-verification \ + --genesis genesis.json \ + --data-dir gnoland-data +``` + +The `--skip-genesis-sig-verification` flag is required (known incompatibility between genesis signatures and custom package metadata). + +## 5. Verify & Join + +1. After block 1, verify the AppHash at block 2 matches (confirms deterministic genesis execution): + + ```shell + curl -s http://localhost:26657/block?height=2 | jq -r '.result.block.header.app_hash' + # expected: TBD + ``` + +2. Confirm your node is syncing — `latest_block_height` should be increasing and eventually match [the network RPC](https://rpc.betanet.gno.land/status). +3. Make sure your RPC endpoint is publicly reachable: `http(s)://:26657/status` +4. Ping the team on the validators Signal group so we can add you via a GovDAO proposal. diff --git a/misc/deployments/gnoland1/config.toml b/misc/deployments/gnoland1/config.toml new file mode 100644 index 00000000000..49e486fd388 --- /dev/null +++ b/misc/deployments/gnoland1/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 = "g19v5h4fn72p3zud6qasqtrq3j5vrvdkeuad2jm7@gno-core-sen-01.betanet.testnets.gno.land:26656" + +# 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 = "g19v5h4fn72p3zud6qasqtrq3j5vrvdkeuad2jm7@gno-core-sen-01.betanet.testnets.gno.land:26656" + +# 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 = "gnoland1" +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/gnoland1/gen-genesis.sh b/misc/deployments/gnoland1/gen-genesis.sh new file mode 100755 index 00000000000..cb090dc4f4a --- /dev/null +++ b/misc/deployments/gnoland1/gen-genesis.sh @@ -0,0 +1,498 @@ +#!/usr/bin/env bash +# Generate gnoland1 genesis.json. +# +# Usage: +# ./gen-genesis.sh # full build + genesis generation +# ./gen-genesis.sh --debug # show every command being run +# ./gen-genesis.sh --txs-only # stop after generating txs (skip balance calculation) +# ./gen-genesis.sh --no-install # reuse previously built binaries +set -eo pipefail + +# ============================================================================= +# REVIEW THIS SECTION — update before each genesis generation. +# ============================================================================= + +# Packages to include in genesis (resolved with transitive dependencies). +# Use "..." suffix to match all sub-packages. +FILTERED_PACKAGES=( + ./gno.land/r/sys/... + ./gno.land/r/gov/... + ./gno.land/r/gnoland/blog/... + ./gno.land/r/gnoland/wugnot/... + ./gno.land/r/gnoland/coins/... + ./gno.land/r/gnoland/boards2/... + ./gno.land/r/gnops/valopers/... +) + +# Initial validator set. Format: "name power address pub_key" +# More validators can be added post-genesis via govDAO proposals (see govdao-scripts/add-validator.sh). +# 7 validators — BFT >2/3 threshold (floor(2n/3)+1) means 5 nodes must be up for consensus. +INITIAL_VALSET=( + "gnocore-val-01 1 g1vta7dwp4guuhkfzksenfcheky4xf9hue8mgne4 gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpu5muc9ksphk3cayrduhathd2rw4talmtedpef3a44c2qfzzqalgl4c55y" + "gnocore-val-02 1 g1d5hh9fw3l00gugfzafskaxqlmsyvxfaj6l2q60 gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpnj5vt2vkv94exe6cmdgqgtxmyfkvlhztnl0kj4xv97uz2t0muwe9mka0q" + "moul-val-01 1 g1uhv7wr7nku89se3t7v8fpquc7n5sf8rfkywxpc gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqavtgten8l8k4f72j8klpu4l7tk2qw4kl8394krsaysmz2q0765ynvjag0" + "aeddi-val-01 1 g10jdd8vlgydfypynrk23ul90jnsg5twrtvmcmh4 gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqve8jffvhy97sfc5gyvag09h8g9g3d9e4cta7s7m6vcmzug84kjywg7fn2y" + "berty-val-01 1 g1eueypc9w524ctda3y0kwd4jruw5p4zqpjna0jq gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqsdatvn76ru9pck7zrt7zc33y8p2kweaujx82utgmtwljypuz9p8uctk3m" + "samourai-crew-1 1 g1kn7p0wqumvqlcqzhkwnavkhf0z4qnr73ltwsae gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpufsm93d5fmzxrug76esaxsdsmw0guy9e6geypw7ekz92sl3mte492q62e" + "onbloc-val-01 1 g10j90aqjv6uju3dksq8m08s6u47x59glkdxqzm2 gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqvssjyeck9z7azw2hhw5ujtuds63ezd6w0yee2z5avatcqjtnue978kftp" +) + +# Chain parameters. +CHAIN_ID=gnoland1 +GENESIS_TIME=1773651600 # Monday, March 16th 2026 10:00 GMT+0100 (CET) + +# Airdrop balances (independence-day snapshot). +BALANCES_GZ_URL="https://github.com/gnolang/independence-day/raw/9dec38a4a72c9e84db7e78ae010370de250f2d64/mkgenesis/balances.txt.gz" + +# ============================================================================= +# INTERNAL — everything below is glue, you shouldn't need to change it. +# ============================================================================= + +# Deployer key mnemonic (deterministic — used only for genesis tx signing). +DEPLOYER_MNEMONIC="anchor hurt name seed oak spread anchor filter lesson shaft wasp home improve text behind toe segment lamp turn marriage female royal twice wealth" + +# ---- Flags + +STOP_AFTER_TXS_EXPORT=false +DEBUG=false +NO_INSTALL=false +GENESIS_FILE=genesis.json # set to absolute path below, after SCRIPT_DIR +for arg in "$@"; do + case "$arg" in + --txs-only) STOP_AFTER_TXS_EXPORT=true ;; + --debug) DEBUG=true ;; + --no-install) NO_INSTALL=true ;; + *) + echo "Unknown argument: $arg" + exit 1 + ;; + esac +done + +# run executes a command, printing it first when --debug is set. +run() { + if [ "$DEBUG" = true ]; then + printf " \033[2m\$ %s\033[0m\n" "$*" >&2 + fi + "$@" +} + +# Clean up background node on exit. +NODE_PID="" +cleanup() { [ -n "$NODE_PID" ] && kill "$NODE_PID" 2>/dev/null || true; } +trap cleanup EXIT + +# ---- Derived paths (do not edit) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GENESIS_FILE="$SCRIPT_DIR/$GENESIS_FILE" +WORK_DIR="$SCRIPT_DIR/genesis-work" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +EXAMPLES_DIR="$REPO_ROOT/examples" +GNO_CMD="$REPO_ROOT/gnovm/cmd/gno" +GNOKEY_CMD="$REPO_ROOT/gno.land/cmd/gnokey" +GNOLAND_CMD="$REPO_ROOT/gno.land/cmd/gnoland" +GNOGENESIS_CMD="$REPO_ROOT/contribs/gnogenesis" +WORK_DIR_BIN="$WORK_DIR/bin" +GNO_BIN="$WORK_DIR_BIN/gno" +GNOKEY_BIN="$WORK_DIR_BIN/gnokey" +GNOLAND_BIN="$WORK_DIR_BIN/gnoland" +GNOGENESIS_BIN="$WORK_DIR_BIN/gnogenesis" + +# Clean up previous work directory (preserve bin/ when --no-install). +if [ "$NO_INSTALL" = true ]; then + # Keep binaries, remove everything else. + find "$WORK_DIR" -mindepth 1 -maxdepth 1 ! -name bin -exec rm -rf {} + 2>/dev/null || true +else + rm -rf "$WORK_DIR" +fi + +# ---- 1. Build binaries from source to ensure we have the right versions. + +if [ "$NO_INSTALL" = true ]; then + printf "\n=== Step 1/7: Skipping build (--no-install) ===\n" + for bin in "$GNO_BIN" "$GNOKEY_BIN" "$GNOLAND_BIN" "$GNOGENESIS_BIN"; do + if [ ! -x "$bin" ]; then + echo "ERROR: --no-install but $bin not found. Run without --no-install first." + exit 1 + fi + done +else + printf "\n=== Step 1/7: Building binaries ===\n" + mkdir -p "$WORK_DIR_BIN" + + printf " gno... " + run go build -C "$GNO_CMD" -o "$GNO_BIN" . + printf "ok\n" + + printf " gnokey... " + run go build -C "$GNOKEY_CMD" -o "$GNOKEY_BIN" . + printf "ok\n" + + printf " gnoland... " + run go build -C "$GNOLAND_CMD" -o "$GNOLAND_BIN" . + printf "ok\n" + + printf " gnogenesis... " + run go build -C "$GNOGENESIS_CMD" -o "$GNOGENESIS_BIN" . + printf "ok\n" +fi + +# ---- 2. Generate filtered examples genesis txs. + +printf "\n=== Step 2/7: Generating addpkg txs ===\n" + +printf " Resolving dependencies...\n" +pkg_dirs=$(cd "$EXAMPLES_DIR" && "$GNO_BIN" tool deplist -test-dep "${FILTERED_PACKAGES[@]}") +pkg_count=$(echo "$pkg_dirs" | wc -l | tr -d ' ') +printf " Resolved %s packages in topological order\n" "$pkg_count" + +# Save resolved package list for inspection. +{ + echo "# Generated by gen-genesis.sh — do not edit." + echo "$pkg_dirs" | sed "s|$EXAMPLES_DIR/||g" +} >"$WORK_DIR/packages.gen.txt" + +# Copy resolved packages into the working directory. +printf " Copying packages to staging...\n" +WORK_DIR_EXAMPLES="$WORK_DIR/examples" +mkdir -p "$WORK_DIR_EXAMPLES" +while IFS= read -r dir; do + [[ -z "$dir" ]] && continue + rel="${dir#$EXAMPLES_DIR/}" + dest="$WORK_DIR_EXAMPLES/$rel" + mkdir -p "$dest" + # Copy only regular files (skip subdirectories), except preserve filetests/. + find "$dir" -maxdepth 1 -type f -exec cp {} "$dest/" \; + [[ -d "$dir/filetests" ]] && cp -r "$dir/filetests" "$dest/filetests" +done <<<"$pkg_dirs" + +# Create deployer key (needed to sign MsgAddPackage and MsgRun txs). +printf " Creating deployer key...\n" +WORK_DIR_GNOKEY_HOME="$WORK_DIR/gnokey-home" +WORK_DIR_GENESIS="$WORK_DIR/genesis.json" +WORK_DIR_GENESIS_TXS="$WORK_DIR/genesis_txs.jsonl" +printf '%s\n\n' "$DEPLOYER_MNEMONIC" | run "$GNOKEY_BIN" add --recover GenesisDeployer --home "$WORK_DIR_GNOKEY_HOME" --insecure-password-stdin 2>&1 | sed 's/^/ /' + +printf " Generating empty genesis...\n" +run "$GNOGENESIS_BIN" generate -chain-id "$CHAIN_ID" -genesis-time "$GENESIS_TIME" --output-path "$WORK_DIR_GENESIS" 2>&1 | sed 's/^/ /' + +printf " Adding %s packages to genesis...\n" "$pkg_count" +echo "" | run "$GNOGENESIS_BIN" txs add packages "$WORK_DIR_EXAMPLES" -gno-home "$WORK_DIR_GNOKEY_HOME" -key-name GenesisDeployer --genesis-path "$WORK_DIR_GENESIS" --insecure-password-stdin 2>&1 | sed 's/^/ /' + +printf " Exporting txs...\n" +run "$GNOGENESIS_BIN" txs export "$WORK_DIR_GENESIS_TXS" --genesis-path "$WORK_DIR_GENESIS" 2>&1 | sed 's/^/ /' + +# ---- 3. Generate setup transaction (validators, members, deployer cleanup) + +printf "\n=== Step 3/7: Generating MsgRun setup tx (govdao_prop1.gno) ===\n" + +SETUP_FILE="$SCRIPT_DIR/govdao_prop1.gno" + +printf " Generating MsgRun tx from %s...\n" "$(basename "$SETUP_FILE")" +SETUP_TX="$WORK_DIR/genesis_setup_tx.json" +SETUP_TX_FILE="$WORK_DIR/genesis_setup_tx.jsonl" +run "$GNOKEY_BIN" maketx run \ + --gas-wanted 100000000 \ + --gas-fee 1ugnot \ + --chainid "$CHAIN_ID" \ + --home "$WORK_DIR_GNOKEY_HOME" \ + GenesisDeployer \ + "$SETUP_FILE" >"$SETUP_TX" + +printf " Signing tx...\n" +echo "" | run "$GNOKEY_BIN" sign \ + --tx-path "$SETUP_TX" \ + --chainid "$CHAIN_ID" \ + --account-number 0 \ + --account-sequence 0 \ + --home "$WORK_DIR_GNOKEY_HOME" \ + --insecure-password-stdin \ + GenesisDeployer +jq -c '{tx: .}' <"$SETUP_TX" >"$SETUP_TX_FILE" + +printf " Adding setup tx to genesis...\n" +run "$GNOGENESIS_BIN" txs add sheets "$SETUP_TX_FILE" --genesis-path "$WORK_DIR_GENESIS" 2>&1 | sed 's/^/ /' +cat "$SETUP_TX_FILE" >>"$WORK_DIR_GENESIS_TXS" + +tx_count=$(wc -l <"$WORK_DIR_GENESIS_TXS" | tr -d ' ') +printf " Total txs so far: %s\n" "$tx_count" + +if [ "$STOP_AFTER_TXS_EXPORT" = true ]; then + cp "$WORK_DIR/packages.gen.txt" "$SCRIPT_DIR/packages.gen.txt" + cp "$WORK_DIR_GENESIS_TXS" "$SCRIPT_DIR/genesis_txs.jsonl" + printf "\n=== Done (--txs-only) ===\n" + printf " %s txs exported\n" "$tx_count" + printf " -> packages.gen.txt\n" + printf " -> genesis_txs.jsonl\n" + exit 0 +fi + +# ---- 4. Calculate the deployers balances + +printf "\n=== Step 4/7: Calculating deployer balances ===\n" + +WORK_DIR_DEPLOYER_BALANCES="$WORK_DIR/deployers_balances.txt" +BALANCES_TMP_DIR="$WORK_DIR/balances-work" +BALANCES_TMP_FILE="$BALANCES_TMP_DIR/balances.txt" +BALANCES_TMP_GNOLAND_DATA="$BALANCES_TMP_DIR/gnoland-data" +BALANCES_TMP_GNOLAND_LOG="$BALANCES_TMP_DIR/node.log" +BALANCES_TMP_GENESIS="$BALANCES_TMP_DIR/genesis.json" +BALANCES_TMP_CREATOR_ADDRESSES="$BALANCES_TMP_DIR/gen-creators.txt" +INITIAL_BALANCE=1000000000000000 +NODE_TIMEOUT=120 +# Pick a free port for the temporary node (avoid collisions with running nodes). +NODE_RPC_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()') +NODE_P2P_PORT=$((NODE_RPC_PORT + 1)) +NODE_RPC_ADDR="127.0.0.1:$NODE_RPC_PORT" + +rm -rf "$BALANCES_TMP_DIR" +mkdir -p "$BALANCES_TMP_DIR" + +printf " Extracting creator addresses...\n" +# Extract addresses from both MsgAddPackage ("creator") and MsgRun ("caller") txs. +grep -oE '"(creator|caller)":"[^"]*"' "$WORK_DIR_GENESIS_TXS" | + sed 's/"creator":"//;s/"caller":"//;s/"//g' | + sort -u >"$BALANCES_TMP_CREATOR_ADDRESSES" +addr_count=$(wc -l <"$BALANCES_TMP_CREATOR_ADDRESSES" | tr -d ' ') +printf " Found %s unique creator/caller addresses\n" "$addr_count" + +printf " Generating over-provisioned balances...\n" +while IFS= read -r addr; do + echo "${addr}=${INITIAL_BALANCE}ugnot" >>"$BALANCES_TMP_FILE" +done <"$BALANCES_TMP_CREATOR_ADDRESSES" + +printf " Setting up temporary node...\n" +run "$GNOGENESIS_BIN" generate -chain-id "$CHAIN_ID" -genesis-time "$(date +%s)" -output-path "$BALANCES_TMP_GENESIS" +run "$GNOGENESIS_BIN" txs add sheets "$WORK_DIR_GENESIS_TXS" -genesis-path "$BALANCES_TMP_GENESIS" +run "$GNOGENESIS_BIN" balances add -balance-sheet "$BALANCES_TMP_FILE" -genesis-path "$BALANCES_TMP_GENESIS" +run "$GNOLAND_BIN" config init -config-path "$BALANCES_TMP_GNOLAND_DATA/config/config.toml" +run "$GNOLAND_BIN" config set rpc.laddr "tcp://$NODE_RPC_ADDR" -config-path "$BALANCES_TMP_GNOLAND_DATA/config/config.toml" +run "$GNOLAND_BIN" config set p2p.laddr "tcp://127.0.0.1:$NODE_P2P_PORT" -config-path "$BALANCES_TMP_GNOLAND_DATA/config/config.toml" +run "$GNOLAND_BIN" secrets init -data-dir "$BALANCES_TMP_GNOLAND_DATA/secrets" +run "$GNOGENESIS_BIN" validator add \ + --address "$("$GNOLAND_BIN" secrets get validator_key.address --raw -data-dir "$BALANCES_TMP_GNOLAND_DATA/secrets")" \ + --pub-key "$("$GNOLAND_BIN" secrets get validator_key.pub_key --raw -data-dir "$BALANCES_TMP_GNOLAND_DATA/secrets")" \ + --name balance_generator \ + --power 1 \ + -genesis-path "$BALANCES_TMP_GENESIS" + +printf " Starting node (run 1: measure gas costs)...\n" +"$GNOLAND_BIN" start --skip-genesis-sig-verification -data-dir "$BALANCES_TMP_GNOLAND_DATA" -genesis "$BALANCES_TMP_GENESIS" >"$BALANCES_TMP_GNOLAND_LOG" 2>&1 & +NODE_PID=$! + +elapsed=0 +node_started=false +while [ "$elapsed" -lt "$NODE_TIMEOUT" ]; do + if ! kill -0 "$NODE_PID" 2>/dev/null; then + echo "ERROR: Node stopped unexpectedly. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + exit 1 + fi + if curl -sf "http://$NODE_RPC_ADDR/status" >/dev/null 2>&1; then + node_started=true + break + fi + sleep 1 + elapsed=$((elapsed + 1)) +done + +if [ "$node_started" = false ]; then + kill "$NODE_PID" 2>/dev/null || true + echo "ERROR: Node did not start within ${NODE_TIMEOUT}s. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + exit 1 +fi +printf " Node ready (%ss)\n" "$elapsed" + +printf " Querying remaining balances...\n" +rm -f "$BALANCES_TMP_FILE" +while IFS= read -r addr; do + remaining="" + retry=0 + while [ "$retry" -lt "$NODE_TIMEOUT" ]; do + if ! kill -0 "$NODE_PID" 2>/dev/null; then + echo "ERROR: Node stopped unexpectedly during balance query. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + exit 1 + fi + query_output=$("$GNOKEY_BIN" query -remote "$NODE_RPC_ADDR" "bank/balances/$addr" 2>&1 || true) + if echo "$query_output" | grep -q '^data:'; then + remaining=$(echo "$query_output" | sed -n 's/.*"\([0-9]*\)ugnot".*/\1/p' | head -1) + # Empty data field means 0 balance + remaining=${remaining:-0} + break + fi + sleep 1 + retry=$((retry + 1)) + done + if [ -z "$remaining" ]; then + echo "ERROR: Could not query balance for $addr after ${NODE_TIMEOUT}s. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + kill "$NODE_PID" 2>/dev/null || true + exit 1 + fi + final=$((INITIAL_BALANCE - remaining)) + printf " %s = %s ugnot\n" "$addr" "$final" + echo "${addr}=${final}ugnot" >>"$BALANCES_TMP_FILE" +done <"$BALANCES_TMP_CREATOR_ADDRESSES" + +kill "$NODE_PID" 2>/dev/null || true +wait "$NODE_PID" 2>/dev/null || true +NODE_PID="" + +printf " Setting up temporary node (run 2: verify)...\n" +rm -rf "$BALANCES_TMP_GNOLAND_DATA" "$BALANCES_TMP_GENESIS" + +# Pick fresh ports for run 2 (previous ports may be in TIME_WAIT). +NODE_RPC_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()') +NODE_P2P_PORT=$((NODE_RPC_PORT + 1)) +NODE_RPC_ADDR="127.0.0.1:$NODE_RPC_PORT" + +run "$GNOGENESIS_BIN" generate -chain-id "$CHAIN_ID" -genesis-time "$(date +%s)" -output-path "$BALANCES_TMP_GENESIS" +run "$GNOGENESIS_BIN" txs add sheets "$WORK_DIR_GENESIS_TXS" -genesis-path "$BALANCES_TMP_GENESIS" +run "$GNOGENESIS_BIN" balances add -balance-sheet "$BALANCES_TMP_FILE" -genesis-path "$BALANCES_TMP_GENESIS" +run "$GNOLAND_BIN" config init -config-path "$BALANCES_TMP_GNOLAND_DATA/config/config.toml" +run "$GNOLAND_BIN" config set rpc.laddr "tcp://$NODE_RPC_ADDR" -config-path "$BALANCES_TMP_GNOLAND_DATA/config/config.toml" +run "$GNOLAND_BIN" config set p2p.laddr "tcp://127.0.0.1:$NODE_P2P_PORT" -config-path "$BALANCES_TMP_GNOLAND_DATA/config/config.toml" +run "$GNOLAND_BIN" secrets init -data-dir "$BALANCES_TMP_GNOLAND_DATA/secrets" +run "$GNOGENESIS_BIN" validator add \ + --address "$("$GNOLAND_BIN" secrets get validator_key.address --raw -data-dir "$BALANCES_TMP_GNOLAND_DATA/secrets")" \ + --pub-key "$("$GNOLAND_BIN" secrets get validator_key.pub_key --raw -data-dir "$BALANCES_TMP_GNOLAND_DATA/secrets")" \ + --name balance_generator \ + --power 1 \ + -genesis-path "$BALANCES_TMP_GENESIS" + +printf " Starting node (run 2: verify zero balances)...\n" +"$GNOLAND_BIN" start --skip-genesis-sig-verification -data-dir "$BALANCES_TMP_GNOLAND_DATA" -genesis "$BALANCES_TMP_GENESIS" >"$BALANCES_TMP_GNOLAND_LOG" 2>&1 & +NODE_PID=$! + +elapsed=0 +node_started=false +while [ "$elapsed" -lt "$NODE_TIMEOUT" ]; do + if ! kill -0 "$NODE_PID" 2>/dev/null; then + echo "ERROR: Node stopped unexpectedly. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + exit 1 + fi + if curl -sf "http://$NODE_RPC_ADDR/status" >/dev/null 2>&1; then + node_started=true + break + fi + sleep 1 + elapsed=$((elapsed + 1)) +done + +if [ "$node_started" = false ]; then + kill "$NODE_PID" 2>/dev/null || true + echo "ERROR: Node did not start within ${NODE_TIMEOUT}s. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + exit 1 +fi +printf " Node ready (%ss)\n" "$elapsed" + +printf " Verifying all balances are zero...\n" +all_zero=true +while IFS= read -r addr; do + remaining="" + retry=0 + while [ "$retry" -lt "$NODE_TIMEOUT" ]; do + if ! kill -0 "$NODE_PID" 2>/dev/null; then + echo "ERROR: Node stopped unexpectedly during balance verification. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + exit 1 + fi + query_output=$("$GNOKEY_BIN" query -remote "$NODE_RPC_ADDR" "bank/balances/$addr" 2>&1 || true) + if echo "$query_output" | grep -q '^data:'; then + remaining=$(echo "$query_output" | sed -n 's/.*"\([0-9]*\)ugnot".*/\1/p' | head -1) + # Empty data field means 0 balance + remaining=${remaining:-0} + break + fi + sleep 1 + retry=$((retry + 1)) + done + if [ -z "$remaining" ]; then + echo "ERROR: Could not query balance for $addr after ${NODE_TIMEOUT}s. Last log lines:" + tail -20 "$BALANCES_TMP_GNOLAND_LOG" + kill "$NODE_PID" 2>/dev/null || true + exit 1 + fi + if [ "$remaining" -ne 0 ]; then + printf " FAIL: %s has %sugnot remaining\n" "$addr" "$remaining" + all_zero=false + else + printf " ok: %s\n" "$addr" + fi +done <"$BALANCES_TMP_CREATOR_ADDRESSES" + +kill "$NODE_PID" 2>/dev/null || true +wait "$NODE_PID" 2>/dev/null || true +NODE_PID="" + +if [ "$all_zero" = true ]; then + printf " All balances zero — deployer costs verified\n" + cp "$BALANCES_TMP_FILE" "$WORK_DIR_DEPLOYER_BALANCES" +else + echo "ERROR: Some balances are not zero after replay. Check $BALANCES_TMP_FILE." + exit 1 +fi + +# ---- 5. Add the initial validator set to the genesis file +# (Done before balances — gnogenesis is O(filesize) per call, so adding +# validators while the file is still small saves ~6 minutes.) + +printf "\n=== Step 5/7: Adding validators ===\n" + +for validator in "${INITIAL_VALSET[@]}"; do + read -r name power address pub_key <<<"$validator" + printf " %s (power=%s, %s)\n" "$name" "$power" "$address" + run "$GNOGENESIS_BIN" validator add -name "$name" -power "$power" -address "$address" -pub-key "$pub_key" --genesis-path "$WORK_DIR_GENESIS" +done + +# ---- 6. Add balances (deployer + airdrop) +# These are added last since they bloat the genesis file and make +# subsequent gnogenesis calls slow. + +printf "\n=== Step 6/7: Adding balances ===\n" + +printf " Adding deployer balances to genesis...\n" +run "$GNOGENESIS_BIN" balances add -balance-sheet "$WORK_DIR_DEPLOYER_BALANCES" --genesis-path "$WORK_DIR_GENESIS" >/dev/null + +AIRDROP_BALANCES_GZ="$SCRIPT_DIR/airdrop_balances.txt.gz" +AIRDROP_BALANCES_TXT="$WORK_DIR/airdrop_balances.txt" + +if [ -f "$AIRDROP_BALANCES_GZ" ]; then + printf " Using cached airdrop balances\n" +else + printf " Downloading airdrop balances...\n" + run curl -fsSL "$BALANCES_GZ_URL" -o "$AIRDROP_BALANCES_GZ" +fi +gzip -dkc "$AIRDROP_BALANCES_GZ" >"$AIRDROP_BALANCES_TXT" + +airdrop_count=$(wc -l <"$AIRDROP_BALANCES_TXT" | tr -d ' ') +# TODO: We need to verify if there is a colision between deployer and airdrop addresses. +# See: https://github.com/gnolang/gno/pull/5250/changes#discussion_r2925485031 +printf " Adding %s airdrop balances to genesis...\n" "$airdrop_count" +run "$GNOGENESIS_BIN" balances add -balance-sheet "$AIRDROP_BALANCES_TXT" --genesis-path "$WORK_DIR_GENESIS" >/dev/null + +# ---- 7. Verify the generated genesis file + +printf "\n=== Step 7/7: Verifying genesis ===\n" + +run "$GNOGENESIS_BIN" verify -genesis-path "$WORK_DIR_GENESIS" +printf " Verification passed\n" + +cp "$WORK_DIR_GENESIS" "$GENESIS_FILE" +cp "$WORK_DIR/packages.gen.txt" "$SCRIPT_DIR/packages.gen.txt" +cp "$WORK_DIR_GENESIS_TXS" "$SCRIPT_DIR/genesis_txs.jsonl" + +# Generate a redacted genesis (no airdrop balances) for version control. +jq '.app_state.balances = (.app_state.balances[:10])' "$GENESIS_FILE" >"$SCRIPT_DIR/genesis-redacted.json" + +printf "\n=== Done ===\n" +printf " sha256: %s\n" "$(shasum -a 256 "$GENESIS_FILE" | awk '{print $1}')" +printf " -> genesis.json (gitignored, %s)\n" "$(du -h "$GENESIS_FILE" | cut -f1)" +printf " -> genesis-redacted.json (%s)\n" "$(du -h "$SCRIPT_DIR/genesis-redacted.json" | cut -f1)" +printf " -> packages.gen.txt\n" +printf " -> genesis_txs.jsonl\n" diff --git a/misc/deployments/gnoland1/govdao-scripts/README.md b/misc/deployments/gnoland1/govdao-scripts/README.md new file mode 100644 index 00000000000..e63c01c5049 --- /dev/null +++ b/misc/deployments/gnoland1/govdao-scripts/README.md @@ -0,0 +1,13 @@ +# govDAO Scripts + +Scripts for govDAO members to manage the gnoland1 chain. If you're a validator operator (valoper), you can ignore this directory. + +All scripts default to `GNOKEY_NAME=moul`, `CHAIN_ID=gnoland1`, and `REMOTE=https://rpc.betanet.testnets.gno.land:443`. Override via env vars. + +```bash +./add-validator-from-valopers.sh ADDR # add a validator registered at r/gnops/valopers +./add-validator.sh ADDR PUBKEY [POWER] # add a validator with explicit pub_key +./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 +``` diff --git a/misc/deployments/gnoland1/govdao-scripts/add-validator-from-valopers.sh b/misc/deployments/gnoland1/govdao-scripts/add-validator-from-valopers.sh new file mode 100755 index 00000000000..22433b5cb31 --- /dev/null +++ b/misc/deployments/gnoland1/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: gnoland1) +# REMOTE - RPC endpoint (default: https://rpc.betanet.testnets.gno.land:443) +# GAS_WANTED - gas limit (default: 10000000) +# GAS_FEE - gas fee (default: 1000000ugnot) +set -eo pipefail + +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}" + +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: gnoland1) +# REMOTE - RPC endpoint (default: 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:-gnoland1}" +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" <"$TMPDIR/extend_govdao.gno" <<'GOEOF' +package main + +import ( + "gno.land/r/gov/dao/v3/memberstore" +) + +func must(err error) { + if err != nil { + panic(err.Error()) + } +} + +func main() { + ms := memberstore.Get() + must(ms.SetMember(memberstore.T1, address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"), &memberstore.Member{InvitationPoints: 3})) // Jae + must(ms.SetMember(memberstore.T1, address("g1m0rgan0rla00ygmdmp55f5m0unvsvknluyg2a4"), &memberstore.Member{InvitationPoints: 3})) // Morgan + must(ms.SetMember(memberstore.T1, address("g1mx4pum9976th863jgry4sdjzfwu03qan5w2v9j"), &memberstore.Member{InvitationPoints: 3})) // Ray + must(ms.SetMember(memberstore.T1, address("g12vx7dn3dqq89mz550zwunvg4qw6epq73d9csay"), &memberstore.Member{InvitationPoints: 3})) // Dongwon + must(ms.SetMember(memberstore.T1, address("g127l4gkhk0emwsx5tmxe96sp86c05h8vg5tufzq"), &memberstore.Member{InvitationPoints: 3})) // Maxwell + must(ms.SetMember(memberstore.T1, address("g1e6gxg5tvc55mwsn7t7dymmlasratv7mkv0rap2"), &memberstore.Member{InvitationPoints: 3})) // Milos +} +GOEOF + +echo "Extending govDAO T1 with 6 new members" +echo " Key: ${GNOKEY_NAME}" +echo " Chain: ${CHAIN_ID}" +echo " Remote: ${REMOTE}" +echo "" + +gnokey maketx run \ + -gas-wanted "$GAS_WANTED" \ + -gas-fee "$GAS_FEE" \ + -broadcast \ + -chainid "$CHAIN_ID" \ + -remote "$REMOTE" \ + "$GNOKEY_NAME" \ + "$TMPDIR/extend_govdao.gno" + +echo "" +echo "Done — 6 T1 members added (Jae, Morgan, Ray, Dongwon, Maxwell, Milos)." diff --git a/misc/deployments/gnoland1/govdao-scripts/rm-validator.sh b/misc/deployments/gnoland1/govdao-scripts/rm-validator.sh new file mode 100755 index 00000000000..a1c32ab7715 --- /dev/null +++ b/misc/deployments/gnoland1/govdao-scripts/rm-validator.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Remove a validator from gnoland1 via govDAO proposal. +# +# Usage: +# ./rm-validator.sh
+# +# Environment: +# GNOKEY_NAME - gnokey key name (default: moul) +# CHAIN_ID - chain ID (default: gnoland1) +# REMOTE - RPC endpoint (default: 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:-gnoland1}" +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:-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/unrestrict.gno" <2/3 threshold (floor(2n/3)+1) means 5 nodes must be up for consensus. + // Additional validators can be added post-genesis via govDAO proposals + // (see govdao-scripts/add-validator.sh). + govExec(valr.NewPropRequest( + func() []validators.Validator { + return []validators.Validator{ + { + Address: address("g1vta7dwp4guuhkfzksenfcheky4xf9hue8mgne4"), + PubKey: "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpu5muc9ksphk3cayrduhathd2rw4talmtedpef3a44c2qfzzqalgl4c55y", + VotingPower: 1, + }, + { + Address: address("g1d5hh9fw3l00gugfzafskaxqlmsyvxfaj6l2q60"), + PubKey: "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpnj5vt2vkv94exe6cmdgqgtxmyfkvlhztnl0kj4xv97uz2t0muwe9mka0q", + VotingPower: 1, + }, + { + Address: address("g1uhv7wr7nku89se3t7v8fpquc7n5sf8rfkywxpc"), + PubKey: "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqavtgten8l8k4f72j8klpu4l7tk2qw4kl8394krsaysmz2q0765ynvjag0", + VotingPower: 1, + }, + { + Address: address("g10jdd8vlgydfypynrk23ul90jnsg5twrtvmcmh4"), + PubKey: "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqve8jffvhy97sfc5gyvag09h8g9g3d9e4cta7s7m6vcmzug84kjywg7fn2y", + VotingPower: 1, + }, + { + Address: address("g1eueypc9w524ctda3y0kwd4jruw5p4zqpjna0jq"), + PubKey: "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqsdatvn76ru9pck7zrt7zc33y8p2kweaujx82utgmtwljypuz9p8uctk3m", + VotingPower: 1, + }, + { + Address: address("g1kn7p0wqumvqlcqzhkwnavkhf0z4qnr73ltwsae"), + PubKey: "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zpufsm93d5fmzxrug76esaxsdsmw0guy9e6geypw7ekz92sl3mte492q62e", + VotingPower: 1, + }, + { + Address: address("g10j90aqjv6uju3dksq8m08s6u47x59glkdxqzm2"), + PubKey: "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqvssjyeck9z7azw2hhw5ujtuds63ezd6w0yee2z5avatcqjtnue978kftp", + VotingPower: 1, + }, + } + }, + "Add initial validator set", + "Bootstrap the gnoland1 initial validator set", + )) + + // ---- 3. Set chain parameters via governance proposals ---- + + // Unrestricted addresses (can transfer ugnot even when bank is locked). + govExec(params.ProposeAddUnrestrictedAcctsRequest( + address("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh"), // GovDAO T1 multisig + address("g148583t5x66zs6p90ehad6l4qefeyaf54s69wql"), // faucet + )) + + // Lock ugnot transfers (restricted denom). + govExec(params.ProposeLockTransferRequest()) + + // ---- 4. Enable namespace enforcement (r/sys/names) ---- + // This drops ownership and enables the namespace permission check. + // Must be called during genesis; after this, package paths require registration. + names.Enable(cross) + + // ---- 5. Add govDAO T1 member ---- + // Temporarily manfred (moul) is the sole govDAO member. He will manually + // craft and execute proposals to onboard validators and additional members + // until the DAO is large enough for real multi-party governance. + must(ms.SetMember(memberstore.T1, address("g1manfred47kzduec920z88wfr64ylksmdcedlf5"), &memberstore.Member{InvitationPoints: 3})) // moul + + // ---- 6. Remove the genesis deployer ---- + ms.RemoveMember(genesisDeployerAddr) + + // ---- 7. Lock down memberstore access ---- + // From this point on, only gno.land/r/gov/dao/v3/impl can call + // memberstore.Get() — matching the production AllowedDAOs configuration. + dao.UpdateImpl(cross, dao.UpdateRequest{ + AllowedDAOs: []string{"gno.land/r/gov/dao/v3/impl"}, + }) +} + +// govExec creates a proposal, votes YES, and executes it immediately. +// Works because the deployer is the sole T1 member (100% supermajority). +func govExec(r dao.ProposalRequest) { + pid := dao.MustCreateProposal(cross, r) + dao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: pid}) + dao.ExecuteProposal(cross, pid) +} + +func must(err error) { + if err != nil { + panic(err.Error()) + } +} diff --git a/misc/deployments/gnoland1/packages.gen.txt b/misc/deployments/gnoland1/packages.gen.txt new file mode 100644 index 00000000000..f2db2dc0eed --- /dev/null +++ b/misc/deployments/gnoland1/packages.gen.txt @@ -0,0 +1,78 @@ +# Generated by gen-genesis.sh — do not edit. +gno.land/p/nt/avl/v0 +gno.land/p/nt/mux/v0 +gno.land/p/nt/ufmt/v0 +gno.land/p/demo/blog +gno.land/p/nt/cford32/v0 +gno.land/p/nt/seqid/v0 +gno.land/r/gov/dao +gno.land/r/gnoland/blog +gno.land/p/gnoland/boards +gno.land/p/moul/addrset +gno.land/p/nt/avl/v0/rotree +gno.land/p/nt/avl/v0/list +gno.land/p/nt/commondao/v0 +gno.land/p/moul/ulist +gno.land/p/jeronimoalbi/message +gno.land/p/nt/commondao/v0/exts/storage +gno.land/p/gnoland/boards/exts/permissions +gno.land/p/jeronimoalbi/mdform +gno.land/p/jeronimoalbi/pager +gno.land/p/demo/svg +gno.land/p/leon/svgbtn +gno.land/p/moul/md +gno.land/p/moul/mdtable +gno.land/p/moul/realmpath +gno.land/p/moul/txlink +gno.land/p/nt/mdalert/v0 +gno.land/r/sys/users +gno.land/r/gnoland/boards2/v1 +gno.land/p/leon/coinsort +gno.land/p/leon/ctg +gno.land/r/gnoland/coins +gno.land/p/demo/tokens/grc20 +gno.land/p/nt/fqname/v0 +gno.land/r/demo/defi/grc20reg +gno.land/r/gnoland/wugnot +gno.land/p/moul/once +gno.land/p/moul/authz +gno.land/p/nt/avl/v0/pager +gno.land/p/nt/combinederr/v0 +gno.land/p/nt/ownable/v0 +gno.land/p/nt/ownable/v0/exts/authorizable +gno.land/r/gnops/valopers +gno.land/p/sys/validators +gno.land/p/nt/poa/v0 +gno.land/r/sys/validators/v2 +gno.land/r/gnops/valopers/proposal +gno.land/p/aeddi/panictoerr +gno.land/p/moul/helplink +gno.land/p/nt/treasury/v0 +gno.land/p/sunspirit/md +gno.land/p/samcrew/piechart +gno.land/p/mason/md +gno.land/p/samcrew/tablesort +gno.land/p/samcrew/urlfilter +gno.land/r/gov/dao/v3/memberstore +gno.land/r/gov/dao/v3/treasury +gno.land/r/gov/dao/v3/impl +gno.land/r/gov/dao/v3/init +gno.land/r/gov/dao/v3/loader +gno.land/r/gov/dao/v3/treasury/test +gno.land/r/sys/cla +gno.land/r/sys/names +gno.land/r/sys/params +gno.land/r/sys/rewards +gno.land/r/sys/txfees +gno.land/r/sys/users/init +gno.land/p/nt/testutils/v0 +gno.land/p/onbloc/diff +gno.land/p/nt/uassert/v0 +gno.land/p/nt/urequire/v0 +gno.land/p/demo/nestedpkg +gno.land/r/tests/vm/subtests +gno.land/r/tests/vm +gno.land/r/demo/defi/grc20factory +gno.land/p/sunspirit/table +gno.land/p/jeronimoalbi/expect +gno.land/p/moul/typeutil diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 03c2bd455f5..172276a11bb 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -105,6 +105,7 @@ func DefaultNewNode( genesisFile string, evsw events.EventSwitch, logger *slog.Logger, + options ...Option, ) (*Node, error) { // Generate node PrivKey nodeKey, err := p2pTypes.LoadOrMakeNodeKey(config.NodeKeyFile()) @@ -139,12 +140,21 @@ func DefaultNewNode( DefaultDBProvider, evsw, logger, + options..., ) } // Option sets a parameter for the node. type Option func(*Node) +// WithEarlyStart starts RPC and P2P before genesis time, +// deferring only consensus until the genesis timestamp is reached. +func WithEarlyStart() Option { + return func(n *Node) { + n.earlyStart = true + } +} + // ------------------------------------------------------------------------------ // Node is the highest level interface to a full Tendermint node. @@ -179,6 +189,8 @@ type Node struct { txEventStore eventstore.TxEventStore eventStoreService *eventstore.Service firstBlockSignal <-chan struct{} + + earlyStart bool // start RPC+P2P before genesis time, defer only consensus } func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { @@ -570,11 +582,17 @@ func NewNode(config *cfg.Config, // OnStart starts the Node. It implements service.Service. func (n *Node) OnStart() error { - now := tmtime.Now() genTime := n.genesisDoc.GenesisTime - if genTime.After(now) { - n.Logger.Info("Genesis time is in the future. Sleeping until then...", "genTime", genTime) - time.Sleep(genTime.Sub(now)) + + if !n.earlyStart { + // Default: block everything until genesis time. + now := tmtime.Now() + if genTime.After(now) { + n.Logger.Info("Genesis time is in the future. Sleeping until then...", "genTime", genTime) + time.Sleep(genTime.Sub(now)) + } + } else if genTime.After(tmtime.Now()) { + n.Logger.Info("Genesis time is in the future. Starting RPC+P2P early (-x-early-start)", "genTime", genTime) } // Set up the GLOBAL variables in rpc/core which refer to this node. @@ -638,6 +656,15 @@ func (n *Node) OnStart() error { // Dial the persistent peers n.sw.DialPeers(peerAddrs...) + // If early start, wait for genesis time now (RPC+P2P already running). + if n.earlyStart { + now := tmtime.Now() + if genTime.After(now) { + n.Logger.Info("RPC+P2P running. Waiting for genesis time to start consensus...", "genTime", genTime, "sleep", genTime.Sub(now)) + time.Sleep(genTime.Sub(now)) + } + } + return nil }