Files
evm/core/parallel/gpu_bridge.go
zeekay 4eecb66098 evm v0.18.16: fix nil-chainConfig panic at PQ gate + RLP verifier tool
Bug fix (closure-swarm v1.28.15 boot blocker):
PQ gate at plugin/evm/vm.go was setting vm.chainConfig.PQ BEFORE
vm.chainConfig was assigned from g.Config — chainConfig was nil at
that point, panic on dereference. Moved the gate AFTER assignment.

Repro: fresh-PVC bootstrap with chain-config.json pq:true caused
"runtime error: invalid memory address or nil pointer dereference"
in the EVM ZAP plugin → C-Chain never bootstraps. Discovered during
localnet 1337 bring-up against v1.28.15.

Fix: move the gate to AFTER `vm.chainConfig = g.Config`. PQ profile
is now correctly installed on a non-nil chainConfig.

Added:
- cmd/rlp-verify/main.go — decodes C-Chain RLP export, verifies
  genesis hash matches genesis JSON, each block's ParentHash chains
  cleanly. What admin.importChain does sans state execution; sufficient
  proof that an RLP+genesis pair is wireable to a luxd boot.

Indirect dep bumps:
- github.com/ChainSafe/go-schnorrkel v1.1.0 (sr25519Verify precompile)
- github.com/cosmos/go-bip39 v1.0.0 (bridgeRegistrarConfig precompile)
- github.com/luxfi/ai v0.1.0 (aiMiningConfig precompile)

Build tag: drop `gpu` tag on darwin gpu_bridge.go (parallel/gpu is one
concept; the darwin gpu bridge always compiles when cgo+darwin).
2026-06-02 01:04:45 -07:00

153 lines
3.9 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo && darwin
// Bridge that connects the EVM parallel interface to the lux/gpu package.
// Build with: CGO_ENABLED=1 go build -tags parallel
//
// This eliminates the signature recovery bottleneck:
// CPU ecrecover: 1613ms for 47K sigs (87% of block time)
// GPU ecrecover: ~50ms for 47K sigs (32x speedup)
package parallel
import (
"math/big"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/crypto/secp256k1"
luxgpu "github.com/luxfi/gpu"
)
// secp256k1halfN is the half-order of secp256k1 for EIP-2 low-S enforcement.
var secp256k1halfN = new(big.Int).Rsh(secp256k1.S256().Params().N, 1)
type gpuBridge struct{}
func init() {
RegisterGPU(&gpuBridge{})
}
func (g *gpuBridge) Available() bool {
return luxgpu.DefaultContext != nil && luxgpu.GetBackend() != luxgpu.CPU
}
func (g *gpuBridge) BatchEcrecover(txs []*types.Transaction) (map[common.Hash]common.Address, error) {
if len(txs) == 0 {
return nil, nil
}
n := len(txs)
sigs := make([]luxgpu.Signature, n)
// Track which tx indices have valid GPU input (per-tx signer)
valid := make([]bool, n)
for i, tx := range txs {
v, r, s := tx.RawSignatureValues()
if r == nil || s == nil {
continue
}
// EIP-2: reject high-S signatures (malleability protection).
// The CPU path enforces this in ValidateSignatureValues.
// We must enforce it here too, or GPU returns a different address.
if s.Cmp(secp256k1halfN) > 0 {
continue // skip; CPU fallback will reject this tx
}
// Reject oversized R or S (> 32 bytes = > 2^256)
if len(r.Bytes()) > 32 || len(s.Bytes()) > 32 {
continue
}
// Per-tx signer — different chain IDs get different signers.
// CRITICAL: using a single signer for the whole batch would produce
// wrong signing hashes for txs with different chain IDs (e.g., legacy
// unprotected txs mixed with EIP-155 txs), leading to wrong sender recovery.
signer := types.LatestSignerForChainID(tx.ChainId())
hash := signer.Hash(tx)
// Pack r (32 bytes big-endian, left-padded)
copy(sigs[i].R[:], padTo32(r))
// Pack s
copy(sigs[i].S[:], padTo32(s))
// Recovery ID — guard against overflow for large chain IDs
vid, ok := recoverV(v, tx.ChainId())
if !ok {
continue
}
sigs[i].V = vid
// Message hash
copy(sigs[i].MsgHash[:], hash[:])
valid[i] = true
}
results, err := luxgpu.BatchEcrecover(sigs)
if err != nil {
return nil, err
}
addrs := make(map[common.Hash]common.Address, n)
for i, tx := range txs {
if !valid[i] {
continue // was skipped due to validation; CPU fallback handles it
}
if i >= len(results) || !results[i].Valid {
continue
}
var addr common.Address
copy(addr[:], results[i].Address[:])
// Reject zero address — GPU kernel bug or edge case
if addr == (common.Address{}) {
continue
}
addrs[tx.Hash()] = addr
}
return addrs, nil
}
func (g *gpuBridge) BatchKeccak(_ [][]byte) ([]common.Hash, error) {
return nil, nil // not yet available; CPU path used
}
// padTo32 returns a 32-byte big-endian representation of a big.Int.
func padTo32(n *big.Int) []byte {
b := n.Bytes()
if len(b) > 32 {
return nil // caller must check; should not happen after validation
}
if len(b) == 32 {
return b
}
padded := make([]byte, 32)
copy(padded[32-len(b):], b)
return padded
}
// recoverV extracts the recovery ID (0 or 1) from the transaction's V value.
// Returns (id, false) if v or chainID overflows uint64.
func recoverV(v *big.Int, chainID *big.Int) (uint8, bool) {
if v.BitLen() > 63 {
return 0, false
}
vVal := v.Uint64()
if chainID != nil && chainID.Sign() > 0 {
if chainID.BitLen() > 63 {
return 0, false
}
cid := chainID.Uint64()
if vVal >= 35 {
return uint8((vVal - 35 - cid*2) & 1), true
}
}
if vVal != 27 && vVal != 28 {
return 0, false // invalid V for legacy tx
}
return uint8(vVal - 27), true
}