From 7ac618d33999f813601747b6cba42a3d7b8fc8b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 00:45:01 +0000 Subject: [PATCH] feat(consensus): fee extraction with overflow checks TxFee calculates pre-HF4 fees as sum(inputs) - sum(outputs) with overflow detection. Coinbase transactions return zero fee. Co-Authored-By: Charon --- consensus/fee.go | 79 +++++++++++++++++++++++++++++++++++++++++++ consensus/fee_test.go | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 consensus/fee.go create mode 100644 consensus/fee_test.go diff --git a/consensus/fee.go b/consensus/fee.go new file mode 100644 index 0000000..547c76a --- /dev/null +++ b/consensus/fee.go @@ -0,0 +1,79 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package consensus + +import ( + "fmt" + "math" + + "forge.lthn.ai/core/go-blockchain/types" +) + +// TxFee calculates the transaction fee for pre-HF4 (v0/v1) transactions. +// Coinbase transactions return 0. For standard transactions, fee equals +// the difference between total input amounts and total output amounts. +func TxFee(tx *types.Transaction) (uint64, error) { + if isCoinbase(tx) { + return 0, nil + } + + inputSum, err := sumInputs(tx) + if err != nil { + return 0, err + } + + outputSum, err := sumOutputs(tx) + if err != nil { + return 0, err + } + + if outputSum > inputSum { + return 0, fmt.Errorf("%w: inputs=%d, outputs=%d", ErrNegativeFee, inputSum, outputSum) + } + + return inputSum - outputSum, nil +} + +// isCoinbase returns true if the transaction's first input is TxInputGenesis. +func isCoinbase(tx *types.Transaction) bool { + if len(tx.Vin) == 0 { + return false + } + _, ok := tx.Vin[0].(types.TxInputGenesis) + return ok +} + +// sumInputs totals all TxInputToKey amounts, checking for overflow. +func sumInputs(tx *types.Transaction) (uint64, error) { + var total uint64 + for _, vin := range tx.Vin { + toKey, ok := vin.(types.TxInputToKey) + if !ok { + continue + } + if total > math.MaxUint64-toKey.Amount { + return 0, ErrInputOverflow + } + total += toKey.Amount + } + return total, nil +} + +// sumOutputs totals all TxOutputBare amounts, checking for overflow. +func sumOutputs(tx *types.Transaction) (uint64, error) { + var total uint64 + for _, vout := range tx.Vout { + bare, ok := vout.(types.TxOutputBare) + if !ok { + continue + } + if total > math.MaxUint64-bare.Amount { + return 0, ErrOutputOverflow + } + total += bare.Amount + } + return total, nil +} diff --git a/consensus/fee_test.go b/consensus/fee_test.go new file mode 100644 index 0000000..7138aec --- /dev/null +++ b/consensus/fee_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +//go:build !integration + +package consensus + +import ( + "testing" + + "forge.lthn.ai/core/go-blockchain/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTxFee_Good(t *testing.T) { + // Coinbase tx: fee is 0. + coinbase := &types.Transaction{ + Version: types.VersionInitial, + Vin: []types.TxInput{types.TxInputGenesis{Height: 1}}, + } + fee, err := TxFee(coinbase) + require.NoError(t, err) + assert.Equal(t, uint64(0), fee) + + // Normal v1 tx: fee = inputs - outputs. + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: 100}, + types.TxInputToKey{Amount: 50}, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 120}, + }, + } + fee, err = TxFee(tx) + require.NoError(t, err) + assert.Equal(t, uint64(30), fee) +} + +func TestTxFee_Bad(t *testing.T) { + // Outputs exceed inputs. + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: 50}, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 100}, + }, + } + _, err := TxFee(tx) + assert.ErrorIs(t, err, ErrNegativeFee) +} + +func TestTxFee_Ugly(t *testing.T) { + // Input amounts that would overflow uint64. + tx := &types.Transaction{ + Version: types.VersionPreHF4, + Vin: []types.TxInput{ + types.TxInputToKey{Amount: ^uint64(0)}, + types.TxInputToKey{Amount: 1}, + }, + Vout: []types.TxOutput{ + types.TxOutputBare{Amount: 1}, + }, + } + _, err := TxFee(tx) + assert.ErrorIs(t, err, ErrInputOverflow) +}