From 273fb27adb687c0e71b423ff5a49fa7f179df9f0 Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Tue, 17 Dec 2024 01:13:57 -0800 Subject: [PATCH 01/27] feat: dynamic gas price, keeper implementation (#2838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Context This PR is inspired by EIP-1559 and adjusts the gas price based on the ratio of gas used in the last block compared to the target block gas. The gas price is enforced globally across the network. However, validators can still configure a minimum gas price (min-gas-price) to reject low-fee transactions and prevent mempool spam. A higher gas price will take precedence when configured. Current implementation is an alternative to [PR2544](https://github.com/gnolang/gno/pull/2544) and is based on the feedbacks. Here are the main differences: - Dynamic gas prices are managed by a new auth.GasPriceKeeper, rather than being saved in the block header. - Gas price configurations have been moved from consensus parameters to GnoGenesisState and are stored in a new parameter module. - The parameters can be modified later through governance proposals, making it easier to update these configurations without requiring a chain upgrade. - All implementations are on the application side, with no changes made to the consensus layer. # High level flow Start a new node from genesis. The initial gas price and formula parameters are saved in the genesis and loaded into the params keeper and gas keeper. ![image](https://github.com/user-attachments/assets/6f7bbf56-5196-4ee2-9c77-c55331cbfde6) When a node receives a new transaction, the application checks if the user has provided sufficient fees for the transaction. It will reject the transaction if it does not meet the gas price set by the network and individual nodes. ![image](https://github.com/user-attachments/assets/c9123370-0f83-4ef9-a4e6-a09c6aad98c9) The node processes the entire block during the proposal, voting, and restart phases. The GasPriceKeeper will calculate and update the gas price according to the formula in the application’s EndBlock() function. ![image](https://github.com/user-attachments/assets/51d233be-318b-4f05-8a45-3157604657ea) # Formular ![image](https://github.com/user-attachments/assets/ba282aba-a145-46d3-80b8-dcc5787d2a0b) The compressor is used to reduce the impact on price caused by sudden changes in the gas used within a block ## When the last gas used in a block is above the target gas, we increase the gas price ![image](https://github.com/user-attachments/assets/bb31dcbe-aaab-4c1a-b96f-156dafef80fc) ## When the last gas used in a block is below the target gas, we decrease the gas price until it returns to the initial gas price in the block. ![image](https://github.com/user-attachments/assets/c200cd1a-d4f3-4b4d-9198-2af08ad657ab) ## Impact The Cosmos SDK has an optional setting for a minimum gas price. Each validator can configure their own values to only accept transactions with a gas price that meets their setting in the mempool. When a user submits a transaction on-chain, the gas price is calculated as gas-fee / gas-wanted. With the addition of the block gas price, a network-wide minimum gas price is enforced for every validator. Users will need to provide a gas price that meets the requirements set by both the validator and the network.
Contributors' checklist... - [X] Added new tests - [X] Provided an example (e.g. screenshot) to aid review - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--- contribs/gnodev/go.mod | 4 +- contribs/gnodev/pkg/dev/node.go | 31 +- contribs/gnodev/pkg/dev/node_state.go | 16 +- gno.land/cmd/gnoland/start.go | 8 +- gno.land/pkg/gnoclient/integration_test.go | 16 +- gno.land/pkg/gnoland/app.go | 31 +- gno.land/pkg/gnoland/app_test.go | 415 ++++++++++++++++-- gno.land/pkg/gnoland/genesis.go | 20 + gno.land/pkg/gnoland/types.go | 8 +- .../pkg/integration/testing_integration.go | 19 +- gno.land/pkg/integration/testing_node.go | 30 +- gno.land/pkg/sdk/vm/common_test.go | 5 +- tm2/pkg/sdk/auth/abci.go | 19 + tm2/pkg/sdk/auth/ante.go | 29 +- tm2/pkg/sdk/auth/ante_test.go | 80 ++++ tm2/pkg/sdk/auth/consts.go | 3 +- tm2/pkg/sdk/auth/genesis.go | 31 ++ tm2/pkg/sdk/auth/keeper.go | 157 ++++++- tm2/pkg/sdk/auth/keeper_test.go | 105 +++++ tm2/pkg/sdk/auth/params.go | 99 ++++- tm2/pkg/sdk/auth/params_test.go | 107 +++++ tm2/pkg/sdk/auth/test_common.go | 9 +- tm2/pkg/sdk/auth/types.go | 31 ++ tm2/pkg/sdk/bank/common_test.go | 5 +- tm2/pkg/sdk/baseapp.go | 19 +- tm2/pkg/sdk/baseapp_test.go | 10 +- tm2/pkg/sdk/params/keeper.go | 34 +- tm2/pkg/sdk/params/keeper_test.go | 21 + tm2/pkg/std/gasprice.go | 29 ++ tm2/pkg/std/gasprice_test.go | 156 +++++++ tm2/pkg/std/package.go | 4 + tm2/pkg/std/package_test.go | 53 +++ tm2/pkg/telemetry/metrics/metrics.go | 11 + 33 files changed, 1465 insertions(+), 150 deletions(-) create mode 100644 tm2/pkg/sdk/auth/abci.go create mode 100644 tm2/pkg/sdk/auth/genesis.go create mode 100644 tm2/pkg/sdk/auth/params_test.go create mode 100644 tm2/pkg/std/gasprice_test.go diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 3b895975950..b5b5a402c2a 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -1,6 +1,8 @@ module github.com/gnolang/gno/contribs/gnodev -go 1.22.0 +go 1.22 + +toolchain go1.22.4 replace github.com/gnolang/gno => ../.. diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 0502c03c86f..fa9e2d11e29 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -122,12 +122,9 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), } - - // generate genesis state - genesis := gnoland.GnoGenesisState{ - Balances: cfg.BalancesList, - Txs: append(pkgsTxs, cfg.InitialTxs...), - } + genesis := gnoland.DefaultGenState() + genesis.Balances = cfg.BalancesList + genesis.Txs = append(pkgsTxs, cfg.InitialTxs...) if err := devnode.rebuildNode(ctx, genesis); err != nil { return nil, fmt.Errorf("unable to initialize the node: %w", err) @@ -288,10 +285,9 @@ func (n *Node) Reset(ctx context.Context) error { // Append initialTxs txs := append(pkgsTxs, n.initialState...) - genesis := gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: txs, - } + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = txs // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) @@ -413,10 +409,10 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } - - return n.rebuildNode(ctx, gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, Txs: txs, - }) + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = txs + return n.rebuildNode(ctx, genesis) } state, err := n.getBlockStoreState(ctx) @@ -431,10 +427,9 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Create genesis with loaded pkgs + previous state - genesis := gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: append(pkgsTxs, state...), - } + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = append(pkgsTxs, state...) // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 73362a5f1c8..3f996bc7716 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -92,10 +92,9 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { newState := n.state[:newIndex] // Create genesis with loaded pkgs + previous state - genesis := gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: append(pkgsTxs, newState...), - } + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = append(pkgsTxs, newState...) // Reset the node with the new genesis state. if err = n.rebuildNode(ctx, genesis); err != nil { @@ -132,10 +131,11 @@ func (n *Node) ExportStateAsGenesis(ctx context.Context) (*bft.GenesisDoc, error // Get current blockstore state doc := *n.Node.GenesisDoc() // copy doc - doc.AppState = gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: state, - } + + genState := doc.AppState.(gnoland.GnoGenesisState) + genState.Balances = n.config.BalancesList + genState.Txs = state + doc.AppState = genState return &doc, nil } diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 77d7e20b8ef..a420e652810 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -410,10 +410,10 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro genesisTxs = append(pkgsTxs, genesisTxs...) // Construct genesis AppState. - gen.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: genesisTxs, - } + defaultGenState := gnoland.DefaultGenState() + defaultGenState.Balances = balances + defaultGenState.Txs = genesisTxs + gen.AppState = defaultGenState // Write genesis state if err := gen.SaveAs(genesisFile); err != nil { diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 3df6175576f..4b70fb60c49 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -39,7 +39,7 @@ func TestCallSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, @@ -92,7 +92,7 @@ func TestCallMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, @@ -154,7 +154,7 @@ func TestSendSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, @@ -218,7 +218,7 @@ func TestSendMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, @@ -290,7 +290,7 @@ func TestRunSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, @@ -358,7 +358,7 @@ func TestRunMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2300000), GasWanted: 23000000, AccountNumber: 0, SequenceNumber: 0, @@ -451,7 +451,7 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, @@ -536,7 +536,7 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), + GasFee: ugnot.ValueString(2100000), GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index e0c93f6194f..9e8f2163441 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -89,16 +89,18 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. - acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) - bankKpr := bank.NewBankKeeper(acctKpr) paramsKpr := params.NewParamsKeeper(mainKey, "vm") + acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) + gpKpr := auth.NewGasPriceKeeper(mainKey) + bankKpr := bank.NewBankKeeper(acctKpr) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) vmk.Output = cfg.VMOutput // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp - icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.paramsKpr = acctKpr, bankKpr, vmk, paramsKpr + icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.paramsKpr, icc.gpKpr = acctKpr, bankKpr, vmk, paramsKpr, gpKpr baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler @@ -112,9 +114,11 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { func(ctx sdk.Context, tx std.Tx, simulate bool) ( newCtx sdk.Context, res sdk.Result, abort bool, ) { + // Add last gas price in the context + ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx)) + // Override auth params. - ctx = ctx. - WithValue(auth.AuthParamsContextKey{}, auth.DefaultParams()) + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) // Continue on with default auth ante handler. newCtx, res, abort = authAnteHandler(ctx, tx, simulate) return @@ -145,6 +149,8 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.SetEndBlocker( EndBlocker( c, + acctKpr, + gpKpr, vmk, baseApp, ), @@ -236,6 +242,7 @@ type InitChainerConfig struct { acctKpr auth.AccountKeeperI bankKpr bank.BankKeeperI paramsKpr params.ParamsKeeperI + gpKpr auth.GasPriceKeeperI } // InitChainer is the function that can be used as a [sdk.InitChainer]. @@ -293,6 +300,10 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci if !ok { return nil, fmt.Errorf("invalid AppState of type %T", appState) } + cfg.acctKpr.InitGenesis(ctx, state.Auth) + params := cfg.acctKpr.GetParams(ctx) + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params) + auth.InitChainer(ctx, cfg.gpKpr.(auth.GasPriceKeeper), params.InitialGasPrice) // Apply genesis balances. for _, bal := range state.Balances { @@ -370,6 +381,8 @@ type endBlockerApp interface { // validator set changes func EndBlocker( collector *collector[validatorUpdate], + acctKpr auth.AccountKeeperI, + gpKpr auth.GasPriceKeeperI, vmk vm.VMKeeperI, app endBlockerApp, ) func( @@ -377,6 +390,14 @@ func EndBlocker( 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 + // the params to calculate the updated gas price. + if acctKpr != nil { + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) + } + if acctKpr != nil && gpKpr != nil { + auth.EndBlocker(ctx, gpKpr) + } // Check if there was a valset change if len(collector.getEvents()) == 0 { // No valset updates diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 999e04b2c4b..375602cfa4a 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -19,6 +19,10 @@ import ( "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/sdk/params" + "github.com/gnolang/gno/tm2/pkg/sdk/testutils" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" @@ -38,6 +42,36 @@ func TestNewAppWithOptions(t *testing.T) { assert.Equal(t, "gnoland", bapp.Name()) addr := crypto.AddressFromPreimage([]byte("test1")) + + appState := DefaultGenState() + appState.Balances = []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + } + appState.Txs = []TxWithMetadata{ + { + Tx: std.Tx{ + Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*gnovm.MemFile{ + { + Name: "demo.gno", + Body: "package demo; func Hello() string { return `hello`; }", + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, + }, + } + appState.Params = []Param{ + {key: "foo", kind: "string", value: "hello"}, + {key: "foo", kind: "int64", value: int64(-42)}, + {key: "foo", kind: "uint64", value: uint64(1337)}, + {key: "foo", kind: "bool", value: true}, + {key: "foo", kind: "bytes", value: []byte{0x48, 0x69, 0x21}}, + } + resp := bapp.InitChain(abci.RequestInitChain{ Time: time.Now(), ChainID: "dev", @@ -45,35 +79,7 @@ func TestNewAppWithOptions(t *testing.T) { Block: defaultBlockParams(), }, Validators: []abci.ValidatorUpdate{}, - AppState: GnoGenesisState{ - Balances: []Balance{ - { - Address: addr, - Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, - }, - }, - Txs: []TxWithMetadata{ - { - Tx: std.Tx{ - Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*gnovm.MemFile{ - { - Name: "demo.gno", - Body: "package demo; func Hello() string { return `hello`; }", - }, - })}, - Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, - Signatures: []std.Signature{{}}, // one empty signature - }, - }, - }, - Params: []Param{ - {key: "foo", kind: "string", value: "hello"}, - {key: "foo", kind: "int64", value: int64(-42)}, - {key: "foo", kind: "uint64", value: uint64(1337)}, - {key: "foo", kind: "bool", value: true}, - {key: "foo", kind: "bytes", value: []byte{0x48, 0x69, 0x21}}, - }, - }, + AppState: appState, }) require.True(t, resp.IsOK(), "InitChain response: %v", resp) @@ -142,7 +148,7 @@ func TestNewApp(t *testing.T) { }, }, Validators: []abci.ValidatorUpdate{}, - AppState: GnoGenesisState{}, + AppState: DefaultGenState(), }) assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) } @@ -212,8 +218,12 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper vmKpr: mock, CacheStdlibLoad: cached, } + // Construct keepers. + paramsKpr := params.NewParamsKeeper(iavlCapKey, "") + cfg.acctKpr = auth.NewAccountKeeper(iavlCapKey, paramsKpr, ProtoGnoAccount) + cfg.gpKpr = auth.NewGasPriceKeeper(iavlCapKey) cfg.InitChainer(testCtx, abci.RequestInitChain{ - AppState: GnoGenesisState{}, + AppState: DefaultGenState(), }) // assert number of calls @@ -485,7 +495,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](&mockEventSwitch{}, noFilter) // Create the EndBlocker - eb := EndBlocker(c, nil, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, nil, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -525,7 +535,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(gnostdlibs.GnoEvent{}) // Create the EndBlocker - eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -568,7 +578,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(gnostdlibs.GnoEvent{}) // Create the EndBlocker - eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -636,7 +646,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(txEvent) // Create the EndBlocker - eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -651,3 +661,338 @@ func TestEndBlocker(t *testing.T) { } }) } + +func TestGasPriceUpdate(t *testing.T) { + app := newGasPriceTestApp(t) + + // with default initial gas price 0.1 ugnot per gas + gnoGen := gnoGenesisState(t) + + // abci inintChain + app.InitChain(abci.RequestInitChain{ + AppState: gnoGen, + ChainID: "test-chain", + ConsensusParams: &abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxGas: 10000, + }, + }, + }) + baseApp := app.(*sdk.BaseApp) + require.Equal(t, int64(0), baseApp.LastBlockHeight()) + // Case 1 + // CheckTx failed because the GasFee is less than the initial gas price. + + tx := newCounterTx(100) + tx.Fee = std.Fee{ + GasWanted: 100, + GasFee: sdk.Coin{ + Amount: 9, + Denom: "ugnot", + }, + } + txBytes, err := amino.Marshal(tx) + require.NoError(t, err) + r := app.CheckTx(abci.RequestCheckTx{Tx: txBytes}) + assert.False(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // Case 2: + // A previously successful CheckTx failed after the block gas price increased. + // Check Tx Ok + tx2 := newCounterTx(100) + tx2.Fee = std.Fee{ + GasWanted: 1000, + GasFee: sdk.Coin{ + Amount: 100, + Denom: "ugnot", + }, + } + txBytes2, err := amino.Marshal(tx2) + require.NoError(t, err) + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.True(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // After replaying a block, the gas price increased. + header := &bft.Header{ChainID: "test-chain", Height: 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + // Delvier Tx consumes more than that target block gas 6000. + + tx6001 := newCounterTx(6001) + tx6001.Fee = std.Fee{ + GasWanted: 20000, + GasFee: sdk.Coin{ + Amount: 200, + Denom: "ugnot", + }, + } + txBytes6001, err := amino.Marshal(tx6001) + require.NoError(t, err) + res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes6001}) + require.True(t, res.IsOK(), fmt.Sprintf("%v", res)) + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + + // CheckTx failed because gas price increased + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.False(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // Case 3: + // A previously failed CheckTx successed after block gas price reduced. + + // CheckTx Failed + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.False(t, r.IsOK(), fmt.Sprintf("%v", r)) + // Replayed a Block, the gas price decrease + header = &bft.Header{ChainID: "test-chain", Height: 2} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + // Delvier Tx consumes less than that target block gas 6000. + + tx200 := newCounterTx(200) + tx200.Fee = std.Fee{ + GasWanted: 20000, + GasFee: sdk.Coin{ + Amount: 200, + Denom: "ugnot", + }, + } + txBytes200, err := amino.Marshal(tx200) + require.NoError(t, err) + + res = app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes200}) + require.True(t, res.IsOK(), fmt.Sprintf("%v", res)) + + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + + // CheckTx earlier failed tx, now is OK + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.True(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // Case 4 + // require matching expected GasPrice after three blocks ( increase case) + replayBlock(t, baseApp, 8000, 3) + replayBlock(t, baseApp, 8000, 4) + replayBlock(t, baseApp, 6000, 5) + + key := []byte("gasPrice") + query := abci.RequestQuery{ + Path: ".store/main/key", + Data: key, + } + qr := app.Query(query) + var gp std.GasPrice + err = amino.Unmarshal(qr.Value, &gp) + require.NoError(t, err) + require.Equal(t, "108ugnot", gp.Price.String()) + + // Case 5, + // require matching expected GasPrice after low gas blocks ( decrease below initial gas price case) + + replayBlock(t, baseApp, 5000, 6) + replayBlock(t, baseApp, 5000, 7) + replayBlock(t, baseApp, 5000, 8) + + qr = app.Query(query) + err = amino.Unmarshal(qr.Value, &gp) + require.NoError(t, err) + require.Equal(t, "102ugnot", gp.Price.String()) + + replayBlock(t, baseApp, 5000, 9) + + qr = app.Query(query) + err = amino.Unmarshal(qr.Value, &gp) + require.NoError(t, err) + require.Equal(t, "100ugnot", gp.Price.String()) +} + +func newGasPriceTestApp(t *testing.T) abci.Application { + t.Helper() + cfg := TestAppOptions(memdb.NewMemDB()) + cfg.EventSwitch = events.NewEventSwitch() + + // Capabilities keys. + mainKey := store.NewStoreKey("main") + baseKey := store.NewStoreKey("base") + + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) + baseApp.SetAppVersion("test") + + // Set mounts for BaseApp's MultiStore. + baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) + baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) + + // Construct keepers. + paramsKpr := params.NewParamsKeeper(mainKey, "") + acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) + gpKpr := auth.NewGasPriceKeeper(mainKey) + bankKpr := bank.NewBankKeeper(acctKpr) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) + + // Set InitChainer + icc := cfg.InitChainerConfig + icc.baseApp = baseApp + icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.gpKpr = acctKpr, bankKpr, vmk, gpKpr + baseApp.SetInitChainer(icc.InitChainer) + + // Set AnteHandler + baseApp.SetAnteHandler( + // Override default AnteHandler with custom logic. + func(ctx sdk.Context, tx std.Tx, simulate bool) ( + newCtx sdk.Context, res sdk.Result, abort bool, + ) { + // Add last gas price in the context + ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx)) + + // Override auth params. + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) + // Continue on with default auth ante handler. + if ctx.IsCheckTx() { + res := auth.EnsureSufficientMempoolFees(ctx, tx.Fee) + if !res.IsOK() { + return ctx, res, true + } + } + + newCtx = auth.SetGasMeter(false, ctx, tx.Fee.GasWanted) + + count := getTotalCount(tx) + + newCtx.GasMeter().ConsumeGas(count, "counter-ante") + res = sdk.Result{ + GasWanted: getTotalCount(tx), + } + return + }, + ) + + // 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, + acctKpr, + gpKpr, + nil, + baseApp, + ), + ) + + // Set a handler Route. + baseApp.Router().AddRoute("auth", auth.NewHandler(acctKpr)) + baseApp.Router().AddRoute("bank", bank.NewHandler(bankKpr)) + baseApp.Router().AddRoute( + testutils.RouteMsgCounter, + newTestHandler( + func(ctx sdk.Context, msg sdk.Msg) sdk.Result { return sdk.Result{} }, + ), + ) + + baseApp.Router().AddRoute("vm", vm.NewHandler(vmk)) + + // Load latest version. + if err := baseApp.LoadLatestVersion(); err != nil { + t.Fatalf("failed to load the lastest state: %v", err) + } + + // Initialize the VMKeeper. + ms := baseApp.GetCacheMultiStore() + vmk.Initialize(cfg.Logger, ms) + ms.MultiWrite() // XXX why was't this needed? + + return baseApp +} + +// newTx constructs a tx with multiple counter messages. +// we can use the counter as the gas used for the message. + +func newCounterTx(counters ...int64) sdk.Tx { + msgs := make([]sdk.Msg, len(counters)) + + for i, c := range counters { + msgs[i] = testutils.MsgCounter{Counter: c} + } + tx := sdk.Tx{Msgs: msgs} + return tx +} + +func getTotalCount(tx sdk.Tx) int64 { + var c int64 + for _, m := range tx.Msgs { + c = +m.(testutils.MsgCounter).Counter + } + return c +} + +func gnoGenesisState(t *testing.T) GnoGenesisState { + t.Helper() + gen := GnoGenesisState{} + genBytes := []byte(`{ + "@type": "/gno.GenesisState", + "auth": { + "params": { + "gas_price_change_compressor": "8", + "initial_gasprice": { + "gas": "1000", + "price": "100ugnot" + }, + "max_memo_bytes": "65536", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "target_gas_ratio": "60", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + } + }`) + err := amino.UnmarshalJSON(genBytes, &gen) + if err != nil { + t.Fatalf("failed to create genesis state: %v", err) + } + return gen +} + +func replayBlock(t *testing.T, app *sdk.BaseApp, gas int64, hight int64) { + t.Helper() + tx := newCounterTx(gas) + tx.Fee = std.Fee{ + GasWanted: 20000, + GasFee: sdk.Coin{ + Amount: 1000, + Denom: "ugnot", + }, + } + txBytes, err := amino.Marshal(tx) + require.NoError(t, err) + + header := &bft.Header{ChainID: "test-chain", Height: hight} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + // consume gas in the block + res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) + require.True(t, res.IsOK(), fmt.Sprintf("%v", res)) + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() +} + +type testHandler struct { + process func(sdk.Context, sdk.Msg) sdk.Result + query func(sdk.Context, abci.RequestQuery) abci.ResponseQuery +} + +func (th testHandler) Process(ctx sdk.Context, msg sdk.Msg) sdk.Result { + return th.process(ctx, msg) +} + +func (th testHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery { + return th.query(ctx, req) +} + +func newTestHandler(proc func(sdk.Context, sdk.Msg) sdk.Result) sdk.Handler { + return testHandler{ + process: proc, + } +} diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index 778121d59ed..ccc3369766d 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -12,10 +12,13 @@ import ( bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/std" "github.com/pelletier/go-toml" ) +const initGasPrice = "10ugnot/100gas" + // LoadGenesisBalancesFile loads genesis balances from the provided file path. func LoadGenesisBalancesFile(path string) ([]Balance, error) { // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot @@ -187,3 +190,20 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C return tx, nil } + +func DefaultGenState() GnoGenesisState { + authGen := auth.DefaultGenesisState() + gp, err := std.ParseGasPrice(initGasPrice) + if err != nil { + panic(err) + } + authGen.Params.InitialGasPrice = gp + + gs := GnoGenesisState{ + Balances: []Balance{}, + Txs: []TxWithMetadata{}, + Auth: authGen, + } + + return gs +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index a5f76fdcef7..ed35c4141f4 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -8,6 +8,7 @@ import ( "os" "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -25,9 +26,10 @@ func ProtoGnoAccount() std.Account { } type GnoGenesisState struct { - Balances []Balance `json:"balances"` - Txs []TxWithMetadata `json:"txs"` - Params []Param `json:"params"` + Balances []Balance `json:"balances"` + Txs []TxWithMetadata `json:"txs"` + Params []Param `json:"params"` + Auth auth.GenesisState `json:"auth"` } type TxWithMetadata struct { diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 2a0a4cf1106..ce1413134e3 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -134,11 +134,12 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // Track new user balances added via the `adduser` // command and packages added with the `loadpkg` command. // This genesis will be use when node is started. - genesis := &gnoland.GnoGenesisState{ - Balances: LoadDefaultGenesisBalanceFile(t, gnoRootDir), - Params: LoadDefaultGenesisParamFile(t, gnoRootDir), - Txs: []gnoland.TxWithMetadata{}, - } + + genesis := gnoland.DefaultGenState() + genesis.Balances = LoadDefaultGenesisBalanceFile(t, gnoRootDir) + genesis.Params = LoadDefaultGenesisParamFile(t, gnoRootDir) + genesis.Auth.Params.InitialGasPrice = std.GasPrice{Gas: 0, Price: std.Coin{Amount: 0, Denom: "ugnot"}} + genesis.Txs = []gnoland.TxWithMetadata{} // test1 must be created outside of the loop below because it is already included in genesis so // attempting to recreate results in it getting overwritten and breaking existing tests that @@ -147,7 +148,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { env.Setenv("USER_SEED_"+DefaultAccount_Name, DefaultAccount_Seed) env.Setenv("USER_ADDR_"+DefaultAccount_Name, DefaultAccount_Address) - env.Values[envKeyGenesis] = genesis + env.Values[envKeyGenesis] = &genesis env.Values[envKeyPkgsLoader] = newPkgsLoader() env.Setenv("GNOROOT", gnoRootDir) @@ -187,8 +188,10 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader) // grab logger creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) - pkgsTxs, err := pkgs.LoadPackages(creator, defaultFee, nil) - if err != nil { + // we need to define a new err1 otherwise the out err would be shadowed in the case "start": + pkgsTxs, loadErr := pkgs.LoadPackages(creator, defaultFee, nil) + + if loadErr != nil { ts.Fatalf("unable to load packages txs: %s", err) } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 7e34049d352..7eaf3457b03 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -65,11 +65,11 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) txs = append(txs, additionalTxs...) - cfg.Genesis.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: txs, - Params: params, - } + ggs := cfg.Genesis.AppState.(gnoland.GnoGenesisState) + ggs.Balances = balances + ggs.Txs = txs + ggs.Params = params + cfg.Genesis.AppState = ggs return cfg, creator } @@ -97,6 +97,15 @@ func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNode } func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc { + genState := gnoland.DefaultGenState() + genState.Balances = []gnoland.Balance{ + { + Address: crypto.MustAddressFromString(DefaultAccount_Address), + Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), + }, + } + genState.Txs = []gnoland.TxWithMetadata{} + genState.Params = []gnoland.Param{} return &bft.GenesisDoc{ GenesisTime: time.Now(), ChainID: tmconfig.ChainID(), @@ -116,16 +125,7 @@ func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey Name: "self", }, }, - AppState: gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{ - { - Address: crypto.MustAddressFromString(DefaultAccount_Address), - Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), - }, - }, - Txs: []gnoland.TxWithMetadata{}, - Params: []gnoland.Param{}, - }, + AppState: genState, } } diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 8b1b7d909c1..10402f31f64 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -47,9 +47,10 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) - acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount) - bank := bankm.NewBankKeeper(acck) prmk := paramsm.NewParamsKeeper(iavlCapKey, "params") + acck := authm.NewAccountKeeper(iavlCapKey, prmk, std.ProtoBaseAccount) + bank := bankm.NewBankKeeper(acck) + vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, prmk) mcw := ms.MultiCacheWrap() diff --git a/tm2/pkg/sdk/auth/abci.go b/tm2/pkg/sdk/auth/abci.go new file mode 100644 index 00000000000..86cbf962fad --- /dev/null +++ b/tm2/pkg/sdk/auth/abci.go @@ -0,0 +1,19 @@ +package auth + +import ( + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// EndBlocker is called in the EndBlock(), it calcuates the minimum gas price +// for the next gas price +func EndBlocker(ctx sdk.Context, gk GasPriceKeeperI) { + gk.UpdateGasPrice(ctx) +} + +// InitChainer is called in the InitChain(), it set the initial gas price in the +// GasPriceKeeper store +// for the next gas price +func InitChainer(ctx sdk.Context, gk GasPriceKeeper, gp std.GasPrice) { + gk.SetGasPrice(ctx, gp) +} diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index d36b376aa8d..4495a1729ad 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -338,6 +338,31 @@ func DeductFees(bank BankKeeperI, ctx sdk.Context, acc std.Account, fees std.Coi // consensus. func EnsureSufficientMempoolFees(ctx sdk.Context, fee std.Fee) sdk.Result { minGasPrices := ctx.MinGasPrices() + blockGasPrice := ctx.Value(GasPriceContextKey{}).(std.GasPrice) + feeGasPrice := std.GasPrice{ + Gas: fee.GasWanted, + Price: std.Coin{ + Amount: fee.GasFee.Amount, + Denom: fee.GasFee.Denom, + }, + } + // check the block gas price + if blockGasPrice.Price.IsValid() && !blockGasPrice.Price.IsZero() { + ok, err := feeGasPrice.IsGTE(blockGasPrice) + if err != nil { + return abciResult(std.ErrInsufficientFee( + err.Error(), + )) + } + if !ok { + return abciResult(std.ErrInsufficientFee( + fmt.Sprintf( + "insufficient fees; got: {Gas-Wanted: %d, Gas-Fee %s}, fee required: %+v as block gas price", feeGasPrice.Gas, feeGasPrice.Price, blockGasPrice, + ), + )) + } + } + // check min gas price set by the node. if len(minGasPrices) == 0 { // no minimum gas price (not recommended) // TODO: allow for selective filtering of 0 fee txs. @@ -364,7 +389,7 @@ func EnsureSufficientMempoolFees(ctx sdk.Context, fee std.Fee) sdk.Result { } else { return abciResult(std.ErrInsufficientFee( fmt.Sprintf( - "insufficient fees; got: %q required: %q", fee.GasFee, gp, + "insufficient fees; got: {Gas-Wanted: %d, Gas-Fee %s}, fee required: %+v as minimum gas price set by the node", feeGasPrice.Gas, feeGasPrice.Price, gp, ), )) } @@ -374,7 +399,7 @@ func EnsureSufficientMempoolFees(ctx sdk.Context, fee std.Fee) sdk.Result { return abciResult(std.ErrInsufficientFee( fmt.Sprintf( - "insufficient fees; got: %q required (one of): %q", fee.GasFee, minGasPrices, + "insufficient fees; got: {Gas-Wanted: %d, Gas-Fee %s}, required (one of): %q", feeGasPrice.Gas, feeGasPrice.Price, minGasPrices, ), )) } diff --git a/tm2/pkg/sdk/auth/ante_test.go b/tm2/pkg/sdk/auth/ante_test.go index 86e34391770..78018b415eb 100644 --- a/tm2/pkg/sdk/auth/ante_test.go +++ b/tm2/pkg/sdk/auth/ante_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/amino" @@ -810,6 +811,8 @@ func TestEnsureSufficientMempoolFees(t *testing.T) { {std.NewFee(200000, std.NewCoin("stake", 2)), true}, {std.NewFee(200000, std.NewCoin("atom", 5)), false}, } + // Do not set the block gas price + ctx = ctx.WithValue(GasPriceContextKey{}, std.GasPrice{}) for i, tc := range testCases { res := EnsureSufficientMempoolFees(ctx, tc.input) @@ -867,3 +870,80 @@ func TestCustomSignatureVerificationGasConsumer(t *testing.T) { tx = tu.NewTestTx(t, ctx.ChainID(), msgs, privs, accnums, seqs, fee) checkValidTx(t, anteHandler, ctx, tx, false) } + +func TestEnsureBlockGasPrice(t *testing.T) { + p1, err := std.ParseGasPrice("3ugnot/10gas") // 0.3ugnot + require.NoError(t, err) + + p2, err := std.ParseGasPrice("400ugnot/2000gas") // 0.2ugnot + require.NoError(t, err) + + userFeeCases := []struct { + minGasPrice std.GasPrice + blockGasPrice std.GasPrice + input std.Fee + expectedOK bool + }{ + // user's gas wanted and gas fee: 0.1ugnot to 0.5ugnot + // validator's minGasPrice: 0.3 ugnot + // block gas price: 0.2ugnot + + {p1, p2, std.NewFee(100, std.NewCoin("ugnot", 10)), false}, + {p1, p2, std.NewFee(100, std.NewCoin("ugnot", 20)), false}, + {p1, p2, std.NewFee(100, std.NewCoin("ugnot", 30)), true}, + {p1, p2, std.NewFee(100, std.NewCoin("ugnot", 40)), true}, + {p1, p2, std.NewFee(100, std.NewCoin("ugnot", 50)), true}, + + // validator's minGasPrice: 0.2 ugnot + // block gas price2: 0.3ugnot + {p2, p1, std.NewFee(100, std.NewCoin("ugnot", 10)), false}, + {p2, p1, std.NewFee(100, std.NewCoin("ugnot", 20)), false}, + {p2, p1, std.NewFee(100, std.NewCoin("ugnot", 30)), true}, + {p2, p1, std.NewFee(100, std.NewCoin("ugnot", 40)), true}, + {p2, p1, std.NewFee(100, std.NewCoin("ugnot", 50)), true}, + } + + // setup + env := setupTestEnv() + ctx := env.ctx + // validator min gas price // 0.3 ugnot per gas + for i, c := range userFeeCases { + ctx = ctx.WithMinGasPrices( + []std.GasPrice{c.minGasPrice}, + ) + ctx = ctx.WithValue(GasPriceContextKey{}, c.blockGasPrice) + + res := EnsureSufficientMempoolFees(ctx, c.input) + require.Equal( + t, c.expectedOK, res.IsOK(), + "unexpected result; case #%d, input: %v, log: %v", i, c.input, res.Log, + ) + } +} + +func TestInvalidUserFee(t *testing.T) { + minGasPrice, err := std.ParseGasPrice("3ugnot/10gas") // 0.3ugnot + require.NoError(t, err) + + blockGasPrice, err := std.ParseGasPrice("400ugnot/2000gas") // 0.2ugnot + require.NoError(t, err) + + userFee1 := std.NewFee(0, std.NewCoin("ugnot", 50)) + userFee2 := std.NewFee(100, std.NewCoin("uatom", 50)) + + // setup + env := setupTestEnv() + ctx := env.ctx + + ctx = ctx.WithMinGasPrices( + []std.GasPrice{minGasPrice}, + ) + ctx = ctx.WithValue(GasPriceContextKey{}, blockGasPrice) + res1 := EnsureSufficientMempoolFees(ctx, userFee1) + require.False(t, res1.IsOK()) + assert.Contains(t, res1.Log, "GasPrice.Gas cannot be zero;") + + res2 := EnsureSufficientMempoolFees(ctx, userFee2) + require.False(t, res2.IsOK()) + assert.Contains(t, res2.Log, "Gas price denominations should be equal;") +} diff --git a/tm2/pkg/sdk/auth/consts.go b/tm2/pkg/sdk/auth/consts.go index 09bbb15cdbc..462ca0cd64d 100644 --- a/tm2/pkg/sdk/auth/consts.go +++ b/tm2/pkg/sdk/auth/consts.go @@ -19,7 +19,8 @@ const ( // AddressStoreKeyPrefix prefix for account-by-address store AddressStoreKeyPrefix = "/a/" - + // key for gas price + GasPriceKey = "gasPrice" // param key for global account number GlobalAccountNumberKey = "globalAccountNumber" ) diff --git a/tm2/pkg/sdk/auth/genesis.go b/tm2/pkg/sdk/auth/genesis.go new file mode 100644 index 00000000000..c863c237a41 --- /dev/null +++ b/tm2/pkg/sdk/auth/genesis.go @@ -0,0 +1,31 @@ +package auth + +import ( + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// InitGenesis - Init store state from genesis data +func (ak AccountKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { + if amino.DeepEqual(data, GenesisState{}) { + if err := ak.SetParams(ctx, DefaultParams()); err != nil { + panic(err) + } + return + } + + if err := ValidateGenesis(data); err != nil { + panic(err) + } + + if err := ak.SetParams(ctx, data.Params); err != nil { + panic(err) + } +} + +// ExportGenesis returns a GenesisState for a given context and keeper +func (ak AccountKeeper) ExportGenesis(ctx sdk.Context) GenesisState { + params := ak.GetParams(ctx) + + return NewGenesisState(params) +} diff --git a/tm2/pkg/sdk/auth/keeper.go b/tm2/pkg/sdk/auth/keeper.go index c462438e523..fc83997fdc4 100644 --- a/tm2/pkg/sdk/auth/keeper.go +++ b/tm2/pkg/sdk/auth/keeper.go @@ -3,10 +3,12 @@ package auth import ( "fmt" "log/slog" + "math/big" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" ) @@ -15,7 +17,8 @@ import ( type AccountKeeper struct { // The (unexposed) key used to access the store from the Context. key store.StoreKey - + // The keeper used to store auth parameters + paramk params.ParamsKeeper // The prototypical Account constructor. proto func() std.Account } @@ -23,11 +26,12 @@ type AccountKeeper struct { // NewAccountKeeper returns a new AccountKeeper that uses go-amino to // (binary) encode and decode concrete std.Accounts. func NewAccountKeeper( - key store.StoreKey, proto func() std.Account, + key store.StoreKey, pk params.ParamsKeeper, proto func() std.Account, ) AccountKeeper { return AccountKeeper{ - key: key, - proto: proto, + key: key, + paramk: pk, + proto: proto, } } @@ -152,7 +156,6 @@ func (ak AccountKeeper) GetNextAccountNumber(ctx sdk.Context) uint64 { // ----------------------------------------------------------------------------- // Misc. - func (ak AccountKeeper) decodeAccount(bz []byte) (acc std.Account) { err := amino.Unmarshal(bz, &acc) if err != nil { @@ -160,3 +163,147 @@ func (ak AccountKeeper) decodeAccount(bz []byte) (acc std.Account) { } return } + +type GasPriceContextKey struct{} + +type GasPriceKeeper struct { + key store.StoreKey +} + +// GasPriceKeeper +// The GasPriceKeeper stores the history of gas prices and calculates +// new gas price with formula parameters +func NewGasPriceKeeper(key store.StoreKey) GasPriceKeeper { + return GasPriceKeeper{ + key: key, + } +} + +// SetGasPrice is called in InitChainer to store initial gas price set in the genesis +func (gk GasPriceKeeper) SetGasPrice(ctx sdk.Context, gp std.GasPrice) { + if (gp == std.GasPrice{}) { + return + } + stor := ctx.Store(gk.key) + bz, err := amino.Marshal(gp) + if err != nil { + panic(err) + } + stor.Set([]byte(GasPriceKey), bz) +} + +// We store the history. If the formula changes, we can replay blocks +// and apply the formula to a specific block range. The new gas price is +// calculated in EndBlock(). +func (gk GasPriceKeeper) UpdateGasPrice(ctx sdk.Context) { + params := ctx.Value(AuthParamsContextKey{}).(Params) + gasUsed := ctx.BlockGasMeter().GasConsumed() + maxBlockGas := ctx.ConsensusParams().Block.MaxGas + lgp := gk.LastGasPrice(ctx) + newGasPrice := gk.calcBlockGasPrice(lgp, gasUsed, maxBlockGas, params) + gk.SetGasPrice(ctx, newGasPrice) +} + +// calcBlockGasPrice calculates the minGasPrice for the txs to be included in the next block. +// newGasPrice = lastPrice + lastPrice*(gasUsed-TargetBlockGas)/TargetBlockGas/GasCompressor) +// +// The math formula is an abstraction of a simple solution for the underlying problem we're trying to solve. +// 1. What do we do if the gas used is less than the target gas in a block? +// 2. How do we bring the gas used back to the target level, if gas used is more than the target? +// We simplify the solution with a one-line formula to explain the idea. However, in reality, we need to treat +// two scenarios differently. For example, in the first case, we need to increase the gas by at least 1 unit, +// instead of round down for the integer divisions, and in the second case, we should set a floor +// as the target gas price. This is just a starting point. Down the line, the solution might not be even +// representable by one simple formula +func (gk GasPriceKeeper) calcBlockGasPrice(lastGasPrice std.GasPrice, gasUsed int64, maxGas int64, params Params) std.GasPrice { + // If no block gas price is set, there is no need to change the last gas price. + if lastGasPrice.Price.Amount == 0 { + return lastGasPrice + } + + // This is also a configuration to indicate that there is no need to change the last gas price. + if params.TargetGasRatio == 0 { + return lastGasPrice + } + // if no gas used, no need to change the lastPrice + if gasUsed == 0 { + return lastGasPrice + } + var ( + num = new(big.Int) + denom = new(big.Int) + ) + + // targetGas = maxGax*TargetGasRatio/100 + + num.Mul(big.NewInt(maxGas), big.NewInt(params.TargetGasRatio)) + num.Div(num, big.NewInt(int64(100))) + targetGasInt := new(big.Int).Set(num) + + // if used gas is right on target, no need to change + gasUsedInt := big.NewInt(gasUsed) + if targetGasInt.Cmp(gasUsedInt) == 0 { + return lastGasPrice + } + + c := params.GasPricesChangeCompressor + lastPriceInt := big.NewInt(lastGasPrice.Price.Amount) + + bigOne := big.NewInt(1) + if gasUsedInt.Cmp(targetGasInt) == 1 { // gas used is more than the target + // increase gas price + num = num.Sub(gasUsedInt, targetGasInt) + num.Mul(num, lastPriceInt) + num.Div(num, targetGasInt) + num.Div(num, denom.SetInt64(c)) + // increase at least 1 + diff := maxBig(num, bigOne) + num.Add(lastPriceInt, diff) + // XXX should we cap it with a max gas price? + } else { // gas used is less than the target + // decrease gas price down to initial gas price + initPriceInt := big.NewInt(params.InitialGasPrice.Price.Amount) + if lastPriceInt.Cmp(initPriceInt) == -1 { + return params.InitialGasPrice + } + num.Sub(targetGasInt, gasUsedInt) + num.Mul(num, lastPriceInt) + num.Div(num, targetGasInt) + num.Div(num, denom.SetInt64(c)) + + num.Sub(lastPriceInt, num) + // gas price should not be less than the initial gas price, + num = maxBig(num, initPriceInt) + } + + if !num.IsInt64() { + panic("The min gas price is out of int64 range") + } + + lastGasPrice.Price.Amount = num.Int64() + return lastGasPrice +} + +// max returns the larger of x or y. +func maxBig(x, y *big.Int) *big.Int { + if x.Cmp(y) < 0 { + return y + } + return x +} + +// It returns the gas price for the last block. +func (gk GasPriceKeeper) LastGasPrice(ctx sdk.Context) std.GasPrice { + stor := ctx.Store(gk.key) + bz := stor.Get([]byte(GasPriceKey)) + if bz == nil { + return std.GasPrice{} + } + + gp := std.GasPrice{} + err := amino.Unmarshal(bz, &gp) + if err != nil { + panic(err) + } + return gp +} diff --git a/tm2/pkg/sdk/auth/keeper_test.go b/tm2/pkg/sdk/auth/keeper_test.go index d40d96cdb4b..4622fba1a87 100644 --- a/tm2/pkg/sdk/auth/keeper_test.go +++ b/tm2/pkg/sdk/auth/keeper_test.go @@ -1,11 +1,13 @@ package auth import ( + "math/big" "testing" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" ) func TestAccountMapperGetSet(t *testing.T) { @@ -71,3 +73,106 @@ func TestAccountMapperRemoveAccount(t *testing.T) { require.NotNil(t, acc2) require.Equal(t, accSeq2, acc2.GetSequence()) } + +func TestAccountKeeperParams(t *testing.T) { + env := setupTestEnv() + + dp := DefaultParams() + err := env.acck.SetParams(env.ctx, dp) + require.NoError(t, err) + + dp2 := env.acck.GetParams(env.ctx) + require.True(t, dp.Equals(dp2)) +} + +func TestGasPrice(t *testing.T) { + env := setupTestEnv() + gp := std.GasPrice{ + Gas: 100, + Price: std.Coin{ + Denom: "token", + Amount: 10, + }, + } + env.gk.SetGasPrice(env.ctx, gp) + gp2 := env.gk.LastGasPrice(env.ctx) + require.True(t, gp == gp2) +} + +func TestMax(t *testing.T) { + tests := []struct { + name string + x, y *big.Int + expected *big.Int + }{ + { + name: "X is less than Y", + x: big.NewInt(5), + y: big.NewInt(10), + expected: big.NewInt(10), + }, + { + name: "X is greater than Y", + x: big.NewInt(15), + y: big.NewInt(10), + expected: big.NewInt(15), + }, + { + name: "X is equal to Y", + x: big.NewInt(10), + y: big.NewInt(10), + expected: big.NewInt(10), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := maxBig(tc.x, tc.y) + require.Equal(t, tc.expected, result) + }) + } +} + +func TestCalcBlockGasPrice(t *testing.T) { + gk := GasPriceKeeper{} + + lastGasPrice := std.GasPrice{ + Price: std.Coin{ + Amount: 100, + Denom: "atom", + }, + } + gasUsed := int64(5000) + maxGas := int64(10000) + params := Params{ + TargetGasRatio: 50, + GasPricesChangeCompressor: 2, + } + + // Test with normal parameters + newGasPrice := gk.calcBlockGasPrice(lastGasPrice, gasUsed, maxGas, params) + expectedAmount := big.NewInt(100) + num := big.NewInt(gasUsed - maxGas*params.TargetGasRatio/100) + num.Mul(num, expectedAmount) + num.Div(num, big.NewInt(maxGas*params.TargetGasRatio/100)) + num.Div(num, big.NewInt(params.GasPricesChangeCompressor)) + expectedAmount.Add(expectedAmount, num) + require.Equal(t, expectedAmount.Int64(), newGasPrice.Price.Amount) + + // Test with lastGasPrice amount as 0 + lastGasPrice.Price.Amount = 0 + newGasPrice = gk.calcBlockGasPrice(lastGasPrice, gasUsed, maxGas, params) + require.Equal(t, int64(0), newGasPrice.Price.Amount) + + // Test with TargetGasRatio as 0 (should not change the last price) + params.TargetGasRatio = 0 + newGasPrice = gk.calcBlockGasPrice(lastGasPrice, gasUsed, maxGas, params) + require.Equal(t, int64(0), newGasPrice.Price.Amount) + + // Test with gasUsed as 0 (should not change the last price) + params.TargetGasRatio = 50 + lastGasPrice.Price.Amount = 100 + gasUsed = 0 + newGasPrice = gk.calcBlockGasPrice(lastGasPrice, gasUsed, maxGas, params) + require.Equal(t, int64(100), newGasPrice.Price.Amount) +} diff --git a/tm2/pkg/sdk/auth/params.go b/tm2/pkg/sdk/auth/params.go index dfeaa73af71..3fe08ed444d 100644 --- a/tm2/pkg/sdk/auth/params.go +++ b/tm2/pkg/sdk/auth/params.go @@ -5,38 +5,47 @@ import ( "strings" "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" ) type AuthParamsContextKey struct{} // Default parameter values const ( - DefaultMaxMemoBytes int64 = 65536 - DefaultTxSigLimit int64 = 7 - DefaultTxSizeCostPerByte int64 = 10 - DefaultSigVerifyCostED25519 int64 = 590 - DefaultSigVerifyCostSecp256k1 int64 = 1000 + DefaultMaxMemoBytes int64 = 65536 + DefaultTxSigLimit int64 = 7 + DefaultTxSizeCostPerByte int64 = 10 + DefaultSigVerifyCostED25519 int64 = 590 + DefaultSigVerifyCostSecp256k1 int64 = 1000 + DefaultGasPricesChangeCompressor int64 = 10 + DefaultTargetGasRatio int64 = 70 // 70% of the MaxGas in a block ) // Params defines the parameters for the auth module. type Params struct { - MaxMemoBytes int64 `json:"max_memo_bytes" yaml:"max_memo_bytes"` - TxSigLimit int64 `json:"tx_sig_limit" yaml:"tx_sig_limit"` - TxSizeCostPerByte int64 `json:"tx_size_cost_per_byte" yaml:"tx_size_cost_per_byte"` - SigVerifyCostED25519 int64 `json:"sig_verify_cost_ed25519" yaml:"sig_verify_cost_ed25519"` - SigVerifyCostSecp256k1 int64 `json:"sig_verify_cost_secp256k1" yaml:"sig_verify_cost_secp256k1"` + MaxMemoBytes int64 `json:"max_memo_bytes" yaml:"max_memo_bytes"` + TxSigLimit int64 `json:"tx_sig_limit" yaml:"tx_sig_limit"` + TxSizeCostPerByte int64 `json:"tx_size_cost_per_byte" yaml:"tx_size_cost_per_byte"` + SigVerifyCostED25519 int64 `json:"sig_verify_cost_ed25519" yaml:"sig_verify_cost_ed25519"` + SigVerifyCostSecp256k1 int64 `json:"sig_verify_cost_secp256k1" yaml:"sig_verify_cost_secp256k1"` + GasPricesChangeCompressor int64 `json:"gas_price_change_compressor" yaml:"gas_price_change_compressor"` + TargetGasRatio int64 `json:"target_gas_ratio" yaml:"target_gas_ratio"` + InitialGasPrice std.GasPrice `json:"initial_gasprice"` } // NewParams creates a new Params object func NewParams(maxMemoBytes, txSigLimit, txSizeCostPerByte, - sigVerifyCostED25519, sigVerifyCostSecp256k1 int64, + sigVerifyCostED25519, sigVerifyCostSecp256k1, gasPricesChangeCompressor, targetGasRatio int64, ) Params { return Params{ - MaxMemoBytes: maxMemoBytes, - TxSigLimit: txSigLimit, - TxSizeCostPerByte: txSizeCostPerByte, - SigVerifyCostED25519: sigVerifyCostED25519, - SigVerifyCostSecp256k1: sigVerifyCostSecp256k1, + MaxMemoBytes: maxMemoBytes, + TxSigLimit: txSigLimit, + TxSizeCostPerByte: txSizeCostPerByte, + SigVerifyCostED25519: sigVerifyCostED25519, + SigVerifyCostSecp256k1: sigVerifyCostSecp256k1, + GasPricesChangeCompressor: gasPricesChangeCompressor, + TargetGasRatio: targetGasRatio, } } @@ -48,11 +57,13 @@ func (p Params) Equals(p2 Params) bool { // DefaultParams returns a default set of parameters. func DefaultParams() Params { return Params{ - MaxMemoBytes: DefaultMaxMemoBytes, - TxSigLimit: DefaultTxSigLimit, - TxSizeCostPerByte: DefaultTxSizeCostPerByte, - SigVerifyCostED25519: DefaultSigVerifyCostED25519, - SigVerifyCostSecp256k1: DefaultSigVerifyCostSecp256k1, + MaxMemoBytes: DefaultMaxMemoBytes, + TxSigLimit: DefaultTxSigLimit, + TxSizeCostPerByte: DefaultTxSizeCostPerByte, + SigVerifyCostED25519: DefaultSigVerifyCostED25519, + SigVerifyCostSecp256k1: DefaultSigVerifyCostSecp256k1, + GasPricesChangeCompressor: DefaultGasPricesChangeCompressor, + TargetGasRatio: DefaultTargetGasRatio, } } @@ -65,5 +76,51 @@ func (p Params) String() string { sb.WriteString(fmt.Sprintf("TxSizeCostPerByte: %d\n", p.TxSizeCostPerByte)) sb.WriteString(fmt.Sprintf("SigVerifyCostED25519: %d\n", p.SigVerifyCostED25519)) sb.WriteString(fmt.Sprintf("SigVerifyCostSecp256k1: %d\n", p.SigVerifyCostSecp256k1)) + sb.WriteString(fmt.Sprintf("GasPricesChangeCompressor: %d\n", p.GasPricesChangeCompressor)) + sb.WriteString(fmt.Sprintf("TargetGasRatio: %d\n", p.TargetGasRatio)) return sb.String() } + +func (p Params) Validate() error { + if p.TxSigLimit == 0 { + return fmt.Errorf("invalid tx signature limit: %d", p.TxSigLimit) + } + if p.SigVerifyCostED25519 == 0 { + return fmt.Errorf("invalid ED25519 signature verification cost: %d", p.SigVerifyCostED25519) + } + if p.SigVerifyCostSecp256k1 == 0 { + return fmt.Errorf("invalid SECK256k1 signature verification cost: %d", p.SigVerifyCostSecp256k1) + } + if p.TxSizeCostPerByte == 0 { + return fmt.Errorf("invalid tx size cost per byte: %d", p.TxSizeCostPerByte) + } + if p.GasPricesChangeCompressor <= 0 { + return fmt.Errorf("invalid gas prices change compressor: %d, it should be larger or equal to 1", p.GasPricesChangeCompressor) + } + if p.TargetGasRatio < 0 || p.TargetGasRatio > 100 { + return fmt.Errorf("invalid target block gas ratio: %d, it should be between 0 and 100, 0 is unlimited", p.TargetGasRatio) + } + return nil +} + +func (ak AccountKeeper) SetParams(ctx sdk.Context, params Params) error { + if err := params.Validate(); err != nil { + return err + } + err := ak.paramk.SetParams(ctx, ModuleName, params) + return err +} + +func (ak AccountKeeper) GetParams(ctx sdk.Context) Params { + params := &Params{} + + ok, err := ak.paramk.GetParams(ctx, ModuleName, params) + + if !ok { + panic("params key " + ModuleName + " does not exist") + } + if err != nil { + panic(err.Error()) + } + return *params +} diff --git a/tm2/pkg/sdk/auth/params_test.go b/tm2/pkg/sdk/auth/params_test.go new file mode 100644 index 00000000000..4b5a6b15789 --- /dev/null +++ b/tm2/pkg/sdk/auth/params_test.go @@ -0,0 +1,107 @@ +package auth + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + params Params + expectsError bool + }{ + { + name: "Valid Params", + params: Params{ + MaxMemoBytes: 256, + TxSigLimit: 10, + TxSizeCostPerByte: 1, + SigVerifyCostED25519: 100, + SigVerifyCostSecp256k1: 200, + GasPricesChangeCompressor: 1, + TargetGasRatio: 50, + }, + expectsError: false, + }, + { + name: "Invalid TxSigLimit", + params: Params{ + TxSigLimit: 0, + }, + expectsError: true, + }, + { + name: "Invalid SigVerifyCostED25519", + params: Params{ + SigVerifyCostED25519: 0, + }, + expectsError: true, + }, + { + name: "Invalid GasPricesChangeCompressor", + params: Params{ + GasPricesChangeCompressor: 0, + }, + expectsError: true, + }, + { + name: "Invalid TargetGasRatio", + params: Params{ + TargetGasRatio: 150, + }, + expectsError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.params.Validate() + if tc.expectsError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestNewParams(t *testing.T) { + // Define expected values for each parameter + maxMemoBytes := int64(256) + txSigLimit := int64(10) + txSizeCostPerByte := int64(5) + sigVerifyCostED25519 := int64(100) + sigVerifyCostSecp256k1 := int64(200) + gasPricesChangeCompressor := int64(50) + targetGasRatio := int64(75) + + // Call NewParams with the values + params := NewParams( + maxMemoBytes, + txSigLimit, + txSizeCostPerByte, + sigVerifyCostED25519, + sigVerifyCostSecp256k1, + gasPricesChangeCompressor, + targetGasRatio, + ) + + // Create an expected Params struct with the same values + expectedParams := Params{ + MaxMemoBytes: maxMemoBytes, + TxSigLimit: txSigLimit, + TxSizeCostPerByte: txSizeCostPerByte, + SigVerifyCostED25519: sigVerifyCostED25519, + SigVerifyCostSecp256k1: sigVerifyCostSecp256k1, + GasPricesChangeCompressor: gasPricesChangeCompressor, + TargetGasRatio: targetGasRatio, + } + + // Check if the returned params struct matches the expected struct + if !reflect.DeepEqual(params, expectedParams) { + t.Errorf("NewParams() = %+v, want %+v", params, expectedParams) + } +} diff --git a/tm2/pkg/sdk/auth/test_common.go b/tm2/pkg/sdk/auth/test_common.go index f833a0b0564..e0a6316bead 100644 --- a/tm2/pkg/sdk/auth/test_common.go +++ b/tm2/pkg/sdk/auth/test_common.go @@ -6,8 +6,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "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/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/iavl" @@ -17,6 +17,7 @@ type testEnv struct { ctx sdk.Context acck AccountKeeper bank BankKeeperI + gk GasPriceKeeper } func setupTestEnv() testEnv { @@ -28,8 +29,10 @@ func setupTestEnv() testEnv { ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() - acck := NewAccountKeeper(authCapKey, std.ProtoBaseAccount) + paramk := params.NewParamsKeeper(authCapKey, "") + acck := NewAccountKeeper(authCapKey, paramk, std.ProtoBaseAccount) bank := NewDummyBankKeeper(acck) + gk := NewGasPriceKeeper(authCapKey) ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) ctx = ctx.WithValue(AuthParamsContextKey{}, DefaultParams()) @@ -46,7 +49,7 @@ func setupTestEnv() testEnv { }, }) - return testEnv{ctx: ctx, acck: acck, bank: bank} + return testEnv{ctx: ctx, acck: acck, bank: bank, gk: gk} } // DummyBankKeeper defines a supply keeper used only for testing to avoid diff --git a/tm2/pkg/sdk/auth/types.go b/tm2/pkg/sdk/auth/types.go index 8bbc5e39e3b..3fb2d10fbb5 100644 --- a/tm2/pkg/sdk/auth/types.go +++ b/tm2/pkg/sdk/auth/types.go @@ -13,6 +13,8 @@ type AccountKeeperI interface { GetAllAccounts(ctx sdk.Context) []std.Account SetAccount(ctx sdk.Context, acc std.Account) IterateAccounts(ctx sdk.Context, process func(std.Account) bool) + InitGenesis(ctx sdk.Context, data GenesisState) + GetParams(ctx sdk.Context) Params } var _ AccountKeeperI = AccountKeeper{} @@ -21,3 +23,32 @@ var _ AccountKeeperI = AccountKeeper{} type BankKeeperI interface { SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error } + +type GasPriceKeeperI interface { + LastGasPrice(ctx sdk.Context) std.GasPrice + SetGasPrice(ctx sdk.Context, gp std.GasPrice) + UpdateGasPrice(ctx sdk.Context) +} + +var _ GasPriceKeeperI = GasPriceKeeper{} + +// GenesisState - all auth state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of auth genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.Params.Validate() +} diff --git a/tm2/pkg/sdk/bank/common_test.go b/tm2/pkg/sdk/bank/common_test.go index 95b93157165..c8210be7175 100644 --- a/tm2/pkg/sdk/bank/common_test.go +++ b/tm2/pkg/sdk/bank/common_test.go @@ -9,6 +9,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/iavl" @@ -28,10 +29,10 @@ func setupTestEnv() testEnv { ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() - + paramk := params.NewParamsKeeper(authCapKey, "") ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) acck := auth.NewAccountKeeper( - authCapKey, std.ProtoBaseAccount, + authCapKey, paramk, std.ProtoBaseAccount, ) bank := NewBankKeeper(acck) diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 1802a21f453..ea729abd6ae 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -179,7 +179,13 @@ func (app *BaseApp) initFromMainStore() error { // Load the consensus params from the main store. If the consensus params are // nil, it will be saved later during InitChain. // - // TODO: assert that InitChain hasn't yet been called. + // assert that InitChain hasn't yet been called. + // the app.checkState will be set in InitChain. + // We assert that InitChain hasn't yet been called so + // we don't over write the consensus params in the app. + if app.checkState != nil { + panic("Consensus Params are already set in app, we should not overwrite it here") + } consensusParamsBz := mainStore.Get(mainConsensusParamsKey) if consensusParamsBz != nil { consensusParams := &abci.ConsensusParams{} @@ -354,6 +360,12 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC } } } + // In app.initChainer(), we set the initial parameter values in the params keeper. + // The params keeper store needs to be accessible in the CheckTx state so that + // the first CheckTx can verify the gas price set right after the chain is initialized + // with the genesis state. + app.checkState.ctx.ms = app.deliverState.ctx.ms + app.checkState.ms = app.deliverState.ms // NOTE: We don't commit, but BeginBlock for block 1 starts from this // deliverState. @@ -868,7 +880,10 @@ func (app *BaseApp) runTx(ctx Context, tx Tx) (result Result) { // EndBlock implements the ABCI interface. func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { if app.endBlocker != nil { - res = app.endBlocker(app.deliverState.ctx, req) + // we need to load consensusParams to the end blocker Context + // end blocker use consensusParams to calculat the gas price changes. + ctx := app.deliverState.ctx.WithConsensusParams(app.consensusParams) + res = app.endBlocker(ctx, req) } return diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 52f917ed822..67a880c9ffa 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -353,11 +353,6 @@ func TestInitChainer(t *testing.T) { Data: key, } - // initChainer is nil - nothing happens - app.InitChain(abci.RequestInitChain{ChainID: "test-chain"}) - res := app.Query(query) - require.Equal(t, 0, len(res.Value)) - // set initChainer and try again - should see the value app.SetInitChainer(initChainer) @@ -366,6 +361,11 @@ func TestInitChainer(t *testing.T) { require.Nil(t, err) require.Equal(t, int64(0), app.LastBlockHeight()) + // initChainer is nil - nothing happens + app.InitChain(abci.RequestInitChain{ChainID: "test-chain"}) + res := app.Query(query) + require.Equal(t, 0, len(res.Value)) + app.InitChain(abci.RequestInitChain{AppState: nil, ChainID: "test-chain-id"}) // must have valid JSON genesis file, even if empty // assert that chainID is set correctly in InitChain diff --git a/tm2/pkg/sdk/params/keeper.go b/tm2/pkg/sdk/params/keeper.go index 523e8d54f69..c99b9dbfde1 100644 --- a/tm2/pkg/sdk/params/keeper.go +++ b/tm2/pkg/sdk/params/keeper.go @@ -11,9 +11,16 @@ import ( const ( ModuleName = "params" - StoreKey = ModuleName + + StoreKey = ModuleName + // ValueStorePrevfix is "/pv/" for param value. + ValueStoreKeyPrefix = "/pv/" ) +func ValueStoreKey(key string) []byte { + return append([]byte(ValueStoreKeyPrefix), []byte(key)...) +} + type ParamsKeeperI interface { GetString(ctx sdk.Context, key string, ptr *string) GetInt64(ctx sdk.Context, key string, ptr *int64) @@ -49,7 +56,30 @@ func NewParamsKeeper(key store.StoreKey, prefix string) ParamsKeeper { } } -// Logger returns a module-specific logger. +// GetParam gets a param value from the global param store. +func (pk ParamsKeeper) GetParams(ctx sdk.Context, key string, target interface{}) (bool, error) { + stor := ctx.Store(pk.key) + + bz := stor.Get(ValueStoreKey(key)) + if bz == nil { + return false, nil + } + + return true, amino.UnmarshalJSON(bz, target) +} + +// SetParam sets a param value to the global param store. +func (pk ParamsKeeper) SetParams(ctx sdk.Context, key string, param interface{}) error { + stor := ctx.Store(pk.key) + bz, err := amino.MarshalJSON(param) + if err != nil { + return err + } + + stor.Set(ValueStoreKey(key), bz) + return nil +} + // XXX: why do we expose this? func (pk ParamsKeeper) Logger(ctx sdk.Context) *slog.Logger { return ctx.Logger().With("module", ModuleName) diff --git a/tm2/pkg/sdk/params/keeper_test.go b/tm2/pkg/sdk/params/keeper_test.go index 832d16229ee..aedfaa9d5a3 100644 --- a/tm2/pkg/sdk/params/keeper_test.go +++ b/tm2/pkg/sdk/params/keeper_test.go @@ -140,3 +140,24 @@ func TestKeeper_internal(t *testing.T) { type s struct{ I int } func indirect(ptr interface{}) interface{} { return reflect.ValueOf(ptr).Elem().Interface() } + +type Params struct { + p1 int + p2 string +} + +func TestGetAndSetParams(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + keeper := env.keeper + // SetParams + a := Params{p1: 1, p2: "a"} + err := keeper.SetParams(ctx, ModuleName, a) + require.NoError(t, err) + + // GetParams + a1 := Params{} + _, err1 := keeper.GetParams(ctx, ModuleName, &a1) + require.NoError(t, err1) + require.True(t, amino.DeepEqual(a, a1), "a and a1 should equal") +} diff --git a/tm2/pkg/std/gasprice.go b/tm2/pkg/std/gasprice.go index f68ee190e41..fd082a93371 100644 --- a/tm2/pkg/std/gasprice.go +++ b/tm2/pkg/std/gasprice.go @@ -1,6 +1,7 @@ package std import ( + "math/big" "strings" "github.com/gnolang/gno/tm2/pkg/errors" @@ -28,6 +29,9 @@ func ParseGasPrice(gasprice string) (GasPrice, error) { if gas.Denom != "gas" { return GasPrice{}, errors.New("invalid gas price: %s (invalid gas denom)", gasprice) } + if gas.Amount == 0 { + return GasPrice{}, errors.New("invalid gas price: %s (gas can not be zero)", gasprice) + } return GasPrice{ Gas: gas.Amount, Price: price, @@ -48,3 +52,28 @@ func ParseGasPrices(gasprices string) (res []GasPrice, err error) { } return res, nil } + +// IsGTE compares the GasPrice with another gas price B. If the coin denom matches AND the fee per gas +// is greater than or equal to gas price B, return true; otherwise, return false. +func (gp GasPrice) IsGTE(gpB GasPrice) (bool, error) { + if gp.Price.Denom != gpB.Price.Denom { + return false, errors.New("Gas price denominations should be equal; %s, %s", gp.Price.Denom, gpB.Price.Denom) + } + if gp.Gas == 0 || gpB.Gas == 0 { + return false, errors.New("GasPrice.Gas cannot be zero; %+v, %+v", gp, gpB) + } + + gpg := big.NewInt(gp.Gas) + gpa := big.NewInt(gp.Price.Amount) + + gpBg := big.NewInt(gpB.Gas) + gpBa := big.NewInt(gpB.Price.Amount) + + prod1 := big.NewInt(0).Mul(gpa, gpBg) // gp's price amount * gpB's gas + prod2 := big.NewInt(0).Mul(gpg, gpBa) // gpB's gas * pg's price amount + // This is equivalent to checking + // That the Fee / GasWanted ratio is greater than or equal to the minimum GasPrice per gas. + // This approach helps us avoid dealing with configurations where the value of + // the minimum gas price is set to 0.00001ugnot/gas. + return prod1.Cmp(prod2) >= 0, nil +} diff --git a/tm2/pkg/std/gasprice_test.go b/tm2/pkg/std/gasprice_test.go new file mode 100644 index 00000000000..d4ec0832b88 --- /dev/null +++ b/tm2/pkg/std/gasprice_test.go @@ -0,0 +1,156 @@ +package std + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGasPriceGTE(t *testing.T) { + t.Parallel() + tests := []struct { + name string + gp GasPrice + gpB GasPrice + expectError bool + errorMsg string + expected bool // for non-error cases: whether gp.IsGTE(gpB) should return true or false + }{ + // Error cases: Different denominations + { + name: "Different denominations error", + gp: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + gpB: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "btc", // Different denomination + Amount: 500, + }, + }, + expectError: true, + errorMsg: "Gas price denominations should be equal;", + }, + // Error cases: Zero Gas values + { + name: "Zero Gas in gp error", + gp: GasPrice{ + Gas: 0, // Zero Gas in gp + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + gpB: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + expectError: true, + errorMsg: "GasPrice.Gas cannot be zero;", + }, + { + name: "Zero Gas in gpB error", + gp: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + gpB: GasPrice{ + Gas: 0, // Zero Gas in gpB + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + expectError: true, + errorMsg: "GasPrice.Gas cannot be zero;", + }, + // Valid cases: No errors, just compare gas prices + { + name: "Greater Gas Price", + gp: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 600, // Greater price + }, + }, + gpB: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + expectError: false, + expected: true, + }, + { + name: "Equal Gas Price", + gp: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + gpB: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + expectError: false, + expected: true, + }, + { + name: "Lesser Gas Price", + gp: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 400, // Lesser price + }, + }, + gpB: GasPrice{ + Gas: 100, + Price: Coin{ + Denom: "atom", + Amount: 500, + }, + }, + expectError: false, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := tt.gp.IsGTE(tt.gpB) + if !tt.expectError { + require.NoError(t, err) + assert.Equal(t, tt.expected, got, "Expect that %v is less than %v", tt.gp, tt.gpB) + if got != tt.expected { + t.Errorf("Test %s failed: expected result %v, got %v", tt.name, tt.expected, got) + } + } else { + require.Error(t, err) + errorMsg := err.Error() + assert.Contains(t, errorMsg, tt.errorMsg, "expected error message containing %q, but got %q", tt.errorMsg, errorMsg) + } + }) + } +} diff --git a/tm2/pkg/std/package.go b/tm2/pkg/std/package.go index 3f71c69f0ce..a1aadc17cb6 100644 --- a/tm2/pkg/std/package.go +++ b/tm2/pkg/std/package.go @@ -12,6 +12,10 @@ var Package = amino.RegisterPackage(amino.NewPackage( // Account &BaseAccount{}, "BaseAccount", + // Coin + &Coin{}, "Coin", + // GasPrice + &GasPrice{}, "GasPrice", // Errors InternalError{}, "InternalError", diff --git a/tm2/pkg/std/package_test.go b/tm2/pkg/std/package_test.go index 0a21188737b..2659d9b6955 100644 --- a/tm2/pkg/std/package_test.go +++ b/tm2/pkg/std/package_test.go @@ -24,3 +24,56 @@ func TestAminoBaseAccount(t *testing.T) { err := amino.UnmarshalJSON(b, &acc) require.NoError(t, err) } + +func TestAminoGasPrice(t *testing.T) { + gp := std.GasPrice{ + Gas: 100, + Price: std.Coin{ + Denom: "token", + Amount: 10, + }, + } + // Binary + bz, err := amino.Marshal(gp) + require.NoError(t, err) + err = amino.Unmarshal(bz, &gp) + require.NoError(t, err) + + // JSON + bz, err = amino.MarshalJSON(gp) + require.NoError(t, err) + + err = amino.UnmarshalJSON(bz, &gp) + require.NoError(t, err) + + bz = []byte(`{ + "gas": "10", + "price": "100token" + }`) + err = amino.UnmarshalJSON(bz, &gp) + require.NoError(t, err) +} + +func TestAminoCoin(t *testing.T) { + coin := std.Coin{ + Denom: "token", + Amount: 10, + } + + // Binary + bz, err := amino.Marshal(coin) + require.NoError(t, err) + + err = amino.Unmarshal(bz, &coin) + require.NoError(t, err) + + // JSON + bz, err = amino.MarshalJSON(coin) + require.NoError(t, err) + err = amino.UnmarshalJSON(bz, &coin) + require.NoError(t, err) + + bz = []byte(`"10token"`) + err = amino.UnmarshalJSON(bz, &coin) + require.NoError(t, err) +} diff --git a/tm2/pkg/telemetry/metrics/metrics.go b/tm2/pkg/telemetry/metrics/metrics.go index 7a3e182e06d..e3ae932612f 100644 --- a/tm2/pkg/telemetry/metrics/metrics.go +++ b/tm2/pkg/telemetry/metrics/metrics.go @@ -35,6 +35,7 @@ const ( blockIntervalKey = "block_interval_hist" blockTxsKey = "block_txs_hist" blockSizeKey = "block_size_hist" + gasPriceKey = "block_gas_price_hist" httpRequestTimeKey = "http_request_time_hist" wsRequestTimeKey = "ws_request_time_hist" @@ -96,6 +97,9 @@ var ( // BlockSizeBytes measures the size of the latest block in bytes BlockSizeBytes metric.Int64Histogram + // BlockGasPriceAmount measures the block gas price of the last block + BlockGasPriceAmount metric.Int64Histogram + // RPC // // HTTPRequestTime measures the HTTP request response time @@ -271,6 +275,13 @@ func Init(config config.Config) error { return fmt.Errorf("unable to create histogram, %w", err) } + if BlockGasPriceAmount, err = meter.Int64Histogram( + gasPriceKey, + metric.WithDescription("block gas price"), + metric.WithUnit("token"), + ); err != nil { + return fmt.Errorf("unable to create histogram, %w", err) + } // RPC // if HTTPRequestTime, err = meter.Int64Histogram( From fdedae90bfc1157b895ccdc463bba080c5ad736a Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Tue, 17 Dec 2024 22:32:27 +0900 Subject: [PATCH 02/27] fix: gnoland homepage (#3351) Update the titles and remove social icons from gno.land homepage --- examples/gno.land/r/gnoland/home/home.gno | 42 +++++++++---------- .../gno.land/r/gnoland/home/home_filetest.gno | 41 +++++++++--------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index facb1817fe2..2d1aad8a1a0 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -70,14 +70,14 @@ func Render(_ string) string { func lastBlogposts(limit int) ui.Element { posts := blog.RenderLastPostsWidget(limit) return ui.Element{ - ui.H3("[Latest Blogposts](/r/gnoland/blog)"), + ui.H2("[Latest Blogposts](/r/gnoland/blog)"), ui.Text(posts), } } func lastContributions(limit int) ui.Element { return ui.Element{ - ui.H3("Latest Contributions"), + ui.H2("Latest Contributions"), // TODO: import r/gh to ui.Link{Text: "View latest contributions", URL: "https://github.com/gnolang/gno/pulls"}, } @@ -86,7 +86,7 @@ func lastContributions(limit int) ui.Element { func upcomingEvents() ui.Element { out, _ := events.RenderEventWidget(events.MaxWidgetSize) return ui.Element{ - ui.H3("[Latest Events](/r/gnoland/events)"), + ui.H2("[Latest Events](/r/gnoland/events)"), ui.Text(out), } } @@ -95,14 +95,14 @@ func latestHOFItems(num int) ui.Element { submissions := hof.RenderExhibWidget(num) return ui.Element{ - ui.H3("[Hall of Fame](/r/leon/hof)"), + ui.H2("[Hall of Fame](/r/leon/hof)"), ui.Text(submissions), } } func introSection() ui.Element { return ui.Element{ - ui.H3("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."), + ui.Text("**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.**"), ui.Paragraph("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 using fully transparent, auditable code that anyone can inspect and reuse."), ui.Paragraph("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."), } @@ -135,7 +135,7 @@ func worxDAO() ui.Element { ## Contributors ``*/ return ui.Element{ - ui.H3("Contributions (WorxDAO & GoR)"), + ui.H2("Contributions (WorxDAO & GoR)"), // TODO: GoR dashboard + WorxDAO topics ui.Text(`coming soon`), } @@ -154,28 +154,28 @@ func quoteOfTheBlock() ui.Element { qotb := quotes[idx] return ui.Element{ - ui.H3(ufmt.Sprintf("Quote of the ~Day~ Block#%d", height)), + ui.H2(ufmt.Sprintf("Quote of the ~Day~ Block#%d", height)), ui.Quote(qotb), } } func socialLinks() ui.Element { return ui.Element{ - ui.H3("Socials"), + ui.H2("Socials"), ui.BulletList{ // XXX: improve UI to support a nice GO api for such links ui.Text("Check out our [community projects](https://github.com/gnolang/awesome-gno)"), - ui.Text("![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)"), - ui.Text("![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)"), - ui.Text("![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)"), - ui.Text("![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)"), + ui.Text("[Discord](https://discord.gg/S8nKUqwkPn)"), + ui.Text("[Twitter](https://twitter.com/_gnoland)"), + ui.Text("[Youtube](https://www.youtube.com/@_gnoland)"), + ui.Text("[Telegram](https://t.me/gnoland)"), }, } } func playgroundSection() ui.Element { return ui.Element{ - ui.H3("[Gno Playground](https://play.gno.land)"), + ui.H2("[Gno Playground](https://play.gno.land)"), ui.Paragraph(`Gno Playground is a web application designed for building, running, testing, and interacting with your Gno code, enhancing your understanding of the Gno language. With Gno Playground, you can share your code, execute tests, deploy your realms and packages to gno.land, and explore a multitude of other features.`), @@ -186,12 +186,12 @@ execute tests, deploy your realms and packages to gno.land, and explore a multit func packageStaffPicks() ui.Element { // XXX: make it modifiable from a DAO return ui.Element{ - ui.H3("Explore New Packages and Realms"), + ui.H2("Explore New Packages and Realms"), ui.Columns{ 3, []ui.Element{ { - ui.H4("[r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)"), + ui.H3("[r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)"), ui.BulletList{ ui.Link{URL: "r/gnoland/blog"}, ui.Link{URL: "r/gnoland/dao"}, @@ -199,14 +199,14 @@ func packageStaffPicks() ui.Element { ui.Link{URL: "r/gnoland/home"}, ui.Link{URL: "r/gnoland/pages"}, }, - ui.H4("[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)"), + ui.H3("[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)"), ui.BulletList{ ui.Link{URL: "r/sys/names"}, ui.Link{URL: "r/sys/rewards"}, ui.Link{URL: "/r/sys/validators/v2"}, }, }, { - ui.H4("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"), + ui.H3("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"), ui.BulletList{ ui.Link{URL: "r/demo/boards"}, ui.Link{URL: "r/demo/users"}, @@ -222,7 +222,7 @@ func packageStaffPicks() ui.Element { ui.Text("..."), }, }, { - ui.H4("[p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)"), + ui.H3("[p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)"), ui.BulletList{ ui.Link{URL: "p/demo/avl"}, ui.Link{URL: "p/demo/blog"}, @@ -247,7 +247,7 @@ func discoverLinks() ui.Element { ui.Text(`
-### Learn about gno.land +## Learn about gno.land - [About](/about) - [GitHub](https://github.com/gnolang) @@ -262,7 +262,7 @@ func discoverLinks() ui.Element {
-### Build with Gno +## Build with Gno - [Write Gno in the browser](https://play.gno.land) - [Read about the Gno Language](/gnolang) @@ -274,7 +274,7 @@ func discoverLinks() ui.Element {
-### Explore the universe +## Explore the universe - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) - [Gnoscan](https://gnoscan.io) diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index 4825c9fc588..5b5ff5740c3 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -11,8 +11,7 @@ func main() { // // # Welcome to gno.land // -// ### 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. -// +// **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 using fully transparent, auditable code that anyone can inspect and reuse. // @@ -24,7 +23,7 @@ func main() { //
//
// -// ### Learn about gno.land +// ## Learn about gno.land // // - [About](/about) // - [GitHub](https://github.com/gnolang) @@ -39,7 +38,7 @@ func main() { // //
// -// ### Build with Gno +// ## Build with Gno // // - [Write Gno in the browser](https://play.gno.land) // - [Read about the Gno Language](/gnolang) @@ -51,7 +50,7 @@ func main() { //
//
// -// ### Explore the universe +// ## Explore the universe // // - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) // - [Gnoscan](https://gnoscan.io) @@ -66,19 +65,19 @@ func main() { //
//
// -// ### [Latest Blogposts](/r/gnoland/blog) +// ## [Latest Blogposts](/r/gnoland/blog) // // No posts. //
//
// -// ### [Latest Events](/r/gnoland/events) +// ## [Latest Events](/r/gnoland/events) // // No events. //
//
// -// ### [Hall of Fame](/r/leon/hof) +// ## [Hall of Fame](/r/leon/hof) // // //
@@ -87,7 +86,7 @@ func main() { // // --- // -// ### [Gno Playground](https://play.gno.land) +// ## [Gno Playground](https://play.gno.land) // // // Gno Playground is a web application designed for building, running, testing, and interacting @@ -100,12 +99,12 @@ func main() { // // --- // -// ### Explore New Packages and Realms +// ## Explore New Packages and Realms // //
//
// -// #### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland) +// ### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland) // // - [r/gnoland/blog](r/gnoland/blog) // - [r/gnoland/dao](r/gnoland/dao) @@ -113,7 +112,7 @@ func main() { // - [r/gnoland/home](r/gnoland/home) // - [r/gnoland/pages](r/gnoland/pages) // -// #### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys) +// ### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys) // // - [r/sys/names](r/sys/names) // - [r/sys/rewards](r/sys/rewards) @@ -122,7 +121,7 @@ func main() { //
//
// -// #### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo) +// ### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo) // // - [r/demo/boards](r/demo/boards) // - [r/demo/users](r/demo/users) @@ -140,7 +139,7 @@ func main() { //
//
// -// #### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo) +// ### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo) // // - [p/demo/avl](p/demo/avl) // - [p/demo/blog](p/demo/blog) @@ -160,7 +159,7 @@ func main() { // // --- // -// ### Contributions (WorxDAO & GoR) +// ## Contributions (WorxDAO & GoR) // // coming soon // @@ -170,18 +169,18 @@ func main() { //
//
// -// ### Socials +// ## Socials // // - Check out our [community projects](https://github.com/gnolang/awesome-gno) -// - ![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn) -// - ![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland) -// - ![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland) -// - ![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland) +// - [Discord](https://discord.gg/S8nKUqwkPn) +// - [Twitter](https://twitter.com/_gnoland) +// - [Youtube](https://www.youtube.com/@_gnoland) +// - [Telegram](https://t.me/gnoland) // //
//
// -// ### Quote of the ~Day~ Block#123 +// ## Quote of the ~Day~ Block#123 // // > Now, you Gno. // From 9855f53616b943dc82beae23d663ebef5958aa16 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:00:30 +0100 Subject: [PATCH 03/27] feat(examples): expose ownable & pausable safe objects, update ownable API (#3331) ## Description This PR exposes safe objects where possible for ownable and pausable packages. Let's start exposing variables named `Ownable`, ie: ```go var Ownable = ownable.New() ``` This is the intended use, as exposing this field allows direct, safe, MsgRun calls to access the ownable functions. It's also impossible to directly reassign the value of this variable from another realm. This PR also introduces a BREAKING CHANGE: the `Ownable.CallerIsOwner` API now returns a boolean instead of an error, which makes more sense considering the name of the function. --------- Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> --- .../gno.land/p/demo/memeland/memeland.gno | 4 +-- .../exts/authorizable/authorizable.gno | 4 +-- examples/gno.land/p/demo/ownable/ownable.gno | 19 ++++------- .../gno.land/p/demo/ownable/ownable_test.gno | 32 ++++++------------- .../gno.land/p/demo/pausable/pausable.gno | 8 ++--- .../p/demo/subscription/lifetime/lifetime.gno | 2 +- .../demo/subscription/recurring/recurring.gno | 2 +- examples/gno.land/p/n2p5/mgroup/mgroup.gno | 16 +++++----- examples/gno.land/r/demo/foo20/foo20.gno | 8 ++--- .../r/demo/grc20factory/grc20factory.gno | 11 +++++++ .../r/gnoland/events/administration.gno | 26 --------------- examples/gno.land/r/gnoland/events/events.gno | 10 ++++-- .../events/{rendering.gno => render.gno} | 0 examples/gno.land/r/gnoland/monit/monit.gno | 11 ++----- .../gno.land/r/leon/hof/administration.gno | 24 -------------- examples/gno.land/r/leon/hof/hof.gno | 16 ++++++---- examples/gno.land/r/leon/hof/hof_test.gno | 6 ++-- examples/gno.land/r/leon/hof/render.gno | 4 +-- examples/gno.land/r/sys/users/verify.gno | 16 +++++----- 19 files changed, 82 insertions(+), 137 deletions(-) delete mode 100644 examples/gno.land/r/gnoland/events/administration.gno rename examples/gno.land/r/gnoland/events/{rendering.gno => render.gno} (100%) delete mode 100644 examples/gno.land/r/leon/hof/administration.gno diff --git a/examples/gno.land/p/demo/memeland/memeland.gno b/examples/gno.land/p/demo/memeland/memeland.gno index 9c302ca365b..38f42239bec 100644 --- a/examples/gno.land/p/demo/memeland/memeland.gno +++ b/examples/gno.land/p/demo/memeland/memeland.gno @@ -160,8 +160,8 @@ func (m *Memeland) RemovePost(id string) string { panic("id cannot be empty") } - if err := m.CallerIsOwner(); err != nil { - panic(err) + if !m.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } for i, post := range m.Posts { diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno index f9f0ea15dd9..95bd2ac4959 100644 --- a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno @@ -41,7 +41,7 @@ func NewAuthorizableWithAddress(addr std.Address) *Authorizable { } func (a *Authorizable) AddToAuthList(addr std.Address) error { - if err := a.CallerIsOwner(); err != nil { + if !a.CallerIsOwner() { return ErrNotSuperuser } @@ -55,7 +55,7 @@ func (a *Authorizable) AddToAuthList(addr std.Address) error { } func (a *Authorizable) DeleteFromAuthList(addr std.Address) error { - if err := a.CallerIsOwner(); err != nil { + if !a.CallerIsOwner() { return ErrNotSuperuser } diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index 48a1c15fffa..f565e27c0f2 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -6,6 +6,7 @@ const OwnershipTransferEvent = "OwnershipTransfer" // Ownable is meant to be used as a top-level object to make your contract ownable OR // being embedded in a Gno object to manage per-object ownership. +// Ownable is safe to export as a top-level object type Ownable struct { owner std.Address } @@ -24,9 +25,8 @@ func NewWithAddress(addr std.Address) *Ownable { // TransferOwnership transfers ownership of the Ownable struct to a new address func (o *Ownable) TransferOwnership(newOwner std.Address) error { - err := o.CallerIsOwner() - if err != nil { - return err + if !o.CallerIsOwner() { + return ErrUnauthorized } if !newOwner.IsValid() { @@ -48,9 +48,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error { // Top-level usage: disables all only-owner actions/functions, // Embedded usage: behaves like a burn functionality, removing the owner from the struct func (o *Ownable) DropOwnership() error { - err := o.CallerIsOwner() - if err != nil { - return err + if !o.CallerIsOwner() { + return ErrUnauthorized } prevOwner := o.owner @@ -71,12 +70,8 @@ func (o Ownable) Owner() std.Address { } // CallerIsOwner checks if the caller of the function is the Realm's owner -func (o Ownable) CallerIsOwner() error { - if std.PrevRealm().Addr() == o.owner { - return nil - } - - return ErrUnauthorized +func (o Ownable) CallerIsOwner() bool { + return std.PrevRealm().Addr() == o.owner } // AssertCallerIsOwner panics if the caller is not the owner diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index dee40fa6e1d..f58af9642c6 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" ) var ( @@ -19,18 +20,14 @@ func TestNew(t *testing.T) { o := New() got := o.Owner() - if alice != got { - t.Fatalf("Expected %s, got: %s", alice, got) - } + uassert.Equal(t, got, alice) } func TestNewWithAddress(t *testing.T) { o := NewWithAddress(alice) got := o.Owner() - if alice != got { - t.Fatalf("Expected %s, got: %s", alice, got) - } + uassert.Equal(t, got, alice) } func TestTransferOwnership(t *testing.T) { @@ -39,14 +36,11 @@ func TestTransferOwnership(t *testing.T) { o := New() err := o.TransferOwnership(bob) - if err != nil { - t.Fatalf("TransferOwnership failed, %v", err) - } + urequire.NoError(t, err) got := o.Owner() - if bob != got { - t.Fatalf("Expected: %s, got: %s", bob, got) - } + + uassert.Equal(t, got, bob) } func TestCallerIsOwner(t *testing.T) { @@ -58,8 +52,7 @@ func TestCallerIsOwner(t *testing.T) { std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) std.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed - err := o.CallerIsOwner() - uassert.Error(t, err) // XXX: IsError(..., unauthorizedCaller) + uassert.False(t, o.CallerIsOwner()) } func TestDropOwnership(t *testing.T) { @@ -68,7 +61,7 @@ func TestDropOwnership(t *testing.T) { o := New() err := o.DropOwnership() - uassert.NoError(t, err, "DropOwnership failed") + urequire.NoError(t, err, "DropOwnership failed") owner := o.Owner() uassert.Empty(t, owner, "owner should be empty") @@ -85,13 +78,8 @@ func TestErrUnauthorized(t *testing.T) { std.TestSetRealm(std.NewUserRealm(bob)) std.TestSetOrigCaller(bob) // TODO(bug): should not be needed - err := o.TransferOwnership(alice) - if err != ErrUnauthorized { - t.Fatalf("Should've been ErrUnauthorized, was %v", err) - } - - err = o.DropOwnership() - uassert.ErrorContains(t, err, ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) } func TestErrInvalidAddress(t *testing.T) { diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index e9cce63c1e3..e6a85771fa6 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -34,8 +34,8 @@ func (p Pausable) IsPaused() bool { // Pause sets the state of Pausable to true, meaning all pausable functions are paused func (p *Pausable) Pause() error { - if err := p.CallerIsOwner(); err != nil { - return err + if !p.CallerIsOwner() { + return ownable.ErrUnauthorized } p.paused = true @@ -46,8 +46,8 @@ func (p *Pausable) Pause() error { // Unpause sets the state of Pausable to false, meaning all pausable functions are resumed func (p *Pausable) Unpause() error { - if err := p.CallerIsOwner(); err != nil { - return err + if !p.CallerIsOwner() { + return ownable.ErrUnauthorized } p.paused = false diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno index 8a4c10b687b..be661e70129 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -67,7 +67,7 @@ func (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error { // UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price. func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { - if err := ls.CallerIsOwner(); err != nil { + if !ls.CallerIsOwner() { return ErrNotAuthorized } diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring.gno b/examples/gno.land/p/demo/subscription/recurring/recurring.gno index b5277bd716e..8f116009aa6 100644 --- a/examples/gno.land/p/demo/subscription/recurring/recurring.gno +++ b/examples/gno.land/p/demo/subscription/recurring/recurring.gno @@ -90,7 +90,7 @@ func (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, err // UpdateAmount allows the owner of the subscription contract to change the required subscription amount. func (rs *RecurringSubscription) UpdateAmount(newAmount int64) error { - if err := rs.CallerIsOwner(); err != nil { + if !rs.CallerIsOwner() { return ErrNotAuthorized } diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup.gno b/examples/gno.land/p/n2p5/mgroup/mgroup.gno index 0c029401ff7..566d625a003 100644 --- a/examples/gno.land/p/n2p5/mgroup/mgroup.gno +++ b/examples/gno.land/p/n2p5/mgroup/mgroup.gno @@ -44,8 +44,8 @@ func New(ownerAddress std.Address) *ManagedGroup { // AddBackupOwner adds a backup owner to the group by std.Address. // If the caller is not the owner, an error is returned. func (g *ManagedGroup) AddBackupOwner(addr std.Address) error { - if err := g.owner.CallerIsOwner(); err != nil { - return err + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized } if !addr.IsValid() { return ErrInvalidAddress @@ -57,8 +57,8 @@ func (g *ManagedGroup) AddBackupOwner(addr std.Address) error { // RemoveBackupOwner removes a backup owner from the group by std.Address. // The owner cannot be removed. If the caller is not the owner, an error is returned. func (g *ManagedGroup) RemoveBackupOwner(addr std.Address) error { - if err := g.owner.CallerIsOwner(); err != nil { - return err + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized } if !addr.IsValid() { return ErrInvalidAddress @@ -90,8 +90,8 @@ func (g *ManagedGroup) ClaimOwnership() error { // AddMember adds a member to the group by std.Address. // If the caller is not the owner, an error is returned. func (g *ManagedGroup) AddMember(addr std.Address) error { - if err := g.owner.CallerIsOwner(); err != nil { - return err + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized } if !addr.IsValid() { return ErrInvalidAddress @@ -104,8 +104,8 @@ func (g *ManagedGroup) AddMember(addr std.Address) error { // The owner cannot be removed. If the caller is not the owner, // an error is returned. func (g *ManagedGroup) RemoveMember(addr std.Address) error { - if err := g.owner.CallerIsOwner(); err != nil { - return err + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized } if !addr.IsValid() { return ErrInvalidAddress diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 97b2e52b94b..5c7d7f12b99 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -17,11 +17,11 @@ import ( var ( Token, privateLedger = grc20.NewToken("Foo", "FOO", 4) UserTeller = Token.CallerTeller() - owner = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @manfred + Ownable = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @manfred ) func init() { - privateLedger.Mint(owner.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) + privateLedger.Mint(Ownable.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) getter := func() *grc20.Token { return Token } grc20reg.Register(getter, "") } @@ -66,13 +66,13 @@ func Faucet() { } func Mint(to pusers.AddressOrName, amount uint64) { - owner.AssertCallerIsOwner() + Ownable.AssertCallerIsOwner() toAddr := users.Resolve(to) checkErr(privateLedger.Mint(toAddr, amount)) } func Burn(from pusers.AddressOrName, amount uint64) { - owner.AssertCallerIsOwner() + Ownable.AssertCallerIsOwner() fromAddr := users.Resolve(from) checkErr(privateLedger.Burn(fromAddr, amount)) } diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index cfd32479f9d..58874409d7f 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -120,6 +120,17 @@ func Burn(symbol string, from std.Address, amount uint64) { checkErr(inst.ledger.Burn(from, amount)) } +// instance admin functionality +func DropInstanceOwnership(symbol string) { + inst := mustGetInstance(symbol) + checkErr(inst.admin.DropOwnership()) +} + +func TransferInstanceOwnership(symbol string, newOwner std.Address) { + inst := mustGetInstance(symbol) + checkErr(inst.admin.TransferOwnership(newOwner)) +} + func Render(path string) string { parts := strings.Split(path, "/") c := len(parts) diff --git a/examples/gno.land/r/gnoland/events/administration.gno b/examples/gno.land/r/gnoland/events/administration.gno deleted file mode 100644 index 02914adee69..00000000000 --- a/examples/gno.land/r/gnoland/events/administration.gno +++ /dev/null @@ -1,26 +0,0 @@ -package events - -import ( - "std" - - "gno.land/p/demo/ownable/exts/authorizable" -) - -var ( - su = std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn - auth = authorizable.NewAuthorizableWithAddress(su) -) - -// GetOwner gets the owner of the events realm -func GetOwner() std.Address { - return auth.Owner() -} - -// AddModerator adds a moderator to the events realm -func AddModerator(mod std.Address) { - auth.AssertCallerIsOwner() - - if err := auth.AddToAuthList(mod); err != nil { - panic(err) - } -} diff --git a/examples/gno.land/r/gnoland/events/events.gno b/examples/gno.land/r/gnoland/events/events.gno index baf9ba3d4af..d72638ceaaf 100644 --- a/examples/gno.land/r/gnoland/events/events.gno +++ b/examples/gno.land/r/gnoland/events/events.gno @@ -9,6 +9,7 @@ import ( "strings" "time" + "gno.land/p/demo/ownable/exts/authorizable" "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) @@ -28,6 +29,9 @@ type ( ) var ( + su = std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn + Auth = authorizable.NewAuthorizableWithAddress(su) + events = make(eventsSlice, 0) // sorted idCounter seqid.ID ) @@ -42,7 +46,7 @@ const ( // AddEvent adds auth new event // Start time & end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00 func AddEvent(name, description, link, location, startTime, endTime string) (string, error) { - auth.AssertOnAuthList() + Auth.AssertOnAuthList() if strings.TrimSpace(name) == "" { return "", ErrEmptyName @@ -81,7 +85,7 @@ func AddEvent(name, description, link, location, startTime, endTime string) (str // DeleteEvent deletes an event with auth given ID func DeleteEvent(id string) { - auth.AssertOnAuthList() + Auth.AssertOnAuthList() e, idx, err := GetEventByID(id) if err != nil { @@ -99,7 +103,7 @@ func DeleteEvent(id string) { // It only updates values corresponding to non-empty arguments sent with the call // Note: if you need to update the start time or end time, you need to provide both every time func EditEvent(id string, name, description, link, location, startTime, endTime string) { - auth.AssertOnAuthList() + Auth.AssertOnAuthList() e, _, err := GetEventByID(id) if err != nil { diff --git a/examples/gno.land/r/gnoland/events/rendering.gno b/examples/gno.land/r/gnoland/events/render.gno similarity index 100% rename from examples/gno.land/r/gnoland/events/rendering.gno rename to examples/gno.land/r/gnoland/events/render.gno diff --git a/examples/gno.land/r/gnoland/monit/monit.gno b/examples/gno.land/r/gnoland/monit/monit.gno index 8747ea582b3..be94fbdd2bb 100644 --- a/examples/gno.land/r/gnoland/monit/monit.gno +++ b/examples/gno.land/r/gnoland/monit/monit.gno @@ -20,7 +20,7 @@ var ( lastUpdate time.Time lastCaller std.Address wd = watchdog.Watchdog{Duration: 5 * time.Minute} - owner = ownable.New() // TODO: replace with -> ownable.NewWithAddress... + Ownable = ownable.New() // TODO: replace with -> ownable.NewWithAddress... watchdogDuration = 5 * time.Minute ) @@ -37,9 +37,8 @@ func Incr() int { // Reset resets the realm state. // This function can only be called by the admin. func Reset() { - if owner.CallerIsOwner() != nil { // TODO: replace with owner.AssertCallerIsOwner - panic("unauthorized") - } + Ownable.AssertCallerIsOwner() + counter = 0 lastCaller = std.PrevRealm().Addr() lastUpdate = time.Now() @@ -53,7 +52,3 @@ func Render(_ string) string { counter, lastUpdate, lastCaller, status, ) } - -// TransferOwnership transfers ownership to a new owner. This is a proxy to -// ownable.Ownable.TransferOwnership. -func TransferOwnership(newOwner std.Address) { owner.TransferOwnership(newOwner) } diff --git a/examples/gno.land/r/leon/hof/administration.gno b/examples/gno.land/r/leon/hof/administration.gno deleted file mode 100644 index 4b5b212eddf..00000000000 --- a/examples/gno.land/r/leon/hof/administration.gno +++ /dev/null @@ -1,24 +0,0 @@ -package hof - -import "std" - -// Exposing the ownable & pausable APIs -// Should not be needed as soon as MsgCall supports calling methods on exported variables - -func Pause() error { - return exhibition.Pause() -} - -func Unpause() error { - return exhibition.Unpause() -} - -func GetOwner() std.Address { - return owner.Owner() -} - -func TransferOwnership(newOwner std.Address) { - if err := owner.TransferOwnership(newOwner); err != nil { - panic(err) - } -} diff --git a/examples/gno.land/r/leon/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno index 2722c019497..147a0dd1a95 100644 --- a/examples/gno.land/r/leon/hof/hof.gno +++ b/examples/gno.land/r/leon/hof/hof.gno @@ -14,7 +14,10 @@ import ( var ( exhibition *Exhibition - owner *ownable.Ownable + + // Safe objects + Ownable *ownable.Ownable + Pausable *pausable.Pausable ) type ( @@ -23,7 +26,6 @@ type ( description string items *avl.Tree // pkgPath > Item itemsSorted *avl.Tree // same data but sorted, storing pointers - *pausable.Pausable } Item struct { @@ -41,14 +43,14 @@ func init() { itemsSorted: avl.NewTree(), } - owner = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) - exhibition.Pausable = pausable.NewFromOwnable(owner) + Ownable = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + Pausable = pausable.NewFromOwnable(Ownable) } // Register registers your realm to the Hall of Fame // Should be called from within code func Register() { - if exhibition.IsPaused() { + if Pausable.IsPaused() { return } @@ -113,8 +115,8 @@ func Downvote(pkgpath string) { } func Delete(pkgpath string) { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !Ownable.CallerIsOwner() { + panic(ownable.ErrUnauthorized.Error()) } i, ok := exhibition.items.Get(pkgpath) diff --git a/examples/gno.land/r/leon/hof/hof_test.gno b/examples/gno.land/r/leon/hof/hof_test.gno index 72e8d2159be..4d6f70eab88 100644 --- a/examples/gno.land/r/leon/hof/hof_test.gno +++ b/examples/gno.land/r/leon/hof/hof_test.gno @@ -12,7 +12,7 @@ import ( const rlmPath = "gno.land/r/gnoland/home" var ( - admin = owner.Owner() + admin = Ownable.Owner() adminRealm = std.NewUserRealm(admin) alice = testutils.TestAddress("alice") ) @@ -27,7 +27,7 @@ func TestRegister(t *testing.T) { // Test register while paused std.TestSetRealm(adminRealm) - Pause() + Pausable.Pause() // Set legitimate caller std.TestSetRealm(std.NewCodeRealm(rlmPath)) @@ -37,7 +37,7 @@ func TestRegister(t *testing.T) { // Unpause std.TestSetRealm(adminRealm) - Unpause() + Pausable.Unpause() // Set legitimate caller std.TestSetRealm(std.NewCodeRealm(rlmPath)) diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index 0721c7d6e72..868262bedc7 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -80,9 +80,9 @@ func renderDashboard() string { out += "## Dashboard\n\n" out += ufmt.Sprintf("Total submissions: %d\n\n", exhibition.items.Size()) - out += ufmt.Sprintf("Exhibition admin: %s\n\n", owner.Owner().String()) + out += ufmt.Sprintf("Exhibition admin: %s\n\n", Ownable.Owner().String()) - if !exhibition.IsPaused() { + if !Pausable.IsPaused() { out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.Call("Pause")) } else { out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.Call("Unpause")) diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno index a836e84683d..71869fda1a1 100644 --- a/examples/gno.land/r/sys/users/verify.gno +++ b/examples/gno.land/r/sys/users/verify.gno @@ -48,8 +48,8 @@ func VerifyNameByUser(enable bool, address std.Address, name string) bool { // Enable this package. func AdminEnable() { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } enabled = true @@ -57,8 +57,8 @@ func AdminEnable() { // Disable this package. func AdminDisable() { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } enabled = false @@ -66,8 +66,8 @@ func AdminDisable() { // AdminUpdateVerifyCall updates the method that verifies the namespace. func AdminUpdateVerifyCall(check VerifyNameFunc) { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } checkFunc = check @@ -75,8 +75,8 @@ func AdminUpdateVerifyCall(check VerifyNameFunc) { // AdminTransferOwnership transfers the ownership to a new owner. func AdminTransferOwnership(newOwner std.Address) error { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } return owner.TransferOwnership(newOwner) From 0408d6f30254e9734e556c9d4aef5bfde3304948 Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Wed, 18 Dec 2024 03:42:45 +0900 Subject: [PATCH 04/27] fix: gnoweb UI styles (#3349) This PR aims to fix some of remaining UI bugs on gnoweb after the revamp merge. Some of: #3355 - Fixes Safari select input design and icons - Fixes input hover - Fixes ToC font style - Fixes UI details and improve CSS - Fixes Responsive with long content - Fixes Scrollbar - Fixes fonts loading strategy and size - Fixes ts issue with copy btn (quick clicks) - Fixes some A11y --- gno.land/pkg/gnoweb/components/help.gohtml | 34 ++++-- gno.land/pkg/gnoweb/components/index.gohtml | 16 ++- gno.land/pkg/gnoweb/components/realm.gohtml | 20 ++-- gno.land/pkg/gnoweb/components/source.gohtml | 20 ++-- .../pkg/gnoweb/components/spritesvg.gohtml | 105 ++++++++---------- gno.land/pkg/gnoweb/frontend/css/input.css | 54 ++++----- gno.land/pkg/gnoweb/frontend/css/tx.config.js | 6 +- gno.land/pkg/gnoweb/frontend/js/copy.ts | 28 ++--- .../static/fonts/intervar/Inter.var.woff2 | Bin 324864 -> 0 bytes .../static/fonts/intervar/Intervar.woff2 | Bin 0 -> 73080 bytes .../public/fonts/intervar/Inter.var.woff2 | Bin 324864 -> 0 bytes .../public/fonts/intervar/Intervar.woff2 | Bin 0 -> 73080 bytes gno.land/pkg/gnoweb/public/js/copy.js | 2 +- gno.land/pkg/gnoweb/public/styles.css | 4 +- 14 files changed, 148 insertions(+), 141 deletions(-) delete mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2 delete mode 100644 gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 create mode 100644 gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2 diff --git a/gno.land/pkg/gnoweb/components/help.gohtml b/gno.land/pkg/gnoweb/components/help.gohtml index dea4f683a0a..d3ca9dea81f 100644 --- a/gno.land/pkg/gnoweb/components/help.gohtml +++ b/gno.land/pkg/gnoweb/components/help.gohtml @@ -7,27 +7,36 @@

{{ .RealmName }}

-
+
+ + +
-
- - +
+ +