Files
evm/precompile/registry/bridge.go
zeekayandHanzo Dev d260656efc evm: gate DEX 0x9999 genesis injection on protocol ActivationTime (preserve pre-2025 genesis)
The AlwaysOn DEX settlement precompile (0x9999) had its EXTCODESIZE marker
baked into EVERY chain's genesis state unconditionally (parent==nil in
ApplyPrecompileActivations), mutating the genesis state root. This changed the
genesis hash of every chain genesised before the DEX activation, breaking RLP
re-import of the real archived Lux mainnet (0x3f4fa2a0), Lux testnet
(0x1c5fe377) and Zoo mainnet (0x7c548af4) — all re-genesised Nov 2024, before
the Dec-2025 activation.

Fix: an AlwaysOn system precompile carries a protocol ActivationTime (Dec 2025
for 0x9999, set by the registry bridge — a fork-like constant, not per-net
config). Its genesis marker is installed ONLY IF ActivationTime <= the genesis
timestamp; otherwise it activates later, on the block transition that crosses
ActivationTime during history replay. Dispatch (GetExtrasRules) is gated on the
same timestamp so a codeless address never dispatches. A chain born before
activation reproduces its ORIGINAL genesis byte-for-byte; a fresh chain born
at/after activation still gets 0x9999 at genesis.

Verified against the real RLP block-0 of all three chains (byte-identical
reproduction) plus a fresh post-activation chain. Corrects two pre-existing
tests that had hardcoded the buggy with-0x9999 hashes as "canonical".

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-02 16:50:11 -07:00

671 lines
25 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Bridge between external precompile registry (github.com/luxfi/precompile/modules)
// and the EVM's internal precompile registry (github.com/luxfi/evm/precompile/modules).
//
// External precompiles register into their own registry via init() functions.
// This bridge copies them into the EVM's registry with adapter types so the
// config parser and execution engine can find and run them.
package registry
import (
"context"
"encoding/json"
"fmt"
"math/big"
"github.com/holiman/uint256"
// Internal (EVM) types
"github.com/luxfi/evm/precompile/contract"
"github.com/luxfi/evm/precompile/modules"
"github.com/luxfi/evm/precompile/precompileconfig"
// External precompile types
extcontract "github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/dex"
extmodules "github.com/luxfi/precompile/modules"
extconfig "github.com/luxfi/precompile/precompileconfig"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/tracing"
ethtypes "github.com/luxfi/geth/core/types"
gethvm "github.com/luxfi/geth/core/vm"
"github.com/luxfi/geth/log"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chains/atomic"
)
// DexSettleActivationTime is the Lux-protocol timestamp at which the AlwaysOn DEX
// settlement system precompile (0x9999) becomes present — EXTCODESIZE marker installed
// and Run dispatched — on every Lux chain. 2025-12-25T23:20:00Z.
//
// It is a protocol constant (the analog of a fork timestamp), NOT per-network JSON
// config, so it applies uniformly and needs no genesisPrecompiles / precompileUpgrades
// entry. Every Lux chain in existence was (re-)genesised BEFORE this timestamp (Lux
// mainnet/testnet and Zoo mainnet block-0 are all Nov 2024), so none of them carry
// 0x9999 in their ORIGINAL genesis state — this constant is what keeps their genesis
// hashes byte-identical after the fix, while 0x9999 still activates later, at the block
// that crosses this timestamp during history replay (verified: ZERO 0x9999 usage on
// mainnet before it, so activation introduces no state divergence). A chain genesised
// at/after this timestamp gets 0x9999 in its genesis state.
const DexSettleActivationTime uint64 = 1766704800
func init() {
bridgeExternalModules()
}
// bridgeExternalModules copies all modules from the external precompile registry
// into the EVM's internal registry, wrapping them with adapter types.
func bridgeExternalModules() {
for _, extMod := range extmodules.RegisteredModules() {
// Skip if already registered internally (e.g., warp, allowlist, etc.)
if _, exists := modules.GetPrecompileModule(extMod.ConfigKey); exists {
continue
}
intMod := modules.Module{
ConfigKey: extMod.ConfigKey,
Address: extMod.Address,
Contract: &contractBridge{ext: extMod.Contract},
Configurator: &configuratorBridge{ext: extMod.Configurator, key: extMod.ConfigKey},
AlwaysOn: extMod.AlwaysOn,
}
// Lux activates its AlwaysOn system precompiles (the DEX settlement family,
// 0x9999) at the protocol timestamp DexSettleActivationTime, not at genesis.
// This is the Lux-network activation SCHEDULE for the precompile (the external
// module provides only the CODE); it is set here rather than in the external
// module so pre-activation chains reproduce their original genesis hash while
// 0x9999 still turns on, network-wide, at the crossing block. The external
// AlwaysOn module carries no activation time, so a genesis-active AlwaysOn
// module cannot occur today; if one is ever added it must declare ActivationTime
// explicitly here.
if extMod.AlwaysOn {
intMod.ActivationTime = DexSettleActivationTime
}
if err := modules.RegisterBridgedModule(intMod); err != nil {
log.Warn("Failed to bridge external precompile module",
"key", extMod.ConfigKey,
"address", extMod.Address,
"error", err,
)
continue
}
log.Debug("Bridged external precompile into EVM registry",
"key", extMod.ConfigKey,
"address", extMod.Address,
)
}
}
// =============================================================================
// Contract Bridge
// =============================================================================
// contractBridge wraps an external StatefulPrecompiledContract to implement the
// internal contract.StatefulPrecompiledContract interface.
type contractBridge struct {
ext extcontract.StatefulPrecompiledContract
}
func (b *contractBridge) Run(
accessibleState contract.AccessibleState,
caller common.Address,
addr common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
// Wrap the internal AccessibleState as an external one
extState := &accessibleStateBridge{internal: accessibleState}
return b.ext.Run(extState, caller, addr, input, suppliedGas, readOnly)
}
// =============================================================================
// AccessibleState Bridge (internal → external)
// =============================================================================
// accessibleStateBridge wraps an internal AccessibleState to implement
// the external contract.AccessibleState interface.
type accessibleStateBridge struct {
internal contract.AccessibleState
}
func (a *accessibleStateBridge) GetStateDB() extcontract.StateDB {
return &stateDBBridge{internal: a.internal.GetStateDB()}
}
func (a *accessibleStateBridge) GetBlockContext() extcontract.BlockContext {
return &blockContextBridge{internal: a.internal.GetBlockContext()}
}
func (a *accessibleStateBridge) GetConsensusContext() context.Context {
return a.internal.GetConsensusContext()
}
func (a *accessibleStateBridge) GetChainConfig() extconfig.ChainConfig {
return &extChainConfigBridge{internal: a.internal.GetChainConfig()}
}
// envProvider is the concrete accessor the internal accessibleStateAdapter
// (evm/core/precompile_overrider.go) exposes to hand the registry bridge the raw
// geth precompile execution environment. It is kept OFF the EVM-INTERNAL
// contract.AccessibleState interface (which has many implementers/mocks) and
// type-asserted here so only the production adapter — which actually holds a geth
// env — can supply one. (NOTE: the EXTERNAL contract.AccessibleState interface this
// bridge implements DOES expose GetPrecompileEnv() to every external precompile;
// GetPrecompileEnv below gates which of them actually receive a callable env.)
type envProvider interface {
GetPrecompileEnv() gethvm.PrecompileEnvironment
}
// callableEnvAllowlist is the set of precompile self-addresses that legitimately need
// the EVM Call sub-call surface (to move ERC-20 value through the token contract for
// C<->D settlement). It is the DEX settlement family ONLY:
// - 0x9999 (LXSettleAddress): the sole money path — deposit/withdraw/swap legs pull
// and push ERC-20 via transferFrom/transfer with 0x9999 as msg.sender.
// - 0x9996 (DEXPositionManagerAddress): composes 0x9999; its LP-commit leg moves
// ERC-20 liquidity with 0x9996 as msg.sender.
//
// Every OTHER external precompile (the PQ/ZK verifiers at 0x0122xx, etc.) gets a nil
// callable env and falls back to its fail-secure refusal. Those precompiles do not
// type-assert callableEnv today, so this changes no live behaviour — it is
// defense-in-depth so a FUTURE external precompile cannot silently acquire EVM sub-
// call power (with itself as msg.sender) merely by asserting the optional capability.
// The addresses are the canonical luxfi/precompile/dex constants (one source of
// truth; that package is already in this module's dependency graph via registry.go).
var callableEnvAllowlist = map[common.Address]struct{}{
common.HexToAddress(dex.LXSettleAddress): {},
common.HexToAddress(dex.DEXPositionManagerAddress): {},
}
// GetPrecompileEnv returns the external precompile execution environment with a live
// Call surface, forwarded from the underlying geth env. This is what makes the DEX
// 0x9999 settlement precompile's ERC-20 leg execute on-chain: the precompile sub-
// calls the token contract (transferFrom / transfer / balanceOf) through this env's
// Call. When the internal adapter does not provide a geth env (a non-EVM caller or a
// test mock), this returns nil and the precompile fails-secure (ErrERC20VaultUnavailable)
// rather than mint an unbacked claim.
//
// SHARED-INTERFACE NOTE: this method IS on the shared external contract.AccessibleState
// interface, so every external precompile receives this bridge. The Call surface is a
// powerful capability (the sub-call's msg.sender is the asserting precompile's own
// address), so it is GATED here: only the DEX settlement addresses in
// callableEnvAllowlist receive a non-nil callable env; all others get nil (fail-secure
// refusal). This is defense-in-depth on top of each settlement Run's CALL-only guard.
func (a *accessibleStateBridge) GetPrecompileEnv() extcontract.PrecompileEnvironment {
p, ok := a.internal.(envProvider)
if !ok {
return nil
}
env := p.GetPrecompileEnv()
if env == nil {
return nil
}
// Gate the Call surface to the DEX settlement family. The precompile self-address
// the geth env carries identifies WHO is asking; only those addresses get Call.
if _, allowed := callableEnvAllowlist[env.Addresses().Self]; !allowed {
return nil
}
return &precompileEnvBridge{env: env}
}
// precompileEnvBridge adapts a geth vm.PrecompileEnvironment to the external
// contract.PrecompileEnvironment, ADDING the Call method the DEX precompile type-
// asserts (callableEnv). contract.PrecompileEnvironment itself declares only
// ReadOnly(); Call is an OPTIONAL concrete capability the precompile reaches via a
// type assertion, so adding it here does not widen the shared interface.
type precompileEnvBridge struct {
env gethvm.PrecompileEnvironment
}
func (p *precompileEnvBridge) ReadOnly() bool { return p.env.ReadOnly() }
// Call forwards a contract sub-call to the geth env. The caller of the sub-call is
// the asserting precompile's SELF address (the geth env's default — NO caller
// proxying). For the DEX settlement family this is the settlement address itself
// (0x9999, or 0x9996 for LP commits): that matches the allowance a depositor granted
// the vault via approve(0x9999, amount), so transferFrom pulls against the correct
// allowance. The settlement Run's CALL-only guard guarantees self is the settlement
// address (never a DELEGATECALL delegator). The variadic CallOption tail of the geth
// signature is intentionally dropped (no proxying); the external callableEnv shape is
// the no-option form the precompile expects.
func (p *precompileEnvBridge) Call(addr common.Address, input []byte, gas uint64, value *big.Int) ([]byte, uint64, error) {
return p.env.Call(addr, input, gas, value)
}
// accessibleStateBridge forwards the OPTIONAL cross-chain atomic capability
// (external contract.AtomicState) to the internal AccessibleState when the
// concrete internal adapter implements the internal contract.AtomicState. A
// precompile in github.com/luxfi/precompile/* type-asserts the external
// AtomicState; this bridge makes that assertion succeed by delegating to the
// internal adapter (the EVM's accessibleStateAdapter). When the internal state
// is not atomic-capable (a test mock), AtomicMemory() returns nil so the
// precompile reverts.
var _ extcontract.AtomicState = (*accessibleStateBridge)(nil)
// internalAtomic mirrors the internal contract.AtomicState so the bridge can
// type-assert the internal AccessibleState without importing it by name twice.
type internalAtomic interface {
AtomicMemory() atomic.SharedMemory
NetworkID() uint32
ChainID() ids.ID
CChainID() ids.ID
GovernanceController() common.Address
DChainID() ids.ID
TxID() ids.ID
CallIndex() uint32
}
func (a *accessibleStateBridge) atomicOrNil() internalAtomic {
if at, ok := a.internal.(internalAtomic); ok {
return at
}
return nil
}
func (a *accessibleStateBridge) AtomicMemory() atomic.SharedMemory {
if at := a.atomicOrNil(); at != nil {
return at.AtomicMemory()
}
return nil
}
func (a *accessibleStateBridge) NetworkID() uint32 {
if at := a.atomicOrNil(); at != nil {
return at.NetworkID()
}
return 0
}
func (a *accessibleStateBridge) ChainID() ids.ID {
if at := a.atomicOrNil(); at != nil {
return at.ChainID()
}
return ids.Empty
}
func (a *accessibleStateBridge) CChainID() ids.ID {
if at := a.atomicOrNil(); at != nil {
return at.CChainID()
}
return ids.Empty
}
// GovernanceController delegates to the inner atomic capability — the per-network DEX
// governance authority (a governance contract, never a dev-mnemonic EOA). Returns the
// zero address when the inner state is not atomic-capable, which the precompile treats
// as fail-closed (halt/seed revert).
func (a *accessibleStateBridge) GovernanceController() common.Address {
if at := a.atomicOrNil(); at != nil {
return at.GovernanceController()
}
return common.Address{}
}
func (a *accessibleStateBridge) DChainID() ids.ID {
if at := a.atomicOrNil(); at != nil {
return at.DChainID()
}
return ids.Empty
}
func (a *accessibleStateBridge) TxID() ids.ID {
if at := a.atomicOrNil(); at != nil {
return at.TxID()
}
return ids.Empty
}
func (a *accessibleStateBridge) CallIndex() uint32 {
if at := a.atomicOrNil(); at != nil {
return at.CallIndex()
}
return 0
}
// =============================================================================
// StateDB Bridge (internal → external)
// =============================================================================
// stateDBBridge wraps an internal contract.StateDB to implement
// the external contract.StateDB interface.
//
// The external interface is wider (SubBalance, tracing reasons, MultiCoin, etc.).
// Methods not directly available on the internal interface either:
// - Use type assertions to reach the underlying vm.StateDB
// - Provide reasonable defaults
type stateDBBridge struct {
internal contract.StateDB
}
func (s *stateDBBridge) GetState(addr common.Address, key common.Hash) common.Hash {
return s.internal.GetState(addr, key)
}
func (s *stateDBBridge) SetState(addr common.Address, key, val common.Hash) common.Hash {
s.internal.SetState(addr, key, val)
return val
}
func (s *stateDBBridge) SetNonce(addr common.Address, nonce uint64, _ tracing.NonceChangeReason) {
s.internal.SetNonce(addr, nonce)
}
func (s *stateDBBridge) GetNonce(addr common.Address) uint64 {
return s.internal.GetNonce(addr)
}
func (s *stateDBBridge) GetBalance(addr common.Address) *uint256.Int {
return s.internal.GetBalance(addr)
}
func (s *stateDBBridge) AddBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int {
s.internal.AddBalance(addr, amount)
// Return new balance
bal := s.internal.GetBalance(addr)
if bal != nil {
return *bal
}
return uint256.Int{}
}
func (s *stateDBBridge) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int {
// The internal contract.StateDB doesn't have SubBalance.
// Reach the underlying vm.StateDB via type assertion.
type subBalancer interface {
SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
}
sb, ok := s.internal.(subBalancer)
if !ok {
// FAIL CLOSED. There is NO safe fallback: the internal contract.StateDB has no
// way to subtract, so silently returning a zero "previous balance" without
// debiting (the old behaviour) would let a value-moving precompile — e.g. the
// 0x9999 DEX custody vault on withdraw — treat the debit as successful while the
// balance is untouched, MINTING native value. A failed debit on the money path
// must abort the call, never proceed. We panic: the EVM recovers a precompile
// panic into a reverted call, so the enclosing value move is rolled back rather
// than committed half-done. In production the internal StateDB is always the
// concrete stateDBAdapter{vm.StateDB} (precompile_overrider.go), which DOES
// implement SubBalance, so this panic is structurally unreachable; it exists to
// make any future StateDB that omits SubBalance fail loudly instead of minting.
log.Error("stateDBBridge: SubBalance on a StateDB without subtraction support — failing closed to avoid minting native value",
"address", addr, "amount", amount)
panic(fmt.Sprintf("stateDBBridge: internal StateDB %T cannot SubBalance; refusing to credit without debiting (addr=%s amount=%s)", s.internal, addr, amount))
}
return sb.SubBalance(addr, amount, tracing.BalanceChangeUnspecified)
}
func (s *stateDBBridge) GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int {
// MultiCoin not available through internal interface
return new(big.Int)
}
func (s *stateDBBridge) AddBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) {
// MultiCoin not available through internal interface
}
func (s *stateDBBridge) SubBalanceMultiCoin(addr common.Address, coinID common.Hash, amount *big.Int) {
// MultiCoin not available through internal interface
}
func (s *stateDBBridge) CreateAccount(addr common.Address) {
s.internal.CreateAccount(addr)
}
func (s *stateDBBridge) Exist(addr common.Address) bool {
return s.internal.Exist(addr)
}
func (s *stateDBBridge) AddLog(log *ethtypes.Log) {
s.internal.AddLog(log)
}
func (s *stateDBBridge) Logs() []*ethtypes.Log {
// Try type assertion for Logs()
type logger interface {
Logs() []*ethtypes.Log
}
if l, ok := s.internal.(logger); ok {
return l.Logs()
}
return nil
}
// GetCodeSize forwards the EXTCODESIZE primitive (the C1 live-on-chain asset proof the
// 0x9999 DEX value path's OnChainAssetVerifier reads) to the internal contract.StateDB.
// Like SubBalance above, this is an OPTIONAL capability the narrow internal interface does
// not declare, so we type-assert it. In production s.internal is the concrete
// stateDBAdapter{vm.StateDB} (precompile_overrider.go), which DOES implement GetCodeSize by
// forwarding to the geth StateDB; the assertion succeeds and the real code size flows.
//
// Without this forward, the dex precompile (which receives this *stateDBBridge as its
// external StateDB) could not prove an ERC-20 has live code: poolStateAdapter.CodeSizeOf
// would type-assert THIS bridge for GetCodeSize, miss it, and return -1, making
// codeStaterFor report not-capable so EVERY real-asset market-open/swap fail-closed with
// ErrNoOnChainVerifier — even over a genuine, code-bearing token. We return -1 (not 0) when
// the internal StateDB cannot report code size, matching poolStateAdapter's own convention,
// so the value path fails CLOSED ("no verifier") rather than admitting on a fabricated zero
// (which would let an asset with no code masquerade as real).
func (s *stateDBBridge) GetCodeSize(addr common.Address) int {
if cs, ok := s.internal.(interface {
GetCodeSize(common.Address) int
}); ok {
return cs.GetCodeSize(addr)
}
return -1
}
func (s *stateDBBridge) GetPredicateStorageSlots(addr common.Address, index int) ([]byte, bool) {
return s.internal.GetPredicateStorageSlots(addr, index)
}
func (s *stateDBBridge) TxHash() common.Hash {
return s.internal.GetTxHash()
}
func (s *stateDBBridge) Snapshot() int {
return s.internal.Snapshot()
}
func (s *stateDBBridge) RevertToSnapshot(id int) {
s.internal.RevertToSnapshot(id)
}
// =============================================================================
// BlockContext Bridge (internal → external)
// =============================================================================
type blockContextBridge struct {
internal contract.BlockContext
}
func (b *blockContextBridge) Number() *big.Int {
return b.internal.Number()
}
func (b *blockContextBridge) Timestamp() uint64 {
return b.internal.Timestamp()
}
func (b *blockContextBridge) GetPredicateResults(txHash common.Hash, addr common.Address) []byte {
return b.internal.GetPredicateResults(txHash, addr)
}
// =============================================================================
// ChainConfig Bridge (internal → external)
// =============================================================================
type extChainConfigBridge struct {
internal precompileconfig.ChainConfig
}
func (c *extChainConfigBridge) IsDurango(time uint64) bool {
return c.internal.IsDurango(time)
}
// =============================================================================
// Configurator Bridge (external → internal)
// =============================================================================
// configuratorBridge wraps an external Configurator to implement the
// internal contract.Configurator interface.
type configuratorBridge struct {
ext extcontract.Configurator
key string
}
func (c *configuratorBridge) MakeConfig() precompileconfig.Config {
extCfg := c.ext.MakeConfig()
return &configBridge{ext: extCfg, key: c.key}
}
func (c *configuratorBridge) MakeGenesisConfig() precompileconfig.Config {
extCfg := c.ext.MakeConfig()
// Set timestamp to 0 for genesis activation
cfg := &configBridge{ext: extCfg, key: c.key}
cfg.genesisTimestamp = new(uint64) // *uint64 pointing to 0
return cfg
}
func (c *configuratorBridge) Configure(
chainConfig precompileconfig.ChainConfig,
cfg precompileconfig.Config,
state contract.StateDB,
blockContext contract.ConfigurationBlockContext,
) error {
// Unwrap the config if it's our bridge type
var extCfg extconfig.Config
if bridged, ok := cfg.(*configBridge); ok {
extCfg = bridged.ext
} else {
// Config was not created by us — create a default
extCfg = c.ext.MakeConfig()
}
// Create external StateDB and BlockContext adapters
extState := &stateDBBridge{internal: state}
extBlock := &configBlockContextBridge{internal: blockContext}
extChain := &extChainConfigBridge{internal: chainConfig}
return c.ext.Configure(extChain, extCfg, extState, extBlock)
}
// configBlockContextBridge wraps internal ConfigurationBlockContext for external use
type configBlockContextBridge struct {
internal contract.ConfigurationBlockContext
}
func (b *configBlockContextBridge) Number() *big.Int {
return b.internal.Number()
}
func (b *configBlockContextBridge) Timestamp() uint64 {
return b.internal.Timestamp()
}
// =============================================================================
// Config Bridge (external → internal)
// =============================================================================
// configBridge wraps an external precompileconfig.Config to implement the
// internal precompileconfig.Config interface.
//
// Critically, this must support JSON marshal/unmarshal since the config
// is deserialized from upgrade.json.
type configBridge struct {
ext extconfig.Config
key string
genesisTimestamp *uint64 // override timestamp for genesis configs
}
func (c *configBridge) Key() string {
return c.ext.Key()
}
func (c *configBridge) Timestamp() *uint64 {
if c.genesisTimestamp != nil {
return c.genesisTimestamp
}
return c.ext.Timestamp()
}
func (c *configBridge) IsDisabled() bool {
return c.ext.IsDisabled()
}
func (c *configBridge) Equal(other precompileconfig.Config) bool {
otherBridge, ok := other.(*configBridge)
if !ok {
return false
}
return c.ext.Equal(otherBridge.ext)
}
func (c *configBridge) Verify(chainConfig precompileconfig.ChainConfig) error {
extChain := &extChainConfigBridge{internal: chainConfig}
return c.ext.Verify(extChain)
}
// UnmarshalJSON handles the format difference between internal and external configs.
//
// Internal configs embed precompileconfig.Upgrade (no JSON tag), so fields like
// "blockTimestamp" are at the top level: {"blockTimestamp":0}
//
// External configs use a tagged field: Upgrade `json:"upgrade,omitempty"`, so they
// expect: {"upgrade":{"blockTimestamp":0}}
//
// This method translates between the two formats.
func (c *configBridge) UnmarshalJSON(data []byte) error {
// Try direct unmarshal first
if err := json.Unmarshal(data, c.ext); err != nil {
return err
}
// If timestamp is already set, we're done (external config handled it)
if c.ext.Timestamp() != nil {
return nil
}
// Timestamp is nil — the external config likely uses json:"upgrade" tag
// and the data has top-level "blockTimestamp". Wrap it.
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil // not a JSON object, but initial unmarshal succeeded
}
if ts, ok := raw["blockTimestamp"]; ok {
// Build {"upgrade":{"blockTimestamp":N,"disable":false}}
upgradeObj := map[string]json.RawMessage{"blockTimestamp": ts}
if dis, ok := raw["disable"]; ok {
upgradeObj["disable"] = dis
}
wrapped, _ := json.Marshal(map[string]json.RawMessage{
"upgrade": mustMarshal(upgradeObj),
})
return json.Unmarshal(wrapped, c.ext)
}
return nil // no blockTimestamp in data at all
}
// MarshalJSON delegates to the external config's JSON marshaling.
func (c *configBridge) MarshalJSON() ([]byte, error) {
return json.Marshal(c.ext)
}
// String returns a debug representation
func (c *configBridge) String() string {
return fmt.Sprintf("configBridge{key=%s, ext=%v}", c.key, c.ext)
}
// mustMarshal marshals v to json.RawMessage, panicking on error (should never fail for map types).
func mustMarshal(v interface{}) json.RawMessage {
data, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("mustMarshal: %v", err))
}
return data
}