From cc2b92b6a47ae904fd1d5e1e2a2afbd18710ca60 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Sun, 1 Feb 2026 19:44:53 +0100 Subject: [PATCH] eth: add partial statefulness foundation (Phase 1) Implements EIP-7928 BAL-based partial statefulness infrastructure: - Add PartialStateConfig to eth/ethconfig with CLI flags - Add ContractFilter interface in core/state/partial/ - Add BAL history database accessors in core/rawdb/ - Add PartialState and BALHistory managers This enables nodes to track only configured contracts' storage while maintaining full account trie integrity. --- cmd/geth/chaincmd.go | 4 ++ cmd/geth/main.go | 4 ++ cmd/utils/flags.go | 37 ++++++++++ core/rawdb/accessors_bal.go | 130 ++++++++++++++++++++++++++++++++++ core/rawdb/schema.go | 3 + core/state/partial/filter.go | 96 +++++++++++++++++++++++++ core/state/partial/history.go | 66 +++++++++++++++++ core/state/partial/state.go | 80 +++++++++++++++++++++ eth/ethconfig/config.go | 116 ++++++++++++++++++++++++++++++ eth/ethconfig/gen_config.go | 6 ++ 10 files changed, 542 insertions(+) create mode 100644 core/rawdb/accessors_bal.go create mode 100644 core/state/partial/filter.go create mode 100644 core/state/partial/history.go create mode 100644 core/state/partial/state.go diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 1084100f39..06767f2fce 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -126,6 +126,10 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.StateHistoryFlag, utils.TrienodeHistoryFlag, utils.TrienodeHistoryFullValueCheckpointFlag, + utils.PartialStateFlag, + utils.PartialStateContractsFlag, + utils.PartialStateContractsFileFlag, + utils.PartialStateBALRetentionFlag, }, utils.DatabaseFlags, debug.Flags), Before: func(ctx *cli.Context) error { flags.MigrateGlobalFlags(ctx) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index da1623be7c..5c457d5325 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -95,6 +95,10 @@ var ( utils.StateHistoryFlag, utils.TrienodeHistoryFlag, utils.TrienodeHistoryFullValueCheckpointFlag, + utils.PartialStateFlag, + utils.PartialStateContractsFlag, + utils.PartialStateContractsFileFlag, + utils.PartialStateBALRetentionFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, utils.LegacyWhitelistFlag, // deprecated diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 7d8c47e4f7..760df9f644 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -315,6 +315,28 @@ var ( Value: uint(ethconfig.Defaults.NodeFullValueCheckpoint), Category: flags.StateCategory, } + // Partial statefulness flags + PartialStateFlag = &cli.BoolFlag{ + Name: "partial-state", + Usage: "Enable partial statefulness mode (reduced storage, requires BAL support)", + Category: flags.StateCategory, + } + PartialStateContractsFlag = &cli.StringSliceFlag{ + Name: "partial-state.contracts", + Usage: "Contracts to track storage for in partial state mode (comma-separated addresses)", + Category: flags.StateCategory, + } + PartialStateContractsFileFlag = &cli.StringFlag{ + Name: "partial-state.contracts-file", + Usage: "JSON file containing contracts to track in partial state mode", + Category: flags.StateCategory, + } + PartialStateBALRetentionFlag = &cli.Uint64Flag{ + Name: "partial-state.bal-retention", + Usage: "Number of blocks to retain BAL history for reorg handling", + Value: ethconfig.Defaults.PartialState.BALRetention, + Category: flags.StateCategory, + } TransactionHistoryFlag = &cli.Uint64Flag{ Name: "history.transactions", Usage: "Number of recent blocks to maintain transactions index for (default = about one year, 0 = entire chain)", @@ -1845,6 +1867,21 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(StateSchemeFlag.Name) { cfg.StateScheme = ctx.String(StateSchemeFlag.Name) } + // Partial state configuration + if ctx.IsSet(PartialStateFlag.Name) { + cfg.PartialState.Enabled = ctx.Bool(PartialStateFlag.Name) + } + if ctx.IsSet(PartialStateContractsFlag.Name) { + for _, addr := range ctx.StringSlice(PartialStateContractsFlag.Name) { + cfg.PartialState.Contracts = append(cfg.PartialState.Contracts, common.HexToAddress(addr)) + } + } + if ctx.IsSet(PartialStateContractsFileFlag.Name) { + cfg.PartialState.ContractsFile = ctx.String(PartialStateContractsFileFlag.Name) + } + if ctx.IsSet(PartialStateBALRetentionFlag.Name) { + cfg.PartialState.BALRetention = ctx.Uint64(PartialStateBALRetentionFlag.Name) + } // Parse transaction history flag, if user is still using legacy config // file with 'TxLookupLimit' configured, copy the value to 'TransactionHistory'. if cfg.TransactionHistory == ethconfig.Defaults.TransactionHistory && cfg.TxLookupLimit != ethconfig.Defaults.TxLookupLimit { diff --git a/core/rawdb/accessors_bal.go b/core/rawdb/accessors_bal.go new file mode 100644 index 0000000000..cb0f50ab6e --- /dev/null +++ b/core/rawdb/accessors_bal.go @@ -0,0 +1,130 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "encoding/binary" + + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" +) + +// balHistoryKey constructs the database key for a BAL at a given block number. +// Key format: balHistoryPrefix + block number (uint64 big endian) +func balHistoryKey(blockNum uint64) []byte { + key := make([]byte, len(balHistoryPrefix)+8) + copy(key, balHistoryPrefix) + binary.BigEndian.PutUint64(key[len(balHistoryPrefix):], blockNum) + return key +} + +// ReadBALHistory retrieves the Block Access List for a specific block number. +// Returns nil if the BAL is not found or cannot be decoded. +func ReadBALHistory(db ethdb.KeyValueReader, blockNum uint64) *bal.BlockAccessList { + data, err := db.Get(balHistoryKey(blockNum)) + if err != nil || len(data) == 0 { + return nil + } + var accessList bal.BlockAccessList + if err := rlp.DecodeBytes(data, &accessList); err != nil { + log.Warn("Failed to decode BAL history", "block", blockNum, "err", err) + return nil + } + return &accessList +} + +// WriteBALHistory stores a Block Access List for a specific block number. +func WriteBALHistory(db ethdb.KeyValueWriter, blockNum uint64, accessList *bal.BlockAccessList) { + data, err := rlp.EncodeToBytes(accessList) + if err != nil { + log.Crit("Failed to encode BAL history", "block", blockNum, "err", err) + } + if err := db.Put(balHistoryKey(blockNum), data); err != nil { + log.Crit("Failed to store BAL history", "block", blockNum, "err", err) + } +} + +// DeleteBALHistory removes the Block Access List for a specific block number. +func DeleteBALHistory(db ethdb.KeyValueWriter, blockNum uint64) { + if err := db.Delete(balHistoryKey(blockNum)); err != nil { + log.Crit("Failed to delete BAL history", "block", blockNum, "err", err) + } +} + +// PruneBALHistory removes all BALs before the specified block number. +// This uses range iteration for safe, interruptible pruning. +func PruneBALHistory(db ethdb.Database, beforeBlock uint64) error { + // Create iterator for BAL history range + start := balHistoryKey(0) + end := balHistoryKey(beforeBlock) + + // Use batch deletion for efficiency + batch := db.NewBatch() + it := db.NewIterator(balHistoryPrefix, start) + defer it.Release() + + deleted := 0 + for it.Next() { + key := it.Key() + // Stop if we've passed the end key + if len(key) >= len(balHistoryPrefix)+8 { + blockNum := binary.BigEndian.Uint64(key[len(balHistoryPrefix):]) + if blockNum >= beforeBlock { + break + } + } + // Check if key is within our prefix + if len(key) < len(balHistoryPrefix) { + continue + } + for i := range balHistoryPrefix { + if key[i] != balHistoryPrefix[i] { + goto done + } + } + batch.Delete(key) + deleted++ + + // Commit batch periodically to avoid memory buildup + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + } + } +done: + // Write remaining items + if batch.ValueSize() > 0 { + if err := batch.Write(); err != nil { + return err + } + } + if deleted > 0 { + log.Debug("Pruned BAL history", "deleted", deleted, "beforeBlock", beforeBlock) + } + _ = end // silence unused variable warning (used for documentation) + return it.Error() +} + +// HasBALHistory returns whether a BAL exists for the given block number. +func HasBALHistory(db ethdb.KeyValueReader, blockNum uint64) bool { + has, _ := db.Has(balHistoryKey(blockNum)) + return has +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 54c76143b4..7731e24d1c 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -168,6 +168,9 @@ var ( // Verkle transition information VerkleTransitionStatePrefix = []byte("verkle-transition-state-") + + // Partial statefulness - BAL (Block Access List) history for reorg handling + balHistoryPrefix = []byte("p") // balHistoryPrefix + num (uint64 big endian) -> RLP(bal.BlockAccessList) ) // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary diff --git a/core/state/partial/filter.go b/core/state/partial/filter.go new file mode 100644 index 0000000000..a585afbc6e --- /dev/null +++ b/core/state/partial/filter.go @@ -0,0 +1,96 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package partial + +import "github.com/ethereum/go-ethereum/common" + +// ContractFilter determines which contracts' storage to sync and retain. +// This interface allows flexible filtering strategies for partial statefulness. +type ContractFilter interface { + // ShouldSyncStorage returns true if we should download storage for this contract + // during snap sync. Returns false for contracts whose storage we skip. + ShouldSyncStorage(address common.Address) bool + + // ShouldSyncCode returns true if we should download bytecode for this contract + // during snap sync. Returns false for contracts whose code we skip. + ShouldSyncCode(address common.Address) bool + + // IsTracked returns true if this contract's storage is being tracked. + // Used by RPC handlers to determine if storage queries can be answered. + IsTracked(address common.Address) bool +} + +// ConfiguredFilter implements ContractFilter based on a configured list of addresses. +// This is the primary implementation used in production. +type ConfiguredFilter struct { + contracts map[common.Address]struct{} +} + +// NewConfiguredFilter creates a new filter from a list of contract addresses. +func NewConfiguredFilter(addresses []common.Address) *ConfiguredFilter { + m := make(map[common.Address]struct{}, len(addresses)) + for _, addr := range addresses { + m[addr] = struct{}{} + } + return &ConfiguredFilter{contracts: m} +} + +// ShouldSyncStorage returns true if the contract is in the configured list. +func (f *ConfiguredFilter) ShouldSyncStorage(addr common.Address) bool { + _, ok := f.contracts[addr] + return ok +} + +// ShouldSyncCode returns true if the contract is in the configured list. +func (f *ConfiguredFilter) ShouldSyncCode(addr common.Address) bool { + _, ok := f.contracts[addr] + return ok +} + +// IsTracked returns true if the contract is in the configured list. +func (f *ConfiguredFilter) IsTracked(addr common.Address) bool { + _, ok := f.contracts[addr] + return ok +} + +// Contracts returns the list of tracked contract addresses. +func (f *ConfiguredFilter) Contracts() []common.Address { + result := make([]common.Address, 0, len(f.contracts)) + for addr := range f.contracts { + result = append(result, addr) + } + return result +} + +// AllowAllFilter is a filter that allows all contracts (full node behavior). +// Used when partial state mode is disabled. +type AllowAllFilter struct{} + +// ShouldSyncStorage always returns true for full node behavior. +func (f *AllowAllFilter) ShouldSyncStorage(addr common.Address) bool { + return true +} + +// ShouldSyncCode always returns true for full node behavior. +func (f *AllowAllFilter) ShouldSyncCode(addr common.Address) bool { + return true +} + +// IsTracked always returns true for full node behavior. +func (f *AllowAllFilter) IsTracked(addr common.Address) bool { + return true +} diff --git a/core/state/partial/history.go b/core/state/partial/history.go new file mode 100644 index 0000000000..af53c041e0 --- /dev/null +++ b/core/state/partial/history.go @@ -0,0 +1,66 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package partial + +import ( + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/ethdb" +) + +// BALHistory manages storage and retrieval of Block Access Lists for reorg handling. +// It's a thin wrapper over rawdb accessor functions, following go-ethereum patterns. +type BALHistory struct { + db ethdb.Database + retention uint64 // Number of blocks to retain BAL history +} + +// NewBALHistory creates a new BAL history manager. +func NewBALHistory(db ethdb.Database, retention uint64) *BALHistory { + return &BALHistory{ + db: db, + retention: retention, + } +} + +// Store saves a BAL for a specific block number. +func (h *BALHistory) Store(blockNum uint64, accessList *bal.BlockAccessList) { + rawdb.WriteBALHistory(h.db, blockNum, accessList) +} + +// Get retrieves the BAL for a specific block number. +// Returns nil, false if not found. +func (h *BALHistory) Get(blockNum uint64) (*bal.BlockAccessList, bool) { + accessList := rawdb.ReadBALHistory(h.db, blockNum) + return accessList, accessList != nil +} + +// Delete removes the BAL for a specific block number. +func (h *BALHistory) Delete(blockNum uint64) { + rawdb.DeleteBALHistory(h.db, blockNum) +} + +// Prune removes all BALs before the specified block number. +// Uses SafeDeleteRange for interruptible pruning. +func (h *BALHistory) Prune(beforeBlock uint64) error { + return rawdb.PruneBALHistory(h.db, beforeBlock) +} + +// Retention returns the configured retention window in blocks. +func (h *BALHistory) Retention() uint64 { + return h.retention +} diff --git a/core/state/partial/state.go b/core/state/partial/state.go new file mode 100644 index 0000000000..f69ba47601 --- /dev/null +++ b/core/state/partial/state.go @@ -0,0 +1,80 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package partial + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/triedb" +) + +// PartialState manages state for partial stateful nodes. +// It applies BAL diffs to update state without re-executing transactions. +type PartialState struct { + db ethdb.Database + trieDB *triedb.Database + filter ContractFilter + history *BALHistory + + // Current state root + stateRoot common.Hash +} + +// NewPartialState creates a new partial state manager. +func NewPartialState(db ethdb.Database, trieDB *triedb.Database, filter ContractFilter, balRetention uint64) *PartialState { + return &PartialState{ + db: db, + trieDB: trieDB, + filter: filter, + history: NewBALHistory(db, balRetention), + } +} + +// Filter returns the contract filter used by this partial state. +func (s *PartialState) Filter() ContractFilter { + return s.filter +} + +// SetRoot sets the current state root. +func (s *PartialState) SetRoot(root common.Hash) { + s.stateRoot = root +} + +// Root returns the current state root. +func (s *PartialState) Root() common.Hash { + return s.stateRoot +} + +// ApplyBALAndComputeRoot applies BAL diffs and returns the new state root. +// This is the core function for partial state block processing. +// +// TODO: Implement in Phase 3/4 - this will: +// 1. Open trie at current root +// 2. Apply balance/nonce changes from BAL +// 3. Apply storage changes for tracked contracts +// 4. Commit trie changes using existing pathdb compression +// 5. Return new state root +func (s *PartialState) ApplyBALAndComputeRoot(currentRoot common.Hash, accessList *bal.BlockAccessList) (common.Hash, error) { + // Placeholder - will be implemented in Phase 4 + panic("ApplyBALAndComputeRoot not yet implemented") +} + +// History returns the BAL history manager. +func (s *PartialState) History() *BALHistory { + return s.history +} diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index e0b6c978e9..366f5859e4 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -18,7 +18,10 @@ package ethconfig import ( + "encoding/json" "errors" + "fmt" + "os" "time" "github.com/ethereum/go-ethereum/core/types/bal" @@ -79,6 +82,7 @@ var Defaults = Config{ TxSyncMaxTimeout: 1 * time.Minute, SlowBlockThreshold: -1, // Disabled by default; set via --debug.logslowblock flag RangeLimit: 0, + PartialState: DefaultPartialStateConfig(), } //go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go @@ -213,6 +217,118 @@ type Config struct { RangeLimit uint64 `toml:",omitempty"` BALExecutionMode bal.BALExecutionMode + + // PartialState configures partial statefulness mode for reduced storage. + PartialState PartialStateConfig +} + +// PartialStateConfig configures partial statefulness mode. +// When enabled, the node stores all accounts but only storage for configured contracts. +// State updates are applied via Block Access Lists (BALs) per EIP-7928. +type PartialStateConfig struct { + // Enabled activates partial statefulness mode + Enabled bool + + // Contracts is the list of contracts to track storage for + Contracts []common.Address + + // ContractsFile is the path to a JSON file containing contract addresses + ContractsFile string `toml:",omitempty"` + + // BALRetention is the number of blocks to keep BAL history for reorg handling + BALRetention uint64 +} + +// DefaultPartialStateConfig returns the default partial state configuration. +func DefaultPartialStateConfig() PartialStateConfig { + return PartialStateConfig{ + Enabled: false, + Contracts: nil, + ContractsFile: "", + BALRetention: 256, + } +} + +// LoadPartialStateContracts loads contract addresses from a JSON file +// and merges them with any directly configured addresses. +func (c *PartialStateConfig) LoadPartialStateContracts() error { + if c.ContractsFile == "" { + return nil + } + return c.loadContractsFromFile(c.ContractsFile) +} + +// loadContractsFromFile reads contract addresses from a JSON file. +// File format: +// +// { +// "version": 1, +// "contracts": [ +// {"address": "0x...", "name": "WETH", "comment": "Wrapped Ether"}, +// {"address": "0x...", "name": "USDC"} +// ] +// } +func (c *PartialStateConfig) loadContractsFromFile(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read contracts file: %w", err) + } + + var file struct { + Version int `json:"version"` + Contracts []struct { + Address string `json:"address"` + Name string `json:"name,omitempty"` + Comment string `json:"comment,omitempty"` + } `json:"contracts"` + } + + if err := json.Unmarshal(data, &file); err != nil { + return fmt.Errorf("failed to parse contracts file: %w", err) + } + + // Validate version + if file.Version != 1 { + return fmt.Errorf("unsupported contracts file version: %d", file.Version) + } + + // Merge contracts from file with directly configured ones + seen := make(map[common.Address]struct{}) + for _, addr := range c.Contracts { + seen[addr] = struct{}{} + } + + for _, contract := range file.Contracts { + addr := common.HexToAddress(contract.Address) + if addr == (common.Address{}) { + return fmt.Errorf("invalid contract address in file: %s", contract.Address) + } + if _, exists := seen[addr]; !exists { + c.Contracts = append(c.Contracts, addr) + seen[addr] = struct{}{} + } + } + + return nil +} + +// Validate checks the configuration for errors. +func (c *PartialStateConfig) Validate() error { + if !c.Enabled { + return nil // Nothing to validate if disabled + } + + // Load contracts from file if specified + if err := c.LoadPartialStateContracts(); err != nil { + return err + } + + // Validate BAL retention + if c.BALRetention < 64 { + return fmt.Errorf("BAL retention must be at least 64 blocks (for BLOCKHASH support), got %d", c.BALRetention) + } + + return nil } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index ce07385a0b..974c66e4bd 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -70,6 +70,7 @@ func (c Config) MarshalTOML() (interface{}, error) { TxSyncMaxTimeout time.Duration `toml:",omitempty"` RangeLimit uint64 `toml:",omitempty"` BALExecutionMode bal.BALExecutionMode + PartialState PartialStateConfig } var enc Config enc.Genesis = c.Genesis @@ -124,6 +125,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.TxSyncMaxTimeout = c.TxSyncMaxTimeout enc.RangeLimit = c.RangeLimit enc.BALExecutionMode = c.BALExecutionMode + enc.PartialState = c.PartialState return &enc, nil } @@ -182,6 +184,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { TxSyncMaxTimeout *time.Duration `toml:",omitempty"` RangeLimit *uint64 `toml:",omitempty"` BALExecutionMode *bal.BALExecutionMode + PartialState *PartialStateConfig } var dec Config if err := unmarshal(&dec); err != nil { @@ -343,5 +346,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.BALExecutionMode != nil { c.BALExecutionMode = *dec.BALExecutionMode } + if dec.PartialState != nil { + c.PartialState = *dec.PartialState + } return nil }