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

chore: make sign and verify EIP-712 compatible #4

Merged
merged 9 commits into from
Oct 28, 2024
142 changes: 44 additions & 98 deletions src/__tests__/starknet.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
jest.setTimeout(20000);
import StarknetSigner from "../signing/chains/StarknetSigner";
import type { TypedData } from "starknet";
import { RpcProvider, shortString } from "starknet";
import { createData } from "../../index";
import Crypto from "crypto";
import { RpcProvider } from "starknet";

const tagsTestVariations = [
{ description: "no tags", tags: undefined },
Expand All @@ -18,61 +18,13 @@ const tagsTestVariations = [
},
];

const sampleData: TypedData = {
types: {
StarkNetDomain: [
{ name: "name", type: "felt" },
{ name: "version", type: "felt" },
{ name: "chainId", type: "felt" },
{ name: "verifyingContract", type: "felt" },
],
Person: [
{ name: "name", type: "felt" },
{ name: "wallet", type: "felt" },
],
},
domain: {
name: "Starknet App",
version: "1",
chainId: shortString.encodeShortString("SN_SEPOLIA"),
verifyingContract: "0x123456789abcdef",
},
primaryType: "Person",
message: {
name: "Alice",
wallet: "0xabcdef",
},
};

const sampleDataTwo: TypedData = {
types: {
StarkNetDomain: [
{ name: "name", type: "felt" },
{ name: "version", type: "felt" },
{ name: "chainId", type: "felt" },
],
Vote: [
{ name: "voter", type: "felt" },
{ name: "proposalId", type: "felt" },
{ name: "support", type: "felt" },
],
},
primaryType: "Vote",
domain: {
name: "StarkDAO",
version: "1",
chainId: shortString.encodeShortString("SN_SEPOLIA"),
},
message: {
voter: "0x0123456789abcdef",
proposalId: "0x42",
support: "1",
},
};

const dataTestVariations = [
{ description: "empty string", data: sampleData },
{ description: "small string", data: sampleDataTwo },
{ description: "empty string", data: "" },
{ description: "small string", data: "hello world" },
{ description: "large string", data: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};':\",./<>?`~" },
{ description: "empty buffer", data: Buffer.from([]) },
{ description: "small buffer", data: Buffer.from("hello world") },
{ description: "large buffer", data: Buffer.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};':\",./<>?`~") },
];

describe("Typed Starknet Signer", () => {
Expand All @@ -87,81 +39,75 @@ describe("Typed Starknet Signer", () => {
await signer.init();
});

it("should sign a known value", async () => {
it("should sign a known values", async () => {
const expectedSignature = Buffer.from([
5, 45, 59, 233, 68, 46, 147, 175, 158, 76, 7, 25, 236, 54, 235, 204, 221, 208, 29, 65, 138, 221, 239, 130, 196, 101, 72, 112, 150, 36, 121, 59,
5, 128, 11, 178, 91, 23, 243, 106, 116, 103, 21, 15, 1, 183, 94, 58, 227, 92, 108, 158, 227, 27, 46, 234, 229, 112, 28, 91, 25, 30, 116, 231, 0,
1, 97, 23, 22, 40, 161, 114, 58, 182, 161, 83, 120, 241, 253, 19, 214, 124, 52, 42, 128, 84, 80, 90, 234, 145, 165, 248, 60, 100, 128, 144, 199,
2, 156, 193, 110, 55, 251, 188, 35, 208, 120, 137, 147, 61, 2, 89, 55, 112, 71, 203, 130, 242, 167, 100, 226, 76, 181, 116, 210, 80, 226, 17,
175, 7, 142, 71, 187, 235, 77, 198, 135, 116, 24, 37, 215, 190, 173, 4, 78, 34, 153, 96, 211, 54, 44, 12, 33, 244, 91, 185, 32, 219, 8, 176,
196, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 78, 95, 83, 69, 80, 79, 76, 73, 65,
]);

const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));
const data = Buffer.from("Hello Bundlr!");
const signature = await signer.sign(data);
const signatureBuffer = Buffer.from(signature);
expect(signatureBuffer).toEqual(expectedSignature);
});

it("should fail for an invalid signature", async () => {
const expectedSignature = Buffer.from([
34, 56, 90, 120, 12, 45, 200, 99, 22, 134, 223, 75, 145, 64, 250, 231, 100, 190, 18, 33, 203, 147, 5, 230, 182, 110, 59, 49, 222, 172, 193, 120,
129, 10, 154, 43, 67, 183, 240, 199, 204, 101, 192, 56, 3, 234, 121, 46, 174, 113, 175, 134, 177, 77, 210, 55, 91, 42, 84, 69, 188, 12, 189,
120, 113,
3, 14, 26, 44, 182, 142, 237, 13, 51, 15, 51, 142, 100, 132, 8, 70, 90, 34, 222, 66, 92, 68, 20, 86, 18, 205, 207, 16, 215, 160, 82, 238, 7,
227, 27, 134, 157, 27, 47, 233, 175, 89, 26, 104, 127, 142, 192, 227, 45, 149, 179, 169, 202, 38, 75, 242, 68, 84, 75, 8, 222, 153, 188, 225, 7,
142, 71, 187, 235, 77, 198, 135, 116, 24, 37, 215, 190, 173, 4, 78, 34, 153, 96, 211, 54, 44, 12, 33, 244, 91, 185, 32, 219, 8, 176, 196, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 78, 95, 77, 65, 73, 78,
]);

const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));
const data = Buffer.from("Hello World!");
const signature = await signer.sign(data);
const signatureBuffer = Buffer.from(signature);
expect(signatureBuffer).not.toEqual(expectedSignature);
});

it("should verify a known value", async () => {
const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));

const publicKey = signer.publicKey.toString("hex");
const hexString = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), buffer, signature);
expect(isValid).toEqual(true);
});

it("should sign & verify", async () => {
const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));
it("should sign & verify a known values", async () => {
const data = Buffer.from("Hello Bundlr!");
const signature = await signer.sign(data);

const publicKey = signer.publicKey.toString("hex");
const hexString = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), buffer, signature);
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), data, signature);
expect(isValid).toEqual(true);
});

it("should evaulate to false for invalid signature", async () => {
// generate invalid signature
const signature = Uint8Array.from([
4, 182, 243, 200, 173, 166, 38, 42, 18, 165, 33, 59, 155, 164, 184, 207, 51, 68, 119, 38, 52, 132, 173, 106, 178, 135, 61, 161, 171, 37, 245,
52, 1, 105, 72, 184, 232, 25, 63, 181, 16, 106, 148, 94, 107, 138, 225, 225, 64, 36, 57, 90, 22, 66, 208, 251, 188, 5, 33, 205, 77, 24, 12, 250,
0,
3, 14, 26, 44, 182, 142, 237, 13, 51, 15, 51, 142, 100, 132, 8, 70, 90, 34, 222, 66, 92, 68, 20, 86, 18, 205, 207, 16, 215, 160, 82, 238, 7,
227, 27, 134, 157, 27, 47, 233, 175, 89, 26, 104, 127, 142, 192, 227, 45, 149, 179, 169, 202, 38, 75, 242, 68, 84, 75, 8, 222, 153, 188, 225, 7,
142, 71, 187, 235, 77, 198, 135, 116, 24, 37, 215, 190, 173, 4, 78, 34, 153, 96, 211, 54, 44, 12, 33, 244, 91, 185, 32, 219, 8, 176, 196, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 78, 95, 77, 65, 73, 78,
]);

// try verifying
const publicKey = signer.publicKey.toString("hex");
const hexString = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
const buffer = Buffer.from(JSON.stringify(sampleData));
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), buffer, signature);
const data = Buffer.from("Hello World!");
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), data, signature);
expect(isValid).toEqual(false);
});

it("should evaulate to false for invalid message", async () => {
const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));
const data = Buffer.from("Hello Bundlr!");
Darlington02 marked this conversation as resolved.
Show resolved Hide resolved
const signature = await signer.sign(data);

const publicKey = signer.publicKey.toString("hex");
const hexString = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
const invalidBuffer = Buffer.from(JSON.stringify(sampleDataTwo));
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), invalidBuffer, signature);
const invalidData = Buffer.from("Hello World!");
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), invalidData, signature);
expect(isValid).toEqual(false);
});

describe("Create & Validate DataItems", () => {
it("should create a valid dataItem", async () => {
const data = JSON.stringify(sampleData);
const data = Buffer.from("Hello, Bundlr!");
const tags = [{ name: "Hello", value: "Bundlr" }];
const item = createData(data, signer, { tags });
await item.sign(signer);
Expand All @@ -171,35 +117,35 @@ describe("Typed Starknet Signer", () => {
describe("With an unknown wallet", () => {
it("should sign & verify an unknown value", async () => {
const randSigner = new StarknetSigner(provider, myAddressInStarknet, PrivateKey);
const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await randSigner.sign(Uint8Array.from(buffer));
const randData = Buffer.from(Crypto.randomBytes(256));
const signature = await randSigner.sign(Uint8Array.from(randData));

const publicKey = signer.publicKey.toString("hex");
const hexString = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), buffer, signature);
const isValid = await StarknetSigner.verify(Buffer.from(hexString, "hex"), randData, signature);
expect(isValid).toEqual(true);
});
});

describe("and given we want to create a dataItem", () => {
describe.each(tagsTestVariations)("with $description tags", ({ tags }) => {
describe.each(dataTestVariations)("and with $description data", ({ data }) => {
it("should create a valid dataItem", async () => {
const item = createData(JSON.stringify(data), signer, { tags });
it("should create a valid dataItems", async () => {
const item = createData(Buffer.from(data), signer, { tags });
await item.sign(signer);
expect(await item.isValid()).toBe(true);
});

it("should set the correct tags", async () => {
const item = createData(JSON.stringify(data), signer, { tags });
const item = createData(Buffer.from(data), signer, { tags });
await item.sign(signer);
expect(item.tags).toEqual(tags ?? []);
});

it("should set the correct data", async () => {
const item = createData(JSON.stringify(data), signer, { tags });
const item = createData(Buffer.from(data), signer, { tags });
await item.sign(signer);
expect(item.rawData).toEqual(Buffer.from(JSON.stringify(data)));
expect(item.rawData).toEqual(Buffer.from(Buffer.from(data)));
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const SIG_CONFIG: Record<SignatureConfig, SignatureMeta> = {
sigName: "typedEthereum",
},
[SignatureConfig.STARKNET]: {
sigLength: 65,
sigLength: 128,
pubLength: 33,
sigName: "starknet",
},
Expand Down
75 changes: 59 additions & 16 deletions src/signing/chains/StarknetSigner.ts
Darlington02 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RpcProvider, WeierstrassSignatureType, BigNumberish } from "starknet";
import { Account, ec, encode, hash } from "starknet";
import type { RpcProvider, WeierstrassSignatureType, TypedData, BigNumberish } from "starknet";
import { Account, ec, encode, typedData } from "starknet";
import type { Signer } from "../index";
import { SignatureConfig, SIG_CONFIG } from "../../constants";

Expand Down Expand Up @@ -40,39 +40,83 @@ export default class StarknetSigner implements Signer {
if (!this.signer.signMessage) throw new Error("Selected signer does not support message signing");

// generate message hash and signature
const msg: BigNumberish[] = uint8ArrayToBigNumberishArray(message);
const msgHash = hash.computeHashOnElements(msg);
const signature: WeierstrassSignatureType = ec.starkCurve.sign(msgHash, this.privateKey);
const chainId = this.chainId;
const msg = uint8ArrayToBigNumberishArray(message);
const data: TypedData = getTypedData(msg, chainId);
const signature = (await this.signer.signMessage(data)) as unknown as WeierstrassSignatureType;

const r = BigInt(signature.r).toString(16).padStart(64, "0"); // Convert BigInt to hex string
const s = BigInt(signature.s).toString(16).padStart(64, "0"); // Convert BigInt to hex string
if (!signature.recovery) throw new Error("signature is missing required recovery component");
const recovery = signature.recovery.toString(16).padStart(2, "0"); // Convert recovery to hex string
const address = this.signer.address.replace(/^0x/, "");

const rArray = Uint8Array.from(Buffer.from(r, "hex"));
const sArray = Uint8Array.from(Buffer.from(s, "hex"));
const recoveryArray = Uint8Array.from(Buffer.from(recovery, "hex"));
const addressToArray = Uint8Array.from(Buffer.from(address, "hex"));
const chainIdToArray = Uint8Array.from(Buffer.from(chainId.replace(/^0x/, "").padStart(64, "0"), "hex"));

// Concatenate the arrays
const result = new Uint8Array(rArray.length + sArray.length + recoveryArray.length);
const result = new Uint8Array(rArray.length + sArray.length + addressToArray.length + chainIdToArray.length);
result.set(rArray);
result.set(sArray, rArray.length);
result.set(recoveryArray, rArray.length + sArray.length);
result.set(addressToArray, rArray.length + sArray.length);
result.set(chainIdToArray, rArray.length + sArray.length + addressToArray.length);

return result;
Darlington02 marked this conversation as resolved.
Show resolved Hide resolved
}

static async verify(_pk: Buffer, message: Uint8Array, _signature: Uint8Array, _opts?: any): Promise<boolean> {
// generate message hash and signature
const msg: BigNumberish[] = uint8ArrayToBigNumberishArray(message);
const msgHash = hash.computeHashOnElements(msg);
const rLength = 32;
const sLength = 32;
const addressLength = 32;
const chainIdLength = 32;

// retrieve address from signature
const addressArrayRetrieved = _signature.slice(rLength + sLength, rLength + sLength + addressLength);
const originalAddress = "0x" + Buffer.from(addressArrayRetrieved).toString("hex");

// retrieve chainId from signature
const chainIdArrayRetrieved = _signature.slice(rLength + sLength + addressLength, rLength + sLength + addressLength + chainIdLength);
const originalChainId = "0x" + Buffer.from(chainIdArrayRetrieved).toString("hex");

// calculate full public key
const fullPubKey = encode.addHexPrefix(encode.buf2hex(_pk));

// generate message hash and signature
const msg = uint8ArrayToBigNumberishArray(message);
const data: TypedData = getTypedData(msg, originalChainId);
const msgHash = typedData.getMessageHash(data, originalAddress);
const signature = _signature.slice(0, -64);

// verify
return ec.starkCurve.verify(_signature.slice(0, -1), msgHash, fullPubKey);
return ec.starkCurve.verify(signature, msgHash, fullPubKey);
}
}

// helper function to convert Uint8Array -> BigNumberishArray
// convert message to TypedData format
function getTypedData(message: BigNumberish[], chainId: string): TypedData {
const typedData: TypedData = {
types: {
StarkNetDomain: [
{ name: "name", type: "shortstring" },
{ name: "version", type: "shortstring" },
{ name: "chainId", type: "shortstring" },
],
SignedMessage: [{ name: "message", type: "felt*" }],
},
primaryType: "SignedMessage",
domain: {
name: "Bundlr",
Darlington02 marked this conversation as resolved.
Show resolved Hide resolved
version: "1",
chainId: chainId,
},
message: {
message: message,
JesseTheRobot marked this conversation as resolved.
Show resolved Hide resolved
},
};
return typedData;
}

// convert Uint8Array to BigNumberishArray
function uint8ArrayToBigNumberishArray(uint8Arr: Uint8Array): BigNumberish[] {
const chunkSize = 31; // 252 bits = 31.5 bytes, but using 31 bytes for safety
const bigNumberishArray: BigNumberish[] = [];
Expand All @@ -89,6 +133,5 @@ function uint8ArrayToBigNumberishArray(uint8Arr: Uint8Array): BigNumberish[] {

bigNumberishArray.push(bigIntValue);
}

return bigNumberishArray;
}