mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
P-chain stake is not natively readable from the C-chain EVM, and the obvious fix — a precompile that reads the validator set in Run() — is consensus-fatal. The BlockContext handed to Run() (precompile/contract/interfaces.go:63) carries Number, Timestamp and GetPredicateResults, and nothing else; a validator-set read from there would have to use the node's own current P-chain height (node vms/platformvm/validators/manager.go:214 getCurrentHeight -> state.GetLastAccepted), so two honest nodes executing the same block a second apart would compute different weights and fork the chain. The one place a P-chain height IS agreed is predicate verification: Block.VerifyWithContext passes ProposerVMBlockCtx.PChainHeight to VerifyPredicate (plugin/evm/block.go:283), the pass/fail bitset is written into the header (miner/worker.go:514) and re-checked on replay (plugin/evm/block.go:366). A ballot verified there is a consensus-committed fact, and historical sets stay reconstructible from P-chain weight diffs. So the ballot CLAIMS a weight and the predicate refuses any overstatement: weight <= weightAt(ballot.pChainHeight) - no flash-stake weight <= weightAt(block.pChainHeight) - no exited or shrunken validator totalWeight == totalAt(snapshot) - the denominator is exact Under-claiming is always allowed, so a voter never has to predict which block its transaction lands in. The unit of weight is the validator, and it already includes delegations: the P-chain keeps one weight per nodeID and folds each delegator into it (node vms/platformvm/state/state_validators.go:570,582 — AddStaker(own stake) then AddWeight(each delegator) under the same key). One map entry per nodeID is what makes double-counting a delegation impossible. Delegators do not get a separate ballot; their override is exit, which needs no code. Authorisation reuses the BLS key the P-chain already binds to the nodeID, so there is no registry to capture, no admin to hold it and no key ceremony. The signature covers (domain, chainID, nodeID, voter, authEpoch) only — a standing authorisation, reusable for every later proposal, rotated by signing a strictly greater epoch. Uptime is deliberately not a gate: it is absent from GetValidatorOutput and is a per-node subjective measurement (all five mainnet validators report uptime 0.0000 through the public API), so no deterministic rule can read it. Proven against a read-only fork of mainnet 96369 — the real five-validator set, real registered BLS keys, real 5e17/2.5e18 weights — including that no ballot is accepted for any of them without the operator's key. Address 0x0200...0006 is the next free slot after warp; eth_getCode on 96369 returns 0x for it and 0x01 for warp, so predicates are already live there. Co-authored-by: Hanzo Dev <[email protected]>
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package stakeweight
|
|
|
|
import (
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
|
|
"github.com/luxfi/evm/accounts/abi"
|
|
"github.com/luxfi/evm/precompile/contract"
|
|
"github.com/luxfi/evm/predicate"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/math/set"
|
|
)
|
|
|
|
// GetVerifiedStakeWeightGasCost prices the state read Run() performs. The
|
|
// expensive work — two validator-set materialisations and a BLS verification —
|
|
// was already paid for in PredicateGas before the block was executed.
|
|
const GetVerifiedStakeWeightGasCost uint64 = 2_000
|
|
|
|
var (
|
|
//go:embed contract.abi
|
|
StakeWeightRawABI string
|
|
|
|
StakeWeightABI = contract.ParseABI(StakeWeightRawABI)
|
|
|
|
StakeWeightPrecompile = createStakeWeightPrecompile()
|
|
|
|
errInvalidIndexInput = errors.New("invalid index to specify stake-weight ballot")
|
|
|
|
invalidOutput []byte
|
|
)
|
|
|
|
// StakeWeight is the ABI struct returned to Solidity.
|
|
//
|
|
// weight and totalWeight are P-chain uint64 values widened to uint256 at the
|
|
// boundary: a tally sums many of them and multiplies by a numerator, and
|
|
// forcing that arithmetic through uint64 in Solidity would invite an overflow
|
|
// at every integration site. Widening is lossless.
|
|
type StakeWeight struct {
|
|
NodeID [20]byte
|
|
Voter common.Address
|
|
AuthEpoch uint64
|
|
PChainHeight uint64
|
|
Weight *big.Int
|
|
TotalWeight *big.Int
|
|
}
|
|
|
|
func init() {
|
|
res, err := PackGetVerifiedStakeWeightOutput(GetVerifiedStakeWeightOutput{Valid: false})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
invalidOutput = res
|
|
}
|
|
|
|
// GetVerifiedStakeWeightOutput is the full return tuple.
|
|
type GetVerifiedStakeWeightOutput struct {
|
|
StakeWeight StakeWeight
|
|
Valid bool
|
|
}
|
|
|
|
// PackGetVerifiedStakeWeight packs [index] including the 4-byte selector.
|
|
func PackGetVerifiedStakeWeight(index uint32) ([]byte, error) {
|
|
return StakeWeightABI.Pack("getVerifiedStakeWeight", index)
|
|
}
|
|
|
|
// UnpackGetVerifiedStakeWeightInput unpacks [input] (selector already stripped).
|
|
func UnpackGetVerifiedStakeWeightInput(input []byte) (uint32, error) {
|
|
res, err := StakeWeightABI.UnpackInput("getVerifiedStakeWeight", input, false)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return *abi.ConvertType(res[0], new(uint32)).(*uint32), nil
|
|
}
|
|
|
|
// PackGetVerifiedStakeWeightOutput packs [out] to conform to the ABI outputs.
|
|
func PackGetVerifiedStakeWeightOutput(out GetVerifiedStakeWeightOutput) ([]byte, error) {
|
|
if out.StakeWeight.Weight == nil {
|
|
out.StakeWeight.Weight = new(big.Int)
|
|
}
|
|
if out.StakeWeight.TotalWeight == nil {
|
|
out.StakeWeight.TotalWeight = new(big.Int)
|
|
}
|
|
return StakeWeightABI.PackOutput("getVerifiedStakeWeight", out.StakeWeight, out.Valid)
|
|
}
|
|
|
|
// UnpackGetVerifiedStakeWeightOutput unpacks [output] as the return tuple.
|
|
func UnpackGetVerifiedStakeWeightOutput(output []byte) (GetVerifiedStakeWeightOutput, error) {
|
|
out := GetVerifiedStakeWeightOutput{}
|
|
err := StakeWeightABI.UnpackIntoInterface(&out, "getVerifiedStakeWeight", output)
|
|
return out, err
|
|
}
|
|
|
|
// getVerifiedStakeWeight returns the ballot at [index] of the transaction's
|
|
// access list for this precompile, together with whether the block's committed
|
|
// predicate results attest that its claims held against P-chain state.
|
|
//
|
|
// Run() re-derives nothing. It reads the ballot the sender committed to and the
|
|
// verdict consensus already recorded, so every node computes the same answer
|
|
// from data inside the block.
|
|
func getVerifiedStakeWeight(accessibleState contract.AccessibleState, _ common.Address, _ common.Address, input []byte, suppliedGas uint64, _ bool) ([]byte, uint64, error) {
|
|
remainingGas, err := contract.DeductGas(suppliedGas, GetVerifiedStakeWeightGasCost)
|
|
if err != nil {
|
|
return nil, remainingGas, err
|
|
}
|
|
index, err := UnpackGetVerifiedStakeWeightInput(input)
|
|
if err != nil {
|
|
return nil, remainingGas, fmt.Errorf("%w: %w", errInvalidIndexInput, err)
|
|
}
|
|
if index > math.MaxInt32 {
|
|
return nil, remainingGas, fmt.Errorf("%w: larger than MaxInt32", errInvalidIndexInput)
|
|
}
|
|
|
|
state := accessibleState.GetStateDB()
|
|
predicateBytes, exists := state.GetPredicateStorageSlots(ContractAddress, int(index))
|
|
// A set bit means VerifyPredicate returned an error for that index.
|
|
failed := set.BitsFromBytes(
|
|
accessibleState.GetBlockContext().GetPredicateResults(state.GetTxHash(), ContractAddress),
|
|
).Contains(int(index))
|
|
if !exists || failed {
|
|
return invalidOutput, remainingGas, nil
|
|
}
|
|
|
|
unpacked, err := predicate.UnpackPredicate(predicateBytes)
|
|
if err != nil {
|
|
// Unreachable: the same unpack succeeded during predicate verification.
|
|
return nil, remainingGas, fmt.Errorf("%w: %w", errInvalidPredicateBytes, err)
|
|
}
|
|
ballot, err := ParseBallot(unpacked)
|
|
if err != nil {
|
|
return nil, remainingGas, err
|
|
}
|
|
|
|
packed, err := PackGetVerifiedStakeWeightOutput(GetVerifiedStakeWeightOutput{
|
|
StakeWeight: StakeWeight{
|
|
NodeID: ballot.NodeID,
|
|
Voter: ballot.Voter,
|
|
AuthEpoch: ballot.AuthEpoch,
|
|
PChainHeight: ballot.PChainHeight,
|
|
Weight: new(big.Int).SetUint64(ballot.Weight),
|
|
TotalWeight: new(big.Int).SetUint64(ballot.TotalWeight),
|
|
},
|
|
Valid: true,
|
|
})
|
|
if err != nil {
|
|
return nil, remainingGas, err
|
|
}
|
|
return packed, remainingGas, nil
|
|
}
|
|
|
|
func createStakeWeightPrecompile() contract.StatefulPrecompiledContract {
|
|
method, ok := StakeWeightABI.Methods["getVerifiedStakeWeight"]
|
|
if !ok {
|
|
panic("getVerifiedStakeWeight missing from the stakeweight ABI")
|
|
}
|
|
statefulContract, err := contract.NewStatefulPrecompileContract(nil, []*contract.StatefulPrecompileFunction{
|
|
contract.NewStatefulPrecompileFunction(method.ID, getVerifiedStakeWeight),
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return statefulContract
|
|
}
|