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
7 changes: 6 additions & 1 deletion .changeset/funny-bears-arrive.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
---
"@evm-effect/crypto": patch
"@evm-effect/ethereum-types": patch
"@evm-effect/evm": patch
"@evm-effect/rlp": patch
"@evm-effect/shared": patch
"@evm-effect/solc": patch
---

Fix Effect-based account lookup call sites in transaction and VM execution paths so account existence, liveness, and CREATE collision checks use yielded booleans and preserve expected state transitions.
Migrate the workspace to Effect 4.x (core APIs, `Schema`, and related Effect modules), not only schema definitions. Touches domain types, RLP, crypto transactions, shared utilities, solc JSON schemas, and the EVM (including tests and examples). Removes obsolete schema helpers and aligns decoding/encoding and fixture schemas with the updated stack.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ scratchpad/*
.env.development.local
.env.test.local
.env.production.local
.llms
.llms/
.history
.venv
__pycache__/
Expand All @@ -31,3 +31,4 @@ execution-specs/
test-output/
generated/


17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"args": ["--experimental-strip-types"],
"program": "${workspaceFolder}/packages/rlp/test/playground.ts",
"outFiles": ["${workspaceFolder}/**/*.js"]
}
]
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal debug launch config accidentally committed

Low Severity

The .vscode/launch.json file appears to be a personal debug configuration hardcoded to launch packages/rlp/test/playground.ts — a scratch/playground file. While the project intentionally commits shared .vscode/settings.json and extensions.json, this launch config is developer-specific and not useful to other contributors.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c394bf8. Configure here.

584 changes: 201 additions & 383 deletions bun.lock

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
"packages/*"
],
"catalog": {
"effect": "^3.21.0",
"@effect/experimental": "^0.60.0",
"@effect/platform": "^0.96.0",
"@effect/platform-bun": "^0.89.0",
"effect": "4.0.0-beta.64",
"@effect/opentelemetry": "4.0.0-beta.64",
"@effect/platform-bun": "4.0.0-beta.64",
"@types/bun": "1.3.0",
"typescript": "5.9.3",
"fast-check": "^4.3.0",
Expand Down Expand Up @@ -42,11 +41,9 @@
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@changesets/changelog-github": "^0.5.2",
"@changesets/cli": "^2.31.0",
"@effect/build-utils": "^0.8.9",
"@effect/language-service": "latest",
"@manypkg/cli": "^0.25.1",
"@types/bun": "catalog:",
"@types/node": "^24.12.2",
"@types/node": "^24.12.3",
"babel-plugin-annotate-pure-calls": "^0.5.0",
"glob": "^11.1.0",
"knip": "^5.88.1",
Expand Down
78 changes: 42 additions & 36 deletions packages/crypto/src/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { hmac } from "@noble/hashes/hmac.js";
import { sha256 } from "@noble/hashes/sha2.js";
import type { ECDSASignOpts } from "@noble/secp256k1";
import * as secp256k1 from "@noble/secp256k1";
import { Data, Effect, Either, Match, Schema } from "effect";
import { Data, Effect, Match, Result, Schema } from "effect";
import { keccak256 } from "./keccak256.js";

secp256k1.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
Expand Down Expand Up @@ -63,6 +63,12 @@ export const Access = Schema.TaggedStruct("Access", {
});
export type Access = (typeof Access)["Type"];

const defaultU256 = U256.pipe(
Schema.withConstructorDefault(Effect.succeed(U256.constant(0n))),
);
const defaultU8 = U8.pipe(
Schema.withConstructorDefault(Effect.succeed(U8.constant(0n))),
);
/**
* Legacy transaction (pre-EIP-2718).
*
Expand All @@ -74,9 +80,9 @@ export const LegacyTransaction = Schema.TaggedStruct("LegacyTransaction", {
to: Schema.optional(Address),
value: U256,
data: Bytes,
v: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
r: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
s: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
v: defaultU256,
r: defaultU256,
s: defaultU256,
});
export type LegacyTransaction = (typeof LegacyTransaction)["Type"];
/**
Expand All @@ -97,9 +103,9 @@ export const AccessListTransaction = Schema.TaggedStruct(
value: U256,
data: Bytes,
accessList: Schema.Array(Access),
yParity: Schema.optionalWith(U8, { default: () => U8.constant(0n) }),
r: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
s: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
yParity: defaultU8,
r: defaultU256,
s: defaultU256,
},
);
export type AccessListTransaction = (typeof AccessListTransaction)["Type"];
Expand All @@ -118,9 +124,9 @@ export const FeeMarketTransaction = Schema.TaggedStruct(
value: U256,
data: Bytes,
accessList: Schema.Array(Access),
yParity: Schema.optionalWith(U8, { default: () => U8.constant(0n) }),
r: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
s: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
yParity: defaultU8,
r: defaultU256,
s: defaultU256,
},
);
export type FeeMarketTransaction = (typeof FeeMarketTransaction)["Type"];
Expand All @@ -139,9 +145,9 @@ export const BlobTransaction = Schema.TaggedStruct("BlobTransaction", {
accessList: Schema.Array(Access),
maxFeePerBlobGas: U256,
blobVersionedHashes: Schema.Array(Bytes32),
yParity: Schema.optionalWith(U8, { default: () => U8.constant(0n) }),
r: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
s: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
yParity: defaultU8,
r: defaultU256,
s: defaultU256,
});
export type BlobTransaction = (typeof BlobTransaction)["Type"];

Expand All @@ -159,33 +165,33 @@ export const SetCodeTransaction = Schema.TaggedStruct("SetCodeTransaction", {
data: Bytes,
accessList: Schema.Array(Access),
authorizations: Schema.Array(Authorization),
yParity: Schema.optionalWith(U8, { default: () => U8.constant(0n) }),
r: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
s: Schema.optionalWith(U256, { default: () => U256.constant(0n) }),
yParity: defaultU8,
r: defaultU256,
s: defaultU256,
});
export type SetCodeTransaction = (typeof SetCodeTransaction)["Type"];

export const Transaction = Schema.Union(
export const Transaction = Schema.Union([
LegacyTransaction,
AccessListTransaction,
FeeMarketTransaction,
BlobTransaction,
SetCodeTransaction,
);
]);

export type Transaction = (typeof Transaction)["Type"];

export const encodeTransaction = (
transaction: Transaction,
): Either.Either<Bytes | LegacyTransaction, RlpEncodeError> => {
): Result.Result<Bytes | LegacyTransaction, RlpEncodeError> => {
switch (transaction._tag) {
case "LegacyTransaction":
return Either.right(transaction);
return Result.succeed(transaction);
case "AccessListTransaction":
return rlp
.encodeTo(AccessListTransaction, transaction)
.pipe(
Either.map(
Result.map(
(bytes) =>
new Bytes({ value: new Uint8Array([0x01, ...bytes.value]) }),
),
Expand All @@ -194,7 +200,7 @@ export const encodeTransaction = (
return rlp
.encodeTo(FeeMarketTransaction, transaction)
.pipe(
Either.map(
Result.map(
(bytes) =>
new Bytes({ value: new Uint8Array([0x02, ...bytes.value]) }),
),
Expand All @@ -203,7 +209,7 @@ export const encodeTransaction = (
return rlp
.encodeTo(BlobTransaction, transaction)
.pipe(
Either.map(
Result.map(
(bytes) =>
new Bytes({ value: new Uint8Array([0x03, ...bytes.value]) }),
),
Expand All @@ -212,7 +218,7 @@ export const encodeTransaction = (
return rlp
.encodeTo(SetCodeTransaction, transaction)
.pipe(
Either.map(
Result.map(
(bytes) =>
new Bytes({ value: new Uint8Array([0x04, ...bytes.value]) }),
),
Expand All @@ -222,9 +228,9 @@ export const encodeTransaction = (

export const decodeTransaction = (
transaction: LegacyTransaction | Bytes,
): Either.Either<Transaction, RlpDecodeError> => {
): Result.Result<Transaction, RlpDecodeError> => {
if (transaction._tag === "LegacyTransaction") {
return Either.right(transaction);
return Result.succeed(transaction);
}
switch (transaction.value[0]) {
case 0x01:
Expand All @@ -233,30 +239,30 @@ export const decodeTransaction = (
AccessListTransaction,
new Bytes({ value: transaction.value.slice(1) }),
)
.pipe(Either.map((transaction) => transaction));
.pipe(Result.map((transaction) => transaction));
case 0x02:
return rlp
.decodeTo(
FeeMarketTransaction,
new Bytes({ value: transaction.value.slice(1) }),
)
.pipe(Either.map((transaction) => transaction));
.pipe(Result.map((transaction) => transaction));
case 0x03:
return rlp
.decodeTo(
BlobTransaction,
new Bytes({ value: transaction.value.slice(1) }),
)
.pipe(Either.map((transaction) => transaction));
.pipe(Result.map((transaction) => transaction));
case 0x04:
return rlp
.decodeTo(
SetCodeTransaction,
new Bytes({ value: transaction.value.slice(1) }),
)
.pipe(Either.map((transaction) => transaction));
.pipe(Result.map((transaction) => transaction));
default:
return Either.left(
return Result.fail(
new RlpDecodeError({ message: "Unknown transaction type", path: [] }),
);
}
Expand Down Expand Up @@ -416,8 +422,8 @@ export const recoverFromSignature = ({
});
export const recoverPublicKey = (
tx: Transaction,
): Either.Either<Bytes, FailedToRecoverPublicKeyError> => {
return Either.try({
): Result.Result<Bytes, FailedToRecoverPublicKeyError> => {
return Result.try({
try: () => {
let recoveryBit = 0;
if (tx._tag === "LegacyTransaction") {
Expand Down Expand Up @@ -485,7 +491,7 @@ export const getAddressFromPrivateKey = (privateKey: Bytes32) => {
};

export const recoverSender = (tx: Transaction) => {
return recoverPublicKey(tx).pipe(Either.map(publicKeyToAddress));
return recoverPublicKey(tx).pipe(Result.map(publicKeyToAddress));
};

export const signHash = ({
Expand Down Expand Up @@ -552,15 +558,15 @@ export const recoverAuthority = (authorization: Authorization) => {
}

if (r.value <= 0n || r.value >= SECP256K1N) {
return Either.left(
return Result.fail(
new FailedToRecoverPublicKeyError({
message: "Invalid r value in authorization",
}),
);
}

if (s.value <= 0n || s.value > SECP256K1N / 2n) {
return Either.left(
return Result.fail(
new FailedToRecoverPublicKeyError({
message: "Invalid s value in authorization",
}),
Expand Down
1 change: 0 additions & 1 deletion packages/ethereum-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"dependencies": {
"@evm-effect/shared": "workspace:*",
"@noble/hashes": "catalog:",
"@effect/platform": "catalog:",
"@effect/platform-bun": "catalog:",
"effect": "catalog:",
"ts-dedent": "^2.2.0"
Expand Down
10 changes: 5 additions & 5 deletions packages/ethereum-types/src/bytes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bufferFromHex, bufferToHex } from "@evm-effect/shared/bytes";
import { Either, Equal, Hash, Schema } from "effect";
import { Equal, Hash, Result, Schema } from "effect";
import { EvmTypeError } from "./exceptions.js";
import {
type Byteish,
Expand All @@ -12,7 +12,7 @@ import {
* Variable-length byte array
*/
export class Bytes extends Schema.TaggedClass<Bytes>("Bytes")("Bytes", {
value: Schema.instanceOf(Uint8Array),
value: Schema.Uint8Array,
}) {
constructor({ value }: { value: Uint8Array }) {
super({ value: padBuffer(value, value.length) });
Expand Down Expand Up @@ -45,15 +45,15 @@ export class Bytes extends Schema.TaggedClass<Bytes>("Bytes")("Bytes", {
return new Bytes({ value: normalizeToUint8Array(value) });
}

static fromHex(hex: string): Either.Either<Bytes, EvmTypeError> {
static fromHex(hex: string): Result.Result<Bytes, EvmTypeError> {
try {
if (hex.startsWith("0x")) {
hex = hex.slice(2);
}
const bytes = bufferFromHex(hex);
return Either.right(new Bytes({ value: bytes }));
return Result.succeed(new Bytes({ value: bytes }));
} catch (_) {
return Either.left(
return Result.fail(
new EvmTypeError({ message: "Invalid hex string", input: hex }),
);
}
Expand Down
22 changes: 12 additions & 10 deletions packages/ethereum-types/src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { bufferFromHex, bufferToHex } from "@evm-effect/shared/bytes";
import { Either, Equal, Hash, Schema } from "effect";
import { Equal, Hash, Result, Schema } from "effect";
import type { Bytes32 } from "./bytes.js";
import { Bytes, Bytes20, type Bytes256 } from "./bytes.js";
import { EvmTypeError } from "./exceptions.js";
Expand Down Expand Up @@ -55,34 +55,36 @@ export class Address extends Schema.TaggedClass<Address>("Address")("Address", {
/**
* Create an address from raw bytes (must be exactly 20 bytes)
*/
static fromBytes(bytes: Uint8Array): Either.Either<Address, EvmTypeError> {
static fromBytes(bytes: Uint8Array): Result.Result<Address, EvmTypeError> {
if (bytes.length !== 20) {
return Either.left(
return Result.fail(
new EvmTypeError({
message: `Address must be exactly 20 bytes, got ${bytes.length}`,
}),
);
}
return Either.right(new Address({ value: new Bytes20({ value: bytes }) }));
return Result.succeed(
new Address({ value: new Bytes20({ value: bytes }) }),
);
}

/**
* Create an address from a hex string
*/
static fromHex(hex: string): Either.Either<Address, EvmTypeError> {
static fromHex(hex: string): Result.Result<Address, EvmTypeError> {
const bytesResult = Bytes.fromHex(hex);
if (Either.isLeft(bytesResult)) {
return Either.left(bytesResult.left);
if (Result.isFailure(bytesResult)) {
return Result.fail(bytesResult.failure);
}
const bytes = bytesResult.right;
const bytes = bytesResult.success;
if (bytes.value.length !== 20) {
return Either.left(
return Result.fail(
new EvmTypeError({
message: `Address must be exactly 20 bytes, got ${bytes.value.length}`,
}),
);
}
return Either.right(
return Result.succeed(
new Address({ value: new Bytes20({ value: bytes.value }) }),
);
}
Expand Down
Loading
Loading