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 7 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
17 changes: 12 additions & 5 deletions src/DataItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { sign } from "./ar-data-bundle";
import type { BundleItem } from "./BundleItem";
import type { Signer } from "./signing/index";
import { indexToType } from "./signing/index";
import getSignatureData from "./ar-data-base";
import { getSignatureData, getStarknetSignatureData } from "./ar-data-base";
import { SIG_CONFIG, SignatureConfig } from "./constants";
import { getCryptoDriver } from "$/utils";
import { deserializeTags } from "./tags";
Expand Down Expand Up @@ -237,13 +237,20 @@ export class DataItem implements BundleItem {
// eslint-disable-next-line @typescript-eslint/naming-convention
const Signer = indexToType[sigType];

const signatureData = await getSignatureData(item);
let signatureData;
if (sigType == 8) {
signatureData = await getStarknetSignatureData(item);
}
else {
signatureData = await getSignatureData(item);
}

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
2 changes: 1 addition & 1 deletion src/__tests__/arDataBase.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getSignatureData from "../ar-data-base";
import { getSignatureData } from "../ar-data-base";
import { createData, EthereumSigner } from "../../index";

describe("getSignatureData", () => {
Expand Down
170 changes: 170 additions & 0 deletions src/__tests__/starknet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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 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);
});

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)));
});
});
});
});
});
});
9 changes: 8 additions & 1 deletion src/ar-data-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@ async function getSignatureData(item: DataItem): Promise<Uint8Array> {
]);
}

export default getSignatureData;
async function getStarknetSignatureData(item: DataItem): Promise<Uint8Array> {
return Uint8Array.from(item.rawData)
}

export {
getSignatureData,
getStarknetSignatureData
}
10 changes: 8 additions & 2 deletions src/ar-data-bundle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getSignatureData from "./ar-data-base";
import { getSignatureData, getStarknetSignatureData} from "./ar-data-base";
import { longTo32ByteArray } from "./utils";
import type DataItem from "./DataItem";
import Bundle from "./Bundle";
Expand Down Expand Up @@ -64,7 +64,13 @@ export async function bundleAndSignData(dataItems: DataItem[], signer: Signer):
* @returns signings - signature and id in byte-arrays
*/
export async function getSignatureAndId(item: DataItem, signer: Signer): Promise<{ signature: Buffer; id: Buffer }> {
const signatureData = await getSignatureData(item);
let signatureData;
if (signer.signatureType == 8) {
signatureData = await getStarknetSignatureData(item);
}
else {
signatureData = await getSignatureData(item);
}

const signatureBytes = await signer.sign(signatureData);
const idBytes = await getCryptoDriver().hash(signatureBytes);
Expand Down
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'
}
};
102 changes: 102 additions & 0 deletions src/signing/chains/StarknetSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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 static address: string;
private static privateKey: string;
public static 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) {
StarknetSigner.provider = provider;
StarknetSigner.address = address;
StarknetSigner.privateKey = pKey;
this.signer = new Account(provider, address, pKey);
}

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

this.publicKey = Buffer.from(hexKey, 'hex');
this.chainId = await StarknetSigner.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) {
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, StarknetSigner.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));
console.log(fullPubKey)

// 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