Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
refactor(experimental): a sham for Keypair
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Nov 17, 2023
1 parent d0504a4 commit 69ba06a
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/library-legacy-sham/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"node": ">=17.4"
},
"dependencies": {
"@noble/ed25519": "^2.0.0",
"@noble/hashes": "^1.3.2",
"@solana/addresses": "workspace:*"
},
"devDependencies": {
Expand Down
81 changes: 81 additions & 0 deletions packages/library-legacy-sham/src/__tests__/key-pair-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { utils } from '@noble/ed25519';

import { Keypair } from '../key-pair';
import { PublicKey } from '../public-key';

const MOCK_PRIVATE_KEY_BYTES = [
151, 227, 37, 180, 104, 169, 5, 53, 191, 115, 132, 187, 223, 228, 25, 52, 7, 50, 86, 18, 151, 45, 105, 68, 31, 21,
128, 21, 32, 16, 222, 239,
];
const MOCK_PUBLIC_KEY_BYTES = [
117, 62, 75, 185, 26, 65, 209, 23, 95, 56, 97, 216, 197, 215, 208, 14, 138, 142, 59, 114, 43, 60, 190, 86, 21, 58,
46, 232, 77, 145, 46, 101,
];

describe('KeypairSham', () => {
it.each(['fromSecretKey', 'fromSeed'] as (keyof typeof Keypair)[])('throws when calling `%s`', method => {
expect(() =>
// This is basically just complaining that `prototype` is not callable.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Keypair[method]()
).toThrow(`Keypair#${method.toString()} is unimplemented`);
});
describe('generate()', () => {
it('returns a `Keypair` instance', () => {
expect(Keypair.generate()).toBeInstanceOf(Keypair);
});
});
describe.each([
[
'generated keypair',
() => {
jest.spyOn(utils, 'randomPrivateKey').mockReturnValue(new Uint8Array(MOCK_PRIVATE_KEY_BYTES));
return new Keypair();
},
],
[
'user-supplied keypair',
() =>
new Keypair({
publicKey: new Uint8Array(MOCK_PUBLIC_KEY_BYTES),
secretKey: new Uint8Array([...MOCK_PRIVATE_KEY_BYTES, ...MOCK_PUBLIC_KEY_BYTES]),
}),
],
[
'user-supplied keypair whose `publicKey` does not correspond to the supplied private key',
() =>
new Keypair({
publicKey: new Uint8Array(Array(32).fill(9)),
secretKey: new Uint8Array([...MOCK_PRIVATE_KEY_BYTES, ...MOCK_PUBLIC_KEY_BYTES]),
}),
],
[
"user-supplied keypair whose last 32 bytes of the `secretKey` do not represent the private key's public key",
() =>
new Keypair({
publicKey: new Uint8Array(MOCK_PUBLIC_KEY_BYTES),
secretKey: new Uint8Array([...MOCK_PRIVATE_KEY_BYTES, ...Array(32).fill(9)]),
}),
],
])('given a %s', (_, createKeyPair) => {
let keyPair: Keypair;
beforeEach(() => {
keyPair = createKeyPair();
});
it('vends the a public key instance at `publicKey`', () => {
expect(keyPair.publicKey).toBeInstanceOf(PublicKey);
});
it('vends the public key associated with the secret key', () => {
expect(keyPair.publicKey.toBytes()).toEqual(new Uint8Array(MOCK_PUBLIC_KEY_BYTES));
});
it('vends a 64 byte array at `secretKey`, the first half of which is the private key and the second half which is the public key', () => {
expect(keyPair.secretKey).toEqual(new Uint8Array([...MOCK_PRIVATE_KEY_BYTES, ...MOCK_PUBLIC_KEY_BYTES]));
});
it('throws when accessing `_keypair`', () => {
expect(() => {
keyPair._keypair;
}).toThrow();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Keypair as LegacyKeypairWithPrivateKeypairProperty } from '@solana/web3.js-legacy';

import { Keypair } from '../key-pair';

type LegacyKeypair = Omit<LegacyKeypairWithPrivateKeypairProperty, '_keypair'>;

new Keypair() satisfies LegacyKeypair;
new Keypair({
publicKey: new Uint8Array([]),
secretKey: new Uint8Array([]),
}) satisfies LegacyKeypair;

// I want this to pass, but there's no way to match the `_keypair` properties
// in each of these classes, because the legacy one has `private` visibilty.
// @ts-expect-error
Keypair satisfies typeof LegacyKeypairWithPrivateKeypairProperty;
2 changes: 1 addition & 1 deletion packages/library-legacy-sham/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './key-pair';
export * from './public-key';

export const LAMPORTS_PER_SOL = 1_000_000_000;
export const MAX_SEED_LENGTH = 32;
export const NONCE_ACCOUNT_LENGTH = 80;
Expand Down
64 changes: 64 additions & 0 deletions packages/library-legacy-sham/src/key-pair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { etc, getPublicKey, utils } from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha512';
import { getAddressDecoder } from '@solana/addresses';

import { PublicKey } from './public-key';
import { createUnimplementedFunction } from './unimplemented';

export class Keypair {
#cachedPublicKey: PublicKey | undefined;
#cachedPublicKeyBytes: Uint8Array | undefined;
#secretKeyBytes: Uint8Array;
constructor(keypair?: {
publicKey: Uint8Array;
/**
* A 64 byte secret key, the first 32 bytes of which is the
* private scalar and the last 32 bytes is the public key.
* Read more: https://blog.mozilla.org/warner/2011/11/29/ed25519-keys/
*/
secretKey: Uint8Array;
}) {
if (keypair) {
this.#secretKeyBytes = keypair.secretKey.slice(0, 32);
} else {
this.#secretKeyBytes = this.#generateSecretKeyBytes();
}
}
get #publicKeyBytes() {
if (!this.#cachedPublicKeyBytes) {
if (!etc.sha512Sync) {
etc.sha512Sync = (...m) => sha512(etc.concatBytes(...m));
}
this.#cachedPublicKeyBytes = getPublicKey(this.#secretKeyBytes);
}
return this.#cachedPublicKeyBytes;
}
#generateSecretKeyBytes() {
return utils.randomPrivateKey();
}
get _keypair() {
throw new Error(
'This error is being thrown from `@solana/web3.js-legacy-sham`. The legacy ' +
'implementation of `Keypair` historically exposed the internal property ' +
'`_keypair` but the sham does not. Please eliminate this access of `_keypair` ' +
'and replace it with an implementation that makes use of the available public ' +
'methods.'
);
}
get publicKey(): PublicKey {
if (!this.#cachedPublicKey) {
const publicKeyBytes = this.#publicKeyBytes;
const [address] = getAddressDecoder().decode(publicKeyBytes);
this.#cachedPublicKey = new PublicKey(address);
}
return this.#cachedPublicKey;
}
get secretKey(): Uint8Array {
return new Uint8Array([...this.#secretKeyBytes, ...this.#publicKeyBytes]);
}
static fromSecretKey = createUnimplementedFunction('Keypair#fromSecretKey');
static fromSeed = createUnimplementedFunction('Keypair#fromSeed');
static generate() {
return new Keypair();
}
}
1 change: 1 addition & 0 deletions packages/test-config/jest-unit.config.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const config: Partial<Config.InitialProjectOptions> = {
},
],
},
transformIgnorePatterns: [],
};

export default config;
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 69ba06a

Please sign in to comment.