Skip to content

Commit

Permalink
feat: add ERC-55 support to ExecutionAddress (#6355)
Browse files Browse the repository at this point in the history
* feat: add ERC-55 support to ExecutionAddress

* chore: address PR comments

* chore: address PR comments

* fix: cleanup

* fix: lower case

* chore: address PR comments
  • Loading branch information
jeluard committed Feb 6, 2024
1 parent 3adf2c2 commit b6890ad
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 4 deletions.
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);
}

/**
* Formats an address according to [ERC55](https://eips.ethereum.org/EIPS/eip-55)
*/
export function toChecksumAddress(address: string): string {
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("");
}
File renamed without changes.
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);
}
});
});

0 comments on commit b6890ad

Please sign in to comment.