diff --git a/core/types/celo_transaction_marshalling.go b/core/types/celo_transaction_marshalling.go index be0e7cf684..719faea22c 100644 --- a/core/types/celo_transaction_marshalling.go +++ b/core/types/celo_transaction_marshalling.go @@ -37,6 +37,7 @@ func celoTransactionMarshal(tx *Transaction) ([]byte, bool, error) { enc.Nonce = (*hexutil.Uint64)(&itx.Nonce) enc.To = tx.To() enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.GasPrice = (*hexutil.Big)(itx.GasPrice) enc.Value = (*hexutil.Big)(itx.Value) enc.Input = (*hexutil.Bytes)(&itx.Data) enc.V = (*hexutil.Big)(itx.V) diff --git a/core/types/celo_transaction_marshalling_test.go b/core/types/celo_transaction_marshalling_test.go index 2ba98fe7b5..7a5727a31a 100644 --- a/core/types/celo_transaction_marshalling_test.go +++ b/core/types/celo_transaction_marshalling_test.go @@ -18,6 +18,7 @@ package types import ( "encoding/json" + "fmt" "math/big" "testing" @@ -27,10 +28,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCeloTransactionMarshal(t *testing.T) { +// TestCeloTransactionMarshalUnmarshal tests that each Celo transactions marshal and unmarshal correctly +func TestCeloTransactionMarshalUnmarshal(t *testing.T) { t.Parallel() var ( + chainId = big.NewInt(params.CeloMainnetChainID) gingerbreadForkHeight int64 = 5 signerBlockTime uint64 = 10 cel2Time uint64 = 15 @@ -38,12 +41,12 @@ func TestCeloTransactionMarshal(t *testing.T) { key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") signer = makeCeloSigner( ¶ms.ChainConfig{ - ChainID: big.NewInt(params.CeloMainnetChainID), + ChainID: chainId, Cel2Time: &cel2Time, GingerbreadBlock: big.NewInt(gingerbreadForkHeight), }, signerBlockTime, - NewEIP155Signer(big.NewInt(params.CeloMainnetChainID)), + NewEIP155Signer(chainId), ) feeCurrencyAddress = common.HexToAddress("0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B") @@ -54,15 +57,49 @@ func TestCeloTransactionMarshal(t *testing.T) { ) tests := []struct { - name string - tx *Transaction - expectedJson string + txType string + isCeloTx bool + tx *Transaction + json string + requiredFields []string }{ { - name: "Celo LegacyTx", + txType: "Ethereum LegacyTx", + isCeloTx: false, + tx: MustSignNewTx(key, signer, &LegacyTx{ + Nonce: 10, + Gas: 1e6, + GasPrice: big.NewInt(1e7), + To: &toAddress, + Value: big.NewInt(1e8), + Data: []byte{0x11, 0x22, 0x33, 0x44}, + CeloLegacy: false, + }), + json: `{ + "type": "0x0", + "chainId": "0xa4ec", + "nonce": "0xa", + "to": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", + "gas": "0xf4240", + "gasPrice": "0x989680", + "maxPriorityFeePerGas": null, + "maxFeePerGas": null, + "value": "0x5f5e100", + "input": "0x11223344", + "v": "0x149fc", + "r": "0x444416344542ecfd5824c0173395cca148cfa58cf7572d81196314ad4f5bf1f1", + "s": "0x23f6fd845489499c1170c8d0bd745f9fd3b99c2f4c979891b94e6764a03dcef0", + "hash": "0xf0051b6799141b669b18cf456ffb3509e089c00544878e74769819b345f00866" + }`, + requiredFields: []string{"nonce", "gas", "gasPrice", "value", "input", "v", "r", "s"}, + }, + { + txType: "Celo LegacyTx", + isCeloTx: true, tx: MustSignNewTx(key, signer, &LegacyTx{ Nonce: 10, Gas: 1e6, + GasPrice: big.NewInt(1e7), FeeCurrency: &feeCurrencyAddress, GatewayFeeRecipient: &gatewayFeeRecipient, GatewayFee: big.NewInt(1e7), @@ -71,31 +108,33 @@ func TestCeloTransactionMarshal(t *testing.T) { Data: []byte{0x11, 0x22, 0x33, 0x44}, CeloLegacy: true, }), - expectedJson: `{ + json: `{ "type": "0x0", "chainId": "0xa4ec", "nonce": "0xa", "to": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "gas": "0xf4240", - "gasPrice": null, + "gasPrice": "0x989680", "maxPriorityFeePerGas": null, "maxFeePerGas": null, "value": "0x5f5e100", "input": "0x11223344", - "v": "0x149fc", - "r": "0x87e31aaf469f90072f46a87f48a60c6833f08216c0976e83d0f6d07ee16c2944", - "s": "0x6366cf4f800913df4db35d6e3360b7bf3db087aff63291ab7bfbe6bef4865bc9", - "hash": "0xa956b0aa70bc8e92ba260bb6865b46f36d8fc60cd72aaf472a3b2badc4379638", + "v": "0x149fb", + "r": "0x8ce12cd818c57c73354a1d18b5075bed356170f615246a5e4f7ac3a6a6c2d4c8", + "s": "0x64ab56cbf08dd6cf742f083084ed66167fc110192ee3365555e643f102b5cec7", + "hash": "0xfa30135c37ab29654ed0f75c07d6ba75cbb5d6739ab3249731ae80f80237bd5a", "feeCurrency": "0x2f25deb3848c207fc8e0c34035b3ba7fc157602b", "ethCompatible": false, "gatewayFee": "0x989680", "gatewayFeeRecipient": "0x471ece3750da237f93b8e339c536989b8978a438" }`, + requiredFields: []string{"nonce", "gas", "gasPrice", "value", "input", "v", "r", "s"}, }, { - name: "CeloDynamicFeeTx", + txType: "CeloDynamicFeeTx", + isCeloTx: true, tx: MustSignNewTx(key, signer, &CeloDynamicFeeTx{ - ChainID: big.NewInt(params.CeloMainnetChainID), + ChainID: chainId, Nonce: 10, GasTipCap: big.NewInt(1), GasFeeCap: big.NewInt(5e9), @@ -115,7 +154,7 @@ func TestCeloTransactionMarshal(t *testing.T) { }, }, }), - expectedJson: `{ + json: `{ "type": "0x7c", "chainId": "0xa4ec", "nonce": "0xa", @@ -133,7 +172,7 @@ func TestCeloTransactionMarshal(t *testing.T) { "0x2ab2bf4c5cabc3000e2502e33470a863db2755809d7561237424a0eb373154c2" ] } - ], + ], "v": "0x0", "r": "0xe5c3c7490d804f15ab18a2c864eecd824cde653593c3e2bd1898d09bf0d59a51", "s": "0x2c49f096c89f45dc0e0b24b7ce6e9b2b9cf13e7ab331e2e09c496080b4a6af2e", @@ -142,11 +181,13 @@ func TestCeloTransactionMarshal(t *testing.T) { "gatewayFee": "0x989680", "gatewayFeeRecipient": "0x471ece3750da237f93b8e339c536989b8978a438" }`, + requiredFields: []string{"chainId", "nonce", "gas", "maxPriorityFeePerGas", "maxFeePerGas", "value", "input", "v", "r", "s"}, }, { - name: "CeloDynamicFeeTxV2", + txType: "CeloDynamicFeeTxV2", + isCeloTx: true, tx: MustSignNewTx(key, signer, &CeloDynamicFeeTxV2{ - ChainID: big.NewInt(params.CeloMainnetChainID), + ChainID: chainId, Nonce: 10, GasTipCap: big.NewInt(1), GasFeeCap: big.NewInt(5e9), @@ -164,7 +205,7 @@ func TestCeloTransactionMarshal(t *testing.T) { }, }, }), - expectedJson: `{ + json: `{ "type": "0x7b", "chainId": "0xa4ec", "nonce": "0xa", @@ -189,12 +230,14 @@ func TestCeloTransactionMarshal(t *testing.T) { "hash": "0x8710502f18e464a4e44d6d660127c6173e1ba70ca5684e09d736d3b5e9e63e16", "feeCurrency": "0x2f25deb3848c207fc8e0c34035b3ba7fc157602b" }`, + requiredFields: []string{"chainId", "nonce", "gas", "maxPriorityFeePerGas", "maxFeePerGas", "value", "input", "v", "r", "s"}, }, { - name: "CeloDenominatedTx", + txType: "CeloDenominatedTx", + isCeloTx: true, // Skip signing due to unsupported transaction type tx: NewTx(&CeloDenominatedTx{ - ChainID: big.NewInt(params.CeloMainnetChainID), + ChainID: chainId, Nonce: 10, GasTipCap: big.NewInt(1), GasFeeCap: big.NewInt(5e9), @@ -213,7 +256,7 @@ func TestCeloTransactionMarshal(t *testing.T) { }, }, }), - expectedJson: `{ + json: `{ "type": "0x7a", "chainId": "0xa4ec", "nonce": "0xa", @@ -239,25 +282,87 @@ func TestCeloTransactionMarshal(t *testing.T) { "hash": "0x4812438d07f69839658264fd6bf9022ceb97e87f71ac7ebc56a463f550a8c065", "feeCurrency": "0x2f25deb3848c207fc8e0c34035b3ba7fc157602b" }`, + requiredFields: []string{"chainId", "nonce", "gas", "maxPriorityFeePerGas", "maxFeePerGas", "value", "input", "v", "r", "s"}, }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - txJsonOuter, err := json.Marshal(test.tx) + // testMarshaling tests that the transaction marshals to the expected JSON + testMarshaling := func(t *testing.T, tx *Transaction, expectedJson string, isCeloTxType bool) { + t.Helper() + + txJsonOuter, err := json.Marshal(tx) + assert.NoError(t, err) + + txJsonInner, isCeloTx, err := celoTransactionMarshal(tx) + assert.NoError(t, err) + + assert.Equal(t, isCeloTxType, isCeloTx) + + if isCeloTx { + // For Celo transaction types + // Make sure that celoTransactionMarshal produces the same JSON output as Transaction.MarshalJSON + assert.Equal(t, txJsonOuter, txJsonInner) + } + + // Make sure the output JSON is as expected + assert.JSONEq(t, expectedJson, string(txJsonOuter)) + } + + // testUnmarshaling tests that the transaction unmarshals to the expected Transaction + testUnmarshaling := func(t *testing.T, expectedTx *Transaction, jsonData string) { + t.Helper() + + tx := new(Transaction) + + err := json.Unmarshal([]byte(jsonData), tx) + assert.NoError(t, err) + + // Reassign the signature values because *hexutil.Big decodes "0x0" as nil for the `abs` field. + // This causes a mismatch with `big.NewInt(0)` + v2, r2, s2 := tx.inner.rawSignatureValues() + tx.inner.setSignatureValues( + chainId, + new(big.Int).SetBytes(v2.Bytes()), + new(big.Int).SetBytes(r2.Bytes()), + new(big.Int).SetBytes(s2.Bytes()), + ) + + assert.Equal(t, expectedTx.inner, tx.inner) + } + + // testUnmarshalMissingRequiredField tests that the transaction fails to unmarshal if a required field is missing + testUnmarshalMissingRequiredField := func(t *testing.T, jsonData string, requiredFields []string) { + t.Helper() + + for _, field := range requiredFields { + // Create a copy of the JSON data and remove one of the required fields + var jsonMap map[string]interface{} + err := json.Unmarshal([]byte(jsonData), &jsonMap) assert.NoError(t, err) - txJsonInner, isCeloTx, err := celoTransactionMarshal(test.tx) + delete(jsonMap, field) + + newJsonData, err := json.Marshal(jsonMap) assert.NoError(t, err) - // Make sure the transaction is unmarshalled by celoTransactionMarshal - assert.True(t, isCeloTx) + // Attempt to unmarshal the JSON data + tx := new(Transaction) + err = json.Unmarshal(newJsonData, tx) + assert.ErrorContains(t, err, fmt.Sprintf("missing required field '%s'", field)) + } + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s should marshal to valid JSON successfully", test.txType), func(t *testing.T) { + testMarshaling(t, test.tx, test.json, test.isCeloTx) + }) - // Make sure Transaction.MarshalJSON returns the output of celoTransactionMarshal - assert.Equal(t, txJsonOuter, txJsonInner) + t.Run(fmt.Sprintf("%s should unmarshal valid JSON successfully", test.txType), func(t *testing.T) { + testUnmarshaling(t, test.tx, test.json) + }) - // Make sure the output JSON is as expected - assert.JSONEq(t, test.expectedJson, string(txJsonOuter)) + t.Run(fmt.Sprintf("%s should fail to marshal if required fields are missing", test.txType), func(t *testing.T) { + testUnmarshalMissingRequiredField(t, test.json, test.requiredFields) }) } }