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 <charon@lethean.io>
This commit is contained in:
Claude 2026-02-21 00:45:01 +00:00
parent cf20259e96
commit 7ac618d339
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 152 additions and 0 deletions

79
consensus/fee.go Normal file
View file

@ -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
}

73
consensus/fee_test.go Normal file
View file

@ -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)
}