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:
parent
cf20259e96
commit
7ac618d339
2 changed files with 152 additions and 0 deletions
79
consensus/fee.go
Normal file
79
consensus/fee.go
Normal 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
73
consensus/fee_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue