diff --git a/packages/amino/src/signature.spec.ts b/packages/amino/src/signature.spec.ts new file mode 100644 index 00000000..d9cd1ad5 --- /dev/null +++ b/packages/amino/src/signature.spec.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fromBase64 } from "@cosmjs/encoding"; + +import { decodeSignature, encodeSecp256k1Signature, StdSignature } from "./signature"; + +describe("signature", () => { + describe("encodeSecp256k1Signature", () => { + it("encodes a full signature", () => { + const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); + const signature = fromBase64( + "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", + ); + expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({ + pub_key: { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }, + signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", + }); + }); + + it("throws when getting uncompressed public keys", () => { + const pubkey = fromBase64( + "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", + ); + const signature = fromBase64( + "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", + ); + expect(() => encodeSecp256k1Signature(pubkey, signature)).toThrowError( + /public key must be compressed secp256k1/i, + ); + }); + + it("throws if signature contains recovery byte", () => { + const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); + const signature = Uint8Array.from([ + ...fromBase64( + "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", + ), + 99, + ]); + expect(() => encodeSecp256k1Signature(pubkey, signature)).toThrowError( + /signature must be 64 bytes long/i, + ); + }); + }); + + describe("decodeSignature", () => { + it("works for secp256k1", () => { + const signature: StdSignature = { + pub_key: { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }, + signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", + }; + expect(decodeSignature(signature)).toEqual({ + pubkey: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"), + signature: fromBase64( + "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", + ), + }); + }); + }); +}); diff --git a/packages/amino/src/signature.ts b/packages/amino/src/signature.ts new file mode 100644 index 00000000..280653a2 --- /dev/null +++ b/packages/amino/src/signature.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fromBase64, toBase64 } from "@cosmjs/encoding"; + +import { encodeSecp256k1Pubkey } from "./encoding"; +import { Pubkey, pubkeyType } from "./pubkeys"; + +export interface StdSignature { + readonly pub_key: Pubkey; + readonly signature: string; +} + +/** + * Takes a binary pubkey and signature to create a signature object + * + * @param pubkey a compressed secp256k1 public key + * @param signature a 64 byte fixed length representation of secp256k1 signature components r and s + */ +export function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature { + if (signature.length !== 64) { + throw new Error( + "Signature must be 64 bytes long. Cosmos SDK uses a 2x32 byte fixed length encoding for the secp256k1 signature integers r and s.", + ); + } + + return { + pub_key: encodeSecp256k1Pubkey(pubkey), + signature: toBase64(signature), + }; +} + +export function decodeSignature( + signature: StdSignature, +): { readonly pubkey: Uint8Array; readonly signature: Uint8Array } { + switch (signature.pub_key.type) { + // Note: please don't add cases here without writing additional unit tests + case pubkeyType.secp256k1: + return { + pubkey: fromBase64(signature.pub_key.value), + signature: fromBase64(signature.signature), + }; + default: + throw new Error("Unsupported pubkey type"); + } +}