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

Starknet signer #3

Merged
merged 9 commits into from
Oct 7, 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
"base64url": "^3.0.1",
"bs58": "^4.0.1",
"keccak": "^3.0.2",
"secp256k1": "^5.0.0"
"secp256k1": "^5.0.0",
"starknet": "^6.11.0"
},
"optionalDependencies": {
"@randlabs/myalgo-connect": "^1.1.2",
Expand Down
6 changes: 3 additions & 3 deletions src/DataItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ export class DataItem implements BundleItem {
return await Signer.verify(item.rawOwner, signatureData, item.rawSignature);
}

public async getSignatureData(): Promise<Uint8Array> {
return getSignatureData(this);
}
public async getSignatureData(): Promise<Uint8Array> {
return getSignatureData(this);
}

/**
* Returns the start byte of the tags section (number of tags)
Expand Down
217 changes: 217 additions & 0 deletions src/__tests__/starknet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
jest.setTimeout(20000);
import StarknetSigner from "../signing/chains/StarknetSigner";
import { RpcProvider, shortString, TypedData } from "starknet";
import { createData } from "../../index";

const tagsTestVariations = [
{ description: "no tags", tags: undefined },
{ description: "empty tags", tags: [] },
{ description: "single tag", tags: [{ name: "Content-Type", value: "image/png" }] },
{
description: "multiple tags",
tags: [
{ name: "Content-Type", value: "image/png" },
{ name: "hello", value: "world" },
{ name: "lorem", value: "ipsum" },
],
},
];

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 },
];

describe("Typed Starknet Signer", () => {
let signer: StarknetSigner;
const provider = new RpcProvider({ nodeUrl: "https://starknet-sepolia.public.blastapi.io" });

const PrivateKey = "0x0570d0ab0e4bd9735277e8db6c8e19918c64ed50423aa5860235635d2487c7bb";
const myAddressInStarknet = "0x078e47BBEB4Dc687741825d7bEAD044e229960D3362C0C21F45Bb920db08B0c4";

beforeAll(async () => {
signer = new StarknetSigner(provider, myAddressInStarknet, PrivateKey);
await signer.init();
});

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

const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));
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
]);

const buffer = Buffer.from(JSON.stringify(sampleData));
const signature = await signer.sign(Uint8Array.from(buffer));
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 () => {
JesseTheRobot marked this conversation as resolved.
Show resolved Hide resolved
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 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
]);

// 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);
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 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);
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 item = createData(data, signer, { tags });
await item.sign(signer);
expect(await item.isValid()).toBe(true);
});

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

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 });
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 });
await item.sign(signer);
expect(item.tags).toEqual(tags ?? []);
});

it("should set the correct data", async () => {
const item = createData(JSON.stringify(data), signer, { tags });
await item.sign(signer);
expect(item.rawData).toEqual(Buffer.from(JSON.stringify(data)));
});
});
});
});
});
});
2 changes: 1 addition & 1 deletion src/ar-data-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ async function getSignatureData(item: DataItem): Promise<Uint8Array> {
]);
}

export default getSignatureData;
export default getSignatureData;
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum SignatureConfig {
INJECTEDAPTOS = 5,
MULTIAPTOS = 6,
TYPEDETHEREUM = 7,
STARKNET = 8,
}

export interface SignatureMeta {
Expand Down Expand Up @@ -50,4 +51,9 @@ export const SIG_CONFIG: Record<SignatureConfig, SignatureMeta> = {
pubLength: 42,
sigName: "typedEthereum",
},
[SignatureConfig.STARKNET]:{
sigLength:65,
pubLength: 33,
sigName:'starknet'
}
};
101 changes: 101 additions & 0 deletions src/signing/chains/StarknetSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
Account,
RpcProvider,
WeierstrassSignatureType,
ec,
encode,
hash,
BigNumberish
} from "starknet";
import type { Signer } from "../index";
import { SignatureConfig, SIG_CONFIG } from "../../constants";

export default class StarknetSigner implements Signer {
protected signer: Account;
public publicKey: Buffer;
public address: string;
private privateKey: string;
public provider: RpcProvider;
public chainId: string;
readonly ownerLength: number = SIG_CONFIG[SignatureConfig.STARKNET].pubLength;
readonly signatureLength: number = SIG_CONFIG[SignatureConfig.STARKNET].sigLength;
readonly signatureType: number = SignatureConfig.STARKNET;

constructor(provider: RpcProvider, address: string, pKey: string) {
this.provider = provider;
this.address = address;
this.privateKey = pKey;
this.signer = new Account(provider, address, pKey);
}

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

this.publicKey = Buffer.from(hexKey, 'hex');
this.chainId = await this.provider.getChainId();
} catch (error) {
console.error("Error setting public key or chain ID:", error);
}
}

async sign(message: Uint8Array, _opts?: any): Promise<Uint8Array> {
if (!this.publicKey) {
await this.init();
}
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 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
// @ts-ignore
const recovery = signature.recovery.toString(16).padStart(2, "0"); // Convert recovery to hex string

const rArray = Uint8Array.from(Buffer.from(r, "hex"));
const sArray = Uint8Array.from(Buffer.from(s, "hex"));
const recoveryArray = Uint8Array.from(Buffer.from(recovery, "hex"));

// Concatenate the arrays
const result = new Uint8Array(rArray.length + sArray.length + recoveryArray.length);
result.set(rArray);
result.set(sArray, rArray.length);
result.set(recoveryArray, rArray.length + sArray.length);
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 fullPubKey = encode.addHexPrefix(encode.buf2hex(_pk));

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

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

for (let i = 0; i < uint8Arr.length; i += chunkSize) {
// Extract a chunk of size 31 bytes
const chunk = uint8Arr.slice(i, i + chunkSize);

// Convert the chunk to a bigint
let bigIntValue = BigInt(0);
for (let j = 0; j < chunk.length; j++) {
bigIntValue = (bigIntValue << BigInt(8)) + BigInt(chunk[j]);
}

bigNumberishArray.push(bigIntValue);
}

return bigNumberishArray;
}
1 change: 1 addition & 0 deletions src/signing/chains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export { default as MultiSignatureAptosSigner } from "./multiSignatureAptos";
export { default as TypedEthereumSigner } from "./TypedEthereumSigner";
export * from "./InjectedTypedEthereumSigner";
export { default as ArconnectSigner } from "./arconnectSigner";
export { default as StarknetSigner } from "./StarknetSigner";
5 changes: 5 additions & 0 deletions src/signing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
InjectedAptosSigner,
MultiSignatureAptosSigner,
TypedEthereumSigner,
StarknetSigner
} from "./chains/index";

export type IndexToType = Record<
Expand Down Expand Up @@ -42,4 +43,8 @@ export const indexToType: IndexToType = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
7: TypedEthereumSigner,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
8: StarknetSigner

};
Loading
Loading