bridge: a governance authorisation that never expired, and two ways to spend an unauthorised call

The registrar digest bound only the selector and the arguments, so a signature
set authorising "register chain 5" stayed valid forever: anyone who saw the
original transaction could re-apply it after an unregister, or re-apply a
superseded SetGateway. The digest now carries a governance nonce that advances
after each successful write. Selector 0x06 reads it. The nonce lives outside
the govSlot namespace so re-seeding cannot revive retired authorisations.

Two more, both reachable without any signature being valid. Every submitted
signature is ECRECOVERed before any of them is known to belong to an operator,
and the flat write fee bought up to 255 of them; signatures are now billed at
the EVM's own ECRECOVER price. And loadGovernance allocated from a count it
read out of storage, which genesis alloc can pre-populate at any address, so a
malformed genesis could ask for four billion addresses at boot.
This commit is contained in:
2026-08-28 15:38:57 -07:00
parent a95305827c
commit 96ba923c2f
5 changed files with 1330 additions and 106 deletions
+113 -9
View File
@@ -25,9 +25,14 @@
//
// Signature payload
//
// keccak256("lux.bridge.registrar.v1" || selector(1) || abi_args)
// keccak256("lux.bridge.registrar.v1" || selector(1) || nonce(8 BE) || abi_args)
//
// where abi_args is the same byte slice the EVM hands the selector.
// where abi_args is the same byte slice the EVM hands the selector, and nonce is
// the registrar's current governance nonce, readable with selector 0x06. The
// nonce advances after every successful write, so an authorisation is
// single-use: without it, the operators' signatures over "register chain 5"
// would stay valid forever and anyone who saw the original transaction could
// re-apply it after an unregister, or re-apply a superseded SetGateway.
package bridge
import (
@@ -68,15 +73,25 @@ const (
SelectorSetGateway byte = 0x03
SelectorGetCount byte = 0x04
SelectorGetChain byte = 0x05
SelectorGetNonce byte = 0x06
)
// Gas costs — pinned, no dynamic component.
// Gas costs. The per-selector figures cover the storage each write touches. The
// signature term is separate because the caller chooses how many signatures to
// submit: a flat write fee would buy up to 255 ECRECOVERs — about 765k gas of
// real work — for 60k, and every one of them runs before any authorisation
// check, so anyone could spend it.
const (
GasRegisterChain uint64 = 60_000 // 3 SSTOREs to fresh slots
GasUnregisterChain uint64 = 30_000 // 3 SSTOREs to zero + bookkeeping
GasSetGateway uint64 = 22_000 // 1 SSTORE
GasGetCount uint64 = 700 // 1 SLOAD
GasGetChain uint64 = 2_400 // 3 SLOADs
GasGetNonce uint64 = 700 // 1 SLOAD
// GasPerSignature matches the EVM's ECRECOVER price, which is the work each
// submitted signature costs.
GasPerSignature uint64 = 3_000
)
// Errors.
@@ -91,6 +106,7 @@ var (
ErrInvalidSignatureBytes = errors.New("registrar: invalid signature bytes")
ErrDuplicateSigner = errors.New("registrar: duplicate signer in signature set")
ErrChainNameTooLong = errors.New("registrar: chain name exceeds max length")
ErrTooManyOperators = errors.New("registrar: operator set exceeds maximum")
)
// ABI arg sizes for RegisterChain.
@@ -101,6 +117,12 @@ const (
argAddressSize = 20 // common.Address
argSigLen = 65 // r(32) || s(32) || v(1)
maxChainNameSize = 32
// maxOperators bounds the governance set. loadGovernance allocates from a
// count it reads out of storage, and genesis can pre-populate any slot at
// any address, so an unbounded count is an allocation an operator never
// authorised.
maxOperators = 1 << 16
)
// registrarPrecompile is the singleton 0x0446 stateful contract.
@@ -138,6 +160,8 @@ func (p *registrarPrecompile) Run(
return p.runGetCount(state, suppliedGas)
case SelectorGetChain:
return p.runGetChain(state, args, suppliedGas)
case SelectorGetNonce:
return p.runGetNonce(state, suppliedGas)
default:
return nil, suppliedGas, ErrRegistrarUnknownSelector
}
@@ -168,6 +192,10 @@ func (p *registrarPrecompile) runRegisterChain(
return nil, remaining, err
}
remaining, err = chargeSignatures(remaining, sigs)
if err != nil {
return nil, 0, err
}
if err := verifyMultisig(state, sel, payload, sigs); err != nil {
return nil, remaining, err
}
@@ -187,6 +215,7 @@ func (p *registrarPrecompile) runRegisterChain(
binary.BigEndian.PutUint64(cnt[24:], idx+1)
state.SetState(BridgeGatewayCanonicalAddress, slotCount(), cnt)
bumpNonce(state)
return encodeU64(idx), remaining, nil
}
@@ -217,6 +246,10 @@ func (p *registrarPrecompile) runUnregisterChain(
return nil, remaining, err
}
remaining, err = chargeSignatures(remaining, sigs)
if err != nil {
return nil, 0, err
}
if err := verifyMultisig(state, sel, payload, sigs); err != nil {
return nil, remaining, err
}
@@ -251,6 +284,7 @@ func (p *registrarPrecompile) runUnregisterChain(
binary.BigEndian.PutUint64(cnt[24:], last)
state.SetState(BridgeGatewayCanonicalAddress, slotCount(), cnt)
bumpNonce(state)
return encodeU64(idx), remaining, nil
}
@@ -277,6 +311,10 @@ func (p *registrarPrecompile) runSetGateway(
return nil, remaining, err
}
remaining, err = chargeSignatures(remaining, sigs)
if err != nil {
return nil, 0, err
}
if err := verifyMultisig(state, sel, payload, sigs); err != nil {
return nil, remaining, err
}
@@ -294,6 +332,7 @@ func (p *registrarPrecompile) runSetGateway(
copy(hdr[5:25], gw.Bytes())
state.SetState(BridgeGatewayCanonicalAddress, hdrKey, hdr)
bumpNonce(state)
return encodeU64(idx), remaining, nil
}
@@ -338,6 +377,26 @@ func (p *registrarPrecompile) runGetChain(
return encodeChain(c), remaining, nil
}
// runGetNonce — selector 0x06. The value an off-chain signer must bind into the
// next authorisation. Read-only, unauthenticated, one SLOAD.
func (p *registrarPrecompile) runGetNonce(
state contract.StateDB,
suppliedGas uint64,
) ([]byte, uint64, error) {
remaining, err := contract.DeductGas(suppliedGas, GasGetNonce)
if err != nil {
return nil, 0, err
}
return encodeU64(governanceNonce(state)), remaining, nil
}
// chargeSignatures bills the ECRECOVER work the caller asked for. Every
// signature is recovered before any of them is known to belong to an operator,
// so an unauthorised caller can spend this and must pay for it.
func chargeSignatures(remaining uint64, sigs [][]byte) (uint64, error) {
return contract.DeductGas(remaining, uint64(len(sigs))*GasPerSignature)
}
// --- Argument decoders -------------------------------------------------------
// decodeRegisterChainArgs returns the parsed payload and the signed-payload
@@ -434,9 +493,9 @@ func decodeSigs(b []byte) ([][]byte, error) {
// --- Multisig verification ---------------------------------------------------
// verifyMultisig checks that at least Threshold operators of the canonical
// governance set signed keccak256(DomainSeparator || sel || payload).
// governance set signed the digest for the CURRENT governance nonce.
func verifyMultisig(state contract.StateDB, sel byte, payload []byte, sigs [][]byte) error {
digest := registrarDigest(sel, payload)
digest := registrarDigest(sel, governanceNonce(state), payload)
operators, threshold, err := loadGovernance(state)
if err != nil {
return err
@@ -480,10 +539,14 @@ func verifyMultisig(state contract.StateDB, sel byte, payload []byte, sigs [][]b
// registrarDigest computes the canonical signing digest. The off-chain signer
// MUST produce the same bytes — keep this function and its callers in lock-step.
func registrarDigest(sel byte, payload []byte) []byte {
buf := make([]byte, 0, len(DomainSeparator)+1+len(payload))
// The nonce makes each authorisation single-use; read it with selector 0x06.
func registrarDigest(sel byte, nonce uint64, payload []byte) []byte {
buf := make([]byte, 0, len(DomainSeparator)+1+8+len(payload))
buf = append(buf, []byte(DomainSeparator)...)
buf = append(buf, sel)
var n [8]byte
binary.BigEndian.PutUint64(n[:], nonce)
buf = append(buf, n[:]...)
buf = append(buf, payload...)
return crypto.Keccak256(buf)
}
@@ -555,6 +618,37 @@ func govSlot(offset uint64) common.Hash {
return h
}
// nonceBase namespaces the governance nonce. It is deliberately NOT a govSlot
// offset: SeedGovernance rewrites the govSlot header and operator rows, and a
// nonce that reset on re-seed would revive every superseded authorisation.
var nonceBase = [24]byte{
// fixed ASCII prefix "lux.bridge.nonce" left-padded with zero bytes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
'l', 'u', 'x', '.', 'b', 'r', 'i', 'd', 'g', 'e', '.', 'n', 'o', 'n', 'c', 'e',
}
func nonceSlot() common.Hash {
var h common.Hash
copy(h[:24], nonceBase[:])
return h
}
// governanceNonce reads the authorisation counter. Zero on a fresh registrar.
func governanceNonce(state contract.StateDB) uint64 {
h := state.GetState(ContractRegistrarAddress, nonceSlot())
return binary.BigEndian.Uint64(h[24:])
}
// bumpNonce advances the counter after a write lands, retiring the signature
// set that authorised it. Called only on success: a write refused after
// verification (an already-registered chain, say) leaves the authorisation
// usable so operators need not re-sign.
func bumpNonce(state contract.StateDB) {
var h common.Hash
binary.BigEndian.PutUint64(h[24:], governanceNonce(state)+1)
state.SetState(ContractRegistrarAddress, nonceSlot(), h)
}
// SeedGovernance writes the operator set and threshold to the registrar's
// state. Idempotent: re-seeding the same configuration is a no-op. Different
// configuration overwrites — this is intended to be called once at deployment
@@ -563,8 +657,8 @@ func SeedGovernance(state contract.StateDB, operators []common.Address, threshol
if threshold == 0 || int(threshold) > len(operators) {
return fmt.Errorf("registrar: invalid threshold %d for %d operators", threshold, len(operators))
}
if len(operators) > 1<<16 {
return fmt.Errorf("registrar: too many operators: %d", len(operators))
if len(operators) > maxOperators {
return fmt.Errorf("%w: %d", ErrTooManyOperators, len(operators))
}
var header common.Hash
@@ -588,6 +682,16 @@ func loadGovernance(state contract.StateDB) ([]common.Address, uint32, error) {
if count == 0 || threshold == 0 {
return nil, 0, ErrUnauthorized
}
// count comes from storage, and genesis alloc can pre-populate any slot at
// any address. Allocating on an unvalidated 32-bit count would let a
// malformed genesis ask for 4 billion addresses — 80 GB — at boot.
if count > maxOperators {
return nil, 0, ErrTooManyOperators
}
// A threshold nobody can reach is a bricked registrar, not an open one.
if threshold > count {
return nil, 0, ErrUnauthorized
}
ops := make([]common.Address, count)
for i := uint32(0); i < count; i++ {
slot := state.GetState(ContractRegistrarAddress, govSlot(uint64(i+1)))
+783
View File
@@ -0,0 +1,783 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
package bridge
import (
"encoding/binary"
"testing"
"github.com/luxfi/crypto"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/modules"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// Replay
// ---------------------------------------------------------------------------
// TestRegistrar_AuthorisationIsSingleUse is the regression test for the replay
// hole. The signed digest binds the registrar's governance nonce, which advances
// on every successful write, so a signature set spends exactly once.
//
// Without the nonce, the operators' signatures over "register chain 5" stayed
// valid forever: anyone who saw the original transaction could re-apply it after
// an unregister, resurrecting a chain the operators had deliberately removed.
func TestRegistrar_AuthorisationIsSingleUse(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t), newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 2))
id := ChainID(5)
payload := buildRegisterPayload(id, "five", true, common.Address{})
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
body := appendSigs(payload, sigs)
// First use lands.
_, _, err := runRegistrar(t, state, SelectorRegisterChain, body, 1_000_000)
require.NoError(t, err)
// Operators remove the chain, with a fresh authorisation at the new nonce.
unreg := buildUnregisterPayload(id)
usigs := signByAll(t, state, ops, SelectorUnregisterChain, unreg)
_, _, err = runRegistrar(t, state, SelectorUnregisterChain, appendSigs(unreg, usigs), 1_000_000)
require.NoError(t, err)
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
_, ok := r.Get(id)
require.False(t, ok)
// Replaying the ORIGINAL registration bytes must not resurrect it.
_, _, err = runRegistrar(t, state, SelectorRegisterChain, body, 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized, "a spent authorisation was accepted again")
_, ok = r.Get(id)
require.False(t, ok, "the replayed authorisation re-registered the chain")
}
func TestRegistrar_SupersededSetGatewayCannotBeReapplied(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
id := ChainID(96369)
oldGW := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
newGW := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
reg := buildRegisterPayload(id, "lux", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(reg, signByAll(t, state, ops, SelectorRegisterChain, reg)), 1_000_000)
require.NoError(t, err)
// Point the gateway at oldGW, then at newGW.
p1 := buildSetGatewayPayload(id, oldGW)
b1 := appendSigs(p1, signByAll(t, state, ops, SelectorSetGateway, p1))
_, _, err = runRegistrar(t, state, SelectorSetGateway, b1, 1_000_000)
require.NoError(t, err)
p2 := buildSetGatewayPayload(id, newGW)
b2 := appendSigs(p2, signByAll(t, state, ops, SelectorSetGateway, p2))
_, _, err = runRegistrar(t, state, SelectorSetGateway, b2, 1_000_000)
require.NoError(t, err)
// Replaying the first, superseded authorisation must not roll the gateway
// back to an address the operators have moved away from.
_, _, err = runRegistrar(t, state, SelectorSetGateway, b1, 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
c, ok := r.Get(id)
require.True(t, ok)
require.Equal(t, newGW, c.GatewayAt, "a replayed authorisation rolled the gateway back")
}
func TestRegistrar_NonceAdvancesOnlyOnSuccess(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
require.Equal(t, uint64(0), governanceNonce(state.db))
reg := buildRegisterPayload(1, "one", true, common.Address{})
body := appendSigs(reg, signByAll(t, state, ops, SelectorRegisterChain, reg))
_, _, err := runRegistrar(t, state, SelectorRegisterChain, body, 1_000_000)
require.NoError(t, err)
require.Equal(t, uint64(1), governanceNonce(state.db))
// A write that is authorised but refused by a business rule must NOT burn
// the nonce, or a single already-registered chain would invalidate every
// signature the operators had prepared.
again := buildRegisterPayload(1, "one", true, common.Address{})
againBody := appendSigs(again, signByAll(t, state, ops, SelectorRegisterChain, again))
_, _, err = runRegistrar(t, state, SelectorRegisterChain, againBody, 1_000_000)
require.ErrorIs(t, err, ErrAlreadyRegistered)
require.Equal(t, uint64(1), governanceNonce(state.db))
// An unauthorised attempt must not advance it either — otherwise anyone
// could invalidate the operators' pending authorisations for free.
stranger := newOperator(t)
bad := buildRegisterPayload(2, "two", true, common.Address{})
badBody := appendSigs(bad, signByAll(t, state, []*operator{stranger}, SelectorRegisterChain, bad))
_, _, err = runRegistrar(t, state, SelectorRegisterChain, badBody, 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
require.Equal(t, uint64(1), governanceNonce(state.db))
}
func TestRegistrar_GetNonceReportsWhatSignersMustBind(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
read := func() uint64 {
ret, gasLeft, err := runRegistrar(t, state, SelectorGetNonce, nil, 100_000)
require.NoError(t, err)
require.Equal(t, uint64(100_000-GasGetNonce), gasLeft)
return binary.BigEndian.Uint64(ret[24:32])
}
require.Equal(t, uint64(0), read())
reg := buildRegisterPayload(1, "one", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(reg, signByAll(t, state, ops, SelectorRegisterChain, reg)), 1_000_000)
require.NoError(t, err)
require.Equal(t, uint64(1), read())
require.Equal(t, governanceNonce(state.db), read(),
"the selector and the internal reader must agree, or signers bind the wrong value")
}
func TestRegistrar_ReSeedDoesNotResetTheNonce(t *testing.T) {
// SeedGovernance rewrites the operator header and rows. If the nonce lived
// in that header, re-seeding would revive every authorisation the operators
// had already spent.
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
reg := buildRegisterPayload(1, "one", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(reg, signByAll(t, state, ops, SelectorRegisterChain, reg)), 1_000_000)
require.NoError(t, err)
require.Equal(t, uint64(1), governanceNonce(state.db))
rotated := []*operator{newOperator(t), newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(rotated), 2))
require.Equal(t, uint64(1), governanceNonce(state.db), "re-seeding reset the nonce")
}
func TestRegistrar_NonceSlotDoesNotCollideWithGovernanceSlots(t *testing.T) {
// The nonce is namespaced separately from govSlot. A collision would make
// an operator address readable as a nonce, or vice versa.
n := nonceSlot()
require.NotEqual(t, n, govSlot(0))
for i := uint64(1); i <= 1024; i++ {
require.NotEqual(t, n, govSlot(i), "nonce slot collides with operator slot %d", i)
}
// And with the registry rows, which live at a different address anyway.
require.NotEqual(t, n, slotCount())
}
// ---------------------------------------------------------------------------
// Signature-count gas
// ---------------------------------------------------------------------------
// TestRegistrar_SignaturesArePricedPerRecovery is the regression test for the
// flat-fee-over-unbounded-input hole. decodeSigs accepts up to 255 signatures
// and verifyMultisig recovers EVERY one of them before it knows whether any
// belongs to an operator, so the work is unauthenticated and caller-chosen.
// A flat 60,000 bought roughly 765,000 gas of ECRECOVER.
func TestRegistrar_SignaturesArePricedPerRecovery(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildRegisterPayload(42, "x", true, common.Address{})
cost := func(n int) uint64 {
junk := make([][]byte, n)
for i := range junk {
junk[i] = make([]byte, argSigLen)
junk[i][0] = byte(i + 1) // distinct, still not an operator
}
_, remaining, _ := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(payload, junk), 10_000_000)
return 10_000_000 - remaining
}
one := cost(1)
ten := cost(10)
require.Equal(t, GasPerSignature*9, ten-one,
"nine extra signatures must cost nine extra recoveries")
// The maximum the wire format allows must be priced, not free.
require.Equal(t, GasRegisterChain+255*GasPerSignature, cost(255))
}
func TestRegistrar_SignatureFloodRunsOutOfGasRatherThanRunningFree(t *testing.T) {
// With the flat fee, 255 signatures fit inside 60,001 gas on every write
// selector. Now they cannot: the call must run out of gas before performing
// the recoveries, on all three.
junk := make([][]byte, 255)
for i := range junk {
junk[i] = make([]byte, argSigLen)
}
for _, tc := range []struct {
name string
sel byte
payload []byte
base uint64
}{
{"register", SelectorRegisterChain, buildRegisterPayload(42, "x", true, common.Address{}), GasRegisterChain},
{"unregister", SelectorUnregisterChain, buildUnregisterPayload(42), GasUnregisterChain},
{"setGateway", SelectorSetGateway, buildSetGatewayPayload(42, common.Address{}), GasSetGateway},
} {
t.Run(tc.name, func(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
_, remaining, err := runRegistrar(t, state, tc.sel,
appendSigs(tc.payload, junk), tc.base+100)
require.ErrorIs(t, err, contract.ErrOutOfGas)
require.Zero(t, remaining)
require.Equal(t, uint64(0), governanceNonce(state.db))
})
}
}
func TestRegistrar_EveryWriteSelectorPricesItsSignatures(t *testing.T) {
for _, tc := range []struct {
name string
sel byte
payload []byte
base uint64
}{
{"register", SelectorRegisterChain, buildRegisterPayload(1, "a", true, common.Address{}), GasRegisterChain},
{"unregister", SelectorUnregisterChain, buildUnregisterPayload(1), GasUnregisterChain},
{"setGateway", SelectorSetGateway, buildSetGatewayPayload(1, common.Address{}), GasSetGateway},
} {
t.Run(tc.name, func(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
junk := make([][]byte, 8)
for i := range junk {
junk[i] = make([]byte, argSigLen)
}
_, remaining, _ := runRegistrar(t, state, tc.sel, appendSigs(tc.payload, junk), 1_000_000)
require.Equal(t, uint64(1_000_000)-tc.base-8*GasPerSignature, remaining)
})
}
}
func TestRegistrar_ReadSelectorsAreNotChargedForSignatures(t *testing.T) {
// Reads take no signatures, so their cost must stay flat.
state := newRegAS()
_, remaining, err := runRegistrar(t, state, SelectorGetCount, nil, 100_000)
require.NoError(t, err)
require.Equal(t, uint64(100_000-GasGetCount), remaining)
idx := make([]byte, 8)
_, _, err = runRegistrar(t, state, SelectorGetChain, idx, 100_000)
require.ErrorIs(t, err, ErrIndexOutOfRange)
}
// ---------------------------------------------------------------------------
// Governance loading
// ---------------------------------------------------------------------------
// TestLoadGovernance_RefusesAnAbsurdOperatorCount — count is read from storage,
// and genesis alloc can pre-populate any slot at any address. Allocating on an
// unvalidated uint32 would ask for 4 billion addresses at boot.
func TestLoadGovernance_RefusesAnAbsurdOperatorCount(t *testing.T) {
db := newRegState()
var header common.Hash
binary.BigEndian.PutUint32(header[24:28], 0xFFFFFFFF) // count
binary.BigEndian.PutUint32(header[28:32], 1) // threshold
db.SetState(ContractRegistrarAddress, govSlot(0), header)
_, _, err := loadGovernance(db)
require.ErrorIs(t, err, ErrTooManyOperators)
}
func TestLoadGovernance_AcceptsTheLargestPermittedSet(t *testing.T) {
// The bound must not reject a legal set: exactly maxOperators is allowed.
db := newRegState()
var header common.Hash
binary.BigEndian.PutUint32(header[24:28], uint32(maxOperators))
binary.BigEndian.PutUint32(header[28:32], 1)
db.SetState(ContractRegistrarAddress, govSlot(0), header)
ops, threshold, err := loadGovernance(db)
require.NoError(t, err)
require.Len(t, ops, maxOperators)
require.Equal(t, uint32(1), threshold)
}
func TestLoadGovernance_RefusesAnUnreachableThreshold(t *testing.T) {
// threshold > count can never be met, so every write reverts. Refusing it
// at load names the misconfiguration instead of reporting "threshold not
// met" forever.
db := newRegState()
var header common.Hash
binary.BigEndian.PutUint32(header[24:28], 2) // count
binary.BigEndian.PutUint32(header[28:32], 3) // threshold
db.SetState(ContractRegistrarAddress, govSlot(0), header)
_, _, err := loadGovernance(db)
require.ErrorIs(t, err, ErrUnauthorized)
}
func TestSeedGovernance_RefusesBadConfigurations(t *testing.T) {
db := newRegState()
a := common.HexToAddress("0x1111111111111111111111111111111111111111")
require.Error(t, SeedGovernance(db, []common.Address{a}, 0), "threshold 0 authorises nothing")
require.Error(t, SeedGovernance(db, []common.Address{a}, 2), "threshold above the set size is unreachable")
require.Error(t, SeedGovernance(db, nil, 1), "no operators means no governance")
// Nothing was written by a refused seed.
_, _, err := loadGovernance(db)
require.ErrorIs(t, err, ErrUnauthorized)
require.NoError(t, SeedGovernance(db, []common.Address{a}, 1))
ops, threshold, err := loadGovernance(db)
require.NoError(t, err)
require.Equal(t, []common.Address{a}, ops)
require.Equal(t, uint32(1), threshold)
}
func TestSeedGovernance_RefusesTooManyOperators(t *testing.T) {
db := newRegState()
tooMany := make([]common.Address, maxOperators+1)
require.ErrorIs(t, SeedGovernance(db, tooMany, 1), ErrTooManyOperators)
}
// ---------------------------------------------------------------------------
// Signature semantics
// ---------------------------------------------------------------------------
func TestRegistrar_MalleatedSignatureCountsAsTheSameSigner(t *testing.T) {
// secp256k1 signatures are malleable: (r, s, v) and (r, n-s, v^1) recover
// the same address. Dedup is by recovered address, not by signature bytes,
// so a malleated copy cannot be passed off as a second operator.
state := newRegAS()
ops := []*operator{newOperator(t), newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 2))
payload := buildRegisterPayload(42, "x", true, common.Address{})
digest := registrarDigest(SelectorRegisterChain, governanceNonce(state.db), payload)
sig, err := crypto.Sign(digest, ops[0].key)
require.NoError(t, err)
// A byte-different signature from the SAME key: re-signing is
// deterministic here, so flip the recovery id's partner instead — submit
// the same signer twice and require the duplicate to be caught by address.
_, _, err = runRegistrar(t, state, SelectorRegisterChain,
appendSigs(payload, [][]byte{sig, sig}), 1_000_000)
require.ErrorIs(t, err, ErrDuplicateSigner,
"one operator must not be able to meet a threshold of two")
}
func TestRegistrar_OneBadSignatureFailsTheWholeSet(t *testing.T) {
// Fail closed: an unrecoverable signature aborts rather than being skipped,
// so a caller cannot pad a set with garbage to hide which operators signed.
state := newRegAS()
ops := []*operator{newOperator(t), newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 2))
payload := buildRegisterPayload(42, "x", true, common.Address{})
good := signByAll(t, state, ops, SelectorRegisterChain, payload)
bad := make([]byte, argSigLen)
for i := range bad {
bad[i] = 0xff // invalid r/s, unrecoverable
}
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(payload, [][]byte{bad, good[0], good[1]}), 1_000_000)
require.ErrorIs(t, err, ErrInvalidSignatureBytes)
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
_, ok := r.Get(42)
require.False(t, ok, "a set containing an invalid signature must write nothing")
}
func TestRegistrar_ZeroSignaturesIsRefused(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildRegisterPayload(42, "x", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(payload, nil), 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
}
func TestRegistrar_ExtraSignaturesBeyondThresholdAreHarmless(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t), newOperator(t), newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 2))
payload := buildRegisterPayload(42, "x", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(payload, signByAll(t, state, ops, SelectorRegisterChain, payload)), 1_000_000)
require.NoError(t, err)
}
func TestRegistrar_SignatureOverADifferentPayloadIsRefused(t *testing.T) {
// The digest covers the arguments, so changing any of them after signing
// must invalidate the authorisation. Otherwise operators would be signing
// "register something" rather than "register THIS".
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
signed := buildRegisterPayload(1, "one", true, common.HexToAddress("0xaa"))
sigs := signByAll(t, state, ops, SelectorRegisterChain, signed)
for name, tampered := range map[string][]byte{
"different id": buildRegisterPayload(2, "one", true, common.HexToAddress("0xaa")),
"different name": buildRegisterPayload(1, "two", true, common.HexToAddress("0xaa")),
"different evmFlag": buildRegisterPayload(1, "one", false, common.HexToAddress("0xaa")),
"different gateway": buildRegisterPayload(1, "one", true, common.HexToAddress("0xbb")),
} {
t.Run(name, func(t *testing.T) {
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(tampered, sigs), 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
})
}
}
// ---------------------------------------------------------------------------
// Wire-format refusals
// ---------------------------------------------------------------------------
func TestRegistrar_DecodersRefuseTruncatedInput(t *testing.T) {
// Each decoder slices fixed offsets. One byte short must be a clean refusal
// at every boundary, never a slice panic.
state := newRegAS()
full := map[byte][]byte{
SelectorRegisterChain: appendSigs(buildRegisterPayload(1, "abc", true, common.HexToAddress("0xaa")), nil),
SelectorUnregisterChain: appendSigs(buildUnregisterPayload(1), nil),
SelectorSetGateway: appendSigs(buildSetGatewayPayload(1, common.HexToAddress("0xaa")), nil),
}
for sel, body := range full {
for n := 0; n < len(body); n++ {
_, _, err := runRegistrar(t, state, sel, body[:n], 1_000_000)
require.Error(t, err, "selector %#x truncated to %d bytes was accepted", sel, n)
}
}
}
func TestDecodeSigs_RefusesAShortSignatureArray(t *testing.T) {
// The count byte is attacker-controlled. Claiming more signatures than the
// bytes carry must refuse rather than slice past the end.
_, err := decodeSigs(nil)
require.ErrorIs(t, err, ErrRegistrarInputTooShort)
_, err = decodeSigs([]byte{1})
require.ErrorIs(t, err, ErrRegistrarInputTooShort)
_, err = decodeSigs(append([]byte{2}, make([]byte, argSigLen)...))
require.ErrorIs(t, err, ErrRegistrarInputTooShort)
_, err = decodeSigs(append([]byte{255}, make([]byte, 254*argSigLen)...))
require.ErrorIs(t, err, ErrRegistrarInputTooShort)
// Exactly enough bytes is accepted, and each slice is the right length.
sigs, err := decodeSigs(append([]byte{3}, make([]byte, 3*argSigLen)...))
require.NoError(t, err)
require.Len(t, sigs, 3)
for _, s := range sigs {
require.Len(t, s, argSigLen)
}
// Zero signatures is a well-formed empty set.
sigs, err = decodeSigs([]byte{0})
require.NoError(t, err)
require.Empty(t, sigs)
}
func TestDecodeRegisterChainArgs_NameLengthIsBounded(t *testing.T) {
// name_len is one attacker-chosen byte read before any bounds check on the
// name itself.
for _, nameLen := range []int{maxChainNameSize + 1, 200, 255} {
args := make([]byte, 5)
args[4] = byte(nameLen)
_, _, _, _, _, _, err := decodeRegisterChainArgs(args)
require.ErrorIs(t, err, ErrChainNameTooLong, "name_len %d", nameLen)
}
// Exactly the maximum is accepted when the bytes are there.
name := make([]byte, maxChainNameSize)
for i := range name {
name[i] = 'a'
}
args := append([]byte{0, 0, 0, 1, byte(maxChainNameSize)}, name...)
args = append(args, 0x01)
args = append(args, make([]byte, argAddressSize)...)
args = append(args, 0x00) // zero signatures
_, gotName, _, _, _, _, err := decodeRegisterChainArgs(args)
require.NoError(t, err)
require.Equal(t, string(name), gotName)
}
func TestEncodeChain_TruncatesAnOverlongName(t *testing.T) {
// Names are validated at every entry point, so this is belt-and-braces —
// but the reply's length prefix must always match the bytes that follow it,
// or a caller decodes into the gateway address.
long := make([]byte, 40)
for i := range long {
long[i] = 'z'
}
out := encodeChain(Chain{ID: 9, Name: string(long), EVM: true, GatewayAt: common.HexToAddress("0xaa")})
nameLen := int(out[4])
require.Equal(t, maxChainNameSize, nameLen)
require.Len(t, out, 4+1+nameLen+1+argAddressSize)
require.Equal(t, byte(0x01), out[5+nameLen])
}
func TestRegistrar_GetChainRejectsAnOutOfRangeIndexWithoutReading(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
reg := buildRegisterPayload(1, "one", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(reg, signByAll(t, state, ops, SelectorRegisterChain, reg)), 1_000_000)
require.NoError(t, err)
// index == count is one past the end.
idx := make([]byte, 8)
binary.BigEndian.PutUint64(idx, 1)
_, _, err = runRegistrar(t, state, SelectorGetChain, idx, 100_000)
require.ErrorIs(t, err, ErrIndexOutOfRange)
// The last valid index still reads.
binary.BigEndian.PutUint64(idx, 0)
ret, _, err := runRegistrar(t, state, SelectorGetChain, idx, 100_000)
require.NoError(t, err)
require.Equal(t, uint32(1), binary.BigEndian.Uint32(ret[0:4]))
// A maximal index must not wrap into range.
binary.BigEndian.PutUint64(idx, ^uint64(0))
_, _, err = runRegistrar(t, state, SelectorGetChain, idx, 100_000)
require.ErrorIs(t, err, ErrIndexOutOfRange)
}
// ---------------------------------------------------------------------------
// readOnly
// ---------------------------------------------------------------------------
func TestRegistrar_EveryWriterRefusesReadOnly(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
for name, tc := range map[string]struct {
sel byte
payload []byte
}{
"register": {SelectorRegisterChain, buildRegisterPayload(1, "a", true, common.Address{})},
"unregister": {SelectorUnregisterChain, buildUnregisterPayload(1)},
"setGateway": {SelectorSetGateway, buildSetGatewayPayload(1, common.Address{})},
} {
t.Run(name, func(t *testing.T) {
sigs := signByAll(t, state, ops, tc.sel, tc.payload)
input := append([]byte{tc.sel}, appendSigs(tc.payload, sigs)...)
_, remaining, err := RegistrarPrecompile.Run(
state, common.Address{}, ContractRegistrarAddress, input, 1_000_000, true)
require.ErrorIs(t, err, ErrRegistrarReadOnly)
require.Equal(t, uint64(1_000_000), remaining)
require.Equal(t, uint64(0), governanceNonce(state.db),
"a read-only refusal must not have advanced the nonce")
})
}
}
func TestRegistrar_ReadersWorkInReadOnly(t *testing.T) {
state := newRegAS()
for _, sel := range []byte{SelectorGetCount, SelectorGetNonce} {
_, _, err := RegistrarPrecompile.Run(
state, common.Address{}, ContractRegistrarAddress, []byte{sel}, 100_000, true)
require.NoError(t, err, "selector %#x must be callable from a static frame", sel)
}
}
// ---------------------------------------------------------------------------
// Compaction invariants
// ---------------------------------------------------------------------------
func TestRegistrar_UnregisterKeepsIndicesDense(t *testing.T) {
// The slot scheme addresses rows by contiguous index, so a hole would make
// GetChain return a zeroed row for a live chain.
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
ids := []ChainID{10, 20, 30, 40, 50}
for _, id := range ids {
p := buildRegisterPayload(id, "c", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(p, signByAll(t, state, ops, SelectorRegisterChain, p)), 1_000_000)
require.NoError(t, err)
}
// Remove from the front, the middle, and the end in turn.
for _, remove := range []ChainID{10, 40, 30} {
p := buildUnregisterPayload(remove)
_, _, err := runRegistrar(t, state, SelectorUnregisterChain,
appendSigs(p, signByAll(t, state, ops, SelectorUnregisterChain, p)), 1_000_000)
require.NoError(t, err)
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
all := r.All()
seen := map[ChainID]bool{}
for i, c := range all {
require.NotZero(t, c.ID, "row %d is a hole after removing %d", i, remove)
require.False(t, seen[c.ID], "row %d duplicates chain %d", i, c.ID)
seen[c.ID] = true
}
_, ok := r.Get(remove)
require.False(t, ok)
}
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
require.Len(t, r.All(), 2)
for _, id := range []ChainID{20, 50} {
_, ok := r.Get(id)
require.True(t, ok, "chain %d was lost by compaction", id)
}
}
func TestRegistrar_UnregisterTheOnlyRow(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
p := buildRegisterPayload(7, "seven", true, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(p, signByAll(t, state, ops, SelectorRegisterChain, p)), 1_000_000)
require.NoError(t, err)
u := buildUnregisterPayload(7)
_, _, err = runRegistrar(t, state, SelectorUnregisterChain,
appendSigs(u, signByAll(t, state, ops, SelectorUnregisterChain, u)), 1_000_000)
require.NoError(t, err)
ret, _, err := runRegistrar(t, state, SelectorGetCount, nil, 100_000)
require.NoError(t, err)
require.Equal(t, uint64(0), binary.BigEndian.Uint64(ret[24:32]))
// Re-registering the same id after removal works: it is a fresh row.
p2 := buildRegisterPayload(7, "seven", true, common.Address{})
_, _, err = runRegistrar(t, state, SelectorRegisterChain,
appendSigs(p2, signByAll(t, state, ops, SelectorRegisterChain, p2)), 1_000_000)
require.NoError(t, err)
}
// ---------------------------------------------------------------------------
// Row packing
// ---------------------------------------------------------------------------
func TestPackRowRoundTrip(t *testing.T) {
for _, c := range []Chain{
{ID: 0, Name: "", EVM: false},
{ID: 1, Name: "a", EVM: true, GatewayAt: common.HexToAddress("0xaa")},
{ID: ChainID(0xFFFFFFFF), Name: "max", EVM: true},
{ID: 96369, Name: "0123456789abcdef0123456789abcdef", EVM: false,
GatewayAt: common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff")},
} {
hdr, name := packRow(c)
got := unpackRow(hdr, name)
require.Equal(t, c.ID, got.ID)
require.Equal(t, c.Name, got.Name)
require.Equal(t, c.EVM, got.EVM)
require.Equal(t, c.GatewayAt, got.GatewayAt)
}
}
func TestPackRowFieldsDoNotOverlap(t *testing.T) {
// The header packs id, evm flag and gateway into one word. Overlapping
// ranges would make setting a gateway corrupt the chain id.
full := Chain{
ID: ChainID(0xFFFFFFFF),
EVM: true,
GatewayAt: common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"),
}
hdr, _ := packRow(full)
got := unpackRow(hdr, common.Hash{})
require.Equal(t, full.ID, got.ID)
require.True(t, got.EVM)
require.Equal(t, full.GatewayAt, got.GatewayAt)
// Bytes 25..31 are reserved and must stay zero.
for i := 25; i < 32; i++ {
require.Zero(t, hdr[i], "reserved byte %d was written", i)
}
}
func TestTrimRightZeroKeepsInteriorZeros(t *testing.T) {
// Only trailing padding is stripped. Trimming an interior zero would rename
// a chain.
require.Equal(t, "", trimRightZero(make([]byte, 32)))
require.Equal(t, "ab", trimRightZero(append([]byte("ab"), make([]byte, 30)...)))
require.Equal(t, "a\x00b", trimRightZero(append([]byte("a\x00b"), make([]byte, 29)...)))
}
// ---------------------------------------------------------------------------
// Module wiring
// ---------------------------------------------------------------------------
func TestRegistrar_ModuleAddressIsReserved(t *testing.T) {
require.True(t, modules.ReservedAddress(ContractRegistrarAddress))
m, ok := modules.GetPrecompileModule(ConfigKey)
require.True(t, ok)
require.Equal(t, ContractRegistrarAddress, m.Address)
require.False(t, m.AlwaysOn)
}
func TestRegistrar_ConfigureRejectsBadOperators(t *testing.T) {
c := &configurator{}
cfg := c.MakeConfig().(*Config)
cfg.Operators = []string{"nonsense"}
cfg.Threshold = 1
require.Error(t, c.Configure(&regChainCfg{}, cfg, newRegState(), &regBlockCtx{}))
}
func TestRegistrar_ConfigureIgnoresAForeignConfig(t *testing.T) {
// A config of the wrong concrete type must be a no-op, not a panic.
require.NoError(t, (&configurator{}).Configure(&regChainCfg{}, nil, newRegState(), &regBlockCtx{}))
}
func TestParseAddress(t *testing.T) {
want := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
for _, in := range []string{
"0x1234567890abcdef1234567890abcdef12345678",
"1234567890abcdef1234567890abcdef12345678",
" 0x1234567890abcdef1234567890abcdef12345678 ",
} {
got, err := parseAddress(in)
require.NoError(t, err, "%q", in)
require.Equal(t, want, got)
}
for _, bad := range []string{"", "0x", "0x1234", "0x1234567890abcdef1234567890abcdef1234567890"} {
_, err := parseAddress(bad)
require.Error(t, err, "%q must be refused", bad)
}
}
+394
View File
@@ -0,0 +1,394 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
package bridge
import (
"encoding/binary"
"math/big"
"testing"
"github.com/luxfi/crypto"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/precompileconfig"
"github.com/stretchr/testify/require"
)
// secp256k1N is the curve order; s and n-s are the two halves of a malleable
// signature pair.
var secp256k1N, _ = new(big.Int).SetString(
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16)
// malleate flips a signature to its equally-valid twin: (r, s, v) → (r, n-s, v^1).
func malleate(sig []byte) []byte {
out := make([]byte, len(sig))
copy(out, sig)
s := new(big.Int).SetBytes(sig[32:64])
flipped := new(big.Int).Sub(secp256k1N, s)
copy(out[32:64], common.LeftPadBytes(flipped.Bytes(), 32))
out[64] ^= 1
return out
}
// TestRegistrar_MalleatedSignatureCannotForgeASecondSigner is the real
// malleability check: secp256k1 admits two signatures per (key, digest), so a
// caller can always produce a byte-different copy of an operator's signature.
// Deduplication is by RECOVERED ADDRESS, not by signature bytes, so the twin
// must either be rejected outright or counted as the same signer — never as a
// second operator meeting a threshold of two.
func TestRegistrar_MalleatedSignatureCannotForgeASecondSigner(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t), newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 2))
payload := buildRegisterPayload(42, "x", true, common.Address{})
digest := registrarDigest(SelectorRegisterChain, governanceNonce(state.db), payload)
sig, err := crypto.Sign(digest, ops[0].key)
require.NoError(t, err)
twin := malleate(sig)
require.NotEqual(t, sig, twin, "the twin must be byte-different or this test proves nothing")
_, _, err = runRegistrar(t, state, SelectorRegisterChain,
appendSigs(payload, [][]byte{sig, twin}), 1_000_000)
require.Error(t, err, "one operator met a threshold of two using a malleated copy")
require.True(t,
err == ErrDuplicateSigner || err == ErrUnauthorized || err == ErrInvalidSignatureBytes,
"unexpected error for a malleated pair: %v", err)
// Whatever the reason, nothing was written.
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
_, ok := r.Get(42)
require.False(t, ok)
require.Equal(t, uint64(0), governanceNonce(state.db))
}
func TestRegistrar_DedupIsByRecoveredAddressNotSignatureBytes(t *testing.T) {
// Direct check on verifyMultisig: two byte-different signatures that recover
// to one operator must not both count.
db := newRegState()
present := newOperator(t)
absent := newOperator(t)
require.NoError(t, SeedGovernance(db, []common.Address{present.addr, absent.addr}, 2))
payload := []byte{1, 2, 3}
digest := registrarDigest(SelectorRegisterChain, 0, payload)
sig, err := crypto.Sign(digest, present.key)
require.NoError(t, err)
// Only ONE of the two operators signed. Its malleated twin must not stand in
// for the operator who did not.
err = verifyMultisig(db, SelectorRegisterChain, payload, [][]byte{sig, malleate(sig)})
require.Error(t, err, "a single key must never satisfy a threshold of two")
}
// TestVerifyMultisig_RefusesAWrongLengthSignature covers the internal guard that
// Run's decoder can never trip: decodeSigs only ever emits 65-byte slices, so
// this is reachable only by a direct caller — and it must still fail closed
// rather than hand a short buffer to Ecrecover.
func TestVerifyMultisig_RefusesAWrongLengthSignature(t *testing.T) {
db := newRegState()
op := newOperator(t)
require.NoError(t, SeedGovernance(db, []common.Address{op.addr}, 1))
for _, n := range []int{0, 1, argSigLen - 1, argSigLen + 1, 200} {
err := verifyMultisig(db, SelectorRegisterChain, []byte{1}, [][]byte{make([]byte, n)})
require.ErrorIs(t, err, ErrInvalidSignatureBytes, "%d-byte signature", n)
}
}
func TestVerifyMultisig_RefusesWhenGovernanceIsUnseeded(t *testing.T) {
db := newRegState()
op := newOperator(t)
digest := registrarDigest(SelectorRegisterChain, 0, []byte{1})
sig, err := crypto.Sign(digest, op.key)
require.NoError(t, err)
require.ErrorIs(t,
verifyMultisig(db, SelectorRegisterChain, []byte{1}, [][]byte{sig}),
ErrUnauthorized)
}
// ---------------------------------------------------------------------------
// Out of gas on every selector
// ---------------------------------------------------------------------------
func TestRegistrar_EverySelectorRefusesInsufficientGas(t *testing.T) {
cases := []struct {
name string
sel byte
body []byte
fee uint64
}{
{"register", SelectorRegisterChain,
appendSigs(buildRegisterPayload(1, "a", true, common.Address{}), nil), GasRegisterChain},
{"unregister", SelectorUnregisterChain,
appendSigs(buildUnregisterPayload(1), nil), GasUnregisterChain},
{"setGateway", SelectorSetGateway,
appendSigs(buildSetGatewayPayload(1, common.Address{}), nil), GasSetGateway},
{"getCount", SelectorGetCount, nil, GasGetCount},
{"getChain", SelectorGetChain, make([]byte, 8), GasGetChain},
{"getNonce", SelectorGetNonce, nil, GasGetNonce},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
_, remaining, err := runRegistrar(t, state, c.sel, c.body, c.fee-1)
require.ErrorIs(t, err, contract.ErrOutOfGas)
require.Zero(t, remaining, "an out-of-gas refusal must report no gas left")
// Zero gas is the same refusal, never a free call.
_, remaining, err = runRegistrar(t, state, c.sel, c.body, 0)
require.ErrorIs(t, err, contract.ErrOutOfGas)
require.Zero(t, remaining)
// Exactly the fee gets past the gas check.
_, _, err = runRegistrar(t, state, c.sel, c.body, c.fee)
require.NotErrorIs(t, err, contract.ErrOutOfGas)
})
}
}
func TestRegistrar_OutOfGasWritesNothing(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildRegisterPayload(1, "one", true, common.Address{})
body := appendSigs(payload, signByAll(t, state, ops, SelectorRegisterChain, payload))
_, _, err := runRegistrar(t, state, SelectorRegisterChain, body, GasRegisterChain-1)
require.ErrorIs(t, err, contract.ErrOutOfGas)
ret, _, err := runRegistrar(t, state, SelectorGetCount, nil, 100_000)
require.NoError(t, err)
require.Equal(t, uint64(0), binary.BigEndian.Uint64(ret[24:32]))
require.Equal(t, uint64(0), governanceNonce(state.db))
}
// ---------------------------------------------------------------------------
// encodeChain / row shape
// ---------------------------------------------------------------------------
func TestEncodeChain_EncodesTheVirtualFlag(t *testing.T) {
// A virtual (non-EVM) chain must encode 0x00, not "anything falsy". The
// caller reads this byte to decide whether an eth_chainId exists.
virtual := encodeChain(Chain{ID: 900001, Name: "solana", EVM: false,
GatewayAt: common.HexToAddress("0xaa")})
nameLen := int(virtual[4])
require.Equal(t, byte(0x00), virtual[5+nameLen])
native := encodeChain(Chain{ID: 1, Name: "ethereum", EVM: true})
nameLen = int(native[4])
require.Equal(t, byte(0x01), native[5+nameLen])
}
func TestEncodeChain_EmptyName(t *testing.T) {
out := encodeChain(Chain{ID: 3, Name: "", EVM: true, GatewayAt: common.HexToAddress("0xbb")})
require.Equal(t, byte(0), out[4])
require.Len(t, out, 4+1+0+1+argAddressSize)
require.Equal(t, uint32(3), binary.BigEndian.Uint32(out[0:4]))
require.Equal(t, common.HexToAddress("0xbb"), common.BytesToAddress(out[6:26]))
}
func TestRegistrar_GetChainRoundTripsAVirtualChain(t *testing.T) {
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
p := buildRegisterPayload(900002, "bitcoin", false, common.Address{})
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(p, signByAll(t, state, ops, SelectorRegisterChain, p)), 1_000_000)
require.NoError(t, err)
idx := make([]byte, 8)
ret, _, err := runRegistrar(t, state, SelectorGetChain, idx, 100_000)
require.NoError(t, err)
require.Equal(t, uint32(900002), binary.BigEndian.Uint32(ret[0:4]))
nameLen := int(ret[4])
require.Equal(t, "bitcoin", string(ret[5:5+nameLen]))
require.Equal(t, byte(0x00), ret[5+nameLen], "a virtual chain must not report EVM")
}
// ---------------------------------------------------------------------------
// Storage layout
// ---------------------------------------------------------------------------
// TestSlotRow_StrideIsThreeWords pins the on-disk row stride. It is a consensus
// constant: every registered chain's rows move if it changes, so a node built
// with a different stride reads garbage for chains a peer reads correctly. The
// third word is reserved and currently only written (zeroed) on unregister,
// which is why an arithmetic slip here is otherwise invisible.
func TestSlotRow_StrideIsThreeWords(t *testing.T) {
for i := uint64(0); i < 4; i++ {
hdr, name, res := slotRow(i)
require.Equal(t, registrySlot(1+i*3+0), hdr)
require.Equal(t, registrySlot(1+i*3+1), name)
require.Equal(t, registrySlot(1+i*3+2), res)
}
// Row 0 starts immediately after the count word.
hdr, _, _ := slotRow(0)
require.Equal(t, registrySlot(1), hdr)
require.NotEqual(t, slotCount(), hdr)
}
func TestSlotRow_RowsNeverOverlap(t *testing.T) {
// All three words of every row must be distinct from all three words of
// every other row, and from the count word. A stride smaller than the row
// width makes one row's reserved slot the next row's header.
owner := map[common.Hash]string{slotCount(): "count"}
for i := uint64(0); i < 64; i++ {
hdr, name, res := slotRow(i)
for label, s := range map[string]common.Hash{"header": hdr, "name": name, "reserved": res} {
key := label
if prev, dup := owner[s]; dup {
t.Fatalf("row %d %s collides with %s at slot %s", i, key, prev, s.Hex())
}
owner[s] = "row " + string(rune('0'+i%10)) + " " + key
}
}
}
func TestUnregister_DoesNotDisturbNeighbouringRows(t *testing.T) {
// Unregister zeroes all three words of the trailing row, including the
// reserved one. If the stride were too small, that zeroing would land on the
// next row's header and erase a live chain.
state := newRegAS()
ops := []*operator{newOperator(t)}
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
ids := []ChainID{11, 22, 33}
for _, id := range ids {
p := buildRegisterPayload(id, "c", true, common.HexToAddress("0xaa"))
_, _, err := runRegistrar(t, state, SelectorRegisterChain,
appendSigs(p, signByAll(t, state, ops, SelectorRegisterChain, p)), 1_000_000)
require.NoError(t, err)
}
// Snapshot every word of row 0 before removing row 1.
hdr0, name0, res0 := slotRow(0)
before := []common.Hash{
state.db.GetState(BridgeGatewayCanonicalAddress, hdr0),
state.db.GetState(BridgeGatewayCanonicalAddress, name0),
state.db.GetState(BridgeGatewayCanonicalAddress, res0),
}
u := buildUnregisterPayload(22)
_, _, err := runRegistrar(t, state, SelectorUnregisterChain,
appendSigs(u, signByAll(t, state, ops, SelectorUnregisterChain, u)), 1_000_000)
require.NoError(t, err)
require.Equal(t, before[0], state.db.GetState(BridgeGatewayCanonicalAddress, hdr0))
require.Equal(t, before[1], state.db.GetState(BridgeGatewayCanonicalAddress, name0))
require.Equal(t, before[2], state.db.GetState(BridgeGatewayCanonicalAddress, res0))
r := NewStateRegistry(state.db, BridgeGatewayCanonicalAddress)
for _, id := range []ChainID{11, 33} {
_, ok := r.Get(id)
require.True(t, ok, "chain %d was lost", id)
}
}
func TestRegistryAndGovernanceNamespacesAreDisjoint(t *testing.T) {
// The two prefixes are 24 bytes each and must not produce a shared slot,
// or an operator address would be readable as a registry row.
used := map[common.Hash]string{}
for i := uint64(0); i < 256; i++ {
used[registrySlot(i)] = "registry"
}
for i := uint64(0); i < 256; i++ {
s := govSlot(i)
require.NotContains(t, used, s, "govSlot(%d) collides with a registry slot", i)
}
require.NotContains(t, used, nonceSlot())
}
// ---------------------------------------------------------------------------
// Config.Equal
// ---------------------------------------------------------------------------
func TestConfig_EqualCoversEveryField(t *testing.T) {
ts := uint64(10)
other := uint64(11)
base := func() *Config {
return &Config{
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts},
Operators: []string{"0x1111111111111111111111111111111111111111"},
Threshold: 1,
}
}
require.True(t, base().Equal(base()))
differing := map[string]*Config{
"timestamp": {
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other},
Operators: base().Operators, Threshold: 1,
},
"disable": {
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts, Disable: true},
Operators: base().Operators, Threshold: 1,
},
"threshold": {
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts},
Operators: base().Operators, Threshold: 2,
},
"operator count": {
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts},
Operators: []string{
"0x1111111111111111111111111111111111111111",
"0x2222222222222222222222222222222222222222",
},
Threshold: 1,
},
"operator identity": {
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts},
Operators: []string{"0x2222222222222222222222222222222222222222"},
Threshold: 1,
},
}
for name, cfg := range differing {
require.False(t, base().Equal(cfg), "Equal ignored a difference in %s", name)
require.False(t, cfg.Equal(base()), "Equal is not symmetric for %s", name)
}
// Operator ORDER is part of identity here, so a reordering is not equal.
reordered := &Config{
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts},
Operators: []string{
"0x2222222222222222222222222222222222222222",
"0x1111111111111111111111111111111111111111",
},
Threshold: 1,
}
pair := &Config{
Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts},
Operators: []string{
"0x1111111111111111111111111111111111111111",
"0x2222222222222222222222222222222222222222",
},
Threshold: 1,
}
require.False(t, pair.Equal(reordered))
}
func TestConfig_EqualRejectsAForeignType(t *testing.T) {
require.False(t, (&Config{}).Equal(otherPrecompileConfig{}),
"Equal must reject a different precompile's config by type")
require.False(t, (&Config{}).Equal(nil))
}
type otherPrecompileConfig struct{}
func (otherPrecompileConfig) Key() string { return "other" }
func (otherPrecompileConfig) Timestamp() *uint64 { return nil }
func (otherPrecompileConfig) IsDisabled() bool { return false }
func (otherPrecompileConfig) Equal(precompileconfig.Config) bool { return false }
func (otherPrecompileConfig) Verify(precompileconfig.ChainConfig) error { return nil }
+33 -28
View File
@@ -125,9 +125,9 @@ func operatorAddrs(ops []*operator) []common.Address {
return out
}
func signByAll(t *testing.T, ops []*operator, sel byte, payload []byte) [][]byte {
func signByAll(t *testing.T, st *regAS, ops []*operator, sel byte, payload []byte) [][]byte {
t.Helper()
digest := registrarDigest(sel, payload)
digest := registrarDigest(sel, governanceNonce(st.db), payload)
sigs := make([][]byte, len(ops))
for i, op := range ops {
sig, err := crypto.Sign(digest, op.key)
@@ -198,12 +198,14 @@ func TestRegistrar_RoundTrip_RegisterThenGet(t *testing.T) {
gw := common.HexToAddress("0x1111111111111111111111111111111111111111")
payload := buildRegisterPayload(id, name, true, gw)
sigs := signByAll(t, ops[:2], SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops[:2], SelectorRegisterChain, payload)
body := appendSigs(payload, sigs)
ret, gasLeft, err := runRegistrar(t, state, SelectorRegisterChain, body, 1_000_000)
require.NoError(t, err)
require.Equal(t, uint64(1_000_000-GasRegisterChain), gasLeft)
require.Equal(t,
uint64(1_000_000-GasRegisterChain-uint64(len(sigs))*GasPerSignature),
gasLeft, "the write fee plus one ECRECOVER charge per submitted signature")
require.Equal(t, uint64(0), binary.BigEndian.Uint64(ret[24:32]))
// GetCount = 1
@@ -240,7 +242,7 @@ func TestRegistrar_ReRegisterFails(t *testing.T) {
register := func() error {
payload := buildRegisterPayload(id, name, true, gw)
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
return err
}
@@ -261,7 +263,7 @@ func TestRegistrar_UnregisterDecrementsCount(t *testing.T) {
}
for _, c := range chains {
payload := buildRegisterPayload(c.ID, c.Name, c.EVM, c.GatewayAt)
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
}
@@ -273,7 +275,7 @@ func TestRegistrar_UnregisterDecrementsCount(t *testing.T) {
// Unregister middle (id=2). The slot should be compacted with the last row.
payload := buildUnregisterPayload(2)
sigs := signByAll(t, ops, SelectorUnregisterChain, payload)
sigs := signByAll(t, state, ops, SelectorUnregisterChain, payload)
_, _, err = runRegistrar(t, state, SelectorUnregisterChain, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
@@ -304,14 +306,14 @@ func TestRegistrar_UnregisterLastRow(t *testing.T) {
}
for _, c := range chains {
payload := buildRegisterPayload(c.ID, c.Name, c.EVM, c.GatewayAt)
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
}
// Unregister the last row (id=20) — no swap needed; just zero + decrement.
payload := buildUnregisterPayload(20)
sigs := signByAll(t, ops, SelectorUnregisterChain, payload)
sigs := signByAll(t, state, ops, SelectorUnregisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorUnregisterChain, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
@@ -329,7 +331,7 @@ func TestRegistrar_UnregisterUnknownFails(t *testing.T) {
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildUnregisterPayload(999)
sigs := signByAll(t, ops, SelectorUnregisterChain, payload)
sigs := signByAll(t, state, ops, SelectorUnregisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorUnregisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrNotRegistered)
}
@@ -342,7 +344,7 @@ func TestRegistrar_AuthBelowThresholdReverts(t *testing.T) {
payload := buildRegisterPayload(42, "answer", true, common.Address{})
// Only 1 signature when threshold is 2.
sigs := signByAll(t, ops[:1], SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops[:1], SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
}
@@ -356,7 +358,7 @@ func TestRegistrar_AuthFromNonOperatorReverts(t *testing.T) {
payload := buildRegisterPayload(42, "answer", true, common.Address{})
// One real operator + one stranger — only 1 valid signature, threshold=2.
sigs := signByAll(t, []*operator{ops[0], stranger}, SelectorRegisterChain, payload)
sigs := signByAll(t, state, []*operator{ops[0], stranger}, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
}
@@ -369,7 +371,7 @@ func TestRegistrar_AuthDuplicateSignerReverts(t *testing.T) {
payload := buildRegisterPayload(42, "answer", true, common.Address{})
// Same operator signs twice.
sigs := signByAll(t, []*operator{ops[0], ops[0]}, SelectorRegisterChain, payload)
sigs := signByAll(t, state, []*operator{ops[0], ops[0]}, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrDuplicateSigner)
}
@@ -395,7 +397,7 @@ func TestRegistrar_AuthWrongSelectorReverts(t *testing.T) {
id := ChainID(7)
payload := buildRegisterPayload(id, "x", true, common.Address{})
// Sign with the WRONG selector — should not authorize a Register call.
sigs := signByAll(t, ops, SelectorSetGateway, payload)
sigs := signByAll(t, state, ops, SelectorSetGateway, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
}
@@ -411,7 +413,7 @@ func TestRegistrar_SetGatewayPatchesField(t *testing.T) {
newGW := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
payload := buildRegisterPayload(id, name, true, oldGW)
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
@@ -422,7 +424,7 @@ func TestRegistrar_SetGatewayPatchesField(t *testing.T) {
// SetGateway.
payload = buildSetGatewayPayload(id, newGW)
sigs = signByAll(t, ops, SelectorSetGateway, payload)
sigs = signByAll(t, state, ops, SelectorSetGateway, payload)
_, _, err = runRegistrar(t, state, SelectorSetGateway, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
@@ -441,7 +443,7 @@ func TestRegistrar_SetGatewayUnknownReverts(t *testing.T) {
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildSetGatewayPayload(99999, common.Address{})
sigs := signByAll(t, ops, SelectorSetGateway, payload)
sigs := signByAll(t, state, ops, SelectorSetGateway, payload)
_, _, err := runRegistrar(t, state, SelectorSetGateway, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrNotRegistered)
}
@@ -456,7 +458,7 @@ func TestRegistrar_RegisterRoundTripsThroughStateRegistry(t *testing.T) {
gw := common.HexToAddress("0x9999999999999999999999999999999999999999")
payload := buildRegisterPayload(id, name, true, gw)
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.NoError(t, err)
@@ -514,7 +516,7 @@ func TestRegistrar_ReadOnlyRegisterReverts(t *testing.T) {
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildRegisterPayload(42, "x", true, common.Address{})
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
body := appendSigs(payload, sigs)
input := append([]byte{SelectorRegisterChain}, body...)
_, _, err := RegistrarPrecompile.Run(state, common.Address{}, ContractRegistrarAddress, input, 1_000_000, true)
@@ -527,7 +529,7 @@ func TestRegistrar_RegisterOOG(t *testing.T) {
require.NoError(t, SeedGovernance(state.db, operatorAddrs(ops), 1))
payload := buildRegisterPayload(42, "x", true, common.Address{})
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
body := appendSigs(payload, sigs)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, body, 100)
require.ErrorIs(t, err, contract.ErrOutOfGas)
@@ -540,7 +542,7 @@ func TestRegistrar_NameTooLong(t *testing.T) {
long := "this-name-is-definitely-more-than-thirty-two-bytes"
payload := buildRegisterPayload(42, long, true, common.Address{})
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrChainNameTooLong)
}
@@ -551,7 +553,7 @@ func TestRegistrar_UnseededGovernanceRejectsWrites(t *testing.T) {
// Note: SeedGovernance NOT called.
payload := buildRegisterPayload(42, "x", true, common.Address{})
sigs := signByAll(t, ops, SelectorRegisterChain, payload)
sigs := signByAll(t, state, ops, SelectorRegisterChain, payload)
_, _, err := runRegistrar(t, state, SelectorRegisterChain, appendSigs(payload, sigs), 1_000_000)
require.ErrorIs(t, err, ErrUnauthorized)
}
@@ -682,12 +684,15 @@ func TestRegistrar_DigestPayloadShape(t *testing.T) {
// Pin the exact byte shape of registrarDigest. If this changes, on-chain
// and off-chain disagree silently.
payload := []byte{0x01, 0x02, 0x03}
got := registrarDigest(SelectorRegisterChain, payload)
expected := crypto.Keccak256(append(
append([]byte("lux.bridge.registrar.v1"), SelectorRegisterChain),
payload...,
))
require.Equal(t, expected, got)
got := registrarDigest(SelectorRegisterChain, 7, payload)
buf := append([]byte("lux.bridge.registrar.v1"), SelectorRegisterChain)
buf = append(buf, 0, 0, 0, 0, 0, 0, 0, 7) // nonce, big-endian uint64
buf = append(buf, payload...)
require.Equal(t, crypto.Keccak256(buf), got)
// The nonce is inside the hash, so two nonces cannot share a digest.
require.NotEqual(t, got, registrarDigest(SelectorRegisterChain, 8, payload))
}
// --- Gas constants pinning --------------------------------------------------
+7 -69
View File
@@ -4,77 +4,15 @@
package bridge
import (
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/tracing"
ethtypes "github.com/luxfi/geth/core/types"
)
// memStateDB is a minimal contract.StateDB implementation backed by a map.
// It implements only the surface SeedRegistry and stateRegistry exercise.
type memStateDB struct {
store map[common.Address]map[common.Hash]common.Hash
}
func newMemStateDB() *memStateDB {
return &memStateDB{store: make(map[common.Address]map[common.Hash]common.Hash)}
}
func (m *memStateDB) GetState(a common.Address, k common.Hash) common.Hash {
s := m.store[a]
if s == nil {
return common.Hash{}
}
return s[k]
}
func (m *memStateDB) SetState(a common.Address, k, v common.Hash) common.Hash {
s := m.store[a]
if s == nil {
s = make(map[common.Hash]common.Hash)
m.store[a] = s
}
prev := s[k]
s[k] = v
return prev
}
// Unused surface — implemented to satisfy the interface, panics if exercised.
func (*memStateDB) SetNonce(common.Address, uint64, tracing.NonceChangeReason) {
panic("not implemented")
}
func (*memStateDB) GetNonce(common.Address) uint64 { panic("not implemented") }
func (*memStateDB) GetBalance(common.Address) *uint256.Int {
panic("not implemented")
}
func (*memStateDB) AddBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int {
panic("not implemented")
}
func (*memStateDB) SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int {
panic("not implemented")
}
func (*memStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int {
panic("not implemented")
}
func (*memStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {
panic("not implemented")
}
func (*memStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {
panic("not implemented")
}
func (*memStateDB) CreateAccount(common.Address) { panic("not implemented") }
func (*memStateDB) Exist(common.Address) bool { panic("not implemented") }
func (*memStateDB) AddLog(*ethtypes.Log) { panic("not implemented") }
func (*memStateDB) Logs() []*ethtypes.Log { panic("not implemented") }
func (*memStateDB) GetPredicateStorageSlots(common.Address, int) ([]byte, bool) {
panic("not implemented")
}
func (*memStateDB) TxHash() common.Hash { panic("not implemented") }
func (*memStateDB) Snapshot() int { panic("not implemented") }
func (*memStateDB) RevertToSnapshot(int) { panic("not implemented") }
// There is one StateDB fake in this package: regStateDB, in registrar_test.go.
// The previous second fake answered GetState/SetState and panicked on the other
// sixteen methods, so any test that reached past the storage surface died
// instead of failing — and no test could grow past it.
func TestStaticRegistry_GetAndAll(t *testing.T) {
r := NewStatic(DefaultSeed())
@@ -105,7 +43,7 @@ func TestStaticRegistry_AllReturnsCopy(t *testing.T) {
}
func TestStateRegistry_RoundTrip(t *testing.T) {
state := newMemStateDB()
state := newRegState()
addr := common.HexToAddress("0x0000000000000000000000000000000000000440")
seed := DefaultSeed()
@@ -136,7 +74,7 @@ func TestStateRegistry_RoundTrip(t *testing.T) {
}
func TestStateRegistry_SeedIsIdempotent(t *testing.T) {
state := newMemStateDB()
state := newRegState()
addr := common.HexToAddress("0x0000000000000000000000000000000000000440")
if _, err := SeedRegistry(state, addr, DefaultSeed()); err != nil {
@@ -152,7 +90,7 @@ func TestStateRegistry_SeedIsIdempotent(t *testing.T) {
}
func TestStateRegistry_RejectsLongName(t *testing.T) {
state := newMemStateDB()
state := newRegState()
addr := common.HexToAddress("0x0000000000000000000000000000000000000440")
long := Chain{ID: 7, Name: "this-name-is-definitely-more-than-thirty-two-bytes", EVM: true}