diff --git a/src/error-groups/txpool-errors.yaml b/src/error-groups/txpool-errors.yaml index bf6ccc82b..e612b87dd 100644 --- a/src/error-groups/txpool-errors.yaml +++ b/src/error-groups/txpool-errors.yaml @@ -7,3 +7,5 @@ TxPoolErrors: message: "Already known transaction" - code: 1001 message: "Invalid sender" + - code: 1002 + message: "Replacement transaction underpriced" diff --git a/src/eth/execute.yaml b/src/eth/execute.yaml index 1c1d700ed..c57e1fef4 100644 --- a/src/eth/execute.yaml +++ b/src/eth/execute.yaml @@ -13,6 +13,10 @@ name: Return data schema: $ref: '#/components/schemas/bytes' + error-groups: + - $ref: '#/components/error-groups/JSONRPCStandardErrors' + - $ref: '#/components/error-groups/ExecutionErrors' + - $ref: '#/components/error-groups/GasErrors' errors: - code: 3 message: "execution reverted" @@ -47,6 +51,10 @@ name: Gas used schema: $ref: '#/components/schemas/uint' + error-groups: + - $ref: '#/components/error-groups/JSONRPCStandardErrors' + - $ref: '#/components/error-groups/ExecutionErrors' + - $ref: '#/components/error-groups/GasErrors' errors: - code: 3 message: "execution reverted" diff --git a/src/eth/filter.yaml b/src/eth/filter.yaml index 39e5788a0..54dbb9a29 100644 --- a/src/eth/filter.yaml +++ b/src/eth/filter.yaml @@ -155,6 +155,8 @@ schema: $ref: '#/components/schemas/Filter' required: true + error-groups: + - $ref: '#/components/error-groups/JSONRPCStandardErrors' errors: - code: 4444 message: Pruned history unavailable diff --git a/tests/eth_sendRawTransaction/send-already-known.io b/tests/eth_sendRawTransaction/send-already-known.io new file mode 100644 index 000000000..75226ecb7 --- /dev/null +++ b/tests/eth_sendRawTransaction/send-already-known.io @@ -0,0 +1,5 @@ +// re-sends a transaction that is already in the pool +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf86c048405763d658261a894aa000000000000000000000000000000000000000a8211118718e5bb3abd10a0a0a4499575f90691cd45e1535022fa972ab54a4b1c9328a3cc6f2341ef534c1e9ba015d5834bc004a1c7cdc3c1f193a7536c9749de0fa4f23c890250b6b731d64e2c"]} +<< {"jsonrpc":"2.0","id":1,"result":"0x2e8d0f5a901e194f349f21068312b40da5c7936f565b7e4137abaaa81adfc495"} +>> {"jsonrpc":"2.0","id":2,"method":"eth_sendRawTransaction","params":["0xf86c048405763d658261a894aa000000000000000000000000000000000000000a8211118718e5bb3abd10a0a0a4499575f90691cd45e1535022fa972ab54a4b1c9328a3cc6f2341ef534c1e9ba015d5834bc004a1c7cdc3c1f193a7536c9749de0fa4f23c890250b6b731d64e2c"]} +<< {"jsonrpc":"2.0","id":2,"error":{"code":1000,"message":"already known"}} diff --git a/tests/eth_sendRawTransaction/send-insufficient-funds.io b/tests/eth_sendRawTransaction/send-insufficient-funds.io new file mode 100644 index 000000000..5ef2942e6 --- /dev/null +++ b/tests/eth_sendRawTransaction/send-insufficient-funds.io @@ -0,0 +1,3 @@ +// sends a transaction with value exceeding sender balance +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf879048405763d658261a894aa000000000000000000000000000000000000008fc097ce7bc90715b34b9f1000000002808718e5bb3abd109fa0a93b7157208ee6941f4ba0f5b306d2d1a4ee1ee1cf8fb9b3a820e66844c87fefa07b737cb54c1fc62a577ebcef3ae345c666ef077933407854841ff25f8696f226"]} +<< {"jsonrpc":"2.0","id":1,"error":{"code":809,"message":"insufficient funds for gas * price + value: balance 1000000000000000000000000000000000001, tx cost 1000000000000000000000002290876125002, overshot 2290876125001"}} diff --git a/tests/eth_sendRawTransaction/send-intrinsic-gas-too-low.io b/tests/eth_sendRawTransaction/send-intrinsic-gas-too-low.io new file mode 100644 index 000000000..a49b9bc2d --- /dev/null +++ b/tests/eth_sendRawTransaction/send-intrinsic-gas-too-low.io @@ -0,0 +1,3 @@ +// sends a transaction with gas below the intrinsic minimum +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf868048405763d650194aa000000000000000000000000000000000000000a808718e5bb3abd109fa0c413e3d9e2011595cc27847e1a3e3549904d47384d7a71c9b9251fb845603e7fa0644eaaa2def4ca4a1c45c093090a28ea1067a9a4ca118c9dabbe4a8dbd1aecf1"]} +<< {"jsonrpc":"2.0","id":1,"error":{"code":800,"message":"intrinsic gas too low: gas 1, minimum needed 21000"}} diff --git a/tests/eth_sendRawTransaction/send-nonce-too-low.io b/tests/eth_sendRawTransaction/send-nonce-too-low.io new file mode 100644 index 000000000..2c47bd1e9 --- /dev/null +++ b/tests/eth_sendRawTransaction/send-nonce-too-low.io @@ -0,0 +1,3 @@ +// sends a transaction with a nonce below the sender's state nonce +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf86a808405763d658261a894aa000000000000000000000000000000000000000a808718e5bb3abd10a0a05f7f21951b14d685214b378a8d430f72e037ca02004712cbfc75ed124d06547da01f33468ef4837ef389a72fa6b1d6d89b97196f39e96f493ce7f3e3161a577202"]} +<< {"jsonrpc":"2.0","id":1,"error":{"code":1,"message":"nonce too low: next nonce 160, tx nonce 0"}} diff --git a/tests/eth_sendRawTransaction/send-replacement-underpriced.io b/tests/eth_sendRawTransaction/send-replacement-underpriced.io new file mode 100644 index 000000000..726e83e56 --- /dev/null +++ b/tests/eth_sendRawTransaction/send-replacement-underpriced.io @@ -0,0 +1,5 @@ +// replaces a pending transaction without the required price bump +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf86c058405763d658261a894aa000000000000000000000000000000000000000a8222228718e5bb3abd10a0a00143985bad80a4c76d32fd0deca585931c5998b00b00b1f76c0377bb2592b7fda05f1be04cd9e422fb80f4acee750812bec39db8d9782256dbac21cc217793ebbb"]} +<< {"jsonrpc":"2.0","id":1,"result":"0xdf2b049904f977cff86ae87ab3fbd82efafc5bf98c8c285994f415ae09347ba5"} +>> {"jsonrpc":"2.0","id":2,"method":"eth_sendRawTransaction","params":["0xf86c058405763d658261a894aa00000000000000000000000000000000000000148233338718e5bb3abd109fa0dda8fd585c96a17f49cf8c6efa2a5ebdd9ea21f1e5e78f23cb45ddc4ecdc002fa01d48d97ee713c096ed1f5478775e5140fb93b688128ee56d669f0b75c9b5f0ae"]} +<< {"jsonrpc":"2.0","id":2,"error":{"code":1002,"message":"replacement transaction underpriced"}} diff --git a/tests/eth_sendRawTransaction/send-tip-above-fee-cap.io b/tests/eth_sendRawTransaction/send-tip-above-fee-cap.io new file mode 100644 index 000000000..80bfff5c0 --- /dev/null +++ b/tests/eth_sendRawTransaction/send-tip-above-fee-cap.io @@ -0,0 +1,3 @@ +// sends a transaction with maxPriorityFeePerGas greater than maxFeePerGas +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0x02f86b870c72dd9d5e883e048203e8018261a894aa000000000000000000000000000000000000000a80c080a07598a76d407d863e6dfb6a67a736782d0303fe47633fe28c1d7239d557cbd7e2a014f108ef0efc0234d5e64e4de30de849527b71cd1213eaaed1f99f29a0a9d3b5"]} +<< {"jsonrpc":"2.0","id":1,"error":{"code":804,"message":"max priority fee per gas higher than max fee per gas"}} diff --git a/tests/eth_sendRawTransaction/send-tx-gas-exceeds-block-limit.io b/tests/eth_sendRawTransaction/send-tx-gas-exceeds-block-limit.io new file mode 100644 index 000000000..28025e852 --- /dev/null +++ b/tests/eth_sendRawTransaction/send-tx-gas-exceeds-block-limit.io @@ -0,0 +1,3 @@ +// sends a transaction with gasLimit exceeding the block gasLimit +>> {"jsonrpc":"2.0","id":1,"method":"eth_sendRawTransaction","params":["0xf86c048405763d6584047e7c4194aa000000000000000000000000000000000000000a808718e5bb3abd109fa052f7d99eba02d8b7e308b8d0882eae9af8a154770d790052651be01450f9533ba03c08c1f65cecd4a7e1cd218147e9fbcec1431a6e3f299723d093239ef819176d"]} +<< {"jsonrpc":"2.0","id":1,"error":{"code":803,"message":"exceeds block gas limit"}} diff --git a/tools/cmd/rpctestgen/ethclient.go b/tools/cmd/rpctestgen/ethclient.go index 82f63cae5..e038ea340 100644 --- a/tools/cmd/rpctestgen/ethclient.go +++ b/tools/cmd/rpctestgen/ethclient.go @@ -7,11 +7,15 @@ import ( "io" "net/http" "os" + "regexp" "strings" "github.com/ethereum/go-ethereum/rpc" ) +// errorCodeRe matches the `"code":` field inside a JSON-RPC error object. +var errorCodeRe = regexp.MustCompile(`("error"\s*:\s*\{[^}]*?"code"\s*:\s*)-?\d+`) + type ethclientHandler struct { rpc *rpc.Client logFile *os.File @@ -72,6 +76,44 @@ func (l *ethclientHandler) Close() { } } +// RewriteLastErrorCode substitutes the `code` digits in the last "<< " error +// response of the current log file, so fixtures assert the spec-mandated code +// regardless of what the reference client returned. +func (l *ethclientHandler) RewriteLastErrorCode(code int) error { + if l.logFile == nil { + return fmt.Errorf("no log file open") + } + filename := l.logFile.Name() + if err := l.logFile.Close(); err != nil { + return err + } + l.logFile = nil + l.transport.w = nil + + data, err := os.ReadFile(filename) + if err != nil { + return err + } + lines := strings.Split(string(data), "\n") + idx := -1 + for i := len(lines) - 1; i >= 0; i-- { + if strings.HasPrefix(lines[i], "<< ") { + idx = i + break + } + } + if idx < 0 { + return fmt.Errorf("no response line found in %s", filename) + } + replacement := fmt.Sprintf("${1}%d", code) + rewritten := errorCodeRe.ReplaceAllString(lines[idx], replacement) + if rewritten == lines[idx] { + return fmt.Errorf("ExpectErrorCode set but no error.code field found in %s", filename) + } + lines[idx] = rewritten + return os.WriteFile(filename, []byte(strings.Join(lines, "\n")), 0644) +} + // loggingRoundTrip writes requests and responses to the test log. type loggingRoundTrip struct { w io.Writer diff --git a/tools/cmd/rpctestgen/generate.go b/tools/cmd/rpctestgen/generate.go index 072f06b04..8ac5185e0 100644 --- a/tools/cmd/rpctestgen/generate.go +++ b/tools/cmd/rpctestgen/generate.go @@ -89,6 +89,14 @@ func runGenerator(ctx context.Context) error { fails++ continue } + if test.ExpectErrorCode != 0 { + if err := handler.RewriteLastErrorCode(test.ExpectErrorCode); err != nil { + fmt.Println(" fail.") + fmt.Fprintf(os.Stderr, "failed to rewrite error code in %s/%s: %s\n", methodTest.Name, test.Name, err) + fails++ + continue + } + } fmt.Println(" done.") handler.Close() } diff --git a/tools/cmd/speccheck/check.go b/tools/cmd/speccheck/check.go index 586442f1b..23bb03daa 100644 --- a/tools/cmd/speccheck/check.go +++ b/tools/cmd/speccheck/check.go @@ -18,12 +18,16 @@ func checkSpec(methods map[string]*methodSchema, rts []*roundTrip, re *regexp.Re if !ok { return fmt.Errorf("undefined method: %s", rt.method) } - // skip validator of test if name includes "invalid" as the schema - // doesn't yet support it. - // TODO(matt): create error schemas. + // Exempts tests on methods that haven't adopted error-groups yet; + // remove once they do. if strings.Contains(rt.name, "invalid") { continue } + // Error responses: validate the code against the spec + if rt.response.Result == nil && rt.response.Error != nil { + checkError(method, rt) + continue + } if len(method.params) < len(rt.params) { return fmt.Errorf("%s: too many parameters", method.name) } @@ -40,10 +44,6 @@ func checkSpec(methods map[string]*methodSchema, rts []*roundTrip, re *regexp.Re return fmt.Errorf("unable to validate parameter in %s: %s", rt.name, err) } } - if rt.response.Result == nil && rt.response.Error != nil { - // skip validation of errors, they haven't been standardized - continue - } if err := validate(&method.result.schema, rt.response.Result, fmt.Sprintf("%s.result", rt.method)); err != nil { // Print out the value and schema if there is an error to further debug. buf, _ := json.Marshal(method.result.schema) @@ -58,6 +58,27 @@ func checkSpec(methods map[string]*methodSchema, rts []*roundTrip, re *regexp.Re return nil } +// checkError warns when a fixture's error.code isn't in the method's spec errors. +func checkError(method *methodSchema, rt *roundTrip) { + if len(method.errors) == 0 { + return + } + code := rt.response.Error.Code + for _, e := range method.errors { + if e.Code == code { + if rt.response.Error.Message != e.Message { + // Message-mismatch warning is intentionally suppressed until + // clients converge on spec wording. + // fmt.Printf("[WARN]: ERROR MESSAGE: %q does not match expected: %q in %s\n", + // rt.response.Error.Message, e.Message, rt.name) + } + return + } + } + fmt.Printf("[WARN]: ERROR CODE: %d not found for method %s in %s\n", + code, method.name, rt.name) +} + // validateParam validates the provided value against schema using the url base. func validate(schema *openrpc.JSONSchemaObject, val []byte, url string) error { // Set $schema explicitly to force jsonschema to use draft 2019-09. diff --git a/tools/cmd/speccheck/spec.go b/tools/cmd/speccheck/spec.go index 9413ef519..11bbd8096 100644 --- a/tools/cmd/speccheck/spec.go +++ b/tools/cmd/speccheck/spec.go @@ -14,12 +14,18 @@ type ContentDescriptor struct { schema openrpc.JSONSchemaObject } +type specError struct { + Code int `json:"code"` + Message string `json:"message"` +} + // methodSchema stores all the schemas neccessary to validate a request or // response corresponding to the method. type methodSchema struct { name string params []*ContentDescriptor result *ContentDescriptor + errors []specError } // parseSpec reads an OpenRPC specification and parses out each @@ -30,6 +36,12 @@ func parseSpec(filename string) (map[string]*methodSchema, error) { return nil, fmt.Errorf("unable to read spec: %v", err) } + // Re-parse raw JSON for errors — the meta-schema library doesn't expose them. + methodErrors, err := parseMethodErrors(filename) + if err != nil { + return nil, fmt.Errorf("unable to parse method errors: %v", err) + } + // Iterate over each method in the OpenRPC spec and pull out the parameter // schema and result schema. parsed := make(map[string]*methodSchema) @@ -79,12 +91,41 @@ func parseSpec(filename string) (map[string]*methodSchema, error) { required: required, schema: *obj.Schema.JSONSchemaObject, } + + ms.errors = methodErrors[string(*method.Name)] parsed[string(*method.Name)] = &ms } return parsed, nil } +type rawMethod struct { + Name string `json:"name"` + Errors []specError `json:"errors"` +} + +type rawSpec struct { + Methods []rawMethod `json:"methods"` +} + +func parseMethodErrors(filename string) (map[string][]specError, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var spec rawSpec + if err := json.Unmarshal(data, &spec); err != nil { + return nil, err + } + result := make(map[string][]specError) + for _, m := range spec.Methods { + if len(m.Errors) > 0 { + result[m.Name] = m.Errors + } + } + return result, nil +} + // parseParamValues parses each parameter out of the raw json value in its own byte // slice. func parseParamValues(raw json.RawMessage) ([][]byte, error) { diff --git a/tools/testgen/generators.go b/tools/testgen/generators.go index a7095aa82..db3fc501c 100644 --- a/tools/testgen/generators.go +++ b/tools/testgen/generators.go @@ -56,6 +56,11 @@ type Test struct { // checked for spec validity only. SpecOnly bool + // ExpectErrorCode, if non-zero, rewrites the captured error.code in the + // .io fixture so it asserts the spec-mandated code rather than whatever + // the reference client returned. + ExpectErrorCode int + Run func(context.Context, *T) error } @@ -1757,6 +1762,119 @@ var EthSendRawTransaction = MethodTests{ return nil }, }, + { + // Sender idx=13 has a non-zero state nonce from chain.rlp history, + // so Nonce: 0 is guaranteed below state regardless of mempool state. + Name: "send-nonce-too-low", + About: "sends a transaction with a nonce below the sender's state nonce", + ExpectErrorCode: 1, + Run: func(ctx context.Context, t *T) error { + sender, _ := t.chain.GetSender(13) + head := t.chain.Head() + txdata := &types.LegacyTx{ + Nonce: 0, + To: &common.Address{0xaa}, + Value: big.NewInt(10), + Gas: 25000, + GasPrice: new(big.Int).Add(head.BaseFee(), big.NewInt(1)), + } + tx := t.chain.MustSignTx(sender, txdata) + err := t.eth.SendTransaction(ctx, tx) + if err == nil { + return fmt.Errorf("expected error for nonce-too-low transaction") + } + return nil + }, + }, + { + Name: "send-intrinsic-gas-too-low", + About: "sends a transaction with gas below the intrinsic minimum", + ExpectErrorCode: 800, + Run: func(ctx context.Context, t *T) error { + sender, nonce := t.chain.GetSender(0) + head := t.chain.Head() + txdata := &types.LegacyTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: big.NewInt(10), + Gas: 1, + GasPrice: new(big.Int).Add(head.BaseFee(), big.NewInt(1)), + } + tx := t.chain.MustSignTx(sender, txdata) + err := t.eth.SendTransaction(ctx, tx) + if err == nil { + return fmt.Errorf("expected error for intrinsic-gas-too-low transaction") + } + return nil + }, + }, + { + Name: "send-tx-gas-exceeds-block-limit", + About: "sends a transaction with gasLimit exceeding the block gasLimit", + ExpectErrorCode: 803, + Run: func(ctx context.Context, t *T) error { + sender, nonce := t.chain.GetSender(0) + head := t.chain.Head() + txdata := &types.LegacyTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: big.NewInt(10), + Gas: head.GasLimit() + 1, + GasPrice: new(big.Int).Add(head.BaseFee(), big.NewInt(1)), + } + tx := t.chain.MustSignTx(sender, txdata) + err := t.eth.SendTransaction(ctx, tx) + if err == nil { + return fmt.Errorf("expected error for tx-gas-exceeds-block-limit transaction") + } + return nil + }, + }, + { + Name: "send-insufficient-funds", + About: "sends a transaction with value exceeding sender balance", + ExpectErrorCode: 809, + Run: func(ctx context.Context, t *T) error { + sender, nonce := t.chain.GetSender(0) + head := t.chain.Head() + balance := t.chain.Balance(sender) + txdata := &types.LegacyTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: new(big.Int).Add(balance, big.NewInt(1)), + Gas: 25000, + GasPrice: new(big.Int).Add(head.BaseFee(), big.NewInt(1)), + } + tx := t.chain.MustSignTx(sender, txdata) + err := t.eth.SendTransaction(ctx, tx) + if err == nil { + return fmt.Errorf("expected error for insufficient-funds transaction") + } + return nil + }, + }, + { + Name: "send-tip-above-fee-cap", + About: "sends a transaction with maxPriorityFeePerGas greater than maxFeePerGas", + ExpectErrorCode: 804, + Run: func(ctx context.Context, t *T) error { + sender, nonce := t.chain.GetSender(0) + txdata := &types.DynamicFeeTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: big.NewInt(10), + Gas: 25000, + GasTipCap: big.NewInt(1000), + GasFeeCap: big.NewInt(1), + } + tx := t.chain.MustSignTx(sender, txdata) + err := t.eth.SendTransaction(ctx, tx) + if err == nil { + return fmt.Errorf("expected error for tip-above-fee-cap transaction") + } + return nil + }, + }, { Name: "send-blob-tx", About: "sends a blob transaction", @@ -1798,6 +1916,174 @@ var EthSendRawTransaction = MethodTests{ return nil }, }, + { + Name: "send-already-known", + About: "re-sends a transaction that is already in the pool", + ExpectErrorCode: 1000, + Run: func(ctx context.Context, t *T) error { + sender, nonce := t.chain.GetSender(0) + head := t.chain.Head() + txdata := &types.LegacyTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: big.NewInt(10), + Gas: 25000, + GasPrice: new(big.Int).Add(head.BaseFee(), big.NewInt(1)), + Data: common.FromHex("1111"), + } + tx := t.chain.MustSignTx(sender, txdata) + // Send first time (should succeed). + if err := t.eth.SendTransaction(ctx, tx); err != nil { + return err + } + // Send second time (should return already-known error). + err := t.eth.SendTransaction(ctx, tx) + if err == nil { + return fmt.Errorf("expected error for already-known transaction") + } + t.chain.IncNonce(sender, 1) + return nil + }, + }, + { + Name: "send-replacement-underpriced", + About: "replaces a pending transaction without the required price bump", + ExpectErrorCode: 1002, + Run: func(ctx context.Context, t *T) error { + sender, nonce := t.chain.GetSender(0) + head := t.chain.Head() + gasPrice := new(big.Int).Add(head.BaseFee(), big.NewInt(1)) + first := t.chain.MustSignTx(sender, &types.LegacyTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: big.NewInt(10), + Gas: 25000, + GasPrice: gasPrice, + Data: common.FromHex("2222"), + }) + if err := t.eth.SendTransaction(ctx, first); err != nil { + return err + } + // Same nonce, no price bump → replacement underpriced. + replacement := t.chain.MustSignTx(sender, &types.LegacyTx{ + Nonce: nonce, + To: &common.Address{0xaa}, + Value: big.NewInt(20), + Gas: 25000, + GasPrice: gasPrice, + Data: common.FromHex("3333"), + }) + err := t.eth.SendTransaction(ctx, replacement) + if err == nil { + return fmt.Errorf("expected error for replacement-underpriced transaction") + } + t.chain.IncNonce(sender, 1) + return nil + }, + }, + // Tier 2: needs client-launch flags hive doesn't yet expose. + // Each TODO(hive) block names the env var to add; uncomment as the + // corresponding ethereum/hive PR lands. + // + // { + // // TODO(hive): requires HIVE_MIN_GAS_PRICE > 0. + // // Flag mappings: geth --miner.gasprice, besu --min-gas-price, + // // nethermind --Blocks.MinGasPrice. + // Name: "send-gas-price-below-min", + // About: "sends a legacy transaction with gasPrice below the node's configured minimum", + // ExpectErrorCode: 802, + // Run: func(ctx context.Context, t *T) error { + // sender, nonce := t.chain.GetSender(0) + // txdata := &types.LegacyTx{ + // Nonce: nonce, + // To: &common.Address{0xaa}, + // Value: big.NewInt(10), + // Gas: 25000, + // GasPrice: big.NewInt(0), + // } + // tx := t.chain.MustSignTx(sender, txdata) + // err := t.eth.SendTransaction(ctx, tx) + // if err == nil { + // return fmt.Errorf("expected error for gas-price-below-min transaction") + // } + // return nil + // }, + // }, + // { + // // TODO(hive): requires HIVE_RPC_TX_FEECAP to be set to a bounded + // // value (e.g. 1 ETH). Flag mappings: geth --rpc.txfeecap, + // // besu --rpc-tx-feecap. Nethermind has no equivalent — this test + // // may stay skipped there even after hive wiring. + // Name: "send-tx-fee-cap-exceeded", + // About: "sends a transaction whose total fee exceeds the node's configured RPC tx fee cap", + // ExpectErrorCode: 0, // TODO: add dedicated code in gas-errors.yaml + // Run: func(ctx context.Context, t *T) error { + // sender, nonce := t.chain.GetSender(0) + // txdata := &types.LegacyTx{ + // Nonce: nonce, + // To: &common.Address{0xaa}, + // Value: big.NewInt(10), + // Gas: 1_000_000, + // GasPrice: big.NewInt(200_000_000_000_000), // 0.2 Gwei * 1M gas ≈ 200 GWei total; adjust with cap + // } + // tx := t.chain.MustSignTx(sender, txdata) + // err := t.eth.SendTransaction(ctx, tx) + // if err == nil { + // return fmt.Errorf("expected error for tx-fee-cap-exceeded transaction") + // } + // return nil + // }, + // }, + // { + // // TODO(hive): requires HIVE_MAX_TX_SIZE. Default limits differ + // // significantly across clients (geth 128KB, others vary). + // Name: "send-oversized-data", + // About: "sends a transaction whose payload exceeds the node's max tx size", + // ExpectErrorCode: 0, // TODO: add dedicated code (new group or extend txpool-errors.yaml) + // Run: func(ctx context.Context, t *T) error { + // sender, nonce := t.chain.GetSender(0) + // head := t.chain.Head() + // txdata := &types.LegacyTx{ + // Nonce: nonce, + // To: &common.Address{0xaa}, + // Value: big.NewInt(0), + // Gas: head.GasLimit(), + // GasPrice: new(big.Int).Add(head.BaseFee(), big.NewInt(1)), + // Data: make([]byte, 1<<20), // 1 MiB + // } + // tx := t.chain.MustSignTx(sender, txdata) + // err := t.eth.SendTransaction(ctx, tx) + // if err == nil { + // return fmt.Errorf("expected error for oversized-data transaction") + // } + // return nil + // }, + // }, + // { + // // TODO(hive): requires a controlled base fee at chain head that is + // // above the test's GasFeeCap. Current chain has a low base fee; + // // test-chain regeneration with a higher base fee would be needed. + // Name: "send-max-fee-below-base-fee", + // About: "sends a dynamic-fee transaction whose maxFeePerGas is below the current block base fee", + // ExpectErrorCode: 806, + // Run: func(ctx context.Context, t *T) error { + // sender, nonce := t.chain.GetSender(0) + // txdata := &types.DynamicFeeTx{ + // Nonce: nonce, + // To: &common.Address{0xaa}, + // Value: big.NewInt(10), + // Gas: 25000, + // GasTipCap: big.NewInt(1), + // GasFeeCap: big.NewInt(1), // lower than current base fee + // } + // tx := t.chain.MustSignTx(sender, txdata) + // err := t.eth.SendTransaction(ctx, tx) + // if err == nil { + // return fmt.Errorf("expected error for max-fee-below-base-fee transaction") + // } + // return nil + // }, + // }, }, }