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..4bd1501c 100644 --- a/packages/amino/src/encoding.ts +++ b/packages/amino/src/encoding.ts @@ -6,6 +6,7 @@ import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, + MultisigThresholdPubkey, Pubkey, pubkeyType, Secp256k1Pubkey, @@ -61,6 +62,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 +81,79 @@ export function decodeBech32Pubkey(bechEncoded: string): Pubkey { return decodeAminoPubkey(data); } +/** + * Uvarint decoder for Amino. + * @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"); + } + 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]; +} + +/** + * 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 data encoded pubkey + */ +function decodeMultisigPubkey(data: Uint8Array): MultisigThresholdPubkey { + const reader = Array.from(data); + + // 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 (reader.length > 0) { + // 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."); + } + + // read and decode participant pubkey + const encodedPubkey = reader.splice(0, pubkeyLength); + const pubkey = decodeAminoPubkey(Uint8Array.from(encodedPubkey)); + 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.