Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ frame transactions as the hegota-testnet chain (chain ID `8141`) accepts them. T
envelope is a composition of three draft EIPs that none of them specifies on its own:
EIP-8141 frames, EIP-8250 keyed nonces, EIP-8272 recent-root references.

It is a deliberate second implementation. The reference client is
[ethrex, on its `hegota-testnet` branch](https://github.com/lambdaclass/ethrex/tree/hegota-testnet),
It is a deliberate second implementation. The migration is pinned against
[ethrex commit `d587cf9`](https://github.com/lambdaclass/ethrex/tree/d587cf9ff0996315381c4b2784a4d7d499decc0f),
and this library exists partly to disagree with it usefully. Every `.rs`, `.py` and
`docs/*.md` path cited in this repository's docs and comments — `transaction.rs`,
`frame_tx_wire_tests.rs`, `frametx.py` and the rest — is a path in that repository, not in
Expand Down Expand Up @@ -46,15 +46,14 @@ interchangeable.

```bash
bun run test # hermetic, no network
bun run test:live # opt-in, hits rpc1.privacy.ethrex.xyz
bun run test:live # checks rpc1.frames.ethrex.xyz identity
bun run typecheck
bun run build # tsup -> dist/, dual ESM + CJS with both declaration flavours
bunx tsx scripts/capture-fixtures.ts [from-block] [to-block]
```

CI runs `typecheck`, `test`, `build`, and then `@arethetypeswrong/cli --pack .` against
the built package. All four must pass. The live suite is not expected to pass in every
environment and is not a gate.
the built package. All four must pass. The public frames RPC currently has no
frame-aware simulation method, so there is no live simulation gate.

That last check is there because a broken `exports` map or a mismatched `.d.cts` breaks
every consumer while leaving the test suite entirely green — the tests import from `src/`,
Expand All @@ -81,7 +80,7 @@ authority; if the encoder disagrees with them, the encoder is wrong. Fixtures un

**`src/gas.ts` must not import `src/envelope.ts`.** The gas model stays independently
testable, so it can be independently wrong or independently right. The small duplication
this causes (a `sameAddress` helper, the framing for two calldata blobs) is deliberate.
this causes (for example, a `sameAddress` helper) is deliberate.
`src/envelope.ts` must not import `src/signatures.ts`; the reverse direction is fine.

**`encodeFrameTx` deliberately does not validate.** `frameTxSigHash` is defined over a
Expand All @@ -99,8 +98,6 @@ path is `assertValidFrameTx(tx)` then `encodeFrameTx(tx)`.
signature makes the transaction invalid at consensus, not merely unrelayable.
- **RLP scalars are minimal big-endian**; zero is the empty string `0x`, not `0x00`.
Never pass viem's `numberToHex` into `toRlp`; use `rlpUint`.
- **`recentRootCalldata` is empty (`0x`) when no reference is declared**, not `toRlp([])`
(`0xc0`). Getting this wrong adds billed bytes to every transaction on the chain.
- **State gas is added on top of the calldata floor**, never absorbed by it.
- **`limits` is always a two-element list**, including when `state` is zero.
- **The frame's JSON field for the target is `to`, not `target`.** The node's `type` is
Expand Down Expand Up @@ -170,7 +167,7 @@ That means **your commit messages set the version**. Use
Pull request titles are validated by CI against that list, with an optional scope from
`envelope`, `sighash`, `signatures`, `gas`, `divergence`, `rlp`, `rpc`, `viem`,
`fixtures`, `docs`, `deps`, `ci`, `repo` — for example
`feat(gas): model the head EIP-8272 reference arm`. Subjects start lowercase.
`fix(envelope): encode the scalar frame nonce`. Subjects start lowercase.

Do not hand-edit `version` in `package.json` or touch `CHANGELOG.md`; the release commit
owns both. A wire-format or gas change is almost always at least a `fix:`, because
Expand All @@ -184,10 +181,8 @@ someone downstream is encoding bytes with this.
published figure, a golden vector, or captured chain data, not to the code's own output.
- If you touched anything in **Rules that are not style preferences**, say why in the PR
description. Those changes are not refused, but they are argued.
- Fixtures expire. They record the client version and genesis hash they were captured
against and the oracle suite asserts the genesis hash, so a re-genesis fails loudly.
This chain is already on its third genesis; re-capture with the script rather than
hand-editing.
- Files under `test/fixtures/chain` are archived captures from the superseded envelope.
Do not use them as current wire-format oracles.

## Where the rest of the docs are

Expand Down
51 changes: 10 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/JustaLab-co/frametx-kit/badge)](https://scorecard.dev/viewer/?uri=github.com/JustaLab-co/frametx-kit)

TypeScript for **EIP-8141 frame transactions** as hegota-testnet (chain ID `8141`)
actually accepts them: the composed envelope that also carries EIP-8250 keyed nonces
and EIP-8272 recent-root references.
TypeScript for the current **EIP-8141 frame transaction** envelope implemented by
the Ethrex frames devnet (chain ID `81410`).

Decode, build, hash, sign, price, dry-run and broadcast.

Expand All @@ -27,7 +26,7 @@ import { createPublicClient, http } from 'viem'
import { frameActions } from '@jaw.id/frametx-kit/viem'

const client = createPublicClient({
transport: http('https://rpc1.privacy.ethrex.xyz'),
transport: http('https://rpc1.frames.ethrex.xyz'),
}).extend(frameActions)

const tx = await client.getFrameTransaction({ hash })
Expand All @@ -46,7 +45,7 @@ assertValidFrameTx(signed)
const raw = encodeFrameTx(signed)
```

`signFrameTx` also takes an account instead of a key — anything with an `address` and a
`signFrameTx` also takes a `FrameSigner` instead of a key — anything with an `address` and a
raw-digest `sign`, which covers viem's `privateKeyToAccount` and `mnemonicToAccount`, a
`toAccount` source, and your own wrapper around a hardware wallet, an HSM or a remote
signer. It signs every SECP256K1 entry whose `msg` is empty, and refuses if the entry's
Expand Down Expand Up @@ -98,45 +97,17 @@ the node's decoded JSON, re-encodes it, and refuses to return anything whose re-
does not reproduce the transaction hash. What you get back is a transaction this library
can put back on the wire byte-for-byte.

## Gas and rule sets
## Gas

Gas is priced under one of three rule sets, because the deployed binary and the pinned
EIP text disagree:

| Rule set | Meaning |
|---|---|
| `'chain'` | ethrex `31b532266`, what the nodes actually run |
| `'pins'` | the pinned EIP text |
| `'head'` | current drafts, for anticipating the next re-genesis |
`frameTxGas` prices the current EIP-8141 transaction shape. The historical
`'chain'`, `'pins'`, and `'head'` rule-set names remain accepted as compatibility
aliases and currently produce identical results.

```ts
import { compareRuleSets, decodeFrameTx } from '@jaw.id/frametx-kit'
console.log(compareRuleSets(decodeFrameTx(raw), 'chain', 'pins'))
```

The head EIP-8272 draft alters the *envelope*, not just the price: the recent-root field
is gone and references travel as a leading VERIFY frame against
`0x0000000000000000000000000000000000008272`, 72 bytes each. `compareRuleSets` prices
whichever side names `'head'` over `toHeadShape(tx)`, which applies exactly that change
as a transformation of the transaction, so a reference-carrying transaction is surveyed
rather than refused.

```ts
import { toHeadShape } from '@jaw.id/frametx-kit'
compareRuleSets(tx, 'pins', 'head') // prices the head side over toHeadShape(tx)
toHeadShape(tx) // the same transform on its own; identity when no reference is carried
```

The synthetic frame claims zero execution and state. Zero state is normative: the draft
pins `limits.state` for the recent root verifier frame, along with its mode, target, flags
and value. `limits.execution` is the one figure it leaves open — that falls out of the
`STATICCALL` and one `SLOAD` per tuple — so every limit-derived term of the head price is
still a floor, fine to compare against and wrong to budget with. That is why
`frameTxGas(tx, 'head')` still throws for a reference-carrying transaction rather than
hand you a floor that reads like a budget. `'head'` otherwise prices identically to
`'pins'`; the EIP-8250 change it models is an execution-time charge against
`limits.state`, published as `HEAD_KEYED_NONCE_STATE_GAS`.

Every gas constant is written as its published figure rather than derived. ethrex's own
suite missed its intrinsic dropping from 15000 to 12000 across 1372 tests by deriving the
expected value from the constant under test.
Expand All @@ -145,13 +116,11 @@ expected value from the constant under test.

```bash
bun run test # hermetic, no network
bun run test:live # hits rpc1.privacy.ethrex.xyz
bun run typecheck
```

The default suite runs entirely off checked-in fixtures. Those are real transactions
captured with `bunx tsx scripts/capture-fixtures.ts`; each records the client version and
genesis hash it was taken against, so a re-genesis fails loudly rather than silently.
The default suite is hermetic. Its golden RLP and signature hash are pinned to the
current Ethrex `FrameTransaction` encoder.

## Contributing

Expand Down
Loading
Loading