Skip to content

Commit cd11f90

Browse files
Feat/starknet (#6)
* Chore: append address to pubkey (#5) * 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 * chore: append address to pubkey * chore: review modifications * chore: remove logs * feat: add accountContractId --------- Co-authored-by: Darlington Nnam <Darlingtonnnam@gmail.com>
1 parent 9c1285b commit cd11f90

File tree

7 files changed

+209
-471
lines changed

7 files changed

+209
-471
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
"bs58": "^4.0.1",
145145
"keccak": "^3.0.2",
146146
"secp256k1": "^5.0.0",
147-
"starknet": "^6.15.0"
147+
"starknet": "^6.21.0"
148148
},
149149
"optionalDependencies": {
150150
"@randlabs/myalgo-connect": "^1.1.2",
@@ -153,4 +153,4 @@
153153
"multistream": "^4.1.0",
154154
"tmp-promise": "^3.0.2"
155155
}
156-
}
156+
}

src/__tests__/starknet.test.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,17 @@ describe("Typed Starknet Signer", () => {
3333

3434
const PrivateKey = "0x0570d0ab0e4bd9735277e8db6c8e19918c64ed50423aa5860235635d2487c7bb";
3535
const myAddressInStarknet = "0x078e47BBEB4Dc687741825d7bEAD044e229960D3362C0C21F45Bb920db08B0c4";
36-
36+
const accountAbstractionId = 1;
3737
beforeAll(async () => {
38-
signer = new StarknetSigner(provider, myAddressInStarknet, PrivateKey);
38+
signer = new StarknetSigner(provider, myAddressInStarknet, PrivateKey, accountAbstractionId);
3939
await signer.init();
4040
});
4141

4242
it("should sign a known value", async () => {
4343
const expectedSignature = Buffer.from([
4444
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,
4545
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,
46-
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,
47-
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,
46+
165, 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,
4847
]);
4948

5049
const data = Buffer.from("Hello Irys!");
@@ -55,10 +54,9 @@ describe("Typed Starknet Signer", () => {
5554

5655
it("should fail for an invalid signature", async () => {
5756
const expectedSignature = Buffer.from([
58-
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,
59-
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,
60-
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,
61-
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,
57+
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,
58+
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,
59+
165, 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,
6260
]);
6361

6462
const data = Buffer.from("Hello World!");
@@ -80,10 +78,9 @@ describe("Typed Starknet Signer", () => {
8078
it("should evaulate to false for invalid signature", async () => {
8179
// generate invalid signature
8280
const signature = Uint8Array.from([
83-
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,
84-
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,
85-
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,
86-
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,
81+
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,
82+
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,
83+
165, 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,
8784
]);
8885

8986
// try verifying
@@ -116,7 +113,7 @@ describe("Typed Starknet Signer", () => {
116113

117114
describe("With an unknown wallet", () => {
118115
it("should sign & verify an unknown value", async () => {
119-
const randSigner = new StarknetSigner(provider, myAddressInStarknet, PrivateKey);
116+
const randSigner = new StarknetSigner(provider, myAddressInStarknet, PrivateKey, accountAbstractionId);
120117
const randData = Buffer.from(Crypto.randomBytes(256));
121118
const signature = await randSigner.sign(Uint8Array.from(randData));
122119

src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export const SIG_CONFIG: Record<SignatureConfig, SignatureMeta> = {
5252
sigName: "typedEthereum",
5353
},
5454
[SignatureConfig.STARKNET]: {
55-
sigLength: 128, // 64 bytes signature, + 32 bytes address + 32 bytes chainId
56-
pubLength: 33,
55+
sigLength: 96, // 64 bytes signature, + 32 bytes chainId
56+
pubLength: 67, // 33 bytes public key + 32 bytes address (for sdk) + 2 bytes accountContractId
5757
sigName: "starknet",
5858
},
5959
};

src/signing/chains/StarknetSigner.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,49 @@ import type { RpcProvider, WeierstrassSignatureType, TypedData, BigNumberish } f
22
import { Account, ec, encode, hash, typedData } from "starknet";
33
import type { Signer } from "../index";
44
import { SignatureConfig, SIG_CONFIG } from "../../constants";
5+
import { shortTo2ByteArray } from "../../utils";
6+
7+
export function extractX(bytes: Buffer): string {
8+
const hex = bytes.subarray(1).toString("hex");
9+
const stripped = hex.replace(/^0+/gm, ""); // strip leading 0s
10+
return `0x${stripped}`;
11+
}
512

613
export default class StarknetSigner implements Signer {
714
protected signer: Account;
15+
// public key is structured as: actual public key (33 bytes), address (32 bytes), accountContractId (2 bytes)
16+
// this is because normal public key -> address derivation is impossible, so we use an identifer to know which account ABI the signer is using, so we can access the public key from the account contract to verify the public key -> address association
817
public publicKey: Buffer;
918
public address: string;
1019
private privateKey: string;
1120
public provider: RpcProvider;
1221
public chainId: string;
22+
public accountContractId: number;
1323
readonly ownerLength: number = SIG_CONFIG[SignatureConfig.STARKNET].pubLength;
1424
readonly signatureLength: number = SIG_CONFIG[SignatureConfig.STARKNET].sigLength;
1525
readonly signatureType: number = SignatureConfig.STARKNET;
1626

17-
constructor(provider: RpcProvider, address: string, pKey: string) {
27+
constructor(provider: RpcProvider, address: string, privateKey: string, accountContractId: number) {
1828
this.provider = provider;
1929
this.address = address;
20-
this.privateKey = pKey;
21-
this.signer = new Account(provider, address, pKey);
30+
this.privateKey = privateKey;
31+
this.accountContractId = accountContractId;
32+
this.signer = new Account(provider, address, privateKey);
2233
}
2334

2435
public async init(): Promise<void> {
2536
const pubKey = encode.addHexPrefix(encode.buf2hex(ec.starkCurve.getPublicKey(this.privateKey, true)));
26-
const hexKey = pubKey.startsWith("0x") ? pubKey.slice(2) : pubKey;
37+
const address = this.signer.address;
38+
39+
// get pubkey and address buffers
40+
const pubKeyBuffer = Buffer.from(pubKey.startsWith("0x") ? pubKey.slice(2) : pubKey, "hex");
41+
42+
const addressBuffer = Buffer.from(address.startsWith("0x") ? address.slice(2) : address, "hex");
43+
const accountContractId = shortTo2ByteArray(this.accountContractId);
44+
45+
// concatenate buffers as pubKey
46+
this.publicKey = Buffer.concat([pubKeyBuffer, addressBuffer, accountContractId]);
2747

28-
this.publicKey = Buffer.from(hexKey, "hex");
2948
this.chainId = await this.provider.getChainId();
3049
}
3150

@@ -43,54 +62,53 @@ export default class StarknetSigner implements Signer {
4362

4463
const r = BigInt(signature.r).toString(16).padStart(64, "0"); // Convert BigInt to hex string
4564
const s = BigInt(signature.s).toString(16).padStart(64, "0"); // Convert BigInt to hex string
46-
const address = this.signer.address.replace(/^0x0?|^0x/, "").padStart(64, "0");
4765

4866
const rArray = Uint8Array.from(Buffer.from(r, "hex"));
4967
const sArray = Uint8Array.from(Buffer.from(s, "hex"));
50-
const addressToArray = Uint8Array.from(Buffer.from(address, "hex"));
5168
const chainIdToArray = Uint8Array.from(Buffer.from(chainId.replace(/^0x/, "").padStart(64, "0"), "hex"));
5269

5370
// Concatenate the arrays
54-
const result = new Uint8Array(rArray.length + sArray.length + addressToArray.length + chainIdToArray.length);
71+
const result = new Uint8Array(rArray.length + sArray.length + chainIdToArray.length);
5572
result.set(rArray);
5673
result.set(sArray, rArray.length);
57-
result.set(addressToArray, rArray.length + sArray.length);
58-
result.set(chainIdToArray, rArray.length + sArray.length + addressToArray.length);
74+
result.set(chainIdToArray, rArray.length + sArray.length);
5975

6076
// check signature is of required length
61-
if (result.length !== 128) throw new Error("Signature length must be 128 bytes!");
77+
if (result.length !== 96) throw new Error("Signature length must be 96 bytes!");
6278

6379
return result;
6480
}
6581

6682
static async verify(pubkey: Buffer, message: Uint8Array, signature: Uint8Array, _opts?: any): Promise<boolean> {
6783
const rLength = 32;
6884
const sLength = 32;
69-
const addressLength = 32;
70-
const chainIdLength = 32;
7185

72-
// retrieve address from signature
73-
const addressArrayRetrieved = signature.slice(rLength + sLength, rLength + sLength + addressLength);
74-
const originalAddress = "0x" + Buffer.from(addressArrayRetrieved).toString("hex");
86+
// retrieve pubKey and address from pubKey
87+
const [originalPubKey, originalAddressBin] = decomposePubkey(pubkey);
88+
const originalAddress = "0x" + Buffer.from(originalAddressBin).toString("hex");
7589

7690
// retrieve chainId from signature
77-
const chainIdArrayRetrieved = signature.slice(rLength + sLength + addressLength, rLength + sLength + addressLength + chainIdLength);
91+
const chainIdArrayRetrieved = signature.slice(rLength + sLength);
7892
const originalChainId = "0x" + Buffer.from(chainIdArrayRetrieved).toString("hex");
7993

8094
// calculate full public key
81-
const fullPubKey = encode.addHexPrefix(encode.buf2hex(pubkey));
95+
const fullPubKey = encode.addHexPrefix(encode.buf2hex(originalPubKey));
8296

8397
// generate message hash and signature
8498
const msg = hash.computeHashOnElements(uint8ArrayToBigNumberishArray(message));
8599
const data: TypedData = getTypedData(msg, originalChainId);
86100
const msgHash = typedData.getMessageHash(data, originalAddress);
87-
const trimmedSignature = signature.slice(0, -64);
101+
const trimmedSignature = signature.slice(0, -32);
88102

89103
// verify
90104
return ec.starkCurve.verify(trimmedSignature, msgHash, fullPubKey);
91105
}
92106
}
93107

108+
export function decomposePubkey(pubkey: Buffer): [Buffer, Buffer, Buffer] {
109+
return [pubkey.slice(0, 33), pubkey.slice(33, -2), pubkey.slice(-2)];
110+
}
111+
94112
// convert message to TypedData format
95113
export function getTypedData(message: string, chainId: string): TypedData {
96114
const typedData: TypedData = {

src/signing/chains/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ export { default as TypedEthereumSigner } from "./TypedEthereumSigner";
1515
export * from "./InjectedTypedEthereumSigner";
1616
export { default as ArconnectSigner } from "./arconnectSigner";
1717
export { default as StarknetSigner } from "./StarknetSigner";
18-
export { default as InjectedStarknetSigner } from "./injectedStarknetSigner";
18+
export { default as InjectedStarknetSigner } from "./injectedStarknetSigner";

src/signing/chains/injectedStarknetSigner.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import { ec, encode, hash, typedData } from "starknet";
33
import type { Signer } from "../index";
44
import { getTypedData, uint8ArrayToBigNumberishArray } from "./StarknetSigner";
55
import { SignatureConfig, SIG_CONFIG } from "../../constants";
6+
import { shortTo2ByteArray } from "../../utils";
67

78
export default class InjectedStarknetSigner implements Signer {
89
public walletAccount: WalletAccount;
910
public publicKey: Buffer;
1011
public provider: RpcProvider;
1112
public chainId: string;
13+
public accountContractId: number;
1214
readonly ownerLength: number = SIG_CONFIG[SignatureConfig.STARKNET].pubLength;
1315
readonly signatureLength: number = SIG_CONFIG[SignatureConfig.STARKNET].sigLength;
1416
readonly signatureType: number = SignatureConfig.STARKNET;
1517

16-
constructor(provider: RpcProvider, walletAccount: WalletAccount) {
18+
constructor(provider: RpcProvider, walletAccount: WalletAccount, accountContractId: number) {
1719
this.provider = provider;
1820
this.walletAccount = walletAccount;
21+
this.accountContractId = accountContractId;
1922
}
2023

21-
public async init(): Promise<void> {
24+
public async init(pubKey: string): Promise<void> {
25+
const address = this.walletAccount.address;
26+
27+
// get pubkey and address buffers
28+
const pubKeyBuffer = Buffer.from(pubKey.startsWith("0x") ? pubKey.slice(2) : pubKey, "hex");
29+
const addressBuffer = Buffer.from(address.startsWith("0x") ? address.slice(2) : address, "hex");
30+
const accountContractId = shortTo2ByteArray(this.accountContractId);
31+
32+
// concatenate buffers as pubKey
33+
this.publicKey = Buffer.concat([pubKeyBuffer, addressBuffer, accountContractId]);
34+
2235
this.chainId = await this.provider.getChainId();
2336
}
2437

@@ -36,48 +49,43 @@ export default class InjectedStarknetSigner implements Signer {
3649
const rsComponents = Array.from(signature).slice(-2);
3750
const r = BigInt(rsComponents[0]).toString(16).padStart(64, "0");
3851
const s = BigInt(rsComponents[1]).toString(16).padStart(64, "0");
39-
const address = this.walletAccount.address.replace(/^0x0?|^0x/, "").padStart(64, "0");
4052

4153
const rArray = Uint8Array.from(Buffer.from(r, "hex"));
4254
const sArray = Uint8Array.from(Buffer.from(s, "hex"));
43-
const addressToArray = Uint8Array.from(Buffer.from(address, "hex"));
4455
const chainIdToArray = Uint8Array.from(Buffer.from(chainId.replace(/^0x/, "").padStart(64, "0"), "hex"));
4556

4657
// Concatenate the arrays
47-
const result = new Uint8Array(rArray.length + sArray.length + addressToArray.length + chainIdToArray.length);
58+
const result = new Uint8Array(rArray.length + sArray.length + chainIdToArray.length);
4859
result.set(rArray);
4960
result.set(sArray, rArray.length);
50-
result.set(addressToArray, rArray.length + sArray.length);
51-
result.set(chainIdToArray, rArray.length + sArray.length + addressToArray.length);
61+
result.set(chainIdToArray, rArray.length + sArray.length);
5262

5363
// check signature is of required length
54-
if (result.length !== 128) throw new Error("Signature length must be 128 bytes!");
64+
if (result.length !== 96) throw new Error("Signature length must be 96 bytes!");
5565

5666
return result;
5767
}
5868

5969
static async verify(pubkey: Buffer, message: Uint8Array, signature: Uint8Array, _opts?: any): Promise<boolean> {
6070
const rLength = 32;
6171
const sLength = 32;
62-
const addressLength = 32;
63-
const chainIdLength = 32;
6472

65-
// retrieve address from signature
66-
const addressArrayRetrieved = signature.slice(rLength + sLength, rLength + sLength + addressLength);
67-
const originalAddress = "0x" + Buffer.from(addressArrayRetrieved).toString("hex");
73+
// retrieve pubKey and address from pubKey
74+
const originalPubKey = pubkey.slice(0, 33);
75+
const originalAddress = "0x" + Buffer.from(pubkey.slice(33, -2)).toString("hex");
6876

6977
// retrieve chainId from signature
70-
const chainIdArrayRetrieved = signature.slice(rLength + sLength + addressLength, rLength + sLength + addressLength + chainIdLength);
78+
const chainIdArrayRetrieved = signature.slice(rLength + sLength);
7179
const originalChainId = "0x" + Buffer.from(chainIdArrayRetrieved).toString("hex");
7280

7381
// calculate full public key
74-
const fullPubKey = encode.addHexPrefix(encode.buf2hex(pubkey));
82+
const fullPubKey = encode.addHexPrefix(encode.buf2hex(originalPubKey));
7583

7684
// generate message hash and signature
7785
const msg = hash.computeHashOnElements(uint8ArrayToBigNumberishArray(message));
7886
const data: TypedData = getTypedData(msg, originalChainId);
7987
const msgHash = typedData.getMessageHash(data, originalAddress);
80-
const trimmedSignature = signature.slice(0, -64);
88+
const trimmedSignature = signature.slice(0, -32);
8189

8290
// verify
8391
return ec.starkCurve.verify(trimmedSignature, msgHash, fullPubKey);

0 commit comments

Comments
 (0)