-
Notifications
You must be signed in to change notification settings - Fork 0
Extension Points
Everything this plugin plugs into Besu, in one place — both the new extension points the fork adds (which the plugin provides and Besu was taught to consult) and the standard Besu services the plugin consumes. All wiring happens in ClassicPlugin (register() provides services; start() consumes them).
Interfaces the fork adds live in the fork: diega/besu @
plugin-extensions. The standard services already exist in upstream Besu; the plugin just uses them.Classic*links point at this plugin's source.
| Point | Plugin side | Registered via | |
|---|---|---|---|
| Provided (fork's new points) | NetworkProvider |
ClassicNetworkProvider |
addService |
ForkIdProvider |
ClassicForkIdProvider |
addService |
|
ProtocolScheduleCustomizer |
ClassicProtocolSpecs |
addService |
|
| Consumed (standard Besu services) | PicoCLIOptions |
ClassicOptions |
getService |
BlockchainService + BesuEvents
|
ChainTracker |
getService |
Mental model: addService = the plugin implements a point this fork added to Besu · getService = the plugin uses a service Besu already provides.
Definition — plugin-api/.../services/NetworkProvider.java (@Unstable). Consulted as a fallback when --network=<name> is not a built-in network, so a plugin can add networks without touching the core enum.
public interface NetworkProvider extends BesuService {
Optional<NetworkSpec> findNetwork(String networkName); // empty ⇒ not ours
}
// datatypes — implemented by NetworkDefinition and by plugins
public interface NetworkSpec {
BigInteger getNetworkId();
URL getGenesisConfigUrl();
boolean canSnapSync();
}Implementation — ClassicNetworkProvider: findNetwork returns a NetworkSpec for classic (networkId 1) / mordor (networkId 7) over the bundled classic.json / mordor.json, else Optional.empty(). The chain id (61 / 63) is read from the genesis, not the provider. Makes --network=classic and --network=mordor resolve.
Definition — plugin-api/.../services/ForkIdProvider.java. When a provider applies, the fork-ID manager uses its schedule instead of the built-in genesis options, so a non-mainnet chain advertises correct EIP-2124 fork IDs to peers. A single Optional-returning lookup (present ⇒ applies, empty ⇒ use genesis); chain ID is passed per call because the provider is registered before the active chain is known.
public interface ForkIdProvider extends BesuService {
Optional<ForkSchedule> forkScheduleFor(BigInteger chainId); // empty ⇒ not ours
record ForkSchedule(List<Long> blockNumbers, List<Long> timestamps) {}
}Implementation — ClassicForkIdProvider: forkScheduleFor returns a ForkSchedule for ETC chain ids via ClassicGenesisConfig.fromChainId (which parses the bundled classic.json / mordor.json and returns the blocks .distinct().sorted(); timestamps empty — ETC has no timestamp forks), else Optional.empty(). No fork block is hardcoded. (ClassicGenesisConfig also feeds ClassicProtocolSpecs — fork blocks and ecip1017EraRounds.)
Definition — ethereum/core/.../mainnet/ProtocolScheduleCustomizer.java. The mechanism for injecting custom hardfork rules into the schedule.
public interface ProtocolScheduleCustomizer extends BesuService {
// non-null: return an empty map to contribute nothing
Map<Long, Function<ProtocolSpecBuilder, ProtocolSpecBuilder>> createAdapters(GenesisConfigOptions config);
}-
Floor semantics: an adapter registered at block N applies to every milestone ≥ N until the next adapter entry; it receives the
ProtocolSpecBuilderfrom the nearest prior milestone and mutates it. - Applied by
MainnetProtocolSchedule.fromConfig(...), which queries the ServiceManager for the customizer and folds its map into the schedule.
Implementation — ClassicProtocolSpecs::createAdapters. Natively, ETC was wired straight into MainnetProtocolSpecFactory / ProtocolScheduleBuilder as a chain of inherited xxxDefinition methods; a plugin can't hook into that engine, so it was rewritten to emit adapters over the mainnet schedule. Net consensus state per fork is equivalent (verified), fork by fork:
| Fork | Block | Delta over the mainnet builder |
|---|---|---|
| TangerineWhistle | 2,500,000 | replay protection (ECIP-1015) |
| DieHard | 3,000,000 | + DieHardGasCalculator (EIP-160) + paused bomb |
| Gotham | 5,000,000 | + ClassicBlockProcessor (ECIP-1017) + delayed bomb |
| DefuseBomb | 5,900,000 | + removed bomb (ECIP-1041) |
| Atlantis | 8,772,000 |
ClassicBlockProcessor + EIP-100 difficulty |
| Thanos | 11,700,000 | + ECIP-1099 epoch headers + EIP-100 |
| Mystique | 14,525,000 | + LondonGasCalculator + EIP-3541 |
| Spiral | 19,250,000 | + ShanghaiGasCalculator + PUSH0 + warm coinbase |
(Agharta / Phoenix / Magneto have no own entry — they inherit the prior adapter via floor semantics.)
These already exist in upstream Besu (any plugin uses them); the plugin obtains them with context.getService(...). They are not part of the fork.
PicoCLIOptions → ClassicOptions
Service — plugin-api/.../services/PicoCLIOptions.java. The standard way a plugin contributes CLI options.
Use — ClassicPlugin.register() calls getService(PicoCLIOptions.class).ifPresent(o -> o.addPicoCLIOptions("classic", options)), registering ClassicOptions under the classic namespace → the --plugin-classic-safe-block-depth (default 24) and --plugin-classic-finalized-block-depth (default 400) flags.
BlockchainService + BesuEvents → ChainTracker
Services — BlockchainService (read head/blocks, set safe & finalized) and BesuEvents (subscribe to added blocks).
Use — ClassicPlugin.start() seeds a ChainTracker from the current head and subscribes via besuEvents.addBlockAddedListener(...) to recompute on every canonical block. ChainTracker sets safe = head - 24 and finalized = head - 400 (configurable) through BlockchainService.
Why it's needed. ETC is PoW and has no consensus layer / Engine API. In Besu's post-merge codebase, safe and finalized are normally set by the CL (forkchoiceUpdated). With nobody setting them, blockchain.finalizedBlockHeader() / safeBlockHeader() stay empty, and any JSON-RPC call using the "finalized" or "safe" block tag returns UNKNOWN_BLOCK (AbstractBlockParameterMethod#posRelatedResult → orElseGet(UNKNOWN_BLOCK)). That breaks the many post-merge tools — wallets, indexers, bridges, ethers/web3 calls — that read those tags. ChainTracker is a minimal PoW "CL stub" that derives the labels by depth and sets them so those queries work.
On reorgs / finality. The labels are reactive — recomputed as max(0, head - depth) on every head update, with no memory of the previous value — so on a reorg they can move backwards. They are heuristic confirmation depths, not protocol finality, and they do not constrain deep reorgs: Besu's PoW path never rejects a reorg for being below finalized (that label only feeds a metric + these JSON-RPC tags), and the Bonsai trie-log pruner retains above min(finalized, head - maxLayersToLoad), so the plugin's finalized can only extend the retained window, never shrink it below Bonsai's default (~512 blocks). The only observable effect of a deep reorg is that safe/finalized over RPC may move backwards — consistent with there being no real finality on a PoW chain.
The feature also widens a few core types so the plugin can reuse/extend them (the entire content of the chore: increase visibility commit on plugin-extensions):
| Type | Change |
|---|---|
AbstractBlockProcessor#rewardCoinbase |
package-private → protected (lets ClassicBlockProcessor override it cross-package) |
AbstractBlockProcessor#blockReward |
package-private → protected (read by the subclass) |
MainnetBlockHeaderValidator#createLegacyFeeMarketOmmerValidator |
both overloads → public (matches the already-public createPgaBlockHeaderValidator) |
MainnetEVMs#registerIstanbulOperations |
→ public
|
Not widened: MAX_GENERATION (the plugin vendors its own private static final int MAX_GENERATION = 6; — a stable spec constant) and the concrete *TransactionReceiptFactory classes (unused by the plugin).
Documents the plugin-extensions fork of diega/besu. Curated page — not auto-generated from a diff.