go-blockchain/consensus/block.go
Claude 7abac5e011
feat(consensus): full block validation orchestrator
ValidateBlock combines timestamp, miner tx, and reward checks into
a single entry point for block-level consensus validation.

Co-Authored-By: Charon <charon@lethean.io>
2026-02-21 00:53:51 +00:00

151 lines
4.4 KiB
Go

// 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"
"sort"
"forge.lthn.ai/core/go-blockchain/config"
"forge.lthn.ai/core/go-blockchain/types"
)
// IsPoS returns true if the block flags indicate a Proof-of-Stake block.
// Bit 0 of the flags byte is the PoS indicator.
func IsPoS(flags uint8) bool {
return flags&1 != 0
}
// CheckTimestamp validates a block's timestamp against future limits and
// the median of recent timestamps.
func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, recentTimestamps []uint64) error {
// Future time limit.
limit := config.BlockFutureTimeLimit
if IsPoS(flags) {
limit = config.PosBlockFutureTimeLimit
}
if blockTimestamp > adjustedTime+limit {
return fmt.Errorf("%w: %d > %d + %d", ErrTimestampFuture,
blockTimestamp, adjustedTime, limit)
}
// Median check — only when we have enough history.
if uint64(len(recentTimestamps)) < config.TimestampCheckWindow {
return nil
}
median := medianTimestamp(recentTimestamps)
if blockTimestamp < median {
return fmt.Errorf("%w: %d < median %d", ErrTimestampOld,
blockTimestamp, median)
}
return nil
}
// medianTimestamp returns the median of a slice of timestamps.
func medianTimestamp(timestamps []uint64) uint64 {
sorted := make([]uint64, len(timestamps))
copy(sorted, timestamps)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
n := len(sorted)
if n == 0 {
return 0
}
return sorted[n/2]
}
// ValidateMinerTx checks the structure of a coinbase (miner) transaction.
// For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly
// 2 inputs (TxInputGenesis + stake input).
func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error {
if len(tx.Vin) == 0 {
return fmt.Errorf("%w: no inputs", ErrMinerTxInputs)
}
// First input must be TxInputGenesis.
gen, ok := tx.Vin[0].(types.TxInputGenesis)
if !ok {
return fmt.Errorf("%w: first input is not txin_gen", ErrMinerTxInputs)
}
if gen.Height != height {
return fmt.Errorf("%w: got %d, expected %d", ErrMinerTxHeight, gen.Height, height)
}
// PoW blocks: exactly 1 input. PoS: exactly 2.
if len(tx.Vin) == 1 {
// PoW — valid.
} else if len(tx.Vin) == 2 {
// PoS — second input must be a spend input.
switch tx.Vin[1].(type) {
case types.TxInputToKey:
// Pre-HF4 PoS.
default:
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
if !hf4Active {
return fmt.Errorf("%w: invalid PoS stake input type", ErrMinerTxInputs)
}
// Post-HF4: accept ZC inputs.
}
} else {
return fmt.Errorf("%w: %d inputs (expected 1 or 2)", ErrMinerTxInputs, len(tx.Vin))
}
return nil
}
// ValidateBlockReward checks that the miner transaction outputs do not
// exceed the expected reward (base reward + fees for pre-HF4).
func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSize, totalFees uint64, forks []config.HardFork) error {
base := BaseReward(height)
reward, err := BlockReward(base, blockSize, medianSize)
if err != nil {
return err
}
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
expected := MinerReward(reward, totalFees, hf4Active)
// Sum miner tx outputs.
var outputSum uint64
for _, vout := range minerTx.Vout {
if bare, ok := vout.(types.TxOutputBare); ok {
outputSum += bare.Amount
}
}
if outputSum > expected {
return fmt.Errorf("%w: outputs %d > expected %d", ErrRewardMismatch, outputSum, expected)
}
return nil
}
// ValidateBlock performs full consensus validation on a block. It checks
// the timestamp, miner transaction structure, and reward. Transaction
// semantic validation for regular transactions should be done separately
// via ValidateTransaction for each tx in the block.
func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, adjustedTime uint64,
recentTimestamps []uint64, forks []config.HardFork) error {
// Timestamp validation.
if err := CheckTimestamp(blk.Timestamp, blk.Flags, adjustedTime, recentTimestamps); err != nil {
return err
}
// Miner transaction structure.
if err := ValidateMinerTx(&blk.MinerTx, height, forks); err != nil {
return err
}
// Block reward.
if err := ValidateBlockReward(&blk.MinerTx, height, blockSize, medianSize, totalFees, forks); err != nil {
return err
}
return nil
}