From 8daad080c85219d5ee4d98f6bda7f5436d637173 Mon Sep 17 00:00:00 2001 From: paologalligit Date: Thu, 20 Feb 2025 18:32:02 +0100 Subject: [PATCH] feat: add type to receipt and refactor reward calculation function --- api/transactions/transactions_test.go | 2 + api/transactions/types.go | 2 + api/transactions/types_test.go | 22 ++-- consensus/fork/galactica.go | 15 ++- consensus/fork/galactica_test.go | 47 ++++++++ runtime/runtime.go | 6 +- tx/receipt.go | 155 +++++++++++++++++++++++++- tx/receipt_test.go | 114 +++++++++++++++---- 8 files changed, 320 insertions(+), 43 deletions(-) diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index a4c9121f5..605a7a339 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -127,12 +127,14 @@ func getTxReceipt(t *testing.T) { t.Fatal(err) } assert.Equal(t, receipt.GasUsed, legacyTx.Gas(), "receipt gas used not equal to transaction gas") + assert.Equal(t, receipt.Type, legacyTx.Type()) r = httpGetAndCheckResponseStatus(t, "/transactions/"+dynFeeTx.ID().String()+"/receipt", 200) if err := json.Unmarshal(r, &receipt); err != nil { t.Fatal(err) } assert.Equal(t, receipt.GasUsed, legacyTx.Gas(), "receipt gas used not equal to transaction gas") + assert.Equal(t, receipt.Type, dynFeeTx.Type()) } func sendLegacyTx(t *testing.T) { diff --git a/api/transactions/types.go b/api/transactions/types.go index 3640fd5d6..62a0a66ca 100644 --- a/api/transactions/types.go +++ b/api/transactions/types.go @@ -82,6 +82,7 @@ type ReceiptMeta struct { // Receipt for json marshal type Receipt struct { + Type uint8 `json:"type,omitempty"` GasUsed uint64 `json:"gasUsed"` GasPayer thor.Address `json:"gasPayer"` Paid *math.HexOrDecimal256 `json:"paid"` @@ -121,6 +122,7 @@ func convertReceipt(txReceipt *tx.Receipt, header *block.Header, tx *tx.Transact return nil, err } receipt := &Receipt{ + Type: txReceipt.Type, GasUsed: txReceipt.GasUsed, GasPayer: txReceipt.GasPayer, Paid: &paid, diff --git a/api/transactions/types_test.go b/api/transactions/types_test.go index 413e24fa3..8d3fa2577 100644 --- a/api/transactions/types_test.go +++ b/api/transactions/types_test.go @@ -42,8 +42,8 @@ func TestErrorWhileRetrievingTxOriginInConvertReceipt(t *testing.T) { func TestConvertReceiptWhenTxHasNoClauseTo(t *testing.T) { value := big.NewInt(100) txs := []*tx.Transaction{ - newLegacyTx(tx.NewClause(nil).WithValue(value)), - newDynFeeTx(tx.NewClause(nil).WithValue(value)), + newTx(tx.NewClause(nil).WithValue(value), tx.LegacyTxType), + newTx(tx.NewClause(nil).WithValue(value), tx.DynamicFeeTxType), } for _, tr := range txs { b := new(block.Builder).Build() @@ -64,8 +64,8 @@ func TestConvertReceipt(t *testing.T) { addr := randAddress() txs := []*tx.Transaction{ - newLegacyTx(tx.NewClause(&addr).WithValue(value)), - newDynFeeTx(tx.NewClause(&addr).WithValue(value)), + newTx(tx.NewClause(&addr).WithValue(value), tx.LegacyTxType), + newTx(tx.NewClause(&addr).WithValue(value), tx.DynamicFeeTxType), } for _, tr := range txs { b := new(block.Builder).Build() @@ -75,6 +75,7 @@ func TestConvertReceipt(t *testing.T) { convRec, err := convertReceipt(receipt, header, tr) assert.NoError(t, err) + assert.Equal(t, receipt.Type, convRec.Type) assert.Equal(t, 1, len(convRec.Outputs)) assert.Equal(t, 1, len(convRec.Outputs[0].Events)) assert.Equal(t, 1, len(convRec.Outputs[0].Transfers)) @@ -114,17 +115,8 @@ func newReceipt() *tx.Receipt { } } -func newLegacyTx(clause *tx.Clause) *tx.Transaction { - tx := tx.NewTxBuilder(tx.LegacyTxType). - Clause(clause). - MustBuild() - pk, _ := crypto.GenerateKey() - sig, _ := crypto.Sign(tx.SigningHash().Bytes(), pk) - return tx.WithSignature(sig) -} - -func newDynFeeTx(clause *tx.Clause) *tx.Transaction { - tx := tx.NewTxBuilder(tx.DynamicFeeTxType). +func newTx(clause *tx.Clause, txType int) *tx.Transaction { + tx := tx.NewTxBuilder(txType). Clause(clause). MustBuild() pk, _ := crypto.GenerateKey() diff --git a/consensus/fork/galactica.go b/consensus/fork/galactica.go index 9ab781108..9647fb8c3 100644 --- a/consensus/fork/galactica.go +++ b/consensus/fork/galactica.go @@ -124,7 +124,8 @@ func GalacticaGasPrice(tr *tx.Transaction, baseGasPrice *big.Int, galacticaItems feeItems := GalacticaTxGasPriceAdapter(tr, gasPrice) // This gasPrice is the same that will be used when refunding the user - // it takes into account the priority fee that will be paid to the validator + // it takes into account the priority fee that will be paid to the validator and the base fee that will be implicitly burned + // tracked by Energy.TotalAddSub return math.BigMin(new(big.Int).Add(feeItems.MaxPriorityFee, galacticaItems.BaseFee), feeItems.MaxFee) } @@ -144,3 +145,15 @@ func GalacticaPriorityPrice(tr *tx.Transaction, baseGasPrice, provedWork *big.In */ return math.BigMin(feeItems.MaxPriorityFee, new(big.Int).Sub(feeItems.MaxFee, galacticaItems.BaseFee)) } + +func CalculateReward(gasUsed uint64, rewardGasPrice, rewardRatio *big.Int, isGalactica bool) *big.Int { + reward := new(big.Int).SetUint64(gasUsed) + reward.Mul(reward, rewardGasPrice) + if isGalactica { + return reward + } + // Calculating the 30% of the reward + reward.Mul(reward, rewardRatio) + reward.Div(reward, big.NewInt(1e18)) + return reward +} diff --git a/consensus/fork/galactica_test.go b/consensus/fork/galactica_test.go index d7b9a222c..85ed41d9b 100644 --- a/consensus/fork/galactica_test.go +++ b/consensus/fork/galactica_test.go @@ -377,3 +377,50 @@ func TestGalacticaPriorityPrice(t *testing.T) { }) } } + +func TestCalculateReward(t *testing.T) { + rewardRatio := thor.InitialRewardRatio + tests := []struct { + name string + gasUsed uint64 + rewardGasPrice *big.Int + isGalactica bool + expectedReward *big.Int + }{ + { + name: "Galactica active, full reward", + gasUsed: 1000, + rewardGasPrice: big.NewInt(100), + isGalactica: true, + expectedReward: big.NewInt(100000), + }, + { + name: "Galactica inactive, 30% reward", + gasUsed: 1000, + rewardGasPrice: big.NewInt(100), + isGalactica: false, + expectedReward: big.NewInt(30000), + }, + { + name: "Galactica active, zero gas used", + gasUsed: 0, + rewardGasPrice: big.NewInt(100), + isGalactica: true, + expectedReward: big.NewInt(0), + }, + { + name: "Galactica inactive, zero gas used", + gasUsed: 0, + rewardGasPrice: big.NewInt(100), + isGalactica: false, + expectedReward: big.NewInt(0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reward := CalculateReward(tt.gasUsed, tt.rewardGasPrice, rewardRatio, tt.isGalactica) + assert.Equal(t, tt.expectedReward, reward) + }) + } +} diff --git a/runtime/runtime.go b/runtime/runtime.go index 8246857b7..d14151b77 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -501,6 +501,7 @@ func (rt *Runtime) PrepareTransaction(tx *tx.Transaction) (*TransactionExecutor, finalized = true receipt := &Tx.Receipt{ + Type: tx.Type(), Reverted: reverted, Outputs: txOutputs, GasUsed: tx.Gas() - leftOverGas, @@ -523,11 +524,8 @@ func (rt *Runtime) PrepareTransaction(tx *tx.Transaction) (*TransactionExecutor, return nil, err } rewardGasPrice := fork.GalacticaPriorityPrice(tx, baseGasPrice, provedWork, &fork.GalacticaItems{IsActive: galactica, BaseFee: rt.ctx.BaseFee}) + reward := fork.CalculateReward(receipt.GasUsed, rewardGasPrice, rewardRatio, galactica) - reward := new(big.Int).SetUint64(receipt.GasUsed) - reward.Mul(reward, rewardGasPrice) - reward.Mul(reward, rewardRatio) - reward.Div(reward, big.NewInt(1e18)) if err := builtin.Energy.Native(rt.state, rt.ctx.Time).Add(rt.ctx.Beneficiary, reward); err != nil { return nil, err } diff --git a/tx/receipt.go b/tx/receipt.go index 45ad952db..4282108cc 100644 --- a/tx/receipt.go +++ b/tx/receipt.go @@ -6,6 +6,9 @@ package tx import ( + "bytes" + "errors" + "io" "math/big" "github.com/ethereum/go-ethereum/rlp" @@ -13,8 +16,30 @@ import ( "github.com/vechain/thor/v2/trie" ) +var ( + errEmptyTypedReceipt = errors.New("empty typed receipt bytes") + errShortTypedReceipt = errors.New("typed receipt too short") +) + // Receipt represents the results of a transaction. type Receipt struct { + // transaction type this receipt is associated with + Type byte + // gas used by this tx + GasUsed uint64 + // the one who paid for gas + GasPayer thor.Address + // energy paid for used gas + Paid *big.Int + // energy reward given to block proposer + Reward *big.Int + // if the tx reverted + Reverted bool + // outputs of clauses in tx + Outputs []*Output +} + +type receiptRLP struct { // gas used by this tx GasUsed uint64 // the one who paid for gas @@ -56,9 +81,137 @@ func (rs derivableReceipts) Len() int { return len(rs) } func (rs derivableReceipts) GetRlp(i int) []byte { - data, err := rlp.EncodeToBytes(rs[i]) + data, err := rs[i].MarshalBinary() if err != nil { panic(err) } return data } + +// EncodeRLP implements rlp.Encoder, and flattens the consensus fields of a receipt +// into an RLP stream. If no post state is present, byzantium fork is assumed. +func (r *Receipt) EncodeRLP(w io.Writer) error { + data := &receiptRLP{ + r.GasUsed, r.GasPayer, r.Paid, r.Reward, r.Reverted, r.Outputs, + } + if r.Type == LegacyTxType { + return rlp.Encode(w, data) + } + + buf := encodeBufferPool.Get().(*bytes.Buffer) + defer encodeBufferPool.Put(buf) + buf.Reset() + buf.WriteByte(r.Type) + if err := rlp.Encode(buf, data); err != nil { + return err + } + return rlp.Encode(w, buf.Bytes()) +} + +// DecodeRLP implements rlp.Decoder, and loads the consensus fields of a receipt +// from an RLP stream. +func (r *Receipt) DecodeRLP(s *rlp.Stream) error { + kind, _, err := s.Kind() + switch { + case err != nil: + return err + case kind == rlp.List: + // It's a legacy receipt. + var dec receiptRLP + if err := s.Decode(&dec); err != nil { + return err + } + r.Type = LegacyTxType + r.setFromRLP(dec) + case kind == rlp.String: + // It's an EIP-2718 typed tx receipt. + b, err := s.Bytes() + if err != nil { + return err + } + if len(b) == 0 { + return errEmptyTypedReceipt + } + r.Type = b[0] + switch r.Type { + case DynamicFeeTxType: + var dec receiptRLP + if err := rlp.DecodeBytes(b[1:], &dec); err != nil { + return err + } + r.setFromRLP(dec) + default: + return ErrTxTypeNotSupported + } + default: + return rlp.ErrExpectedList + } + + return nil +} + +func (r *Receipt) setFromRLP(dec receiptRLP) { + r.GasUsed = dec.GasUsed + r.GasPayer = dec.GasPayer + r.Paid = dec.Paid + r.Reward = dec.Reward + r.Reverted = dec.Reverted + r.Outputs = dec.Outputs +} + +// MarshalBinary returns the consensus encoding of the receipt. +func (r *Receipt) MarshalBinary() ([]byte, error) { + if r.Type == LegacyTxType { + return rlp.EncodeToBytes(r) + } + data := &receiptRLP{ + r.GasUsed, r.GasPayer, r.Paid, r.Reward, r.Reverted, r.Outputs, + } + var buf bytes.Buffer + err := r.encodeTyped(data, &buf) + return buf.Bytes(), err +} + +// UnmarshalBinary decodes the consensus encoding of receipts. +// It supports legacy RLP receipts and EIP-2718 typed receipts. +func (r *Receipt) UnmarshalBinary(b []byte) error { + if len(b) > 0 && b[0] > 0x7f { + // It's a legacy receipt decode the RLP + var data receiptRLP + err := rlp.DecodeBytes(b, &data) + if err != nil { + return err + } + r.Type = LegacyTxType + r.setFromRLP(data) + return nil + } + // It's an EIP2718 typed transaction envelope. + return r.decodeTyped(b) +} + +// encodeTyped writes the canonical encoding of a typed receipt to w. +func (r *Receipt) encodeTyped(data *receiptRLP, w *bytes.Buffer) error { + w.WriteByte(r.Type) + return rlp.Encode(w, data) +} + +// decodeTyped decodes a typed receipt from the canonical format. +func (r *Receipt) decodeTyped(b []byte) error { + if len(b) <= 1 { + return errShortTypedReceipt + } + switch b[0] { + case DynamicFeeTxType: + var data receiptRLP + err := rlp.DecodeBytes(b[1:], &data) + if err != nil { + return err + } + r.Type = b[0] + r.setFromRLP(data) + return nil + default: + return ErrTxTypeNotSupported + } +} diff --git a/tx/receipt_test.go b/tx/receipt_test.go index 08948009a..f1d119b0a 100644 --- a/tx/receipt_test.go +++ b/tx/receipt_test.go @@ -3,58 +3,128 @@ // Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying // file LICENSE or -package tx_test +package tx import ( + "bytes" "fmt" "math/big" "testing" + "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/thor" - "github.com/vechain/thor/v2/tx" ) -func getMockReceipt() tx.Receipt { - receipt := tx.Receipt{ +func getMockReceipt(txType byte) Receipt { + receipt := Receipt{ + Type: txType, GasUsed: 1000, GasPayer: thor.Address{}, Paid: big.NewInt(100), Reward: big.NewInt(50), Reverted: false, - Outputs: []*tx.Output{}, + Outputs: []*Output{}, } return receipt } func TestReceipt(t *testing.T) { - var rs tx.Receipts + var rs Receipts fmt.Println(rs.RootHash()) - var txs tx.Transactions + var txs Transactions fmt.Println(txs.RootHash()) } func TestReceiptStructure(t *testing.T) { - receipt := getMockReceipt() - - assert.Equal(t, uint64(1000), receipt.GasUsed) - assert.Equal(t, thor.Address{}, receipt.GasPayer) - assert.Equal(t, big.NewInt(100), receipt.Paid) - assert.Equal(t, big.NewInt(50), receipt.Reward) - assert.Equal(t, false, receipt.Reverted) - assert.Equal(t, []*tx.Output{}, receipt.Outputs) + for _, txType := range []int{LegacyTxType, DynamicFeeTxType} { + receipt := getMockReceipt(byte(txType)) + + // assert.Equal(t, byte(txType), receipt.Type) + assert.Equal(t, uint64(1000), receipt.GasUsed) + assert.Equal(t, thor.Address{}, receipt.GasPayer) + assert.Equal(t, big.NewInt(100), receipt.Paid) + assert.Equal(t, big.NewInt(50), receipt.Reward) + assert.Equal(t, false, receipt.Reverted) + assert.Equal(t, []*Output{}, receipt.Outputs) + } } func TestEmptyRootHash(t *testing.T) { - receipt1 := getMockReceipt() - receipt2 := getMockReceipt() + tests := []struct { + name string + receipt1 Receipt + receipt2 Receipt + }{ + { + name: "LegacyReceipts", + receipt1: getMockReceipt(byte(LegacyTxType)), + receipt2: getMockReceipt(byte(LegacyTxType)), + }, + { + name: "DynamicFeeReceipts", + receipt1: getMockReceipt(byte(DynamicFeeTxType)), + receipt2: getMockReceipt(byte(DynamicFeeTxType)), + }, + { + name: "MixedReceipts", + receipt1: getMockReceipt(byte(LegacyTxType)), + receipt2: getMockReceipt(byte(DynamicFeeTxType)), + }, + } - receipts := tx.Receipts{ - &receipt1, - &receipt2, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + receipts := Receipts{ + &tt.receipt1, + &tt.receipt2, + } + + rootHash := receipts.RootHash() + assert.NotEmpty(t, rootHash, "Root hash should be empty") + }) } +} - rootHash := receipts.RootHash() - assert.NotEqual(t, thor.Bytes32{}, rootHash, "Root hash should not be empty") +func TestMarshalAndUnmarshalBinary(t *testing.T) { + for _, txType := range []int{LegacyTxType, DynamicFeeTxType} { + originalReceipt := getMockReceipt(byte(txType)) + + data, err := originalReceipt.MarshalBinary() + assert.Nil(t, err) + + var unmarshalledReceipt Receipt + err = unmarshalledReceipt.UnmarshalBinary(data) + assert.Nil(t, err) + + assert.Equal(t, originalReceipt, unmarshalledReceipt) + } +} + +func TestEncodeAndDecodeReceipt(t *testing.T) { + for _, txType := range []int{LegacyTxType, DynamicFeeTxType} { + originalReceipt := getMockReceipt(byte(txType)) + receiptBuf := new(bytes.Buffer) + // Encoding + err := originalReceipt.EncodeRLP(receiptBuf) + assert.Nil(t, err) + + s := rlp.NewStream(receiptBuf, 0) + var decodedReceipt Receipt + // Decoding + err = decodedReceipt.DecodeRLP(s) + assert.Nil(t, err) + + assert.Equal(t, originalReceipt, decodedReceipt) + } +} + +func TestDecodeEmptyTypedReceipt(t *testing.T) { + input := []byte{0x80} + var r Receipt + err := rlp.DecodeBytes(input, &r) + if err != errEmptyTypedReceipt { + t.Fatal("wrong error:", err) + } }