diff --git a/BEPs/BEP-703.md b/BEPs/BEP-703.md new file mode 100644 index 00000000..46902200 --- /dev/null +++ b/BEPs/BEP-703.md @@ -0,0 +1,449 @@ +
+  BEP: 703
+  Title: Payment Lane on BNB Smart Chain
+  Status: Draft
+  Type: Standards
+  Created: 2026-07-17
+  Description: Reserve a dynamically-sized portion of each block's gas for payment transactions, so basic value transfer stays available during congestion without changing how any transaction is ordered.
+
+ +# BEP-703: Payment Lane on BNB Smart Chain + +- [BEP-703: Payment Lane on BNB Smart Chain](#bep-703-payment-lane-on-bnb-smart-chain) + - [1. Summary](#1-summary) + - [2. Motivation](#2-motivation) + - [3. Specification](#3-specification) + - [3.1 Block Space Model](#31-block-space-model) + - [3.2 Transaction Classification](#32-transaction-classification) + - [3.3 Reserved Gas Accounting Rule](#33-reserved-gas-accounting-rule) + - [3.4 Dynamic Quota Adjustment](#34-dynamic-quota-adjustment) + - [3.4.1 The Recurrence](#341-the-recurrence) + - [3.4.2 The Congestion Signal](#342-the-congestion-signal) + - [3.4.3 The Step](#343-the-step) + - [3.4.4 Bounds](#344-bounds) + - [3.4.5 Activation](#345-activation) + - [3.4.6 Worked Example](#346-worked-example) + - [3.5 Header Commitment](#35-header-commitment) + - [3.5.1 Why the State Must Be Committed](#351-why-the-state-must-be-committed) + - [3.5.2 What Is Committed](#352-what-is-committed) + - [3.5.3 Why UncleHash](#353-why-unclehash) + - [3.5.4 Validation](#354-validation) + - [3.5.5 What This Buys](#355-what-this-buys) + - [3.6 Parameters, Invariants, and Governance](#36-parameters-invariants-and-governance) + - [3.7 Payment Contract List](#37-payment-contract-list) + - [3.8 System Contract](#38-system-contract) + - [4. Rationale](#4-rationale) + - [5. Backward Compatibility](#5-backward-compatibility) + - [6. Security Considerations](#6-security-considerations) + - [7. License](#7-license) + +## 1. Summary + +This BEP reserves a small, dynamically-sized amount of gas in every block — the **Payment Lane** — that only payment transactions may consume. The reservation is a per-block gas accounting rule, not a region of the block: payment and non-payment transactions remain freely interleaved, ordered, and priced exactly as today. During congestion, the reservation guarantees the payment class a minimum amount of block gas — not the inclusion of any particular transaction; at all other times it stays near its minimum and the mechanism is effectively dormant. + +## 2. Motivation + +BSC fixes the base fee at zero ([BEP-226](./BEP226.md)), so transaction inclusion is decided entirely by priority-fee bidding. This works well in the common case, but during demand pulses one class of traffic bids up inclusion prices for every other class. Payment transactions — stablecoin transfers, exchange deposits and withdrawals, remittances, merchant settlement — are the traffic hit hardest: they are individually small in gas, time-sensitive, present in the overwhelming majority of blocks, and yet the least able to re-price themselves block by block. When a pulse arrives they are the first traffic crowded out, and the chain's most basic function — moving funds — becomes its least reliable one. + +This is not a capacity problem. BSC's throughput is on a sustained upward path, and total block space comfortably exceeds payment demand at all times. The defect is structural: heterogeneous traffic shares one undifferentiated bidding dimension, so local congestion in one class propagates to unrelated classes. A bounded, protocol-level reservation for the payment class severs that propagation path at negligible cost to everything else — and does so without touching the fee market or the ordering rules that the rest of the ecosystem relies on. + +## 3. Specification + +### 3.1 Block Space Model + +This section states the model in pictures; the rules that follow are its formalization. Every transaction is either a **payment transaction** or a **general transaction** — how that is decided is specified in [§3.2](#32-transaction-classification). + +``` +Legend # general tx gas = payment tx gas + : reserved for payment but unused - general may NOT take it + . block space nobody used - either class may take it + +Schematic, not to scale: the payment lane is about 4% of the block at rest +and at most about 8% when fully expanded. + + +Today, with no reservation + + <---------------- GasLimit ----------------> + quiet [######====..................................] + congested [############################################] + general demand alone fills the block; payment + traffic arriving during the pulse is not included + + +With the payment lane + + <-------- general space ---------> + quiet [######............................|====:::::] + the block is not full; the lane changes nothing + + congested, [##################################|===::::::] + payment < paymentLaneSize general stops at the lane boundary; the unused + part of the lane stays idle, and is not returned + to general + + congested, [############################======|=========] + payment > paymentLaneSize payment filled the lane and overflowed past it; + the overflow competed for general space under the + ordinary fee rules, so general got less +``` + +Two rules govern the boundary, and they are deliberately asymmetric: + +- **General transactions can never use the payment lane** — not even when it sits empty. This is where the guarantee comes from: reserved gas that could be resold to general traffic would not be a reservation at all. +- **Payment transactions can use general space** — the payment lane is a floor, never a ceiling. Payment gas beyond it carries no privilege and bids for the remaining space on equal terms with everything else. + +The bar is a gas budget, not a layout: it says nothing about where transactions sit inside the block, and both classes remain freely interleaved and ordered exactly as today. The accounting rule in [§3.3](#33-reserved-gas-accounting-rule) is this picture written as a single inequality. + +### 3.2 Transaction Classification + +A transaction is a **payment transaction** if and only if it meets three conditions common to both rules below and then satisfies one of them, evaluated against static transaction fields and the state as of the end of the parent block. Every other transaction is a **general transaction**. Nothing is executed to decide this — the class follows from static fields and a bounded parent-state lookup, so it is known before the block runs rather than as a result of running it. + +The common conditions are that `to` is non-nil, the transaction type is `0x00`, `0x01`, or `0x02`, and `accessList` is empty. A transaction failing any of them is general whatever its destination. + +| Rule | Payment when | Covers | +|---|---|---| +| Listed destination | `to` is on the payment contract list ([§3.7](#37-payment-contract-list)) | Stablecoins and [BEP-702](./BEP-702.md) native tokens, whatever function is called | +| Bare transfer | `data` is empty, `value` is non-zero, and `to` has no code in the parent state | Native BNB payments between accounts | + +With the common conditions, a bare transfer costs exactly 21000 gas: `to` non-nil rules out contract creation, empty `data` rules out calldata cost, and the access-list and type restrictions rule out everything else chargeable before execution. Its other two tests are about substance rather than cost. A non-zero `value` is what makes the transaction a payment at all: a zero-value transfer moves nothing, so reserved gas would be protecting nothing. No code at `to` is a test against the parent state, and so bounds the intrinsic cost rather than guaranteeing that nothing executes — a precompile holds no code, and an account that acquires code earlier in the same block, by deployment or by an [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) authorisation, is still classified as having none, though one already carrying a delegation designator holds code and fails the test. In both residual cases the transfer executes code inside the lane, bounded by its own gas limit; [§4](#4-rationale) says why neither is closed, and why classification is not moved to execution time. + +``` +classify(tx, parentState): + if tx.to == nil: return GENERAL # common + if tx.type not in {0x00, 0x01, 0x02}: return GENERAL # common + if tx.accessList is not empty: return GENERAL # common + + if tx.to in paymentContractList(parentState): return PAYMENT # listed + + if tx.data is empty # bare transfer + and tx.value != 0 + and parentState.codeAt(tx.to) is empty: return PAYMENT + + return GENERAL +``` + +### 3.3 Reserved Gas Accounting Rule + +For each block, let: + +``` +paymentGasUsed = total gas consumed by payment transactions in the block +generalGasUsed = the block's gas used less paymentGasUsed +paymentLaneSize = the block's reserved payment quota (computed per 3.4) +``` + +`generalGasUsed` is a residual, not a sum over general transactions, and the difference is not cosmetic. Parlia appends unsigned system transactions to the end of every block — validator reward, slashing, finality reward, and on a breathe block a validator-set update that has reached 12M gas on mainnet — and their gas is in the header's gas used. Defining the residual puts that gas on the general side without asking [§3.2](#32-transaction-classification) to classify a transaction no user submitted; one of those system transactions is a bare value transfer, so the classification route would instead turn on its destination happening to hold code. The obligation to include them also bounds the quota, in [§3.4.4](#344-bounds). + +The block MUST satisfy: + +``` +generalGasUsed + max(paymentGasUsed, paymentLaneSize) <= GasLimit +``` + +Blocks violating this rule MUST be rejected as invalid. + +The rule has exactly two regimes: + +- **Payment demand meets or exceeds the quota** (`paymentGasUsed >= paymentLaneSize`): the constraint reduces to the ordinary `paymentGasUsed + generalGasUsed <= GasLimit`. Payment gas beyond the quota competes for the remaining space under the existing priority-fee rules on equal terms with general transactions. +- **Payment demand falls short of the quota** (`paymentGasUsed < paymentLaneSize`): the shortfall reduces the gas available to general transactions one-for-one, the unused reservation staying idle for that block rather than being released ([§4](#4-rationale)). + +The rule constrains only gas totals. It says nothing about where transactions sit inside the block: payment and general transactions may be interleaved arbitrarily, and block producers keep full freedom over transaction selection and ordering within the accounting constraint. No new ordering rule, priority rule, or fee rule is introduced. + +### 3.4 Dynamic Quota Adjustment + +`paymentLaneSize` is an accumulator, not a function of the current block. Each block moves it by one step or holds it, according to how congested its parent was; where it stands today is the result of every step taken since the lane took effect, clamped back into range at each one. [§3.5](#35-header-commitment) explains why that memory cannot be avoided, and how a node obtains it without replaying history. + +Throughout, `h` is the block being built or validated and `h−1` is its parent. The arithmetic below is consensus-critical, so each step is pinned: + +- **Ratios** are integers against `RATIO_DENOM = 10000`: a stored `N` means `N / 10000`, and MUST lie in `[0, RATIO_DENOM]`. +- **Converting a ratio to gas** multiplies before dividing and truncates toward zero. +- **Quantities** are unsigned 64-bit integers; subtraction saturates at zero. +- **A ratio times a gas amount** MUST use widened intermediates: the product exceeds 64 bits above `GasLimit ≈ 2^50.7`, and consensus bounds `GasLimit` only by `2^63 − 1`. +- **Governable parameters** are read from the parent's post-state and carry the index `(h−1)`; protocol constants carry none, because they never change. +- **No floating point**, and no comparison done by division. + +#### 3.4.1 The Recurrence + +For every block from `activation + 1`: + +``` +paymentLaneSize(h) = min( quota(h), laneCap(h) ) + +quota(h) = + laneMin(h) h = activation + 1 + clamp( paymentLaneSize(h−1) + step(h), laneMin(h), laneMax(h) ) h > activation + 1 +``` + +The base case needs no clamp: [§3.4.4](#344-bounds) puts `laneMin(h)` inside the range by construction. `laneCap(h)` is the system-transaction reserve, also [§3.4.4](#344-bounds); it is the outermost operation, and the recurrence feeds on `paymentLaneSize(h−1)` — the value the parent committed, after its own cap — never on `quota(h−1)`. + +#### 3.4.2 The Congestion Signal + +``` +signalGasUsed(h−1) = generalGasUsed(h−1) + max(0, paymentGasUsed(h−1) − paymentLaneSize(h−1)) +``` + +`signalGasUsed` is the gas that competed on the open market: all general gas, plus payment gas beyond the reservation, which bid for the remaining space on equal terms. Gas the reservation covered is excluded — that space was closed to general traffic whether payment used it or not, so counting it would report contention that never happened, and would let the lane grow itself. + +Thresholds are compared without dividing: + +``` +signalGasUsed(h−1) × RATIO_DENOM vs trigger(h−1) × GasLimit(h−1) +``` + +#### 3.4.3 The Step + +``` +step(h) = + + EXPAND_STEP_RATIO(h−1) × GasLimit(h) / RATIO_DENOM + if signalGasUsed(h−1) × RATIO_DENOM >= EXPAND_TRIGGER_RATIO(h−1) × GasLimit(h−1) + + − SHRINK_STEP_RATIO(h−1) × GasLimit(h) / RATIO_DENOM + if signalGasUsed(h−1) × RATIO_DENOM < SHRINK_TRIGGER_RATIO(h−1) × GasLimit(h−1) + + 0 otherwise — the hysteresis band +``` + +The condition reads `GasLimit(h−1)` because that is the block whose congestion is being measured; the step scales by `GasLimit(h)` because that is the block whose space is being reserved. + +Both operators are normative as written: `>=` above and `<` below, so the hysteresis band is `[SHRINK_TRIGGER_RATIO, EXPAND_TRIGGER_RATIO)` and a signal landing exactly on the shrink threshold holds instead of shrinking. Invariant (1) of [§3.6](#36-parameters-invariants-and-governance) is what keeps that band non-empty; it does not decide the operators. Since `paymentLaneSize` is an accumulator, two implementations disagreeing on a single boundary block never reconverge. + +#### 3.4.4 Bounds + +``` +laneMax(h) = min( PAYMENT_LANE_MAX_RATIO(h−1) × GasLimit(h) / RATIO_DENOM, + PAYMENT_LANE_MAX(h−1) ) + +laneMin(h) = min( max( PAYMENT_LANE_MIN_RATIO(h−1) × GasLimit(h) / RATIO_DENOM, + PAYMENT_LANE_MIN(h−1) ), + laneMax(h) ) <- so laneMin(h) <= laneMax(h) at every GasLimit + +laneCap(h) = GasLimit(h) − SYSTEM_TXS_GAS_RESERVE +``` + +Each bound is the tighter of a ratio limit that tracks `GasLimit` and an absolute limit in gas ([§4](#4-rationale)). Both are recomputed every block, so the clamp of [§3.4.1](#341-the-recurrence) applies even where `step(h)` is zero. + +`laneCap(h)` is the one place this specification lets the quota fall below `laneMin(h)`. Every block is obliged to include the system transactions of [§3.3](#33-reserved-gas-accounting-rule), so the quota must leave room for them, and `SYSTEM_TXS_GAS_RESERVE` is the gas Parlia reserves for its worst case — a breathe block carrying a validator-set update. A single constant is used rather than a per-block estimate so that both sides of a block agree on it without deriving anything. + +Falling below the floor is the point of the cap. Without it, a low enough `GasLimit` makes the quota exceed what a breathe block can hold, so no valid block exists — and that halt does not clear on its own, because a breathe block is one whose parent sits in the previous UTC day, so every later candidate is again a breathe block and fails identically. With the values of [§3.6](#36-parameters-invariants-and-governance) the cap binds only below a `GasLimit` of about 21.7M, a fraction of today's; raising `PAYMENT_LANE_MAX_RATIO` moves that threshold up. + +#### 3.4.5 Activation + +`activation` is the block at which this fork takes effect and the system contract of [§3.8](#38-system-contract) first holds the parameters. Every derivation reads them from the parent's post-state ([§3.4](#34-dynamic-quota-adjustment)), so `activation + 1` is the first block that can read them, and the first the lane applies to. Block `activation` is outside the mechanism: not subject to the accounting rule of [§3.3](#33-reserved-gas-accounting-rule), and carrying no commitment — its `UncleHash` stays the empty-list hash, as in every block before it. + +`activation + 1` therefore reads parameters from its parent, but no commitment and no class split. That is what the base case of [§3.4.1](#341-the-recurrence) covers, and it is why no pre-fork block is ever asked for a split it never recorded. + +#### 3.4.6 Worked Example + +At `GasLimit = 55M`, BSC mainnet's current value, and a 0.45s block interval, with the parameters of [§3.6](#36-parameters-invariants-and-governance): + +``` +2% × 55M = 1.1M 8% × 55M = 4.4M 0.5% × 55M = 0.275M + +laneMax = min(4.4M, 8M) = 4.4M <- set by the ratio bound +laneMin = min(max(1.1M, 2M), 4.4M) = 2M <- set by the absolute bound +laneCap = 55M − 20M = 35M <- inert this far above laneMax +expand = 1.1M per block +shrink = 0.275M per block + +quiet the quota rests at 2M, 3.6% of the block; the mechanism is dormant + +congestion signal >= 80%: 2M -> 3.1M -> 4.2M -> 4.4M (clamped down from 5.3M) + 3 blocks, about 1.35s + +recovery signal < 70%: 4.4M -> 2M in steps of 0.275M + 9 blocks, about 4s +``` + +At 4.4M the reservation holds roughly 209 native transfers, or between 73 and 126 stablecoin transfers depending on the token and on whether the recipient already holds a balance — an illustration of capacity, not a guaranteed count. + +### 3.5 Header Commitment + +#### 3.5.1 Why the State Must Be Committed + +To derive `paymentLaneSize(h)`, a node needs two things about block `h−1`: the quota that was in force there, and the class split of its gas. Neither can be recovered from the header chain. The split requires the state as of `h−2` to classify `h−1`'s transactions, and the quota chains back, block by block, to `activation + 1`. + +The recurrence state is therefore committed rather than rederived, which bounds the recursion at the parent ([§3.5.5](#355-what-this-buys)). + +#### 3.5.2 What Is Committed + +Two values, encoded into the 32 bytes of `header.UncleHash`: + +| Bytes | Field | Meaning | +|---|---|---| +| `[0:8]` | `paymentLaneSize` | uint64 big-endian — the quota in force for this block | +| `[8:16]` | `paymentGasUsed` | uint64 big-endian — payment gas consumed by this block | +| `[16:32]` | reserved | MUST be zero | + +`generalGasUsed` needs no field of its own — [§3.3](#33-reserved-gas-accounting-rule) already defines it as exactly that residual. Zeroing the reserved bytes makes the encoding canonical, and rejects a header still carrying the pre-activation empty-list hash. + +#### 3.5.3 Why `UncleHash` + +Parlia permits no uncles, so `UncleHash` carries no consensus information on BSC and is pinned to the empty-list hash in every block today. Reusing it adds no header field, no size increase, and no change to block encoding — the header stays structurally identical to upstream Ethereum's. The field already sits inside the producer's seal, so authentication costs nothing. + +This BEP claims `header.UncleHash` exclusively; no other use of the field is defined. + +From `activation + 1` onward: + +1. Clients MUST NOT reject a Parlia block because `UncleHash` differs from the empty-list hash. The only structural constraint on the field is the encoding of [§3.5.2](#352-what-is-committed), checked where [§3.5.4](#354-validation) requires. +2. The uncle list in the block body MUST remain empty. Clients MUST verify this directly on the body, and MUST NOT derive the expected uncle list from `UncleHash`. +3. Every path that recomputes the uncle-list hash from the body and compares it against the header MUST be gated on that same boundary — block validation, block propagation, the block fetcher, and the downloader's body matching. A path left ungated makes committed blocks impossible to import, propagate, or sync. + +#### 3.5.4 Validation + +Every check below is mandatory. + +| Check | Where | Why there | +|---|---|---| +| Reserved bytes are zero | Header verification | A pure function of the header | +| `paymentGasUsed` does not exceed the header's gas used | Header verification | A pure function of the header, and the rule below is evaluated on it | +| `paymentLaneSize` does not exceed the header's gas limit | Header verification | Likewise; together the two bounds keep that rule's sum from wrapping | +| The accounting rule of [§3.3](#33-reserved-gas-accounting-rule) holds on the committed values | Header verification | With `generalGasUsed` a residual, the inequality is a function of the header alone | +| `paymentLaneSize` matches the value derived from the parent | Before execution, against the parent's post-state | The derivation reads governable parameters from that state, but nothing from this block beyond its header | +| `paymentGasUsed` matches the value obtained by re-execution | Block execution | Requires running the block | + +The pre-execution split keeps [BEP-675](./BEP-675.md) unchanged: everything a validator must check before blind-sealing a builder's block is decidable from the parent. + +#### 3.5.5 What This Buys + +A recursion depth of one. The derivation reads exactly four things, all of them available at the parent: + +- whether the parent carries a commitment at all — the base case of [§3.4.1](#341-the-recurrence) turns on this; +- the parent header — its committed quota and payment gas, its gas used and gas limit; +- the parent's post-state — the governable parameters; +- this block's own gas limit. + +The first MUST NOT be inferred from whether the parent's `UncleHash` decodes. A header unavailable or misread for any other reason would then be taken for a bootstrap seed, silently resetting the quota to `laneMin`, and the accumulator never reconverges from that. It is decidable from the parent header alone: a block outside the mechanism carries the empty-list hash ([§3.4.5](#345-activation)), and no commitment can equal it, since the reserved bytes of [§3.5.2](#352-what-is-committed) are zero and the empty-list hash's are not. A genesis parent is covered by the same test. + +Nothing older than the parent's own header is required, so snap sync, restart from disk, reorg, and history pruning all reduce to reading the parent header against state the node already holds: no history to replay, no bodies to fetch, and none to retain. + +### 3.6 Parameters, Invariants, and Governance + +The adjustment algorithm itself (the signal, the hysteresis structure, the accounting rule) is protocol logic and is not governable. The values that drive it are governable parameters, held in the system contract of [§3.8](#38-system-contract) and taking effect only through the standard BSC on-chain governance process: + +| Name | Meaning | Kind | +|---|---|---| +| `PAYMENT_LANE_MIN_RATIO` | Ratio lower bound | Governable | +| `PAYMENT_LANE_MAX_RATIO` | Ratio upper bound | Governable | +| `PAYMENT_LANE_MIN` | Absolute lower bound (gas) | Governable | +| `PAYMENT_LANE_MAX` | Absolute upper bound (gas) | Governable | +| `EXPAND_TRIGGER_RATIO` | Congestion threshold that triggers expansion | Governable | +| `SHRINK_TRIGGER_RATIO` | Slack threshold that triggers contraction | Governable | +| `EXPAND_STEP_RATIO` | Per-block expansion step | Governable | +| `SHRINK_STEP_RATIO` | Per-block contraction step | Governable | +| `TRIGGER_GAP_MIN` | Minimum hysteresis band width, fixed at 1000 (10%) | Protocol constant | +| `RATIO_GAP_MIN` | Minimum ratio range width, fixed at 500 (5%) | Protocol constant | +| `RATIO_DENOM` | Denominator of every ratio parameter, fixed at 10000 | Protocol constant | +| `SYSTEM_TXS_GAS_RESERVE` | Gas the quota must leave for Parlia's system transactions ([§3.4.4](#344-bounds)), fixed at 20,000,000 | Protocol constant | + +Governable parameters are not free-form. The following invariants are validated by the protocol before any parameter change takes effect; **a governance proposal violating any of them is invalid** and never activates. The two gap constants are protocol constants, not governable — governance may move the ranges but can never collapse them: + +``` +(1) EXPAND_TRIGGER_RATIO − SHRINK_TRIGGER_RATIO >= TRIGGER_GAP_MIN + The expansion threshold must sit above the contraction threshold by at + least the minimum band width; otherwise usage hovering near one boundary + makes the quota thrash block by block. + +(2) EXPAND_STEP_RATIO > SHRINK_STEP_RATIO > 0 + Protection must arrive before space is handed back, or a sawtooth + demand pattern contracts the quota faster than it can re-expand. + +(3) PAYMENT_LANE_MAX_RATIO − PAYMENT_LANE_MIN_RATIO >= RATIO_GAP_MIN + Keeps the ratio range from collapsing. It does not guarantee a wide + effective range: the absolute bounds can narrow that independently, and + by how much depends on GasLimit, which is not known when a parameter + change is validated. A narrow effective range leaves the quota + near-constant — a loss of the dynamic behaviour, not a hazard. + +(4) PAYMENT_LANE_MAX > PAYMENT_LANE_MIN > 0 + The absolute range must be non-empty. + +(5) PAYMENT_LANE_MAX_RATIO <= RATIO_DENOM − EXPAND_TRIGGER_RATIO + Reserved gas never competes, so the signal cannot exceed the unreserved + part of the block: once the quota passes RATIO_DENOM − EXPAND_TRIGGER_RATIO + the signal can no longer reach the trigger and the quota stops growing. + A maximum above that point is one the mechanism can never reach. + +(6) EXPAND_STEP_RATIO <= EXPAND_TRIGGER_RATIO − SHRINK_TRIGGER_RATIO + The signal moves one for one with the quota where it responds at all + (3.4.2), so a step wider than the band jumps clean over it: expanding + at one trigger lands the signal past the other, and the quota reverses + the next block instead of settling. Invariant (2) bounds + SHRINK_STEP_RATIO the same way. +``` + +The eight values below are normative, not suggestions: a parameter governance has never written reads as the value listed here, so from activation until the first governance change these *are* the parameters, and one never written keeps reading its listed value indefinitely — which also means revising one of these at a later fork revises every parameter governance has never written. Two clients disagreeing on a single digit derive different quotas from the first lane block onward, and the accumulator of [§3.4](#34-dynamic-quota-adjustment) never reconverges. What does not depend on the specific numbers is the mechanism: any tuple satisfying the invariants above behaves correctly. What every client must share is the same tuple. + +``` +PAYMENT_LANE_MIN_RATIO = 200 (2%) PAYMENT_LANE_MAX_RATIO = 800 (8%) +PAYMENT_LANE_MIN = 2M gas PAYMENT_LANE_MAX = 8M gas +EXPAND_TRIGGER_RATIO = 8000 (80%) SHRINK_TRIGGER_RATIO = 7000 (70%) +EXPAND_STEP_RATIO = 200 (2%) SHRINK_STEP_RATIO = 50 (0.5%) + +Invariant check: + (1) 8000−7000 = 1000 >= 1000 ✓ (2) 200 > 50 > 0 ✓ + (3) 800−200 = 600 >= 500 ✓ (4) 8M > 2M > 0 ✓ + (5) 800 <= 10000−8000 = 2000 ✓ (6) 200 <= 8000−7000 = 1000 ✓ +``` + +[§3.4.6](#346-worked-example) works these values through a congestion episode. + +### 3.7 Payment Contract List + +The **payment contract list** is the first rule of [§3.2](#32-transaction-classification). It lives in the system contract of [§3.8](#38-system-contract) and changes only through the standard BSC governance process (validator voting), taking effect from the block after the change lands. It curates assets, not operations — listing is by address, whatever function a transaction calls — and it does not lift §3.2's common conditions. Two kinds of asset are eligible, on the same vote but different evidence. **The list starts empty: the fork lists nothing, and every entry arrives by governance afterwards.** + +**Stablecoins.** Admission rests on governance judgement alone — off-chain audit history and on-chain reputation — with no mechanical gate behind it. The intended first entries are USDT and USDC: USDT is non-upgradeable, its bytecode fixed since deployment; USDC is an upgradeable proxy, audited on each upgrade under considerable market scrutiny. Governance therefore carries the ongoing monitoring as well as the nomination, and this trust is explicit. + +**[BEP-702](./BEP-702.md) native tokens.** These tokens deploy no bytecode, expose a function surface fixed by the protocol standard, and have no upgradeable implementation to drift, so what an ordinary contract needs an audit to establish holds here by construction. What still varies is the issuer's own configuration, so a voter must additionally establish that the address lies in BEP-702's reserved native-token space with a token actually created there, and that the token's configuration — admin custody, role assignments, transfer policies, supply cap — is fit for payment use; one whose receiver policy is a narrow allowlist is not a general payment asset. None of that is enforced on chain: the protocol reads membership and nothing else, so both kinds of listing are exactly as sound as the vote behind them. + +**Removal** uses the same vote and takes effect the same way, at any time, for either kind. It strips lane eligibility and nothing else: transactions to a removed address are ordinary general transactions from the next block onward. + +### 3.8 System Contract + +*TBD.* + +The governable parameters of [§3.6](#36-parameters-invariants-and-governance) and the payment contract list of [§3.7](#37-payment-contract-list) both live in a system contract, and both are read from the parent block's final state. This section will fix the parts an implementation cannot infer from the rules above: + +- the contract's address, and its storage layout for the parameter set and for a list entry; +- the update interface, and which of BSC's existing governance paths reaches it; +- when a change takes effect, stated against the same block boundary the rest of this specification uses; +- how the invariants of [§3.6](#36-parameters-invariants-and-governance) are enforced on chain. In particular, what "a governance proposal violating any of them is invalid and never activates" means as contract behaviour: whether such a proposal is rejected at submission, reverts at execution, or is accepted and ignored — and what a node observes in each case. + +## 4. Rationale + +**A quota, not a region.** A reserved region would have to sit somewhere in the block, which makes position part of the rule. A gas quota does not: the whole proposal is one accounting inequality checked at validation, with selection, ordering and pricing untouched. + +**Unused quota is idle, not reclaimable.** If the shortfall flowed back to general transactions, excluding payment transactions would cost a producer nothing — the space would simply be resold, and the floor would be a fiction precisely in the congested moments it exists for. Binding the reservation regardless of realized demand inverts the economics: reserved gas earns fees only when payment transactions fill it, so including them is the only way to monetize it. + +**Congestion-only signal, no motive judgment.** A signal that tried to classify *why* the chain is congested — say, the share of payment transactions in recent traffic — would be written by whoever selects transactions, and steerable at near-zero cost. `signalGasUsed` ([§3.4.2](#342-the-congestion-signal)) instead counts only gas that competed on the open market, every unit of which is aligned with fee revenue: suppressing the signal means leaving paying transactions out, so denying congestion costs income. The rule reduces to *congestion expands the quota, regardless of cause*; the price — congestion unrelated to payments expands it too — is bounded by the quota maximum and unwinds once the congestion clears. + +**Ratio and absolute bounds together.** Ratio bounds let the quota scale automatically with block capacity so the mechanism never needs re-tuning as GasLimit grows; absolute bounds cap its footprint so growth in block size can never silently grow the reservation beyond an intended ceiling. + +**Classification by destination, not declaration.** Deriving the class from `to` plus parent state means no new transaction field, no wallet changes, and no user action: the protocol recognises an existing fact rather than accepting a claim. + +Classifying on code costs one thing: code can still run inside the lane, at a precompile or at an account that gains code earlier in the same block ([§3.2](#32-transaction-classification)). Neither is closed, for different reasons. Closing the same-block case would mean classifying against execution-time state, which opens a worse one — whoever orders the block would then decide which users' transfers are lane-eligible, by placing one cheap deployment ahead of them, and classification would stop being a fact the protocol recognises and become a lever the producer holds. No state test catches a precompile, which holds no code at any point, so only an address-set test would — and that set is fork- and chain-dependent: every client would have to carry it identically or diverge on classification, which the accumulator of [§3.4](#34-dynamic-quota-adjustment) never reconverges from. That is a wider divergence surface than the single case the test closes. Both residuals are bounded by the offending transaction's own gas limit, which is what makes leaving them open affordable. + +## 5. Backward Compatibility + +This is a consensus-breaking change and requires a hard fork: it adds a block validity rule, the quota computation behind it, and a header commitment. Transaction formats, the fee mechanism and ordering are untouched, so nothing that submits or handles transactions needs to change. + +Node implementations change substantially: the accounting rule, the quota recurrence, the commitment encoding, and the four uncle-validation paths named in [§3.5.3](#353-why-unclehash) all need fork-gated changes. + +Two observable values do change, and downstream tooling should account for both: + +- `sha3Uncles` is no longer the constant empty-list hash ([§3.5.3](#353-why-unclehash)). Any consumer that compares it against that constant, or derives the uncle list from it, must be updated. +- A full block no longer shows `gasUsed / GasLimit` near 1: the ratio falls short by whatever part of the reservation payment traffic left unused. Explorers, gas-price oracles and alerting that read it as a congestion indicator will read low, by at most the quota. + +Otherwise the quota rests at its minimum and behaviour is as it is today. Under congestion general traffic gives up at most `PAYMENT_LANE_MAX_RATIO` of the block, and gets it back through the contraction path once the congestion clears. + +## 6. Security Considerations + +- **Spam within the lane.** The quota grants no free inclusion: payment transactions compete among themselves for reserved gas under the ordinary priority-fee rules, so a flood pays full fees throughout. Sustained payment traffic beyond the quota does raise the signal and expand the reservation ([§3.4.2](#342-the-congestion-signal)), but general traffic feels that only when it is itself congested — at which point its own usage would have expanded the quota anyway. The reservation bounds volume, not the size of any one transaction: a single payment transaction may occupy all of it. +- **Bounded waste.** Idle reserved gas is capped per block by `laneMax` ([§3.4.4](#344-bounds)). The bound is per block, not cumulative: while the signal that raised the quota persists so does the waste, and it unwinds only through the contraction path. +- **Signal manipulation.** Suppressing the expansion signal requires producing under-filled blocks during peak fee demand — the manipulation is directly and continuously costly ([§4](#4-rationale)). +- **List governance.** Listing and delisting are ordinary governance votes ([§3.7](#37-payment-contract-list)), and a governance failure's blast radius is confined to lane eligibility: a wrongly listed contract gains a bounded gas floor, never new execution powers. For a BEP-702 native token a vote can strip eligibility but can never weaken the properties BEP-702 enforces on it. +- **Parameter safety.** The invariants in [§3.6](#36-parameters-invariants-and-governance) are validated before any parameter change activates, so no governance action can invert the two thresholds, invert the two steps, take a step wide enough to cross the hysteresis band, or set a maximum the quota could never reach. What they cannot guarantee is a wide effective range (invariant (3)) — but a narrow one costs only the dynamic behaviour; no rule is violated. + +## 7. License + +The content is licensed under [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/README.md b/README.md index a7c3b346..73d03792 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Here is the list of subjects of BEPs: | [BEP-677](./BEPs/BEP-677.md) | Implement EIP-8056 Scaled UI Amount | Standards | Draft | | [BEP-682](./BEPs/BEP-682.md) | Reject Duplicate Validators in CometBFT Light Block Validation | Standards | Draft | | [BEP-695](./BEPs/BEP-695.md) | Staking and Governance Security Hardening | Standards | Draft | +| [BEP-703](./BEPs/BEP-703.md) | Payment Lane on BNB Smart Chain | Standards | Draft | # BAPs BAP (BNB Application Proposal) defines standards for application layer interactions on BNB Chain. Unlike BEPs which govern core protocol changes, BAPs focus on establishing conventions and interfaces for how applications communicate and interact with each other within the BNB Chain ecosystem.