feat: Ethereum (secp256k1) accounts + pallet-revive eth_transact submission - #286
Open
peetzweg wants to merge 11 commits into
Open
feat: Ethereum (secp256k1) accounts + pallet-revive eth_transact submission#286peetzweg wants to merge 11 commits into
peetzweg wants to merge 11 commits into
Conversation
Add --scheme ethereum to account add/create (plus --env support): stores a secp256k1 private key whose identity is its EIP-55 H160. The stored publicKey is the deterministic revive fallback AccountId32 (H160 || 0xEE*12), so every existing address-resolution path works unchanged. list/inspect render the H160 as the address with scheme/ss58 attributes; --show-secret reveals the raw key. Ethereum accounts cannot sign substrate extrinsics — sr25519 signer paths error with guidance. Adds ox for secp256k1/EIP-1559/ABI primitives. All ox modules are lazy-loaded through a non-literal dynamic import: Bun eagerly prefetches literal import() specifiers, and ox/Secp256k1 + ox/AbiFunction each cost >100ms — eager loading regressed every CLI spawn measurably.
…ransact When --from resolves to an ethereum-scheme account, dot <chain>.tx.Revive.call takes eth-style args — <dest> [<0xcalldata> | 'sig(types)' args...] and --value <wei> — prices the call via a ReviveApi.eth_transact dry-run (gas, storage deposit, decoded Error(string)/Panic reverts), signs an EIP-1559 transaction with the account's key, and submits it wrapped in the unsigned Revive.eth_transact extrinsic over the existing ws connection. The call executes with the eth address as msg.sender — no eth-rpc sidecar needed. Chain id comes from const Revive.ChainId, the nonce from the fallback account (--nonce overrides), calldata cast-style from a human ABI signature. --tip/--mortality/--asset/--ext are rejected; non-Revive.call targets error with guidance. The GenericTransaction argument is built as JSON and routed through the CLI's own parseTypedArg — hand-built papi structs fail the runtime-entry compatibility check (the metadata field is literally r#type). Verified end-to-end on previewnet asset-hub (chain id 420420417): executed in block #183766 with fees withdrawn from the eth account's fallback.
…skill README and docs/content/_index.md gain matching sections (accounts + transactions, features bullet, --value in the transaction-options table). The bundled skill documents the feature for agents, adds --scheme/--value to the key-flags table and two common-error entries, and the scripting reference gains a check-then-act contract-admin pattern validated against previewnet.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files
... and 5 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Every mnemonic-backed account now has an ethereum identity selected with --from <name>-eth: the MetaMask-compatible BIP44 key (m/44'/60'/0'/0/0) of the same phrase. Dev accounts use their position as the index, which on the substrate dev phrase reproduces the well-known revive/Moonbeam dev accounts (alice-eth = Alith, bob-eth = Baltathar, ...). One phrase, two on-chain identities: substrate-signed Revive.call acts as the mapped keccak-H160, --from <name>-eth acts as the BIP44 address. resolveEthereumIdentity centralizes resolution: a stored account of the exact name always wins (no magic on real names); hex-seed/expanded/watch-only bases error with an import hint since BIP44 needs the phrase. account inspect resolves the -eth form directly (kind, BIP44 path, --show-secret) and shows the derived identity as an Ethereum line on the base account. Docs explain the two-identities model and how Revive.map_account relates (receiving, not signing; AutoMap chains map automatically). Verified live on previewnet: --from alice-eth executed as Alith in block 239273 with fees paid by its fallback account.
Subprocess runCli tests verify behavior but earn no coverage instrumentation, which left tx-eth.ts at ~16% lines. Cover the pure helpers (toHexData, buildGenericTransaction, formatEthTransactError) and the pre-connect guards in-process, and add a live previewnet dry-run (value-0, needs no funds, retried against network flakiness) that exercises the full pricing flow as the derived alice-eth identity. The live test runs as a subprocess deliberately: load-meta.test.ts installs process-global mock.module replacements for core/client.ts, so an in-process variant silently receives the mocked revive-less fixture chain in full-suite runs. The submit/report flow remains covered by real previewnet submissions only — the case codecov.yml already documents.
…IVE_TESTS Review follow-ups: --value was silently ignored when --from resolved to a substrate signer — now a pointed CliError like the other inapplicable-flag guards. The previewnet live dry-run test made bare bun test nondeterministic; it now skips unless DOT_LIVE_TESTS=1. Also hoist the TxEvent type import in tx-eth.ts to match tx.ts.
`account export` now emits `scheme: "ethereum"` for secp256k1 accounts: the secret is a 32-byte hex string either way, so without the marker a re-import resolved it as an sr25519 seed and the account came back as a different identity. `account import` honors the field, re-deriving a blank publicKey with secp256k1 and rejecting a secret that is not an ethereum private key. `account derive` refuses an ethereum source outright — substrate HD derivation on a secp256k1 key would silently mint an unrelated sr25519 account from the ethereum private key rather than erroring.
eth_transact was wrapped in a general transaction carrying a default value for every transaction extension in the chain's metadata. That data was always inert: pallet-revive intercepts the extrinsic in `Checkable::check()` and substitutes its own eth extension, discarding whatever was attached. Inert until the extension set moves. asset-hub-next spec 2000035 replaced `AuthorizeValueTransfer Option<[u8; 64]>` with a `UnitTransactionExtension` placeholder, and the runtime then panics in validate_transaction — the whole ethereum path, contract calls included, failed with a wasm trap on submit while dry-runs (runtime-API reads, which never enter validate_transaction) kept succeeding. spec 2000033 tolerated it, which is why this was not caught when the feature was verified. A bare extrinsic (`0x05 | call`, no extensions at all) is the correct format regardless: eth_transact derives its origin from the secp256k1 signature inside its own payload, not from extension data. Verified on both runtimes. The skill's "wasm trap on submit" entry claimed this symptom is almost always stale metadata; it now covers the case where the metadata is current and names the dry-run-succeeds/submit-traps signature.
`dot <chain>.tx.Revive.instantiate_with_code <0xcode|@file> ['constructor(types)' args...]` deploys through the same identity and transport as Revive.call. At the ethereum layer a deployment is not a call but a distinct transaction kind — `to` omitted from the EIP-1559 envelope — which pallet-revive branches on to route the payload to eth_instantiate_with_code. So `to` becomes nullable through the envelope, the GenericTransaction, and the dry-run pricing. The deployer is the eth address, so the constructor sees it as msg.sender and `owner()` lands on the key in the keystore — the whole point of the feature, and unreachable via a substrate-signed instantiate, which would execute as the signer's mapped H160 instead. Bytecode is inline hex or `@<path>`; the file may omit the 0x prefix and carry trailing whitespace, so `solc --bin` and foundry artifacts work unmodified. Constructor arguments require a literal 'constructor(types)' signature and are ABI-encoded onto the init code with no selector — a function signature is rejected rather than silently prepending four bytes and producing an undeployable blob. The deployed address is read from the Revive.Instantiated event. A dry-run cannot observe that event, so it reports the address predicted from sender and nonce, and prints the size of the runtime code the constructor returns rather than kilobytes of hex. Verified on preview-asset-hub (spec 2000035) and paseo-asset-hub-next (2000033): deploy, read back through greeting(), and an owner-only write. The predicted address matched Revive.Instantiated exactly.
Adds a "Deploying a contract" section to README and the docs site, a deploy block plus flag/error updates to the skill, and a deploy-then-configure-as- owner pattern to the scripting reference. The scripting pattern uses --wait finalized deliberately: reads and dry-runs resolve against the finalized block, so a contract that exists only in a best block is invisible to the next command. The changeset gains the deployment surface and the bare-extrinsic transport change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #284
Adds Ethereum (secp256k1) accounts to the keystore and lets them submit contract calls through pallet-revive's unsigned
Revive.eth_transactextrinsic — over the same WebSocket connectiondotalready holds.Why: Contract admin operations on revive chains (DotNS being the concrete case) must execute as an Ethereum-key identity —
owner()/role holders are addresses derived from secp256k1 keys, which a substrate signer's mapped H160 can by construction never equal. Until now that meant running a dockerized eth-rpc sidecar and hand-writing a viem script per operation.How:
--scheme ethereumstores a secp256k1 key whose storedpublicKeyis the deterministic revive fallback AccountId32 (H160 ‖ 0xEE×12) — the one deliberate trick that makes every existing address-resolution path (balances,--from, list/inspect) work unchanged. When such an account signs,tx.Revive.callswitches transport: price viaReviveApi.eth_transactdry-run → sign an EIP-1559 envelope (ox, lazy-loaded) → submit wrapped in unsignedRevive.eth_transact. The call executes on-chain with the eth address asmsg.sender.Accounts
The identity is the EIP-55 H160; the SS58 is the fallback account — fund it for fees, read nonce/balance through it like any account.
dot account inspect dotns-admin --show-secretreveals the key. Substrate signing paths (dot sign, ordinary extrinsics) error with guidance:One mnemonic, two identities —
alice-ethEvery mnemonic-backed account also has a derived ethereum identity: the MetaMask-compatible BIP44 key (
m/44'/60'/0'/0/0) of the same phrase, selected with the-ethname suffix. Dev accounts use their position as the index, which reproduces the well-known revive/Moonbeam dev accounts —alice-ethis Alith,bob-ethis Baltathar, and so on.One phrase, two on-chain identities: a substrate-signed
Revive.callfromaliceexecutes as her mapped keccak-H160, while--from alice-ethexecutes as the BIP44 address — contracts see differentmsg.sender. Resolution is deliberately unmagical: a real stored account named<name>-ethalways wins, and non-mnemonic secrets (hex seeds, raw sr25519 keys, watch-only) error with a hint to import a key directly.map_accountrelates to receiving, not signing (docs explain; chains withRevive.AutoMap = true, like previewnet, map automatically).Transactions
Signature args:
uint*/int*as integers,boolastrue/false,address/bytes*as 0x-hex,stringverbatim, arrays/tuples as JSON. Chain id, nonce, and gas come from the chain (--nonceoverrides). Failed dry-runs decode SolidityError(string)/Panic(uint256):Guards:
--tip/--mortality/--asset/--extare rejected (not applicable to eth transactions); any target other thanRevive.call/Revive.instantiate_with_codeand raw call hex error with pointed guidance.DOT_DRY_RUN/--dry-run/--no-dry-run,--wait,--jsonNDJSON output, and explorer links behave exactly like substrate txs.Deploying contracts
Revive.instantiate_with_codedeploys through the same identity and transport. At the ethereum layer a deployment is not a call but a different transaction kind —toomitted from the envelope — which pallet-revive branches on to reacheth_instantiate_with_code, sotobecomes nullable through the envelope, theGenericTransaction, and the dry-run pricing. The deployer is the eth address, so the constructor sees it asmsg.senderandowner()lands on the key in the keystore — unreachable via a substrate-signed instantiate, which executes as the signer's mapped H160.Bytecode is inline hex or
@<path>— the file may omit the0xand carry a trailing newline, sosolc --binand foundry artifacts work unmodified. Constructor arguments need a literal'constructor(types)'and are ABI-encoded onto the init code with no selector; a function signature is rejected rather than silently prepending four bytes and producing an undeployable blob. The address is read fromRevive.Instantiated, and--dry-runreports the one predicted from sender+nonce plus the size of the runtime code the constructor would return.eth_transactnow goes out as a bare extrinsicWorth reviewer attention, because it was a live bug rather than a cleanup.
eth_transactwas wrapped in a general transaction carrying a default value for every transaction extension in the chain's metadata. That data was always inert — pallet-revive intercepts the extrinsic inCheckable::check()and substitutes its own eth extension, discarding whatever was attached.Inert until the extension set moves. asset-hub-next spec 2000035 replaced
AuthorizeValueTransfer Option<[u8; 64]>with aUnitTransactionExtensionplaceholder, and the runtime then panics invalidate_transaction— the entire ethereum path, contract calls included, failed with a wasm trap on submit. spec 2000033 tolerated it, which is why the original verification passed. The tell is that dry-runs kept succeeding throughout: they are runtime-API reads and never entervalidate_transaction.A bare extrinsic (
0x05 | call, no extensions at all) is the correct format regardless —eth_transactderives its origin from the secp256k1 signature inside its own payload, not from extension data.Verified live on previewnet
Both identity kinds executed real transactions: a stored-key account in block #183766 and the derived
--from alice-eth(Alith) in block #239273 — in each case with fees withdrawn from the identity's fallback account, the gas estimate within 5% of actual, and the wei→planck ratio (10⁸ on a 10-decimals chain) confirmed by the fee events. Revert path, guards, and value-transfer pricing (incl. storage deposit) exercised against the live chain too.Deployment and the bare-extrinsic switch were then verified on both runtimes — preview-asset-hub (spec 2000035, the one that panics on a general transaction) and paseo-asset-hub-next (2000033). On each: deploy a constructor-arg contract, read the value back through
greeting(), and land an owner-only write, withowner()returning the signing identity's H160. The dry-run's predicted CREATE address matchedRevive.Instantiatedexactly.Two implementation judgment calls worth reviewer attention: the
GenericTransactionruntime-API argument is built as JSON and routed through the CLI's ownparseTypedArg(hand-built papi structs fail the runtime-entry compatibility check — the metadata field is literallyr#type), and alloxmodules load through a non-literal dynamic import because Bun eagerly prefetches literalimport()specifiers, which regressed every CLI spawn by ~250ms.70+ new tests (including a byte-for-byte regression vector of the previewnet-executed payload, a
buildBareTxencoding check, and the previewnet-assigned contract address pinned as a vector); README, docs site, the bundled skill, and the scripting reference are updated. The contract registry /dot <chain>.query.<contract>.<method>porcelain stays in #181, building on this transport.