Skip to content
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

feat: add ERC-55 support to ExecutionAddress #6355

Merged
merged 6 commits into from
Feb 6, 2024
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
3 changes: 2 additions & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"types": "lib/index.d.ts",
"dependencies": {
"@chainsafe/ssz": "^0.14.0",
"@lodestar/params": "^1.15.0"
"@lodestar/params": "^1.15.0",
"ethereum-cryptography": "^2.0.0"
},
"keywords": [
"ethereum",
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/bellatrix/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import {ssz as primitiveSsz} from "../primitive/index.js";
import {ssz as phase0Ssz} from "../phase0/index.js";
import {ssz as altairSsz} from "../altair/index.js";
import {stringType} from "../utils/StringType.js";
import {stringType} from "../utils/stringType.js";

const {
Bytes32,
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ export * as ssz from "./sszTypes.js";
// Typeguards
export * from "./utils/typeguards.js";
// String type
export {StringType, stringType} from "./utils/StringType.js";
export {StringType, stringType} from "./utils/stringType.js";
// Container utils
export * from "./utils/container.js";
3 changes: 2 additions & 1 deletion packages/types/src/primitive/sszTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {ByteVectorType, UintNumberType, UintBigintType, BooleanType} from "@chainsafe/ssz";
import {ExecutionAddressType} from "../utils/executionAddress.js";

export const Boolean = new BooleanType();
export const Byte = new UintNumberType(1);
Expand Down Expand Up @@ -61,4 +62,4 @@ export const BLSPubkey = Bytes48;
export const BLSSignature = Bytes96;
export const Domain = Bytes32;
export const ParticipationFlags = new UintNumberType(1, {setBitwiseOR: true});
export const ExecutionAddress = Bytes20;
export const ExecutionAddress = new ExecutionAddressType();
48 changes: 48 additions & 0 deletions packages/types/src/utils/executionAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {keccak256} from "ethereum-cryptography/keccak.js";
import {ByteVectorType} from "@chainsafe/ssz";

export type ByteVector = Uint8Array;

export class ExecutionAddressType extends ByteVectorType {
constructor() {
super(20, {typeName: "ExecutionAddress"});
}
toJson(value: ByteVector): unknown {
const str = super.toJson(value) as string;
return toChecksumAddress(str);
}
}

function isAddressValid(address: string): boolean {
return /^(0x)?[0-9a-f]{40}$/i.test(address);
jeluard marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Formats an address according to [ERC55](https://eips.ethereum.org/EIPS/eip-55)
*/
export function toChecksumAddress(address: string): string {
Copy link
Member

Choose a reason for hiding this comment

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

is this used anywhere yet? or is this PR just adding the functionality for later?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean specifically because of the export keyword?

Copy link
Member

Choose a reason for hiding this comment

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

I guess I missed L64 of primitive/sszTypes.ts where this is being used.

This is fine to add here for now.

Copy link
Member

Choose a reason for hiding this comment

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

It also needs to be exported for unit testing

Copy link
Member

@nflaig nflaig Feb 6, 2024

Choose a reason for hiding this comment

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

This is fine to add here for now.

yeah, thought so as well, we might need furher custom ssz types (for ssz api refactoring), will likely have to revisit and maybe restructure this at some point

if (!isAddressValid(address)) {
throw Error(`Invalid address: ${address}`);
}

const rawAddress = (address.startsWith("0x") ? address.slice(2) : address).toLowerCase();
const chars = rawAddress.split("");

// Inspired by https://github.com/ethers-io/ethers.js/blob/cac1da1f912c2ae9ba20f25aa51a91766673cd76/src.ts/address/address.ts#L8
const expanded = new Uint8Array(chars.length);
for (let i = 0; i < expanded.length; i++) {
expanded[i] = rawAddress[i].charCodeAt(0);
}

const hashed = keccak256(expanded);
for (let i = 0; i < chars.length; i += 2) {
if (hashed[i >> 1] >> 4 >= 8) {
chars[i] = chars[i].toUpperCase();
}
if ((hashed[i >> 1] & 0x0f) >= 8) {
chars[i + 1] = chars[i + 1].toUpperCase();
}
}

return "0x" + chars.join("");
}
72 changes: 72 additions & 0 deletions packages/types/test/unit/executionAddress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {describe, it, expect} from "vitest";
import {toChecksumAddress} from "../../src/utils/executionAddress.js";

describe("toChecksumAddress", () => {
it("should fail with invalid addresses", () => {
expect(() => toChecksumAddress("1234")).toThrowError("Invalid address: 1234");
expect(() => toChecksumAddress("0x1234")).toThrowError("Invalid address: 0x1234");
});

it("should format addresses as ERC55", () => {
type TestCase = {
address: string;
checksumAddress: string;
};

const testCases: TestCase[] = [
// Input all caps
{
address: "0x52908400098527886E0F7030069857D2E4169EE7",
checksumAddress: "0x52908400098527886E0F7030069857D2E4169EE7",
},
{
address: "0xDE709F2102306220921060314715629080E2FB77",
checksumAddress: "0xde709f2102306220921060314715629080e2fb77",
},
// Without 0x prefix
{
address: "52908400098527886e0f7030069857d2e4169ee7",
checksumAddress: "0x52908400098527886E0F7030069857D2E4169EE7",
},
// All caps
{
address: "0x52908400098527886e0f7030069857d2e4169ee7",
checksumAddress: "0x52908400098527886E0F7030069857D2E4169EE7",
},
{
address: "0x8617e340b3d01fa5f11f306f4090fd50e238070d",
checksumAddress: "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
},
// All lower
{
address: "0xde709f2102306220921060314715629080e2fb77",
checksumAddress: "0xde709f2102306220921060314715629080e2fb77",
},
{
address: "0x27b1fdb04752bbc536007a920d24acb045561c26",
checksumAddress: "0x27b1fdb04752bbc536007a920d24acb045561c26",
},
// Normal
{
address: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
checksumAddress: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
},
{
address: "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359",
checksumAddress: "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
},
{
address: "0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb",
checksumAddress: "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
},
{
address: "0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb",
checksumAddress: "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
},
];

for (const {address, checksumAddress} of testCases) {
expect(toChecksumAddress(address)).toBe(checksumAddress);
}
});
});
Loading