From aeedeb1c5d34c580ac2049bd613022aca13c4382 Mon Sep 17 00:00:00 2001 From: Hleb Albau Date: Wed, 29 Sep 2021 20:07:23 +0200 Subject: [PATCH 1/3] #882 Decode multisig pubkey --- packages/amino/src/encoding.spec.ts | 5 +++ packages/amino/src/encoding.ts | 52 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index c8ce78b1..7b2217b1 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -95,6 +95,11 @@ describe("encoding", () => { value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", }); }); + + it("works for multisig", () => { + const decoded = decodeBech32Pubkey(testgroup1PubkeyBech32); + expect(decoded).toEqual(testgroup1); + }); }); describe("encodeAminoPubkey", () => { diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 79b0e4ab..32228160 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -6,6 +6,8 @@ import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, + isSinglePubkey, + MultisigThresholdPubkey, Pubkey, pubkeyType, Secp256k1Pubkey, @@ -61,6 +63,9 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { type: pubkeyType.sr25519, value: toBase64(rest), }; + } else if (arrayContentStartsWith(data, pubkeyAminoPrefixMultisigThreshold)) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return decodeMultisigPubkey(data); } else { throw new Error("Unsupported public key type. Amino data starts with: " + toHex(data.slice(0, 5))); } @@ -77,6 +82,53 @@ export function decodeBech32Pubkey(bechEncoded: string): Pubkey { return decodeAminoPubkey(data); } +/** + * Decodes a multisig pubkey to type object. + * Pubkey structure [ prefix + const + threshold + loop:(const + pubkeyLength + pubkey ) ] + * [ 4b + 1b + 1b + loop:(1b + 1b + pubkeyLength bytes) ] + * @param data encoded pubkey + */ +function decodeMultisigPubkey(data: Uint8Array): MultisigThresholdPubkey { + if (data.length < 4 + 1 + 1) { + // verify that we can read at least threshold + throw new Error("Invalid multisig data length."); + } + let rest = data.slice(pubkeyAminoPrefixSecp256k1.length); + rest = rest.slice(1); // removing 0x08; + const threshold = rest[0]; + rest = rest.slice(1); // removing threshold + + const pubkeys = []; + while (rest.length > 0) { + if (rest.length < 1 + 1) { + // verify that we can read at least pubkeyLength + throw new Error("Invalid multisig data length."); + } + rest = rest.slice(1); // removing 0x12 + const pubkeyLength = rest[0]; + rest = rest.slice(1); // removing pubkey length + if (rest.length < pubkeyLength) { + // verify that we can read pubkey + throw new Error("Invalid multisig data length."); + } + const encodedPubkey = rest.slice(0, pubkeyLength); + rest = rest.slice(pubkeyLength); // removing pubkey + const pubkey = decodeAminoPubkey(encodedPubkey); + if (!isSinglePubkey(pubkey)) { + throw new Error("Unsupported multisig participant pubkey type"); + } + pubkeys.push(pubkey); + } + + return { + type: pubkeyType.multisigThreshold, + value: { + threshold: threshold.toString(), + pubkeys: pubkeys, + }, + }; +} + /** * Uvarint encoder for Amino. This is the same encoding as `binary.PutUvarint` from the Go * standard library. From 98333c58504c269b757a52a4e11458aa2b4e8d6c Mon Sep 17 00:00:00 2001 From: Hleb Albau Date: Thu, 30 Sep 2021 12:00:51 +0200 Subject: [PATCH 2/3] #882 fix review comments --- packages/amino/src/encoding.ts | 80 +++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index 32228160..b6251304 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -6,7 +6,6 @@ import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, - isSinglePubkey, MultisigThresholdPubkey, Pubkey, pubkeyType, @@ -65,7 +64,7 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { }; } else if (arrayContentStartsWith(data, pubkeyAminoPrefixMultisigThreshold)) { // eslint-disable-next-line @typescript-eslint/no-use-before-define - return decodeMultisigPubkey(data); + return decodeMultisigPubkey(Array.from(data)); } else { throw new Error("Unsupported public key type. Amino data starts with: " + toHex(data.slice(0, 5))); } @@ -82,41 +81,72 @@ export function decodeBech32Pubkey(bechEncoded: string): Pubkey { return decodeAminoPubkey(data); } +/** + * Uvarint decoder for Amino. + * Decoding numbers > 127 is not supported here. Please tell those lazy CosmJS maintainers to port the binary.Varint implementation from the Go standard library and write some tests. + * + * @see https://github.com/tendermint/go-amino/blob/8e779b71f40d175/decoder.go#L64-76 + * @returns varint as number, and bytes count occupied by varaint + */ +function decodeUvarint(reader: number[]): [number, number] { + if (reader.length < 1) { + throw new Error("Can't decode varint. EOF"); + } + return [reader[0], 1]; +} + /** * Decodes a multisig pubkey to type object. * Pubkey structure [ prefix + const + threshold + loop:(const + pubkeyLength + pubkey ) ] - * [ 4b + 1b + 1b + loop:(1b + 1b + pubkeyLength bytes) ] - * @param data encoded pubkey + * [ 4b + 1b + varint + loop:(1b + varint + pubkeyLength bytes) ] + * @param reader encoded pubkey */ -function decodeMultisigPubkey(data: Uint8Array): MultisigThresholdPubkey { - if (data.length < 4 + 1 + 1) { - // verify that we can read at least threshold +function decodeMultisigPubkey(reader: number[]): MultisigThresholdPubkey { + // verify that we can at least start reading threshold + if (reader.length < pubkeyAminoPrefixMultisigThreshold.length + 1 + 1) { throw new Error("Invalid multisig data length."); } - let rest = data.slice(pubkeyAminoPrefixSecp256k1.length); - rest = rest.slice(1); // removing 0x08; - const threshold = rest[0]; - rest = rest.slice(1); // removing threshold + // remove multisig amino prefix; + const prefixFromReader = reader.splice(0, pubkeyAminoPrefixMultisigThreshold.length); + if (!arrayContentStartsWith(prefixFromReader, pubkeyAminoPrefixMultisigThreshold)) { + throw new Error("Invalid multisig prefix."); + } + + // remove 0x08 threshold prefix; + if (reader.shift() != 0x08) { + throw new Error("Invalid multisig data. Expecting 0x08 prefix before threshold."); + } + + // read threshold + const [threshold, thresholdBytesLength] = decodeUvarint(reader); + reader.splice(0, thresholdBytesLength); + + // read participants pubkeys const pubkeys = []; - while (rest.length > 0) { - if (rest.length < 1 + 1) { - // verify that we can read at least pubkeyLength + while (reader.length > 0) { + // verify that we can at least start reading pubkeyLength + if (reader.length < 1 + 1) { throw new Error("Invalid multisig data length."); } - rest = rest.slice(1); // removing 0x12 - const pubkeyLength = rest[0]; - rest = rest.slice(1); // removing pubkey length - if (rest.length < pubkeyLength) { - // verify that we can read pubkey + + // remove 0x12 threshold prefix; + if (reader.shift() != 0x12) { + throw new Error("Invalid multisig data. Expecting 0x12 prefix before participant pubkey length."); + } + + // read pubkey length + const [pubkeyLength, pubkeyLengthBytesSize] = decodeUvarint(reader); + reader.splice(0, pubkeyLengthBytesSize); + + // verify that we can read pubkey + if (reader.length < pubkeyLength) { throw new Error("Invalid multisig data length."); } - const encodedPubkey = rest.slice(0, pubkeyLength); - rest = rest.slice(pubkeyLength); // removing pubkey - const pubkey = decodeAminoPubkey(encodedPubkey); - if (!isSinglePubkey(pubkey)) { - throw new Error("Unsupported multisig participant pubkey type"); - } + + // read and decode participant pubkey + const encodedPubkey = reader.splice(0, pubkeyLength); + const pubkey = decodeAminoPubkey(Uint8Array.from(encodedPubkey)); pubkeys.push(pubkey); } From ab7e4f444bd7619d0d5323e44a1f251a5be0c782 Mon Sep 17 00:00:00 2001 From: Hleb Albau Date: Thu, 30 Sep 2021 15:11:33 +0200 Subject: [PATCH 3/3] #882 fix review comments p2 --- packages/amino/src/encoding.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts index b6251304..4bd1501c 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -64,7 +64,7 @@ export function decodeAminoPubkey(data: Uint8Array): Pubkey { }; } else if (arrayContentStartsWith(data, pubkeyAminoPrefixMultisigThreshold)) { // eslint-disable-next-line @typescript-eslint/no-use-before-define - return decodeMultisigPubkey(Array.from(data)); + return decodeMultisigPubkey(data); } else { throw new Error("Unsupported public key type. Amino data starts with: " + toHex(data.slice(0, 5))); } @@ -83,8 +83,6 @@ export function decodeBech32Pubkey(bechEncoded: string): Pubkey { /** * Uvarint decoder for Amino. - * Decoding numbers > 127 is not supported here. Please tell those lazy CosmJS maintainers to port the binary.Varint implementation from the Go standard library and write some tests. - * * @see https://github.com/tendermint/go-amino/blob/8e779b71f40d175/decoder.go#L64-76 * @returns varint as number, and bytes count occupied by varaint */ @@ -92,6 +90,11 @@ function decodeUvarint(reader: number[]): [number, number] { if (reader.length < 1) { throw new Error("Can't decode varint. EOF"); } + if (reader[0] > 127) { + throw new Error( + "Decoding numbers > 127 is not supported here. Please tell those lazy CosmJS maintainers to port the binary.Varint implementation from the Go standard library and write some tests.", + ); + } return [reader[0], 1]; } @@ -99,13 +102,10 @@ function decodeUvarint(reader: number[]): [number, number] { * Decodes a multisig pubkey to type object. * Pubkey structure [ prefix + const + threshold + loop:(const + pubkeyLength + pubkey ) ] * [ 4b + 1b + varint + loop:(1b + varint + pubkeyLength bytes) ] - * @param reader encoded pubkey + * @param data encoded pubkey */ -function decodeMultisigPubkey(reader: number[]): MultisigThresholdPubkey { - // verify that we can at least start reading threshold - if (reader.length < pubkeyAminoPrefixMultisigThreshold.length + 1 + 1) { - throw new Error("Invalid multisig data length."); - } +function decodeMultisigPubkey(data: Uint8Array): MultisigThresholdPubkey { + const reader = Array.from(data); // remove multisig amino prefix; const prefixFromReader = reader.splice(0, pubkeyAminoPrefixMultisigThreshold.length); @@ -125,11 +125,6 @@ function decodeMultisigPubkey(reader: number[]): MultisigThresholdPubkey { // read participants pubkeys const pubkeys = []; while (reader.length > 0) { - // verify that we can at least start reading pubkeyLength - if (reader.length < 1 + 1) { - throw new Error("Invalid multisig data length."); - } - // remove 0x12 threshold prefix; if (reader.shift() != 0x12) { throw new Error("Invalid multisig data. Expecting 0x12 prefix before participant pubkey length.");