diff --git a/.changelog/2761.breaking.md b/.changelog/2761.breaking.md new file mode 100644 index 00000000000..7a778cad183 --- /dev/null +++ b/.changelog/2761.breaking.md @@ -0,0 +1 @@ +go/consensus: Introduce gas cost based on tx size diff --git a/go/consensus/genesis/genesis.go b/go/consensus/genesis/genesis.go index 7ab7a851763..0dc44b72f22 100644 --- a/go/consensus/genesis/genesis.go +++ b/go/consensus/genesis/genesis.go @@ -24,8 +24,16 @@ type Parameters struct { MaxBlockSize uint64 `json:"max_block_size"` MaxBlockGas transaction.Gas `json:"max_block_gas"` MaxEvidenceAge uint64 `json:"max_evidence_age"` + + // GasCosts are the base transaction gas costs. + GasCosts transaction.Costs `json:"gas_costs,omitempty"` } +const ( + // GasOpTxByte is the gas operation identifier for costing each transaction byte. + GasOpTxByte transaction.Op = "tx_byte" +) + // SanityCheck does basic sanity checking on the genesis state. func (g *Genesis) SanityCheck() error { if g.Parameters.TimeoutCommit < 1*time.Millisecond && !g.Parameters.SkipTimeoutCommit { diff --git a/go/consensus/tendermint/abci/gas.go b/go/consensus/tendermint/abci/gas.go index fac7898cb3a..e392cd14f95 100644 --- a/go/consensus/tendermint/abci/gas.go +++ b/go/consensus/tendermint/abci/gas.go @@ -2,6 +2,7 @@ package abci import ( "errors" + "fmt" "math" "github.com/oasislabs/oasis-core/go/consensus/api/transaction" @@ -53,7 +54,7 @@ func (ga *basicGasAccountant) UseGas(multiplier int, op transaction.Op, costs tr } if ga.usedGas+amount > ga.maxUsedGas { - return ErrOutOfGas + return fmt.Errorf("%w (limit: %d wanted: %d)", ErrOutOfGas, ga.maxUsedGas, ga.usedGas+amount) } ga.usedGas += amount diff --git a/go/consensus/tendermint/abci/gas_test.go b/go/consensus/tendermint/abci/gas_test.go index b5a5f0cfb26..e349d4bd9a4 100644 --- a/go/consensus/tendermint/abci/gas_test.go +++ b/go/consensus/tendermint/abci/gas_test.go @@ -1,6 +1,7 @@ package abci import ( + "errors" "math" "testing" @@ -44,13 +45,13 @@ func TestBasicGasAccountant(t *testing.T) { // Overflow. err = a.UseGas(1, overflowOp, costs) require.Error(err, "UseGas should fail on overflow") - require.Equal(ErrGasOverflow, err) + require.True(errors.Is(err, ErrGasOverflow)) require.EqualValues(30, a.GasUsed(), "GasUsed") // Out of gas. err = a.UseGas(1, expensiveOp, costs) require.Error(err, "UseGas should fail when out of gas") - require.Equal(ErrOutOfGas, err) + require.True(errors.Is(err, ErrOutOfGas)) require.EqualValues(30, a.GasUsed(), "GasUsed") require.EqualValues(100, a.GasWanted(), "GasWanted") @@ -123,7 +124,7 @@ func TestCompositeGasAccountant(t *testing.T) { // Overflow. err = c.UseGas(1, overflowOp, costs) require.Error(err, "UseGas should fail on overflow") - require.Equal(ErrGasOverflow, err) + require.True(errors.Is(err, ErrGasOverflow)) require.EqualValues(10, c.GasUsed(), "GasUsed") require.EqualValues(10, a.GasUsed(), "GasUsed") require.EqualValues(10, b.GasUsed(), "GasUsed") @@ -131,7 +132,7 @@ func TestCompositeGasAccountant(t *testing.T) { // Out of gas. err = a.UseGas(1, expensiveOp, costs) require.Error(err, "UseGas should fail when out of gas") - require.Equal(ErrOutOfGas, err) + require.True(errors.Is(err, ErrOutOfGas)) require.EqualValues(10, c.GasUsed(), "GasUsed") require.EqualValues(10, a.GasUsed(), "GasUsed") require.EqualValues(10, b.GasUsed(), "GasUsed") @@ -146,7 +147,7 @@ func TestCompositeGasAccountant(t *testing.T) { err = c.UseGas(1, cheapOp, costs) require.Error(err, "UseGas should fail when out of gas") - require.Equal(ErrOutOfGas, err) + require.True(errors.Is(err, ErrOutOfGas)) require.EqualValues(10, c.GasUsed(), "GasUsed") require.EqualValues(10, a.GasUsed(), "GasUsed") require.EqualValues(10, b.GasUsed(), "GasUsed") diff --git a/go/consensus/tendermint/abci/mux.go b/go/consensus/tendermint/abci/mux.go index 4836c488a9b..71a2c77d9a5 100644 --- a/go/consensus/tendermint/abci/mux.go +++ b/go/consensus/tendermint/abci/mux.go @@ -9,6 +9,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "math" "sort" "sync" "time" @@ -25,6 +26,7 @@ import ( "github.com/oasislabs/oasis-core/go/common/version" consensus "github.com/oasislabs/oasis-core/go/consensus/api" "github.com/oasislabs/oasis-core/go/consensus/api/transaction" + consensusGenesis "github.com/oasislabs/oasis-core/go/consensus/genesis" epochtime "github.com/oasislabs/oasis-core/go/epochtime/api" genesis "github.com/oasislabs/oasis-core/go/genesis/api" upgrade "github.com/oasislabs/oasis-core/go/upgrade/api" @@ -621,7 +623,7 @@ func (mux *abciMux) decodeTx(ctx *Context, rawTx []byte) (*transaction.Transacti return &tx, &sigTx, nil } -func (mux *abciMux) processTx(ctx *Context, tx *transaction.Transaction) error { +func (mux *abciMux) processTx(ctx *Context, tx *transaction.Transaction, txSize int) error { // Pass the transaction through the fee handler if configured. if txAuthHandler := mux.state.txAuthHandler; txAuthHandler != nil { if err := txAuthHandler.AuthenticateTx(ctx, tx); err != nil { @@ -635,6 +637,15 @@ func (mux *abciMux) processTx(ctx *Context, tx *transaction.Transaction) error { } } + // Charge gas based on the size of the transaction. + params, err := mux.state.ConsensusParameters() + if err != nil { + return fmt.Errorf("failed to fetch consensus parameters: %w", err) + } + if err = ctx.Gas().UseGas(txSize, consensusGenesis.GasOpTxByte, params.GasCosts); err != nil { + return err + } + // Route to correct handler. app := mux.appsByMethod[tx.Method] if app == nil { @@ -678,7 +689,7 @@ func (mux *abciMux) executeTx(ctx *Context, rawTx []byte) error { // Set authenticated transaction signer. ctx.SetTxSigner(sigTx.Signature.PublicKey) - return mux.processTx(ctx, tx) + return mux.processTx(ctx, tx, len(rawTx)) } func (mux *abciMux) EstimateGas(caller signature.PublicKey, tx *transaction.Transaction) (transaction.Gas, error) { @@ -689,11 +700,26 @@ func (mux *abciMux) EstimateGas(caller signature.PublicKey, tx *transaction.Tran ctx := mux.state.NewContext(ContextSimulateTx, time.Time{}) defer ctx.Close() + // Modify transaction to include maximum possible gas in order to estimate the upper limit on + // the serialized transaction size. For amount, use a reasonable amount (in theory the actual + // amount could be bigger depending on the gas price). + tx.Fee = &transaction.Fee{ + Gas: transaction.Gas(math.MaxUint64), + } + _ = tx.Fee.Amount.FromUint64(math.MaxUint64) + ctx.SetTxSigner(caller) + mockSignedTx := transaction.SignedTransaction{ + Signed: signature.Signed{ + Blob: cbor.Marshal(tx), + // Signature is fixed-size, so we can leave it as default. + }, + } + txSize := len(cbor.Marshal(mockSignedTx)) // Ignore any errors that occurred during simulation as we only need to estimate gas even if the // transaction seems like it will fail. - _ = mux.processTx(ctx, tx) + _ = mux.processTx(ctx, tx, txSize) return ctx.Gas().GasUsed(), nil } diff --git a/go/oasis-node/cmd/debug/txsource/workload/oversized.go b/go/oasis-node/cmd/debug/txsource/workload/oversized.go index 21f5256774f..08b8bc0cacd 100644 --- a/go/oasis-node/cmd/debug/txsource/workload/oversized.go +++ b/go/oasis-node/cmd/debug/txsource/workload/oversized.go @@ -13,6 +13,7 @@ import ( "github.com/oasislabs/oasis-core/go/common/logging" consensus "github.com/oasislabs/oasis-core/go/consensus/api" "github.com/oasislabs/oasis-core/go/consensus/api/transaction" + consensusGenesis "github.com/oasislabs/oasis-core/go/consensus/genesis" staking "github.com/oasislabs/oasis-core/go/staking/api" ) @@ -48,10 +49,11 @@ func (oversized) Run( if err != nil { return fmt.Errorf("failed to query state at genesis: %w", err) } + params := genesisDoc.Consensus.Parameters var nonce uint64 fee := transaction.Fee{ - Gas: oversizedTxGasAmount, + Gas: oversizedTxGasAmount + transaction.Gas(params.MaxTxSize)*params.GasCosts[consensusGenesis.GasOpTxByte], } if err = fee.Amount.FromInt64(oversizedTxGasAmount * gasPrice); err != nil { return fmt.Errorf("Fee amount error: %w", err) diff --git a/go/oasis-node/cmd/debug/txsource/workload/parallel.go b/go/oasis-node/cmd/debug/txsource/workload/parallel.go index d4cc2add749..fae3f6bc12a 100644 --- a/go/oasis-node/cmd/debug/txsource/workload/parallel.go +++ b/go/oasis-node/cmd/debug/txsource/workload/parallel.go @@ -24,7 +24,6 @@ const ( parallelSendWaitTimeoutInterval = 30 * time.Second parallelSendTimeoutInterval = 60 * time.Second parallelConcurency = 200 - parallelTxGasAmount = 10 parallelTxTransferAmount = 100 parallelTxFundInterval = 10 ) @@ -43,6 +42,22 @@ func (parallel) Run( ctx := context.Background() var err error + // Estimate gas needed for the used transfer transaction. + var txGasAmount transaction.Gas + xfer := &staking.Transfer{ + To: fundingAccount.Public(), + } + if err = xfer.Tokens.FromInt64(parallelTxTransferAmount); err != nil { + return fmt.Errorf("transfer tokens FromInt64 %d: %w", parallelTxTransferAmount, err) + } + txGasAmount, err = cnsc.EstimateGas(ctx, &consensus.EstimateGasRequest{ + Caller: fundingAccount.Public(), + Transaction: staking.NewTransferTx(0, nil, xfer), + }) + if err != nil { + return fmt.Errorf("failed to estimate gas: %w", err) + } + accounts := make([]signature.Signer, parallelConcurency) fac := memorySigner.NewFactory() for i := range accounts { @@ -53,7 +68,7 @@ func (parallel) Run( // Initial funding of accounts. fundAmount := parallelTxTransferAmount + // self transfer amount - parallelTxFundInterval*parallelTxGasAmount*gasPrice // gas for `parallelTxFundInterval` transfers. + parallelTxFundInterval*txGasAmount*gasPrice // gas for `parallelTxFundInterval` transfers. if err = transferFunds(ctx, parallelLogger, cnsc, fundingAccount, accounts[i].Public(), int64(fundAmount)); err != nil { return fmt.Errorf("account funding failure: %w", err) } @@ -63,9 +78,9 @@ func (parallel) Run( // complete before proceeding with a new batch. var nonce uint64 fee := transaction.Fee{ - Gas: parallelTxGasAmount, + Gas: txGasAmount, } - if err = fee.Amount.FromInt64(parallelTxGasAmount); err != nil { + if err = fee.Amount.FromUint64(uint64(txGasAmount) * gasPrice); err != nil { return fmt.Errorf("Fee amount error: %w", err) } @@ -136,7 +151,7 @@ func (parallel) Run( if i%parallelTxFundInterval == 0 { // Re-fund accounts for next `parallelTxFundInterval` transfers. for i := range accounts { - fundAmount := parallelTxFundInterval * parallelTxGasAmount * gasPrice // gas for `parallelTxFundInterval` transfers. + fundAmount := parallelTxFundInterval * txGasAmount * gasPrice // gas for `parallelTxFundInterval` transfers. if err = transferFunds(ctx, parallelLogger, cnsc, fundingAccount, accounts[i].Public(), int64(fundAmount)); err != nil { return fmt.Errorf("account funding failure: %w", err) } diff --git a/go/oasis-node/cmd/genesis/genesis.go b/go/oasis-node/cmd/genesis/genesis.go index a191d5658bc..ef968d24477 100644 --- a/go/oasis-node/cmd/genesis/genesis.go +++ b/go/oasis-node/cmd/genesis/genesis.go @@ -86,6 +86,7 @@ const ( cfgConsensusMaxBlockSizeBytes = "consensus.tendermint.max_block_size" cfgConsensusMaxBlockGas = "consensus.tendermint.max_block_gas" cfgConsensusMaxEvidenceAge = "consensus.tendermint.max_evidence_age" + CfgConsensusGasCostsTxByte = "consensus.gas_costs.tx_byte" // Consensus backend config flag. cfgConsensusBackend = "consensus.backend" @@ -223,6 +224,9 @@ func doInitGenesis(cmd *cobra.Command, args []string) { MaxBlockSize: uint64(viper.GetSizeInBytes(cfgConsensusMaxBlockSizeBytes)), MaxBlockGas: transaction.Gas(viper.GetUint64(cfgConsensusMaxBlockGas)), MaxEvidenceAge: viper.GetUint64(cfgConsensusMaxEvidenceAge), + GasCosts: transaction.Costs{ + consensusGenesis.GasOpTxByte: transaction.Gas(viper.GetUint64(CfgConsensusGasCostsTxByte)), + }, }, } @@ -720,6 +724,7 @@ func init() { initGenesisFlags.String(cfgConsensusMaxBlockSizeBytes, "21mb", "tendermint maximum block size (in bytes)") initGenesisFlags.Uint64(cfgConsensusMaxBlockGas, 0, "tendermint max gas used per block") initGenesisFlags.Uint64(cfgConsensusMaxEvidenceAge, 100000, "tendermint max evidence age (in blocks)") + initGenesisFlags.Uint64(CfgConsensusGasCostsTxByte, 1, "consensus gas costs: each transaction byte") // Consensus backend flag. initGenesisFlags.String(cfgConsensusBackend, tendermint.BackendName, "consensus backend") diff --git a/go/oasis-test-runner/oasis/oasis.go b/go/oasis-test-runner/oasis/oasis.go index 196ce890188..2225372975d 100644 --- a/go/oasis-test-runner/oasis/oasis.go +++ b/go/oasis-test-runner/oasis/oasis.go @@ -220,6 +220,9 @@ type NetworkCfg struct { // nolint: maligned // ConsensusTimeoutCommit is the consensus commit timeout. ConsensusTimeoutCommit time.Duration `json:"consensus_timeout_commit"` + // ConsensusGasCostsTxByte is the gas cost of each transaction byte. + ConsensusGasCostsTxByte uint64 `json:"consensus_gas_costs_tx_byte"` + // HaltEpoch is the halt epoch height flag. HaltEpoch uint64 `json:"halt_epoch"` @@ -678,6 +681,7 @@ func (net *Network) makeGenesis() error { "--registry.debug.allow_unroutable_addresses", "true", "--" + genesis.CfgRegistryDebugAllowTestRuntimes, "true", "--scheduler.max_validators_per_entity", strconv.Itoa(len(net.Validators())), + "--" + genesis.CfgConsensusGasCostsTxByte, strconv.FormatUint(net.cfg.ConsensusGasCostsTxByte, 10), } if net.cfg.EpochtimeMock { args = append(args, "--epochtime.debug.mock_backend") diff --git a/go/oasis-test-runner/scenario/e2e/basic.go b/go/oasis-test-runner/scenario/e2e/basic.go index e50f1ddc8e1..fbc0f928eaf 100644 --- a/go/oasis-test-runner/scenario/e2e/basic.go +++ b/go/oasis-test-runner/scenario/e2e/basic.go @@ -95,6 +95,7 @@ func (sc *basicImpl) Fixture() (*oasis.NetworkFixture, error) { NodeBinary: viper.GetString(cfgNodeBinary), RuntimeLoaderBinary: viper.GetString(cfgRuntimeLoader), DefaultLogWatcherHandlerFactories: DefaultBasicLogWatcherHandlerFactories, + ConsensusGasCostsTxByte: 1, }, Entities: []oasis.EntityCfg{ oasis.EntityCfg{IsDebugTestEntity: true}, diff --git a/go/oasis-test-runner/scenario/e2e/gas_fees_staking.go b/go/oasis-test-runner/scenario/e2e/gas_fees_staking.go index 474f8afd807..222f6542dad 100644 --- a/go/oasis-test-runner/scenario/e2e/gas_fees_staking.go +++ b/go/oasis-test-runner/scenario/e2e/gas_fees_staking.go @@ -84,6 +84,7 @@ func (sc *gasFeesImpl) Fixture() (*oasis.NetworkFixture, error) { EpochtimeMock: true, StakingGenesis: "tests/fixture-data/gas-fees/staking-genesis.json", DefaultLogWatcherHandlerFactories: DefaultBasicLogWatcherHandlerFactories, + ConsensusGasCostsTxByte: 0, // So we can control gas more easily. } f.Entities = []oasis.EntityCfg{ oasis.EntityCfg{IsDebugTestEntity: true},