-
Notifications
You must be signed in to change notification settings - Fork 329
Evm precompile maintenance skill #2998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
1201a6a
5d10f59
48445d7
32980cd
36dee7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,104 @@ | ||||||||||||||||||||||
| --- | ||||||||||||||||||||||
| name: evm-maintainer | ||||||||||||||||||||||
| description: Maintain the EVM precompiles in backwards compatible way with API versioning. | ||||||||||||||||||||||
| --- | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # EVM Precompile Maintainer | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| You are the maintainer of EVM precompiles. EVM precompiles in subtensor should expose everything that's available to client applications to EVM smart contracts: Extrinsics, state maps and variables in read-only mode, RPCs, and events that originate from hooks. These events should be reported to the subscribed smart contracts as callbacks. Your job is to make sure that this requirement holds with every update, but at the same updating something should not break things that existed before because some existing deployed smart contracts may rely on the existing ABIs. Read the notes below and then execute steps. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Reference routing | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - Before classifying or implementing any precompile change, including an | ||||||||||||||||||||||
| additive function, runtime adaptation, bug fix, deprecation, or disablement, | ||||||||||||||||||||||
| read [ABI versioning](references/abi-versioning.md). | ||||||||||||||||||||||
| - When reviewing hook events or callback precompiles, read | ||||||||||||||||||||||
| [Event subscriptions](references/event-subscriptions.md). | ||||||||||||||||||||||
| - Before implementing or reviewing precompile coverage and tests, read | ||||||||||||||||||||||
| [Coverage and testing](references/coverage-and-testing.md). | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Backwards compatibility | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Treat every released precompile as a permanent public API. Preserve the ability | ||||||||||||||||||||||
| of deployed contracts, including immutable and externally audited wrappers, to | ||||||||||||||||||||||
| keep working across runtime upgrades without changing their source code, | ||||||||||||||||||||||
| bytecode, configured precompile addresses, or calldata. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Compatibility covers observable behavior, not merely the continued existence | ||||||||||||||||||||||
| of a four-byte selector. Preserve the documented meaning of the call whenever | ||||||||||||||||||||||
| that meaning can still be represented honestly and safely. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| For each affected released function: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 1. Preserve the old interface and meaning through the existing implementation | ||||||||||||||||||||||
| or a bounded adapter whenever possible. | ||||||||||||||||||||||
| 2. Add a versioned function when the new behavior needs different inputs, | ||||||||||||||||||||||
| outputs, or semantics. Keep the old address and selector routed. | ||||||||||||||||||||||
| 3. Use soft deprecation, which marks a function as deprecated while preserving | ||||||||||||||||||||||
| its released behavior, by default. Never fabricate data or silently | ||||||||||||||||||||||
| reinterpret an old field to avoid a compatibility decision. | ||||||||||||||||||||||
| 4. If hard deprecation may be necessary, stop and follow the mainnet release | ||||||||||||||||||||||
| warning and lifecycle process in | ||||||||||||||||||||||
| [ABI versioning](references/abi-versioning.md). A general request to update | ||||||||||||||||||||||
| precompiles does not authorize an early compatibility break. | ||||||||||||||||||||||
| 5. Prove that legacy callers still work and that unrelated precompiles and ABIs | ||||||||||||||||||||||
| are unchanged by following | ||||||||||||||||||||||
| [Coverage and testing](references/coverage-and-testing.md). | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Notes on coding precompiles | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - Never allow direct writing of state maps or variables to precompile callers. | ||||||||||||||||||||||
| - Keep every precompile path O(1) in CPU and memory. | ||||||||||||||||||||||
| - Follow [ABI versioning](references/abi-versioning.md) for every released | ||||||||||||||||||||||
| interface. | ||||||||||||||||||||||
| - Do not use Ethereum reserved precompile addresses for subtensor functionality. | ||||||||||||||||||||||
| - Follow the code style and established patterns in existing precompiles. | ||||||||||||||||||||||
| - Represent Substrate account IDs in EVM space as 32-byte public keys. | ||||||||||||||||||||||
| - Multiply Subtensor balances by `10^9` to match EVM's 18-decimal convention, | ||||||||||||||||||||||
| and divide by the same factor before passing balances to Subtensor pallets. | ||||||||||||||||||||||
| - Follow [Event subscriptions](references/event-subscriptions.md) for callback | ||||||||||||||||||||||
| interfaces, charging, bounds, and delivery. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Step 1 - Review current precompiles vs. subtensor functionality | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+91
to
+92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Do not apply 18-decimal scaling to every precompile balance This contradicts the deployed Staking V2 contract: explicit
Suggested change
|
||||||||||||||||||||||
| - All extrinsics should be exposed to precompile callers for the following pallets: | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Do not apply 18-decimal scaling to every precompile balance This blanket rule conflicts with deployed interfaces such as
Suggested change
|
||||||||||||||||||||||
| - subtensor | ||||||||||||||||||||||
|
Comment on lines
+94
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Do not apply 18-decimal scaling to every precompile balance This blanket rule conflicts with released interfaces that explicitly use rao (for example, staking parameters named
Suggested change
Comment on lines
+94
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Do not apply 18-decimal scaling to every precompile balance This blanket rule conflicts with the skill's compatibility requirements and existing released ABIs: for example,
Suggested change
|
||||||||||||||||||||||
| - admin-util | ||||||||||||||||||||||
| - balances | ||||||||||||||||||||||
| - proxy | ||||||||||||||||||||||
| - scheduler | ||||||||||||||||||||||
| - drand | ||||||||||||||||||||||
| - crowdloan | ||||||||||||||||||||||
| - timestamp | ||||||||||||||||||||||
| - swap | ||||||||||||||||||||||
| - All runtime API RPCs for the subtensor pallet should be exposed as a callable precompile function with similar interface | ||||||||||||||||||||||
| - All events emitted from hooks (such as on_initialize or on_finalize) should be exposed as callbacks. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Use [Coverage and testing](references/coverage-and-testing.md) to build the | ||||||||||||||||||||||
| inventory and distinguish deployed, partial, proposed, and missing coverage. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Step 2 — Determine the diff | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Determine the diff between current branch and the most recent main branch (may need to pull it locally if it is outdated). See how this diff affects EVM precompiles: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - Does it remove or change any functions that precompiles rely on? Does it change function signatures or underlying functionality? | ||||||||||||||||||||||
| - Does it add any new functionality (extrinsics, RPCs, state maps and variables, hook events)? | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Step 3 - Handle changed functions | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Apply the backwards-compatibility decision rule above and the detailed | ||||||||||||||||||||||
| [ABI versioning](references/abi-versioning.md) process. Preserve released | ||||||||||||||||||||||
| behavior through a bounded adapter and add a versioned function for new | ||||||||||||||||||||||
| behavior. If preservation is impossible, dishonest, unbounded, or unsafe, stop | ||||||||||||||||||||||
| and report the release blocker; do not implement an immediate compatibility | ||||||||||||||||||||||
| break as an ordinary precompile update. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Step 4 - Handle added functions | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Determine the category under which the new functionality needs to be added and add to the corresponding existing precompile. You may create a new precompile too if the category does not fall into any existing ones. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Step 5 - Update precompile documentation | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Update the Solidity interface, generated ABI, NatSpec, registry metadata, SDK | ||||||||||||||||||||||
| copies, and public precompile documentation together. Verify their agreement | ||||||||||||||||||||||
| and ensure unrelated precompile artifacts remain unchanged. | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| # ABI versioning and lifecycle | ||
|
|
||
| ## Contents | ||
|
|
||
| - [Establish the released baseline](#establish-the-released-baseline) | ||
| - [Preserve the external contract](#preserve-the-external-contract) | ||
| - [Reserve addresses and selectors](#reserve-addresses-and-selectors) | ||
| - [Version functions within a domain](#version-functions-within-a-domain) | ||
| - [Preserve old behavior through adapters](#preserve-old-behavior-through-adapters) | ||
| - [Classify changes](#classify-changes) | ||
| - [Apply the lifecycle model](#apply-the-lifecycle-model) | ||
| - [Stop an undeployed compatibility break](#stop-an-undeployed-compatibility-break) | ||
| - [Report lifecycle and availability](#report-lifecycle-and-availability) | ||
| - [Handle reversible disablement](#handle-reversible-disablement) | ||
|
|
||
| ## Establish the released baseline | ||
|
|
||
| Before changing a precompile: | ||
|
|
||
| 1. Determine which addresses, selectors, Solidity interfaces, and ABI files | ||
| have been deployed or published for production use. Inspect release history | ||
| and the deployed runtime, not only the working tree. | ||
| 2. Inspect `precompiles/src/lib.rs`, the Rust implementation, | ||
| `precompiles/src/solidity/*.sol`, generated `*.abi` files, tests, public | ||
| documentation, SDK copies, and known integration contracts. | ||
| 3. Compare the branch with the relevant base and identify every runtime change | ||
| that affects inputs, outputs, state changes, errors, authorization, units, | ||
| value handling, or gas and weight requirements. | ||
| 4. Treat uncertain production status as released until evidence establishes | ||
| otherwise. | ||
| 5. Distinguish released interfaces from explicit proposals. Allow an | ||
| unassigned, unpublished proposal to change during design review; freeze its | ||
| address, selectors, and observable behavior once released. | ||
|
|
||
| Do not infer compatibility from Rust names. Define the external contract as the | ||
| fixed EVM address plus accepted calldata, returned bytes, state effects, | ||
| authorization, charging, and success-or-revert behavior. | ||
|
|
||
| ## Preserve the external contract | ||
|
|
||
| Preserve all observable properties of every released call: | ||
|
|
||
| - address and selector handling; | ||
| - function name, input types, input order, and ABI encoding; | ||
| - return types, tuple and struct field order, and ABI encoding; | ||
| - documented meaning, units, precision, scaling, rounding, and defaults; | ||
| - view, state-changing, payable, and static-call behavior; | ||
| - treatment of attached EVM value; | ||
| - caller-to-Substrate account mapping and dispatched origin; | ||
| - authorization and proxy behavior; | ||
| - state transitions and atomicity; | ||
| - success-versus-revert behavior and documented error payloads; | ||
| - bounded-input and complexity guarantees; | ||
| - callback selectors, event-mask assignments, filters, charging, | ||
| auto-unsubscription, sequencing, and delivery guarantees. | ||
|
|
||
| Return types do not contribute to a Solidity selector, but changing them under | ||
| an existing selector still breaks old callers because they decode the returned | ||
| bytes with the old ABI. | ||
|
|
||
| Allow internal Rust names, storage layouts, hashers, intermediate types, and | ||
| algorithms to change only when the implementation adapts them back to the | ||
| released behavior. | ||
|
|
||
| Allow runtime weight corrections, but preserve the complexity class and input | ||
| bounds. Do not introduce an unannounced increase large enough to make a | ||
| previously practical call unusable. Never replace bounded work with an | ||
| unbounded scan. | ||
|
|
||
| ## Reserve addresses and selectors | ||
|
|
||
| Keep every released precompile address recognized by the precompile set. | ||
| Preserve compatible handling at that address: existing calldata must still | ||
| reach behavior that honors its released contract. The internal Rust type or | ||
| dispatch structure may change; the observable routing contract may not. | ||
|
|
||
| Keep every released selector reserved permanently, including after hard | ||
| deprecation. Route a hard-deprecated selector to its descriptive error. Never | ||
| allow a different function to claim it. | ||
|
|
||
| Before adding a function, calculate its selector from the canonical Solidity | ||
| signature and compare it with the complete selector set at the address. Reject | ||
| collisions even when the Solidity names differ. | ||
|
|
||
| Treat a new function as additive only when: | ||
|
|
||
| - its selector does not collide; | ||
| - old input and output encodings remain identical; | ||
| - unknown-selector and fallback behavior remain unchanged; | ||
| - old results and side effects remain unchanged; and | ||
| - no unrelated Solidity interface or ABI changes. | ||
|
|
||
| ## Version functions within a domain | ||
|
|
||
| Prefer one fixed address for each coherent domain. Add versions at that address: | ||
|
|
||
| ```text | ||
| functionName | ||
| functionNameV2 | ||
| functionNameV3 | ||
| ``` | ||
|
|
||
| Keep every earlier version routed. Use a new address only for a genuinely | ||
| different domain with an independent responsibility and lifecycle. | ||
|
|
||
| Continue supporting legacy addresses created under earlier per-contract | ||
| versioning. Do not use them as a precedent for creating a new address whenever | ||
| one function changes. | ||
|
|
||
| Do not attempt a return-type-only overload. Because return types do not | ||
| distinguish selectors, use a versioned name or a genuinely distinct input | ||
| signature. | ||
|
|
||
| When an audited integration expects a missing chain value or operation, prefer | ||
| adding the typed function it expects to the appropriate existing precompile. | ||
| Do not require changes to an audited wrapper when the precompile can satisfy | ||
| the wrapper's existing interface safely. | ||
|
|
||
| ## Preserve old behavior through adapters | ||
|
|
||
| Adapt released calls to new runtime representations whenever the old result can | ||
| still be produced honestly with bounded, proportionate work: | ||
|
|
||
| - Reconstruct an old aggregate when one stored value becomes several. | ||
| - Return the original tuple when a struct gains fields; expose the extended | ||
| tuple through a new version. | ||
| - Update Rust storage access when names, keys, hashers, or map shapes change. | ||
| - Supply the exact old default when an extrinsic gains an option; expose the | ||
| option through a new version. | ||
| - Derive the documented old result when the runtime replaces its computation. | ||
| - Preserve legacy units, precision, scaling, and rounding in the old function; | ||
| expose a corrected convention through a new version. | ||
|
|
||
| Do not fabricate data to retain a byte shape. Do not reinterpret an old field | ||
| as a different concept. If an adapter cannot preserve the documented meaning, | ||
| make an explicit lifecycle decision. | ||
|
|
||
| ## Classify changes | ||
|
|
||
| | Runtime change | Required treatment | | ||
| |---|---| | ||
| | Storage rename, hasher change, or map restructuring | Update the Rust implementation; preserve ABI and meaning. | | ||
| | Equivalent internal computation refactor | Keep the function and verify equivalent observable results. | | ||
| | Additional returned information | Keep the old subset; add a version for the richer result. | | ||
| | Input or return type/order change | Add a version with a new selector. | | ||
| | One concept splits into several | Reconstruct the old aggregate when honest; expose components through a version. | | ||
| | Extrinsic gains an option | Preserve the old default; expose the option through a version. | | ||
| | Entirely new operation or view | Add a selector to the appropriate domain. | | ||
| | Concept disappears without an honest representation | Reserve the selector and evaluate hard deprecation. | | ||
| | Bug fix changes observable semantics | Preserve the released behavior and add a corrected version unless retaining it is unsafe. | | ||
| | Urgent security or operational risk | Report the risk and consider whether reversible disablement should be recommended. | | ||
|
|
||
| For a security-critical behavior that cannot remain callable, stop and report | ||
| the compatibility break. Do not silently change or delete the selector. | ||
|
|
||
| ## Apply the lifecycle model | ||
|
|
||
| Keep function lifecycle separate from precompile availability: | ||
|
|
||
| | Condition | Required call behavior | | ||
| |---|---| | ||
| | Active and enabled | Execute normally. | | ||
| | Soft-deprecated and enabled | Preserve the documented behavior and encoding. | | ||
| | Hard-deprecated and enabled | Keep routing the selector and return a descriptive precompile error. | | ||
| | Disabled | Return the precompile-disabled error regardless of function lifecycle. | | ||
|
|
||
| Use soft deprecation by default. Preserve the call, mark the Solidity function | ||
| with `@deprecated`, and publish replacement metadata without adding | ||
| deprecation-only work to every invocation. | ||
|
|
||
| Use hard deprecation only when old behavior cannot be represented honestly or | ||
| safely, for example because: | ||
|
|
||
| - the underlying concept no longer exists and has no representation; | ||
| - the semantics changed beyond what the old return type can describe; or | ||
| - preservation requires fabricated data, dead state, unbounded work, or an | ||
| unacceptable security risk. | ||
|
|
||
| Do not hard-deprecate because a replacement is newer, easier to maintain, or | ||
| more complete. First document why an adapter is impossible or disproportionate, | ||
| identify affected released functions and known callers, provide a replacement | ||
| when possible, and complete the agreed migration process. | ||
|
|
||
| ## Stop an undeployed compatibility break | ||
|
|
||
| If the runtime change that makes old behavior impossible has not reached | ||
| mainnet, treat mainnet deployment as blocked by the compatibility break. Do not | ||
| interpret a request to update precompiles as authorization to deploy the break | ||
| or hard-deprecate affected functions immediately. | ||
|
|
||
| Stop and give the developer this prominent warning: | ||
|
|
||
| > **Mainnet compatibility warning:** This change would force hard deprecation | ||
| > of `<precompile address, function, and selector>` and break contracts that | ||
| > still call it. Do not deploy the incompatible runtime change to mainnet until | ||
| > `<replacement>` is available, the old function has been soft-deprecated for | ||
| > the agreed migration window, and the phase-out criteria have been satisfied. | ||
|
|
||
| State why an adapter cannot work, which released functions and known callers | ||
| are affected, what replacement is available or required, and which phase-out | ||
| steps remain. Continue only with non-breaking preparation such as adding the | ||
| replacement, tests, documentation, and lifecycle metadata. Preserve current | ||
| mainnet behavior throughout the migration window. Hard-deprecate only in the | ||
| later release that completes the planned phase-out. | ||
|
|
||
| ## Report lifecycle and availability | ||
|
|
||
| Use this proposed registry shape as the compatibility target: | ||
|
|
||
| ```solidity | ||
| struct PrecompileStatus { | ||
| bool isDeprecated; | ||
| bool isDisabled; | ||
| address newPrecompile; | ||
| bytes4 newSelector; | ||
| string message; | ||
| } | ||
| ``` | ||
|
|
||
| Interpret `isDeprecated` as soft or hard function deprecation. Interpret | ||
| `isDisabled` as current unavailability through a reversible operational switch. | ||
| Use `newPrecompile` and `newSelector` for the recommended replacement; zero | ||
| replacement fields mean that none is available. Use `message` for | ||
| human-readable status or migration guidance. | ||
|
|
||
| Do not infer deprecation from disablement. Do not clear deprecation when a | ||
| precompile is re-enabled. Do not describe the registry as callable until its | ||
| address and implementation are released. | ||
|
|
||
| Keep registry metadata, Solidity NatSpec, public documentation, and call | ||
| behavior consistent. Prefer static registry queries over emitting a log on | ||
| every deprecated call. | ||
|
|
||
| ## Handle reversible disablement | ||
|
|
||
| Treat disablement as an external, reversible operational action, not a normal | ||
| deprecation step. An agent may identify a risk, verify the mechanism, and | ||
| recommend that responsible decision-makers consider it. An agent cannot | ||
| perform or authorize the action. | ||
|
|
||
| Require re-enablement to restore each function's previous active, | ||
| soft-deprecated, or hard-deprecated behavior. Never erase lifecycle metadata | ||
| when availability changes. | ||
|
|
||
| Before recommending disablement, verify that the address routes through | ||
| `PrecompileExt::try_execute` and uses the intended `PrecompileEnum` entry. | ||
| Check whether multiple addresses share that entry and report the complete | ||
| effect of a toggle. Do not claim an address is toggleable merely because the | ||
| general mechanism exists. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[HIGH] Do not apply 18-decimal scaling to every precompile balance
This blanket rule remains unsafe: released precompiles can define different units, and applying
10^9universally can change economic values by that factor or double-convert an existing interface. Require each function to preserve its documented ABI units and make any conversion explicit and tested.