Skip to content

Commit

Permalink
chore: make sign and verify EIP-712 compatible (#4)
Browse files Browse the repository at this point in the history
* chore: rm recovery component

* chore: make `sign` and `verify` EIP-712 compatible

* chore: fix `verify`

* fix: include `chainId`

* chore: pad chainId to 32 bytes

* chore: fix review changes

* chore: rep tx hash as hex

* feat: injected starknet signer

* chore: update injected starknet signer to account for multiple signatures
  • Loading branch information
Darlington02 authored Oct 28, 2024
1 parent a10b324 commit 458224b
Show file tree
Hide file tree
Showing 7 changed files with 1,537 additions and 1,220 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
"bs58": "^4.0.1",
"keccak": "^3.0.2",
"secp256k1": "^5.0.0",
"starknet": "^6.11.0"
"starknet": "^6.15.0"
},
"optionalDependencies": {
"@randlabs/myalgo-connect": "^1.1.2",
Expand All @@ -153,4 +153,4 @@
"multistream": "^4.1.0",
"tmp-promise": "^3.0.2"
}
}
}
140 changes: 43 additions & 97 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 @@ -89,80 +41,74 @@ describe("Typed Starknet Signer", () => {

it("should sign a known value", 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,
4, 122, 51, 60, 218, 66, 57, 104, 199, 126, 49, 15, 195, 203, 209, 15, 62, 214, 104, 245, 237, 79, 12, 252, 141, 242, 95, 4, 176, 235, 231, 189,
7, 126, 187, 220, 69, 127, 240, 85, 198, 31, 219, 33, 230, 0, 142, 230, 0, 200, 246, 208, 144, 191, 118, 88, 85, 216, 105, 65, 129, 174, 37,
165, 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 Irys!");
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 value", async () => {
const data = Buffer.from("Hello Irys!");
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 Irys!");
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 tags = [{ name: "Hello", value: "Bundlr" }];
const data = Buffer.from("Hello, Irys!");
const tags = [{ name: "Hello", value: "Irys" }];
const item = createData(data, signer, { tags });
await item.sign(signer);
expect(await item.isValid()).toBe(true);
Expand All @@ -171,12 +117,12 @@ 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);
});
});
Expand All @@ -185,21 +131,21 @@ describe("Typed Starknet Signer", () => {
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 });
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, // 64 bytes signature, + 32 bytes address + 32 bytes chainId
pubLength: 33,
sigName: "starknet",
},
Expand Down
85 changes: 66 additions & 19 deletions src/signing/chains/StarknetSigner.ts
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, hash, typedData } from "starknet";
import type { Signer } from "../index";
import { SignatureConfig, SIG_CONFIG } from "../../constants";

Expand All @@ -23,8 +23,8 @@ export default class StarknetSigner implements Signer {

public async init(): Promise<void> {
try {
const pub_key = encode.addHexPrefix(encode.buf2hex(ec.starkCurve.getPublicKey(this.privateKey, true)));
const hexKey = pub_key.startsWith("0x") ? pub_key.slice(2) : pub_key;
const pubKey = encode.addHexPrefix(encode.buf2hex(ec.starkCurve.getPublicKey(this.privateKey, true)));
const hexKey = pubKey.startsWith("0x") ? pubKey.slice(2) : pubKey;

this.publicKey = Buffer.from(hexKey, "hex");
this.chainId = await this.provider.getChainId();
Expand All @@ -37,43 +37,90 @@ export default class StarknetSigner implements Signer {
if (!this.publicKey) {
await this.init();
}
if (!this.signer.signMessage) throw new Error("Selected signer does not support message signing");
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 = hash.computeHashOnElements(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(/^0x0?|^0x/, "").padStart(64, "0");

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);

// check signature is of required length
if (result.length != 128) throw new Error("Signature length must be 128 bytes!");

return result;
}

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 = hash.computeHashOnElements(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
function uint8ArrayToBigNumberishArray(uint8Arr: Uint8Array): BigNumberish[] {
// convert message to TypedData format
export function getTypedData(message: string, chainId: string): TypedData {
const typedData: TypedData = {
types: {
StarkNetDomain: [
{ name: "name", type: "shortstring" },
{ name: "version", type: "shortstring" },
{ name: "chainId", type: "shortstring" },
],
SignedMessage: [{ name: "transactionHash", type: "shortstring" }],
},
primaryType: "SignedMessage",
domain: {
name: "Irys",
version: "1",
chainId: chainId,
},
message: {
transactionHash: message,
},
};
return typedData;
}

// convert Uint8Array to BigNumberish
export function uint8ArrayToBigNumberishArray(uint8Arr: Uint8Array): BigNumberish[] {
const chunkSize = 31; // 252 bits = 31.5 bytes, but using 31 bytes for safety
const bigNumberishArray: BigNumberish[] = [];

Expand Down
1 change: 1 addition & 0 deletions src/signing/chains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { default as TypedEthereumSigner } from "./TypedEthereumSigner";
export * from "./InjectedTypedEthereumSigner";
export { default as ArconnectSigner } from "./arconnectSigner";
export { default as StarknetSigner } from "./StarknetSigner";
export { default as InjectedStarknetSigner } from "./injectedStarknetSigner";
Loading

0 comments on commit 458224b

Please sign in to comment.