mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Four defects, each confirmed by executing it before it was fixed and by mutation after. A block whose parent was an OLD accepted block satisfied height == parent+1, so Verify passed it and Accept applied it: the chain at height 3 rewound to 2, the last-accepted pointer and the height index moved with it, and the index went on naming the abandoned block at 3. A peer bootstrapping from it was served an incoherent chain. The parent must now be the tip or a block verified above it; Accept decides again under its own hold of the lock, because the tip moves between the two. Nothing bound a transaction or a block to the chain it was meant for. An address is the hash of a public key, so the same payer exists on every F-Chain, and a captured transaction authenticated verbatim on all of them and burned a balance there. Every chain sharing a genesis timestamp also shared a genesis id, so one chain's block resolved its parent on the others. The chain id now enters the signing preimage and the block id without travelling on the wire: each side supplies its own, so a mismatch is a failed signature and an unresolvable parent rather than a check someone can forget. A read that FAILED was reported as a read that found NOTHING. An unreadable last-accepted pointer left a live chain looking like a fresh one, with Initialize returning nil and the node ready to build height 1 over height 2. The same conflation on the epoch pointer seats a committee nobody elected, and on a nonce it reopens the replay window a nonce exists to close. One reader now separates absent from unreadable, and nonceOf reports rather than returns zero. HealthCheck likewise stops reporting a ledger it could not read as zero burned. MaxBlockTxs was applied by Verify, and Verify runs after the parse, so an 8 MB message carrying 1,524 transactions parsed to completion before any check ran. Bytes and count are both bounded at the first byte, and neither implies the other; selection stops at whichever comes first, so a proposer cannot build what its own parser refuses. Deleted: the tx-blob accounting check, which the canonical-encoding rule subsumes exactly (a blob with unread bytes re-serializes shorter), and the empty-path branch in NewHTTPHandler, whose one producer returns a literal. fhevm 89.7% -> 98.9%. The twelve statements left are named unreachable in the report, not padded. The harness now holds constant what production holds constant: one chain id and one genesis across every node of a chain, rather than a fresh id per node.
110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package fhevm
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/luxfi/chains/fee"
|
|
)
|
|
|
|
// MaxBlockTxs bounds how many transactions one block carries. Without it a
|
|
// proposer's block size is whatever the mempool happens to hold.
|
|
const MaxBlockTxs = 1024
|
|
|
|
// batch is the running state a block's transactions must satisfy AS A
|
|
// SEQUENCE: nonces strictly in order per payer, fees affordable against a
|
|
// running per-payer debit, and no two transactions claiming one effect.
|
|
//
|
|
// One rule, two policies. Block.Verify runs it over a received block and
|
|
// refuses the whole block on the first transaction that does not fit;
|
|
// BuildBlock runs it over the mempool and simply leaves out what does not fit.
|
|
// A proposer therefore cannot build a block its own Verify would reject, and
|
|
// one unfit transaction can no longer take the rest of the mempool down with
|
|
// it.
|
|
//
|
|
// What is deliberately NOT here is authorization. checkAuth reads state that
|
|
// earlier transactions in the same block may change, so a block-time
|
|
// authorization verdict can differ from the application-time one — and a block
|
|
// that passes Verify on every validator and then fails to apply on every
|
|
// validator halts the chain. Authorization is decided once, at application
|
|
// time, and a transaction that fails it REVERTS: it pays its fee and has no
|
|
// effect (block.go settleAndApply). Verify's job is that the block is
|
|
// well-formed, authentic, ordered and paid for.
|
|
type batch struct {
|
|
vm *VM
|
|
nonce map[fee.Account]uint64 // next nonce owed by each payer
|
|
spent map[fee.Account]uint64 // running debit per payer
|
|
claimed map[[32]byte]struct{} // effects already taken in this block
|
|
}
|
|
|
|
func newBatch(vm *VM) *batch {
|
|
return &batch{
|
|
vm: vm,
|
|
nonce: make(map[fee.Account]uint64),
|
|
spent: make(map[fee.Account]uint64),
|
|
claimed: make(map[[32]byte]struct{}),
|
|
}
|
|
}
|
|
|
|
// admit tests one transaction against the running state and, if it fits,
|
|
// records its consumption of that state. Caller holds vm.stateLock.
|
|
func (b *batch) admit(tx *Transaction) error {
|
|
if err := tx.SyntacticVerify(); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.authenticate(b.vm.chainID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Replay/order guard: a payer's nonces are consecutive from its committed
|
|
// one, counting the transactions already taken from it in this block.
|
|
want, seen := b.nonce[tx.Payer]
|
|
if !seen {
|
|
committed, err := b.vm.nonceOf(tx.Payer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
want = committed + 1
|
|
}
|
|
if tx.Nonce != want {
|
|
return ErrBadNonce
|
|
}
|
|
|
|
eff := tx.effect()
|
|
if _, dup := b.claimed[eff]; dup {
|
|
return ErrDuplicateEffect
|
|
}
|
|
|
|
gasUsed, err := GasFor(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if uint64(gasUsed) > tx.GasLimit {
|
|
return fmt.Errorf("fhevm: %w: gas %d > limit %d", fee.ErrOutOfGas, gasUsed, tx.GasLimit)
|
|
}
|
|
feeAmt, err := fee.Cost(gasUsed, GasPrice)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bal, err := b.vm.ledger.Balance(tx.Payer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
next, over := addSpend(b.spent[tx.Payer], feeAmt)
|
|
if over || bal < next {
|
|
return fee.ErrInsufficientFunds
|
|
}
|
|
|
|
b.nonce[tx.Payer] = want + 1
|
|
b.spent[tx.Payer] = next
|
|
b.claimed[eff] = struct{}{}
|
|
return nil
|
|
}
|
|
|
|
func addSpend(a, b uint64) (uint64, bool) {
|
|
s := a + b
|
|
return s, s < a
|
|
}
|