diff --git a/package.json b/package.json index 9c40e131b..b23bfaaa1 100644 --- a/package.json +++ b/package.json @@ -97,4 +97,4 @@ "typescript": "^5.6.2" }, "version": "1.33.0" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16c7a18d8..234841730 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: poseidon-lite: specifier: ^0.2.0 version: 0.2.1 + secp256k1: + specifier: ^5.0.1 + version: 5.0.1 devDependencies: '@babel/traverse': @@ -3434,6 +3437,10 @@ packages: readable-stream: 3.6.2 dev: true + /bn.js@4.12.1: + resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -3454,6 +3461,10 @@ packages: fill-range: 7.1.1 dev: true + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: false + /browserslist@4.24.0: resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4149,6 +4160,18 @@ packages: resolution: {integrity: sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==} dev: true + /elliptic@6.6.1: + resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + dependencies: + bn.js: 4.12.1 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: false + /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -5088,6 +5111,13 @@ packages: has-symbols: 1.0.3 dev: true + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: false + /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -5124,6 +5154,14 @@ packages: tslib: 2.8.0 dev: true + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true @@ -5236,7 +5274,6 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true /ini@2.0.0: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} @@ -6462,6 +6499,14 @@ packages: engines: {node: '>=10'} dev: false + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -6535,6 +6580,10 @@ packages: tslib: 2.8.0 dev: true + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + /node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -6547,6 +6596,11 @@ packages: whatwg-url: 5.0.0 dev: true + /node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -7259,6 +7313,16 @@ packages: resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} dev: true + /secp256k1@5.0.1: + resolution: {integrity: sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==} + engines: {node: '>=18.0.0'} + requiresBuild: true + dependencies: + elliptic: 6.6.1 + node-addon-api: 5.1.0 + node-gyp-build: 4.8.4 + dev: false + /seed-random@2.2.0: resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} dev: true diff --git a/src/core/crypto/secp256k1.ts b/src/core/crypto/secp256k1.ts index abab873d9..92cea11c4 100644 --- a/src/core/crypto/secp256k1.ts +++ b/src/core/crypto/secp256k1.ts @@ -23,6 +23,9 @@ export class Secp256k1PublicKey extends PublicKey { // Secp256k1 ecdsa public keys contain a prefix indicating compression and two 32-byte coordinates. static readonly LENGTH: number = 65; + // If it's compressed, it is only 33 bytes + static readonly COMPRESSED_LENGTH: number = 33; + // Hex value of the public key private readonly key: Hex; @@ -37,10 +40,17 @@ export class Secp256k1PublicKey extends PublicKey { super(); const hex = Hex.fromHexInput(hexInput); - if (hex.toUint8Array().length !== Secp256k1PublicKey.LENGTH) { - throw new Error(`PublicKey length should be ${Secp256k1PublicKey.LENGTH}`); + const { length } = hex.toUint8Array(); + if (length === Secp256k1PublicKey.LENGTH) { + this.key = hex; + } else if (length === Secp256k1PublicKey.COMPRESSED_LENGTH) { + const point = secp256k1.ProjectivePoint.fromHex(hex.toUint8Array()); + this.key = Hex.fromHexInput(point.toRawBytes(false)); + } else { + throw new Error( + `PublicKey length should be ${Secp256k1PublicKey.LENGTH} or ${Secp256k1PublicKey.COMPRESSED_LENGTH}, received ${length}`, + ); } - this.key = hex; } // region PublicKey diff --git a/tests/unit/secp256k1.test.ts b/tests/unit/secp256k1.test.ts index e95ef544d..32546385e 100644 --- a/tests/unit/secp256k1.test.ts +++ b/tests/unit/secp256k1.test.ts @@ -29,6 +29,27 @@ describe("Secp256k1PublicKey", () => { expect(publicKey2.toUint8Array()).toEqual(hexUint8Array); }); + it("should work with compressed public keys", () => { + const expectedPublicKey = new Secp256k1PublicKey(secp256k1TestObject.publicKey); + const uncompressedPublicKey = Hex.fromHexInput(secp256k1TestObject.publicKey); + expect(uncompressedPublicKey.toUint8Array().length).toEqual(65); + + const point = secp256k1.ProjectivePoint.fromHex(uncompressedPublicKey.toUint8Array()); + const compressedPublicKey = point.toRawBytes(true); + const compressedPublicKeyHex = Hex.fromHexInput(compressedPublicKey); + expect(compressedPublicKey.length).toEqual(33); + + const publicKey1 = new Secp256k1PublicKey(compressedPublicKeyHex.toString()); + expect(publicKey1).toBeInstanceOf(Secp256k1PublicKey); + expect(publicKey1).toEqual(expectedPublicKey); + + const publicKey2 = new Secp256k1PublicKey(compressedPublicKeyHex.toUint8Array()); + expect(publicKey2).toBeInstanceOf(Secp256k1PublicKey); + expect(publicKey2).toEqual(expectedPublicKey); + + expect(publicKey1).toEqual(publicKey2); + }); + it("should throw an error with invalid hex input length", () => { const invalidHexInput = "0123456789abcdef"; // Invalid length expect(() => new Secp256k1PublicKey(invalidHexInput)).toThrowError(