diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts index 838ba881..ad98e97d 100644 --- a/packages/amino/src/encoding.spec.ts +++ b/packages/amino/src/encoding.spec.ts @@ -7,7 +7,17 @@ import { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./encoding"; -import { MultisigThresholdPubkey, Pubkey } from "./pubkeys"; +import { Pubkey } from "./pubkeys"; +import { + testgroup1, + testgroup1Address, + testgroup2, + testgroup2Address, + testgroup3, + testgroup3Address, + testgroup4, + testgroup4Address, +} from "./testutils.spec"; describe("encoding", () => { describe("encodeSecp256k1Pubkey", () => { @@ -137,84 +147,16 @@ describe("encoding", () => { }); it("works for multisig", () => { - // ./build/wasmd keys add test1 - // ./build/wasmd keys add test2 - // ./build/wasmd keys add test3 - // ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 - // ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 - // # By default pubkeys are sorted by its address data (https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174) - // ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 - // ./build/wasmd keys add testgroup4 --multisig=test3,test1 --nosort --multisig-threshold 2 - - const test1 = decodeBech32Pubkey( - "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", - // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 - // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm - // address data: 9028577891aee00d15157444c069b4bc60bc764d - ); - const test2 = decodeBech32Pubkey( - "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", - // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 - // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg - // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 - ); - const test3 = decodeBech32Pubkey( - "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", - // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 - // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np - // address data: eeb861aa9b71c0838851fabac83650666f5220b0 - ); - - // 2/3 multisig - const testgroup1: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test1, test2, test3], - }, - }; - const expected1 = Bech32.decode( - "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7v7aysdd", - ).data; + const expected1 = Bech32.decode(testgroup1Address).data; expect(encodeAminoPubkey(testgroup1)).toEqual(expected1); - // 1/3 multisig - const testgroup2: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "1", - pubkeys: [test1, test2, test3], - }, - }; - const expected2 = Bech32.decode( - "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke", - ).data; + const expected2 = Bech32.decode(testgroup2Address).data; expect(encodeAminoPubkey(testgroup2)).toEqual(expected2); - // 2/2 multisig - const testgroup3: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test1, test3], - }, - }; - const expected3 = Bech32.decode( - "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu", - ).data; + const expected3 = Bech32.decode(testgroup3Address).data; expect(encodeAminoPubkey(testgroup3)).toEqual(expected3); - // 2/2 multisig with custom sorting - const testgroup4: MultisigThresholdPubkey = { - type: "tendermint/PubKeyMultisigThreshold", - value: { - threshold: "2", - pubkeys: [test3, test1], - }, - }; - const expected4 = Bech32.decode( - "wasmpub1ytql0csgqgfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vujvg56k", - ).data; + const expected4 = Bech32.decode(testgroup4Address).data; expect(encodeAminoPubkey(testgroup4)).toEqual(expected4); }); }); diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index cb2ae3bf..28b4d9e5 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -18,3 +18,4 @@ export { isSinglePubkey, pubkeyType, } from "./pubkeys"; +export { createMultisigThresholdPubkey } from "./multisig"; diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts new file mode 100644 index 00000000..ce239345 --- /dev/null +++ b/packages/amino/src/multisig.spec.ts @@ -0,0 +1,63 @@ +import { compareArrays, createMultisigThresholdPubkey } from "./multisig"; +import { test1, test2, test3, testgroup1, testgroup2, testgroup3, testgroup4 } from "./testutils.spec"; + +describe("multisig", () => { + describe("compareArrays", () => { + it("return 0 for equal arrays", () => { + expect(compareArrays(new Uint8Array([]), new Uint8Array([]))).toEqual(0); + expect(compareArrays(new Uint8Array([1]), new Uint8Array([1]))).toEqual(0); + expect(compareArrays(new Uint8Array([3, 2, 1]), new Uint8Array([3, 2, 1]))).toEqual(0); + }); + + it("return > 0 for left > right", () => { + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5, 4]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 4, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([4, 5, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([]))).toBeGreaterThan(0); + }); + + it("return < 0 for left < right", () => { + expect(compareArrays(new Uint8Array([5, 5, 4]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5, 4, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([4, 5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + }); + + it("can be used with sort", () => { + const values = [ + new Uint8Array([2]), + new Uint8Array([1]), + new Uint8Array([2, 5]), + new Uint8Array([3]), + new Uint8Array([]), + ].sort(compareArrays); + expect(values).toEqual([ + new Uint8Array([]), + new Uint8Array([1]), + new Uint8Array([2]), + new Uint8Array([2, 5]), + new Uint8Array([3]), + ]); + }); + }); + + describe("MultisigThresholdPubkey", () => { + it("works with sorting", () => { + expect(createMultisigThresholdPubkey([test1, test2, test3], 2)).toEqual(testgroup1); + expect(createMultisigThresholdPubkey([test1, test2, test3], 1)).toEqual(testgroup2); + expect(createMultisigThresholdPubkey([test3, test1], 2)).toEqual(testgroup3); + + expect(createMultisigThresholdPubkey([test1, test2, test3], 2, false)).toEqual(testgroup1); + expect(createMultisigThresholdPubkey([test1, test2, test3], 1, false)).toEqual(testgroup2); + expect(createMultisigThresholdPubkey([test3, test1], 2, false)).toEqual(testgroup3); + }); + + it("works with nosort", () => { + expect(createMultisigThresholdPubkey([test3, test1], 2, true)).toEqual(testgroup4); + }); + }); +}); diff --git a/packages/amino/src/multisig.ts b/packages/amino/src/multisig.ts new file mode 100644 index 00000000..51203f87 --- /dev/null +++ b/packages/amino/src/multisig.ts @@ -0,0 +1,38 @@ +import { toHex } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; + +import { pubkeyToRawAddress } from "./addresses"; +import { MultisigThresholdPubkey, SinglePubkey } from "./pubkeys"; + +/** + * Compare arrays lexicographically. + * + * Returns value < 0 if `a < b`. + * Returns value > 0 if `a > b`. + * Returns 0 if `a === b`. + */ +export function compareArrays(a: Uint8Array, b: Uint8Array): number { + return toHex(a).localeCompare(toHex(b)); +} + +export function createMultisigThresholdPubkey( + pubkeys: readonly SinglePubkey[], + threshold: number, + nosort = false, +): MultisigThresholdPubkey { + const outPubkeys = nosort + ? pubkeys + : Array.from(pubkeys).sort((lhs, rhs) => { + // https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174 + const addressLhs = pubkeyToRawAddress(lhs); + const addressRhs = pubkeyToRawAddress(rhs); + return compareArrays(addressLhs, addressRhs); + }); + return { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: new Uint53(threshold).toString(), + pubkeys: outPubkeys, + }, + }; +} diff --git a/packages/amino/src/testutils.spec.ts b/packages/amino/src/testutils.spec.ts new file mode 100644 index 00000000..36cbc26d --- /dev/null +++ b/packages/amino/src/testutils.spec.ts @@ -0,0 +1,73 @@ +import { decodeBech32Pubkey } from "./encoding"; +import { MultisigThresholdPubkey } from "./pubkeys"; + +// ./build/wasmd keys add test1 +// ./build/wasmd keys add test2 +// ./build/wasmd keys add test3 +// ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 +// ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 +// # By default pubkeys are sorted by its address data (https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174) +// ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 +// ./build/wasmd keys add testgroup4 --multisig=test3,test1 --nosort --multisig-threshold 2 + +export const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 + // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm + // address data: 9028577891aee00d15157444c069b4bc60bc764d +); +export const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 + // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg + // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 +); +export const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 + // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np + // address data: eeb861aa9b71c0838851fabac83650666f5220b0 +); + +// 2/3 multisig +export const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, +}; +export const testgroup1Address = + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7v7aysdd"; + +export const testgroup2: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "1", + pubkeys: [test1, test2, test3], + }, +}; +export const testgroup2Address = + "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke"; + +// 2/2 multisig +export const testgroup3: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test3], + }, +}; +export const testgroup3Address = + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu"; + +// 2/2 multisig with custom sorting +export const testgroup4: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test3, test1], + }, +}; +export const testgroup4Address = + "wasmpub1ytql0csgqgfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vujvg56k";