Skip to content

Commit

Permalink
Starknet signer (#3)
Browse files Browse the repository at this point in the history
* feat: add starknet implementation

* update

* feat: sign and verify methods

* fix: sign/verify

* chore: include ts error

* chore: include more sample data for tests

* chore: make `verify` method independent

* chore: fix ar-data-base.ts

* chore: add extra tests

---------

Co-authored-by: Adegbite Ademola Kelvin <[email protected]>
Co-authored-by: Darlington02 <[email protected]>
  • Loading branch information
3 people authored Oct 7, 2024
1 parent eb3eea7 commit f06294c
Show file tree
Hide file tree
Showing 9 changed files with 2,840 additions and 2,535 deletions.
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 () => {
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

0 comments on commit f06294c

Please sign in to comment.