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

feat(bls): validate proof of possession #815

Merged
merged 23 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ import { avm, pvm, evm } from '@avalabs/avalanchejs';
## Importing Essentials

```ts
import { avm /** X-chain */, pvm /** P-chain */, evm /** C-chain */, utils } from "@avalabs/avalanchejs"
import { avm /** X-chain */, pvm /** P-chain */, evm /** C-chain */, utils, secp256k1 } from "@avalabs/avalanchejs"

// example calls
const exportTx = avm.newExportTx(...) // constructs a new export tx from X
const addValidatorTx = pvm.newAddPermissionlessValidatorTx(...) // constructs a new add validator tx on P
const importTx = evm.newImportTx(...) // constructs a new import tx to C

const publicKeyBytes = utils.hexToBuffer(publicKeyHex)
const signature = utils.signHash(bytes, privateKeyBytes)
const signature = secp256k1.signHash(bytes, privateKeyBytes)
```

Please check out the `examples` folder for more info.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"prepare": "husky install"
},
"dependencies": {
"@noble/curves": "1.3.0",
"@noble/hashes": "1.3.3",
"@noble/secp256k1": "2.0.0",
"@scure/base": "1.1.5",
Expand All @@ -48,8 +49,8 @@
"@semantic-release/git": "10.0.1",
"@semantic-release/github": "9.2.6",
"@semantic-release/npm": "11.0.2",
"@types/node": "20.11.10",
"@types/jest": "29.5.11",
"@types/node": "20.11.10",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"commitizen": "4.3.0",
Expand Down
2 changes: 0 additions & 2 deletions src/constants/bls.ts

This file was deleted.

47 changes: 47 additions & 0 deletions src/crypto/bls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { stringToBytes } from '@scure/base';
import { hexToBuffer } from '../utils/buffer';
import * as bls from './bls';

const msg = stringToBytes('utf8', 'test');
const skStr =
'233428aaadf8a5d11ebba263d97b85a286750540f4abd04f109321e07b746277';
const pkStr =
'adf6062df01fc18456140f7126567a84834d85b2af70454a7aacad932b92d0d7d0dab897d2f9bf46021511969f5b62f8';
const popStr =
'98e8d8e33a51ecdcbcca2166370d99fdc02134e8e84ca34327cd2ec4412eb3b39619050a0146cba5d5948cb43c32a7f00f5df841700e3937d58b64e6f74493891b2a70402111841f69e9fc73236beb79f2e63e9a7caa55b724c61a139969ff57';
const sigStr =
'9254acb2bfe4638daef4424b07f7a03987245c8945e634a7fca3302a2bb45e0aa9d2f8f5198e37d41aa65f8ab81efa4608d23ab55ccf06122f9718b37d42e0274297966191e3de2852f3a328727fe0dcced453c943405205b0f23038b7409e66';

describe('bls', () => {
it('serializes correctly', async () => {
const sk = bls.secretKeyFromBytes(skStr);
expect(bls.secretKeyToBytes(sk)).toEqual(hexToBuffer(skStr));

const pk = bls.publicKeyFromBytes(pkStr);
expect(bls.publicKeyToBytes(pk)).toEqual(hexToBuffer(pkStr));

const pk2 = bls.publicKeyFromBytes(hexToBuffer(pkStr));
expect(bls.publicKeyToBytes(pk2)).toEqual(hexToBuffer(pkStr));

const pop = bls.signatureFromBytes(hexToBuffer(popStr));
expect(bls.signatureToBytes(pop)).toEqual(hexToBuffer(popStr));

const sig = bls.signatureFromBytes(hexToBuffer(sigStr));
expect(bls.signatureToBytes(sig)).toEqual(hexToBuffer(sigStr));
});

it('verifies signature correctly', async () => {
const pk = bls.publicKeyFromBytes(pkStr);
const sig = bls.signatureFromBytes(hexToBuffer(sigStr));

expect(bls.verify(pk, sig, msg)).toEqual(true);
});

it('verifies proof of possession correctly', async () => {
const pk = bls.publicKeyFromBytes(pkStr);
const pop = bls.signatureFromBytes(hexToBuffer(popStr));
const pkBytes = bls.publicKeyToBytes(pk);

expect(bls.verifyProofOfPossession(pk, pop, pkBytes)).toEqual(true);
});
});
58 changes: 58 additions & 0 deletions src/crypto/bls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { bls12_381 } from '@noble/curves/bls12-381';
import type { ProjPointType } from '@noble/curves/abstract/weierstrass';
import { hexToBuffer } from '../utils/buffer';

export type PublicKey = ProjPointType<bigint>;
export type SecretKey = bigint;
export type Signature = ProjPointType<typeof bls12_381.fields.Fp2.ZERO>;
export type Message = ProjPointType<typeof bls12_381.fields.Fp2.ZERO>;

export const PUBLIC_KEY_LENGTH = 48;
export const SIGNATURE_LENGTH = 96;

const signatureDST = 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_';
const proofOfPossessionDST = 'BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_';

export function secretKeyFromBytes(skBytes: Uint8Array | string): SecretKey {
return bls12_381.G1.normPrivateKeyToScalar(skBytes);
}

export function secretKeyToBytes(sk: SecretKey): Uint8Array {
return hexToBuffer(sk.toString(16));
}

export function publicKeyFromBytes(pkBytes: Uint8Array | string): PublicKey {
return bls12_381.G1.ProjectivePoint.fromHex(pkBytes);
}

export function publicKeyToBytes(pk: PublicKey): Uint8Array {
return pk.toRawBytes();
}

export function signatureFromBytes(sigBytes: Uint8Array): Signature {
return bls12_381.Signature.fromHex(sigBytes);
}

export function signatureToBytes(sig: Signature): Uint8Array {
return sig.toRawBytes();
}

export function verify(
pk: PublicKey,
sig: Signature,
msg: Uint8Array | string | Message,
): boolean {
return bls12_381.verify(sig, msg, pk, {
DST: signatureDST,
});
}

export function verifyProofOfPossession(
pk: PublicKey,
sig: Signature,
msg: Uint8Array | string | Message,
): boolean {
return bls12_381.verify(sig, msg, pk, {
DST: proofOfPossessionDST,
});
}
2 changes: 2 additions & 0 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as secp256k1 from './secp256k1';
export * as bls from './bls';
28 changes: 16 additions & 12 deletions src/utils/secp256k1.spec.ts → src/crypto/secp256k1.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { sha256 } from '@noble/hashes/sha256';
import { base58check } from './base58';
import { bufferToHex, hexToBuffer } from './buffer';
import * as secp from './secp256k1';
import { base58check } from '../utils/base58';
import { bufferToHex, hexToBuffer } from '../utils/buffer';
import * as secp256k1 from './secp256k1';

describe('secp256k1', function () {
it('works correctly', async () => {
const key = '24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5';
const privKey = base58check.decode(key);
const pubKey = secp.getPublicKey(privKey);
const pubKey = secp256k1.getPublicKey(privKey);

expect(base58check.encode(secp.publicKeyBytesToAddress(pubKey))).toEqual(
'Q4MzFZZDPHRPAHFeDs3NiyyaZDvxHKivf',
);
expect(
base58check.encode(secp256k1.publicKeyBytesToAddress(pubKey)),
).toEqual('Q4MzFZZDPHRPAHFeDs3NiyyaZDvxHKivf');

const tests = [
{
Expand Down Expand Up @@ -52,10 +52,14 @@ describe('secp256k1', function () {
for (const test of tests) {
const hash = sha256(test.msg);

await expect(secp.sign(test.msg, privKey)).resolves.toEqual(test.sig);
await expect(secp.signHash(hash, privKey)).resolves.toEqual(test.sig);
expect(secp.recoverPublicKey(hash, test.sig)).toEqual(pubKey);
expect(secp.verify(test.sig, hash, pubKey)).toEqual(true);
await expect(secp256k1.sign(test.msg, privKey)).resolves.toEqual(
test.sig,
);
await expect(secp256k1.signHash(hash, privKey)).resolves.toEqual(
test.sig,
);
expect(secp256k1.recoverPublicKey(hash, test.sig)).toEqual(pubKey);
expect(secp256k1.verify(test.sig, hash, pubKey)).toEqual(true);
}
});

Expand All @@ -64,7 +68,7 @@ describe('secp256k1', function () {
'04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde2250d5f271f3563606672ebc45e0b7ea2e816ecb70ca03137b1c9476eec63d4632e990020b7b6fba39',
);

const ethAddrKey = bufferToHex(secp.publicKeyToEthAddress(publicKey));
const ethAddrKey = bufferToHex(secp256k1.publicKeyToEthAddress(publicKey));
expect(ethAddrKey).toBe(
'0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'.toLocaleLowerCase(),
);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/secp256k1.ts → src/crypto/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ripemd160 } from '@noble/hashes/ripemd160';
import { sha256 } from '@noble/hashes/sha256';
import * as secp from '@noble/secp256k1';
import { Address } from 'micro-eth-signer';
import { concatBytes, hexToBuffer } from './buffer';
import { concatBytes, hexToBuffer } from '../utils/buffer';

export function randomPrivateKey() {
return secp.utils.randomPrivateKey();
Expand Down
20 changes: 8 additions & 12 deletions src/fixtures/vms.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import {
getPublicKey,
hexToBuffer,
publicKeyBytesToAddress,
publicKeyToEthAddress,
} from '../utils';
import { secp256k1 } from '../crypto';
import { hexToBuffer } from '../utils';

export const testPrivateKey1 = hexToBuffer(
'9c0523e7611e62f5dca291ad335e950db076c5ee31c4107355abde0d357bbd29',
);
export const testPublicKey1 = getPublicKey(testPrivateKey1);
export const testAddress1 = publicKeyBytesToAddress(testPublicKey1);
export const testEthAddress1 = publicKeyToEthAddress(testPublicKey1);
export const testPublicKey1 = secp256k1.getPublicKey(testPrivateKey1);
export const testAddress1 = secp256k1.publicKeyBytesToAddress(testPublicKey1);
export const testEthAddress1 = secp256k1.publicKeyToEthAddress(testPublicKey1);

export const testPrivateKey2 = hexToBuffer(
'd11e7aa633eb15682bc2456d399c2a9861c82e0a308dbfd4d3a51ffa972f2b62',
);
export const testPublicKey2 = getPublicKey(testPrivateKey2);
export const testAddress2 = publicKeyBytesToAddress(testPublicKey2);
export const testEthAddress2 = publicKeyToEthAddress(testPublicKey2);
export const testPublicKey2 = secp256k1.getPublicKey(testPrivateKey2);
export const testAddress2 = secp256k1.publicKeyBytesToAddress(testPublicKey2);
export const testEthAddress2 = secp256k1.publicKeyToEthAddress(testPublicKey2);
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * as networkIDs from './constants/networkIDs';
export * from './serializable';
export { Utxo } from './serializable/avax/utxo';
export * from './signer';
export * from './utils';
export * as utils from './utils';
export * from './vms';
export { Info } from './info/info';
export * from './crypto';
20 changes: 11 additions & 9 deletions src/serializable/pvm/proofOfPossession.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ProofOfPossession } from './proofOfPossession';

const pubkey = new Uint8Array([
const publicKey = new Uint8Array([
0x85, 0x02, 0x5b, 0xca, 0x6a, 0x30, 0x2d, 0xc6, 0x13, 0x38, 0xff, 0x49, 0xc8,
0xba, 0xa5, 0x72, 0xde, 0xd3, 0xe8, 0x6f, 0x37, 0x59, 0x30, 0x4c, 0x7f, 0x61,
0x8a, 0x2a, 0x25, 0x93, 0xc1, 0x87, 0xe0, 0x80, 0xa3, 0xcf, 0xde, 0xc9, 0x50,
Expand All @@ -20,27 +20,29 @@ const signature = new Uint8Array([

describe('proofOfPossession', function () {
it('can init', () => {
const proof = new ProofOfPossession(pubkey, signature);
expect(proof instanceof ProofOfPossession).toBe(true);
const pop = new ProofOfPossession(publicKey, signature);
expect(pop instanceof ProofOfPossession).toBe(true);
});

it('throws for invalid pubkey', () => {
expect(() => {
const invalidPub = pubkey.slice(0, pubkey.length - 2);
new ProofOfPossession(invalidPub, signature);
const popBytes = new Uint8Array([...publicKey, ...signature]);
popBytes[2] = 0x00;
ProofOfPossession.fromBytes(popBytes);
}).toThrow();
});

it('throws for invalid signature', () => {
expect(() => {
const invalidSig = signature.slice(0, signature.length - 2);
new ProofOfPossession(pubkey, invalidSig);
const popBytes = new Uint8Array([...publicKey, ...signature]);
popBytes[64] = 0x00;
ProofOfPossession.fromBytes(popBytes);
}).toThrow();
});

it('can call toString', () => {
const proof = new ProofOfPossession(pubkey, signature);
const pop = new ProofOfPossession(publicKey, signature);
const expected = `0x85025bca6a302dc61338ff49c8baa572ded3e86f3759304c7f618a2a2593c187e080a3cfdec95040309ad1f1589530678b1d6133d17e3483220ad960b6fde11e4e1214a8ce21ef616227e5d5eef070d7500e6f7d4452c5a760620cc06795cbe218e072eba76d94788d9d01176ce4ecadfb96b47f942281894ddfadd1c1743f7f549f1d07d59d55655927f72bc6bf7c12`;
expect(proof.toString()).toEqual(expected);
expect(pop.toString()).toEqual(expected);
});
});
23 changes: 14 additions & 9 deletions src/serializable/pvm/proofOfPossession.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { serializable } from '../common/types';
import { bufferToHex, concatBytes } from '../../utils/buffer';
import { BLS_PUBKEY_LENGTH, BLS_SIGNATURE_LENGTH } from '../../constants/bls';
import { bls } from '../../crypto';
import { TypeSymbols } from '../constants';

/**
Expand All @@ -14,19 +14,24 @@ export class ProofOfPossession {
public readonly publicKey: Uint8Array,
public readonly signature: Uint8Array,
) {
if (publicKey.length !== BLS_PUBKEY_LENGTH)
throw new Error(`public key must be ${BLS_PUBKEY_LENGTH} bytes`);
if (signature.length !== BLS_SIGNATURE_LENGTH)
throw new Error(`signature must be ${BLS_SIGNATURE_LENGTH} bytes`);
const pk = bls.publicKeyFromBytes(publicKey);
const sig = bls.signatureFromBytes(signature);

pk.assertValidity();
sig.assertValidity();

if (!bls.verifyProofOfPossession(pk, sig, bls.publicKeyToBytes(pk))) {
throw new Error(`Invalid signature`);
}
}

static fromBytes(bytes: Uint8Array): [ProofOfPossession, Uint8Array] {
const pubkey = bytes.slice(0, BLS_PUBKEY_LENGTH);
const pubkey = bytes.slice(0, bls.PUBLIC_KEY_LENGTH);
const signature = bytes.slice(
BLS_PUBKEY_LENGTH,
BLS_PUBKEY_LENGTH + BLS_SIGNATURE_LENGTH,
bls.PUBLIC_KEY_LENGTH,
bls.PUBLIC_KEY_LENGTH + bls.SIGNATURE_LENGTH,
);
const rest = bytes.slice(BLS_PUBKEY_LENGTH + BLS_SIGNATURE_LENGTH);
const rest = bytes.slice(bls.PUBLIC_KEY_LENGTH + bls.SIGNATURE_LENGTH);
return [new ProofOfPossession(pubkey, signature), rest];
}

Expand Down
5 changes: 3 additions & 2 deletions src/signer/addTxSignatures.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { id } from '../fixtures/common';
import { BaseTx } from '../serializable/avm';
import { transferableOutput, utxoId } from '../fixtures/avax';
import { Input, TransferInput } from '../serializable/fxs/secp256k1';
import { AddressMaps, AddressMap, getPublicKey, hexToBuffer } from '../utils';
import { AddressMaps, AddressMap, hexToBuffer } from '../utils';
import { Address } from '../serializable/fxs/common';
import {
testAddress1,
Expand All @@ -18,6 +18,7 @@ import {
testPublicKey2,
} from '../fixtures/vms';
import { addTxSignatures } from './addTxSignatures';
import { secp256k1 } from '../crypto';

describe('addTxSignatures', () => {
beforeEach(() => {
Expand All @@ -30,7 +31,7 @@ describe('addTxSignatures', () => {
const unknownPrivateKey = hexToBuffer(
'1d4ff8f6582d995354f5c03a28a043d22aa1bb6aa15879a632134aaf1f225cf4',
);
const unknownPublicKey = getPublicKey(unknownPrivateKey);
const unknownPublicKey = secp256k1.getPublicKey(unknownPrivateKey);

const unsignedTx = new UnsignedTx(
new BaseTx(
Expand Down
6 changes: 3 additions & 3 deletions src/signer/addTxSignatures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UnsignedTx } from '../vms/common/unsignedTx';
import { getPublicKey, sign } from '../utils';
import { secp256k1 } from '../crypto';

export const addTxSignatures = async ({
unsignedTx,
Expand All @@ -12,10 +12,10 @@ export const addTxSignatures = async ({

await Promise.all(
privateKeys.map(async (privateKey) => {
const publicKey = getPublicKey(privateKey);
const publicKey = secp256k1.getPublicKey(privateKey);

if (unsignedTx.hasPubkey(publicKey)) {
const signature = await sign(unsignedBytes, privateKey);
const signature = await secp256k1.sign(unsignedBytes, privateKey);
unsignedTx.addSignature(signature);
}
}),
Expand Down
6 changes: 3 additions & 3 deletions src/utils/address.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { base58check } from './base58';
import * as secp from './secp256k1';
import { secp256k1 } from '../crypto';
import * as address from './address';

describe('address', () => {
it('parses and formats correctly', async () => {
const key = '24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5';
const privKey = base58check.decode(key);
const pubKey = secp.getPublicKey(privKey);
const pubKey = secp256k1.getPublicKey(privKey);

const addrBytes = secp.publicKeyBytesToAddress(pubKey);
const addrBytes = secp256k1.publicKeyBytesToAddress(pubKey);

const addr = address.format('X', 'avax', addrBytes);
expect(addr).toEqual('X-avax1lnk637g0edwnqc2tn8tel39652fswa3xmgyghf');
Expand Down
Loading