diff --git a/packages/bcp/src/encode.spec.ts b/packages/bcp/src/encode.spec.ts index 26aac966..5c63bfe9 100644 --- a/packages/bcp/src/encode.spec.ts +++ b/packages/bcp/src/encode.spec.ts @@ -67,8 +67,8 @@ describe("encode", () => { }, ]; - describe("encodePubKey", () => { - it("encodes a Secp256k1 pubkey", () => { + describe("encodePubkey", () => { + it("works for compressed public key", () => { expect(encodePubkey(defaultPubkey)).toEqual({ type: "tendermint/PubKeySecp256k1", value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", diff --git a/packages/bcp/src/encode.ts b/packages/bcp/src/encode.ts index ca606156..49056033 100644 --- a/packages/bcp/src/encode.ts +++ b/packages/bcp/src/encode.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { encodeSecp256k1Signature, types } from "@cosmwasm/sdk"; +import { encodeSecp256k1Pubkey, encodeSecp256k1Signature, types } from "@cosmwasm/sdk"; import { Algorithm, Amount, @@ -10,19 +10,18 @@ import { SignedTransaction, UnsignedTransaction, } from "@iov/bcp"; +import { Secp256k1 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import { BankTokens, Erc20Token } from "./types"; const { toBase64 } = Encoding; +// TODO: This function seems to be unused and is not well tested (e.g. uncompressed secp256k1 or ed25519) export function encodePubkey(pubkey: PubkeyBundle): types.PubKey { switch (pubkey.algo) { case Algorithm.Secp256k1: - return { - type: types.pubkeyType.secp256k1, - value: toBase64(pubkey.data), - }; + return encodeSecp256k1Pubkey(pubkey.data); case Algorithm.Ed25519: return { type: types.pubkeyType.ed25519, @@ -70,8 +69,11 @@ export function encodeFee(fee: Fee, tokens: BankTokens): types.StdFee { export function encodeFullSignature(fullSignature: FullSignature): types.StdSignature { switch (fullSignature.pubkey.algo) { - case Algorithm.Secp256k1: - return encodeSecp256k1Signature(fullSignature.pubkey.data, fullSignature.signature); + case Algorithm.Secp256k1: { + const compressedPubkey = Secp256k1.compressPubkey(fullSignature.pubkey.data); + const normalizedSignature = Secp256k1.trimRecoveryByte(fullSignature.signature); + return encodeSecp256k1Signature(compressedPubkey, normalizedSignature); + } default: throw new Error("Unsupported signing algorithm"); } diff --git a/packages/cli/README.md b/packages/cli/README.md index cc0104c9..04df7756 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -65,7 +65,7 @@ const sendTokensMsg: types.MsgSend = { }; const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account_number, sequence); -const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); +const signature = await pen.sign(signBytes); const signedTx: types.StdTx = { msg: [sendTokensMsg], fee: defaultFee, diff --git a/packages/cli/examples/helpers.ts b/packages/cli/examples/helpers.ts index 5f175a05..c76f05f3 100644 --- a/packages/cli/examples/helpers.ts +++ b/packages/cli/examples/helpers.ts @@ -40,7 +40,7 @@ const instantiateContract = async (initClient: RestClient, initPen: Secp256k1Pen }; const account = (await initClient.authAccounts(faucetAddress)).result.value; const signBytes = makeSignBytes([instantiateContractMsg], defaultFee, networkId, memo, account); - const signature = encodeSecp256k1Signature(initPen.pubkey, await initPen.createSignature(signBytes)); + const signature = await initPen.sign(signBytes); const signedTx = { msg: [instantiateContractMsg], fee: defaultFee, @@ -71,7 +71,7 @@ const executeContract = async (execClient: RestClient, execPen: Secp256k1Pen, co }; const account = (await execClient.authAccounts(faucetAddress)).result.value; const signBytes = makeSignBytes([instantiateContractMsg], defaultFee, networkId, memo, account); - const signature = encodeSecp256k1Signature(execPen.pubkey, await execPen.createSignature(signBytes)); + const signature = await execPen.sign(signBytes); const signedTx = { msg: [instantiateContractMsg], fee: defaultFee, diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 521fc987..50b4171b 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -136,7 +136,7 @@ export function main(originalArgs: readonly string[]): void { const pubkey = encodeSecp256k1Pubkey(pen.pubkey); const address = encodeAddress(pubkey, "cosmos"); const data = Encoding.toAscii("foo bar"); - const signature = await pen.createSignature(data); + const signature = await pen.sign(data); console.info("Done testing, will exit now."); process.exit(0); diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 740b00aa..27ff42e3 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -1,5 +1,5 @@ import { CosmWasmClient } from "./cosmwasmclient"; -import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; +import { makeSignBytes, marshalTx } from "./encoding"; import { findAttribute } from "./logs"; import { Secp256k1Pen } from "./pen"; import { RestClient } from "./restclient"; @@ -95,7 +95,7 @@ describe("CosmWasmClient", () => { const chainId = await client.chainId(); const { accountNumber, sequence } = await client.getNonce(faucet.address); const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + const signature = await pen.sign(signBytes); const signedTx = { msg: [sendMsg], fee: fee, @@ -113,9 +113,7 @@ describe("CosmWasmClient", () => { it("works", async () => { pendingWithoutCosmos(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { - return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - }); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); const codeId = await client.upload(getRandomizedHackatom()); expect(codeId).toBeGreaterThanOrEqual(1); }); @@ -125,9 +123,7 @@ describe("CosmWasmClient", () => { it("works with transfer amount", async () => { pendingWithoutCosmos(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { - return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - }); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); const codeId = await client.upload(getRandomizedHackatom()); const transferAmount: readonly Coin[] = [ @@ -159,9 +155,7 @@ describe("CosmWasmClient", () => { it("can instantiate one code multiple times", async () => { pendingWithoutCosmos(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { - return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - }); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); const codeId = await client.upload(getRandomizedHackatom()); const contractAddress1 = await client.instantiate(codeId, { @@ -180,9 +174,7 @@ describe("CosmWasmClient", () => { it("works", async () => { pendingWithoutCosmos(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { - return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - }); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); const codeId = await client.upload(getRandomizedHackatom()); // instantiate diff --git a/packages/sdk/src/encoding.spec.ts b/packages/sdk/src/encoding.spec.ts index 6aeb8f61..ccdb8a4a 100644 --- a/packages/sdk/src/encoding.spec.ts +++ b/packages/sdk/src/encoding.spec.ts @@ -1,59 +1 @@ -import { Encoding } from "@iov/encoding"; - -import { encodeSecp256k1Signature } from "./encoding"; - -const { fromBase64 } = Encoding; - -describe("encoding", () => { - 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({ - // eslint-disable-next-line @typescript-eslint/camelcase - pub_key: { - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }, - signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", - }); - }); - - it("compresses uncompressed public keys", () => { - const pubkey = fromBase64( - "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", - ); - const signature = fromBase64( - "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", - ); - expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({ - // eslint-disable-next-line @typescript-eslint/camelcase - pub_key: { - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }, - signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", - }); - }); - - it("removes recovery values from signature data", () => { - const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); - const signature = Uint8Array.from([ - ...fromBase64( - "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", - ), - 99, - ]); - expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({ - // eslint-disable-next-line @typescript-eslint/camelcase - pub_key: { - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }, - signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", - }); - }); - }); -}); +describe("encoding", () => {}); diff --git a/packages/sdk/src/encoding.ts b/packages/sdk/src/encoding.ts index c8045b36..8a782944 100644 --- a/packages/sdk/src/encoding.ts +++ b/packages/sdk/src/encoding.ts @@ -1,10 +1,8 @@ -import { Secp256k1 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; -import { encodeSecp256k1Pubkey } from "./pubkey"; -import { Msg, StdFee, StdSignature, StdTx } from "./types"; +import { Msg, StdFee, StdTx } from "./types"; -const { toBase64, toUtf8 } = Encoding; +const { toUtf8 } = Encoding; function sortJson(json: any): any { if (typeof json !== "object" || json === null) { @@ -59,12 +57,3 @@ export function makeSignBytes( const signMsg = sortJson(signJson); return toUtf8(JSON.stringify(signMsg)); } - -export function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature { - return { - // eslint-disable-next-line @typescript-eslint/camelcase - pub_key: encodeSecp256k1Pubkey(pubkey), - // Recovery seems to be unused - signature: toBase64(Secp256k1.trimRecoveryByte(signature)), - }; -} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index a3331203..294c81f8 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -4,8 +4,9 @@ export { logs, types }; export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./address"; export { unmarshalTx } from "./decoding"; -export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; +export { makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; +export { encodeSecp256k1Signature } from "./signature"; export { CosmWasmClient, ExecuteResult, GetNonceResult, PostTxResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { diff --git a/packages/sdk/src/pen.spec.ts b/packages/sdk/src/pen.spec.ts index 7ea39cee..57c71c78 100644 --- a/packages/sdk/src/pen.spec.ts +++ b/packages/sdk/src/pen.spec.ts @@ -2,6 +2,7 @@ import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import { Secp256k1Pen } from "./pen"; +import { decodeSignature } from "./signature"; const { fromHex } = Encoding; @@ -33,12 +34,12 @@ describe("Sec256k1Pen", () => { "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling", ); const data = Encoding.toAscii("foo bar"); - const signature = await pen.createSignature(data); + const { pubkey, signature } = decodeSignature(await pen.sign(data)); const valid = await Secp256k1.verifySignature( new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)), new Sha256(data).digest(), - pen.pubkey, + pubkey, ); expect(valid).toEqual(true); }); diff --git a/packages/sdk/src/pen.ts b/packages/sdk/src/pen.ts index f50d2465..86037fe2 100644 --- a/packages/sdk/src/pen.ts +++ b/packages/sdk/src/pen.ts @@ -9,6 +9,9 @@ import { Slip10RawIndex, } from "@iov/crypto"; +import { encodeSecp256k1Signature } from "./signature"; +import { StdSignature } from "./types"; + export type PrehashType = "sha256" | "sha512" | null; /** @@ -23,7 +26,7 @@ export type PrehashType = "sha256" | "sha512" | null; */ export interface Pen { readonly pubkey: Uint8Array; - readonly createSignature: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; + readonly sign: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; } function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array { @@ -73,14 +76,12 @@ export class Secp256k1Pen implements Pen { } /** - * Creates a fixed length encoding of the signature parameters r (32 bytes) and s (32 bytes). + * Creates and returns a signature */ - public async createSignature( - signBytes: Uint8Array, - prehashType: PrehashType = "sha256", - ): Promise { + public async sign(signBytes: Uint8Array, prehashType: PrehashType = "sha256"): Promise { const message = prehash(signBytes, prehashType); const signature = await Secp256k1.createSignature(message, this.privkey); - return new Uint8Array([...signature.r(32), ...signature.s(32)]); + const fixedLengthSignature = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return encodeSecp256k1Signature(this.pubkey, fixedLengthSignature); } } diff --git a/packages/sdk/src/pubkey.spec.ts b/packages/sdk/src/pubkey.spec.ts index e5b3962b..51427488 100644 --- a/packages/sdk/src/pubkey.spec.ts +++ b/packages/sdk/src/pubkey.spec.ts @@ -7,7 +7,7 @@ const { fromBase64 } = Encoding; describe("pubkey", () => { describe("encodeSecp256k1Pubkey", () => { - it("encodes a full signature", () => { + it("encodes a compresed pubkey", () => { const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ type: "tendermint/PubKeySecp256k1", @@ -15,14 +15,11 @@ describe("pubkey", () => { }); }); - it("compresses uncompressed public keys", () => { + it("throws for uncompressed public keys", () => { const pubkey = fromBase64( "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", ); - expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }); + expect(() => encodeSecp256k1Pubkey(pubkey)).toThrowError(/public key must be compressed secp256k1/i); }); }); diff --git a/packages/sdk/src/pubkey.ts b/packages/sdk/src/pubkey.ts index 49a3cfd2..5da2e0a5 100644 --- a/packages/sdk/src/pubkey.ts +++ b/packages/sdk/src/pubkey.ts @@ -1,13 +1,15 @@ -import { Secp256k1 } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; import equal from "fast-deep-equal"; import { Bech32PubKey, PubKey, pubkeyType } from "./types"; export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey { + if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) { + throw new Error("Public key must be compressed secp256k1, i.e. 33 bytes starting with 0x02 or 0x03"); + } return { type: pubkeyType.secp256k1, - value: Encoding.toBase64(Secp256k1.compressPubkey(pubkey)), + value: Encoding.toBase64(pubkey), }; } diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 26a654b7..9862d178 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -3,7 +3,7 @@ import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import { assert } from "@iov/utils"; -import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; +import { makeSignBytes, marshalTx } from "./encoding"; import { findAttribute, parseLogs } from "./logs"; import { Pen, Secp256k1Pen } from "./pen"; import { encodeBech32Pubkey } from "./pubkey"; @@ -86,7 +86,7 @@ async function uploadCustomContract( const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); return client.postTx(marshalTx(signedTx)); } @@ -127,7 +127,7 @@ async function instantiateContract( const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); return client.postTx(marshalTx(signedTx)); } @@ -159,7 +159,7 @@ async function executeContract( const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); return client.postTx(marshalTx(signedTx)); } @@ -261,7 +261,7 @@ describe("RestClient", () => { const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); const result = await client.postTx(marshalTx(signedTx)); // console.log("Raw log:", result.raw_log); diff --git a/packages/sdk/src/signature.spec.ts b/packages/sdk/src/signature.spec.ts new file mode 100644 index 00000000..b0e782c1 --- /dev/null +++ b/packages/sdk/src/signature.spec.ts @@ -0,0 +1,69 @@ +import { Encoding } from "@iov/encoding"; + +import { decodeSignature, encodeSecp256k1Signature } from "./signature"; +import { StdSignature } from "./types"; + +const { fromBase64 } = Encoding; + +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({ + // eslint-disable-next-line @typescript-eslint/camelcase + 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 = { + // eslint-disable-next-line @typescript-eslint/camelcase + 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/sdk/src/signature.ts b/packages/sdk/src/signature.ts new file mode 100644 index 00000000..d31eb119 --- /dev/null +++ b/packages/sdk/src/signature.ts @@ -0,0 +1,39 @@ +import { Encoding } from "@iov/encoding"; + +import { encodeSecp256k1Pubkey } from "./pubkey"; +import { pubkeyType, StdSignature } from "./types"; + +/** + * 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 { + // eslint-disable-next-line @typescript-eslint/camelcase + pub_key: encodeSecp256k1Pubkey(pubkey), + signature: Encoding.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: Encoding.fromBase64(signature.pub_key.value), + signature: Encoding.fromBase64(signature.signature), + }; + default: + throw new Error("Unsupported pubkey type"); + } +} diff --git a/packages/sdk/types/encoding.d.ts b/packages/sdk/types/encoding.d.ts index e4a6c77f..4d32b80b 100644 --- a/packages/sdk/types/encoding.d.ts +++ b/packages/sdk/types/encoding.d.ts @@ -1,4 +1,4 @@ -import { Msg, StdFee, StdSignature, StdTx } from "./types"; +import { Msg, StdFee, StdTx } from "./types"; export declare function marshalTx(tx: StdTx): Uint8Array; export declare function makeSignBytes( msgs: readonly Msg[], @@ -8,4 +8,3 @@ export declare function makeSignBytes( accountNumber: number, sequence: number, ): Uint8Array; -export declare function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature; diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 9250b24c..ec6bdaca 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -3,8 +3,9 @@ import * as types from "./types"; export { logs, types }; export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./address"; export { unmarshalTx } from "./decoding"; -export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; +export { makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; +export { encodeSecp256k1Signature } from "./signature"; export { CosmWasmClient, ExecuteResult, GetNonceResult, PostTxResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { diff --git a/packages/sdk/types/pen.d.ts b/packages/sdk/types/pen.d.ts index 08a3f8d8..ee129395 100644 --- a/packages/sdk/types/pen.d.ts +++ b/packages/sdk/types/pen.d.ts @@ -1,4 +1,5 @@ import { Slip10RawIndex } from "@iov/crypto"; +import { StdSignature } from "./types"; export declare type PrehashType = "sha256" | "sha512" | null; /** * A pen is the most basic tool you can think of for signing. It works @@ -12,7 +13,7 @@ export declare type PrehashType = "sha256" | "sha512" | null; */ export interface Pen { readonly pubkey: Uint8Array; - readonly createSignature: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; + readonly sign: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; } /** * The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a` @@ -25,7 +26,7 @@ export declare class Secp256k1Pen implements Pen { private readonly privkey; private constructor(); /** - * Creates a fixed length encoding of the signature parameters r (32 bytes) and s (32 bytes). + * Creates and returns a signature */ - createSignature(signBytes: Uint8Array, prehashType?: PrehashType): Promise; + sign(signBytes: Uint8Array, prehashType?: PrehashType): Promise; } diff --git a/packages/sdk/types/signature.d.ts b/packages/sdk/types/signature.d.ts new file mode 100644 index 00000000..c23e7167 --- /dev/null +++ b/packages/sdk/types/signature.d.ts @@ -0,0 +1,14 @@ +import { StdSignature } from "./types"; +/** + * 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 declare function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature; +export declare function decodeSignature( + signature: StdSignature, +): { + readonly pubkey: Uint8Array; + readonly signature: Uint8Array; +}; diff --git a/scripts/cosm/deploy_erc20.js b/scripts/cosm/deploy_erc20.js index 94ed3522..1420a113 100755 --- a/scripts/cosm/deploy_erc20.js +++ b/scripts/cosm/deploy_erc20.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /* eslint-disable @typescript-eslint/camelcase */ -const { CosmWasmClient, encodeSecp256k1Signature, Secp256k1Pen } = require("@cosmwasm/sdk"); +const { CosmWasmClient, Secp256k1Pen } = require("@cosmwasm/sdk"); const fs = require("fs"); const httpUrl = "http://localhost:1317"; @@ -56,9 +56,7 @@ const initMsgCash = { async function main() { const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { - return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - }); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); const wasm = fs.readFileSync(__dirname + "/contracts/cw-erc20.wasm"); const codeId = await client.upload(wasm, "Upload ERC20 contract");