diff --git a/integration_tests/fixtures/users.ts b/integration_tests/fixtures/users.ts index 5093d1e9..4dc3a4fb 100644 --- a/integration_tests/fixtures/users.ts +++ b/integration_tests/fixtures/users.ts @@ -38,6 +38,9 @@ export const aliceWords = [ export const bobB58 = '13M8dUbxymE3xtiAXszRkGMmezMhBS8Li7wEsMojLdb4Sdxc4wc' export const aliceB58 = '148d8KTRcKA5JKPekBcKFd4KfvprvFRpjGtivhtmRmnZ8MFYnP3' +export const bobAliceMultisig2of2B58 = '1SYJnDnV2G1HSzoBF9nwd5apBX3pS7nLeLkjnVXemBZTP8C8F44TBYnr' +export const bobAliceMultisig1of2B58 = '1SVRdbavwiw4SM6cQFq6DN2nhK4YSqTd7cPhELjshVxzdQvoKbhQWocF' +export const testnetBobAliceMultisig2of2B58 ='14x4TpdfsLeL9MMcaJp6EVXFnA5tsgqXCr2u8MCL4qMEpKEYPCHZEEGJo' export const bobBip39Words = bobWords.map(word => word !== 'energy' ? word : 'episode') diff --git a/packages/address/package-lock.json b/packages/address/package-lock.json new file mode 100644 index 00000000..260cd20c --- /dev/null +++ b/packages/address/package-lock.json @@ -0,0 +1,117 @@ +{ + "name": "@helium/address", + "version": "4.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@helium/address", + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "bs58": "^4.0.1", + "js-sha256": "^0.9.0", + "multiformats": "^9.6.4" + }, + "devDependencies": { + "@types/bs58": "4.0.1" + } + }, + "node_modules/@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "dev": true, + "dependencies": { + "base-x": "^3.0.6" + } + }, + "node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/multiformats": { + "version": "9.6.4", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.6.4.tgz", + "integrity": "sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + } + }, + "dependencies": { + "@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "dev": true, + "requires": { + "base-x": "^3.0.6" + } + }, + "base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "multiformats": { + "version": "9.6.4", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.6.4.tgz", + "integrity": "sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } +} diff --git a/packages/address/package.json b/packages/address/package.json index 886497a1..1af3f7ee 100644 --- a/packages/address/package.json +++ b/packages/address/package.json @@ -27,8 +27,9 @@ }, "gitHead": "16442bef09f90dd9a83d4c9e1a347de3e80575e4", "dependencies": { - "bs58": "4.0.1", - "js-sha256": "^0.9.0" + "bs58": "^5.0.0", + "js-sha256": "^0.9.0", + "multiformats": "^9.6.4" }, "devDependencies": { "@types/bs58": "4.0.1" diff --git a/packages/address/src/KeyTypes.ts b/packages/address/src/KeyTypes.ts index 1676ab41..1d243ec9 100644 --- a/packages/address/src/KeyTypes.ts +++ b/packages/address/src/KeyTypes.ts @@ -1,9 +1,11 @@ export const ECC_COMPACT_KEY_TYPE = 0 export const ED25519_KEY_TYPE = 1 +export const MULTISIG_KEY_TYPE = 2 export const SUPPORTED_KEY_TYPES = [ ECC_COMPACT_KEY_TYPE, ED25519_KEY_TYPE, + MULTISIG_KEY_TYPE, ] export type KeyType = number diff --git a/packages/address/src/MultisigAddress.ts b/packages/address/src/MultisigAddress.ts new file mode 100644 index 00000000..da30ab7a --- /dev/null +++ b/packages/address/src/MultisigAddress.ts @@ -0,0 +1,102 @@ +import { + bs58M, + bs58N, + bs58Version, + bs58MultisigPublicKey, + bs58NetType, + byteToNetType, + byteToKeyType, + sortAddresses, + bs58KeyType, +} from './utils' +import { sha256 } from 'multiformats/hashes/sha2' +import { MULTISIG_KEY_TYPE } from './KeyTypes' +import { NetType, MAINNET} from './NetTypes' +import Address from './Address' + +export class MultisigAddress extends Address { + public M!: number + + public N!: number + + public constructor(version: number, netType: NetType, M: number, N: number, publicKey: Uint8Array) { + if (M > 256) { + throw new Error('required signers cannot exceed 256') + } + if (N > 256) { + throw new Error('total signers cannot exceed 256') + } + if (M > N) { + throw new Error('required signers cannot exceed total signers') + } + super(version, netType, MULTISIG_KEY_TYPE, publicKey); + this.M = M + this.N = N + } + + get bin(): Buffer { + return Buffer.concat([ + // eslint-disable-next-line no-bitwise + Buffer.from([this.netType | this.keyType]), + Buffer.from(new Uint8Array([this.M])), + Buffer.from(new Uint8Array([this.N])), + Buffer.from(this.publicKey), + ]) + } + + static fromB58(b58: string): MultisigAddress { + const keyType = bs58KeyType(b58) + if (keyType !== MULTISIG_KEY_TYPE) { + throw new Error('invalid keytype for multisig address') + } + const version = bs58Version(b58) + const netType = bs58NetType(b58) + const M = bs58M(b58) + const N = bs58N(b58) + const publicKey = bs58MultisigPublicKey(b58) + return new MultisigAddress(version, netType, M, N, publicKey) + } + + static fromBin(bin: Buffer): MultisigAddress { + const version = 0 + const byte = bin[0] + const netType = byteToNetType(byte) + const keyType = byteToKeyType(byte) + if (keyType !== MULTISIG_KEY_TYPE) { + throw new Error('invalid keytype for multisig address') + } + const M = bin[1] + const N = bin[2] + const publicKey = bin.slice(3, bin.length) + return new MultisigAddress(version, netType, M, N, publicKey) + } + + public static async create(addresses: Address[], M: number, netType?: NetType): Promise { + const version = 0 + if (!netType) { + netType = MAINNET + } + + let multisigPubKeysBin = new Uint8Array() + for (const address of sortAddresses(addresses)) { + if (address.keyType === MULTISIG_KEY_TYPE) { + return Promise.reject(new Error('cannot craeate multisig with invalid child keytype')) + } + multisigPubKeysBin = new Uint8Array([...multisigPubKeysBin, ...address.bin]) + } + + const publicKey = (await sha256.digest(multisigPubKeysBin)) + return new MultisigAddress(version, netType, M, addresses.length, publicKey.bytes) + } + + static isValid(b58: string): boolean { + try { + MultisigAddress.fromB58(b58) + return true + } catch (error) { + return false + } + } +} + +export default MultisigAddress diff --git a/packages/address/src/__tests__/MultisigAddress.spec.ts b/packages/address/src/__tests__/MultisigAddress.spec.ts new file mode 100644 index 00000000..68f9a1d0 --- /dev/null +++ b/packages/address/src/__tests__/MultisigAddress.spec.ts @@ -0,0 +1,85 @@ +import Address from '..' +import { MultisigAddress } from '../MultisigAddress' +import { + bobB58, + aliceB58, + bobAliceMultisig1of2B58, + bobAliceMultisig2of2B58, + testnetBobAliceMultisig2of2B58, +} from '../../../../integration_tests/fixtures/users' +import { TESTNET } from '../NetTypes' + + +describe('multisig b58', () => { + it('returns a b58 check encoded representation of a multisig address', async () => { + const addressMultisig2of2 = await MultisigAddress.create([Address.fromB58(bobB58), Address.fromB58(aliceB58)], 2) + expect(addressMultisig2of2.b58).toBe(bobAliceMultisig2of2B58) + }) + + it('supports multisig addresses', () => { + const addressMultisig2of2 = MultisigAddress.fromB58(bobAliceMultisig2of2B58) + expect(addressMultisig2of2.b58).toBe(bobAliceMultisig2of2B58) + }) +}) + +describe('bin', () => { + it('returns a binary representation of the multisig ddress', async () => { + const addressMultisig2of2 = await MultisigAddress.create([Address.fromB58(bobB58), Address.fromB58(aliceB58)], 2) + expect(addressMultisig2of2.bin[0]).toBe(2) + }) +}) + +describe('fromBin', () => { + it('builds a MultisigAddress from a binary representation', async () => { + const multisigAddress = await MultisigAddress.create([Address.fromB58(bobB58), Address.fromB58(aliceB58)], 2) + const multisigAddressFromBin = MultisigAddress.fromBin(multisigAddress.bin) + expect(multisigAddressFromBin.b58).toBe(multisigAddress.b58) + }) +}) + +describe('fromB58', () => { + it('builds an Address from a b58 string', () => { + const multisigAddressFromB58 = Address.fromB58(bobAliceMultisig2of2B58) + expect(multisigAddressFromB58.b58).toBe(bobAliceMultisig2of2B58) + }) +}) + +describe('unsupported child key types', () => { + it('throws an error if creating address with multisig key type', async () => { + expect(async () => { + await MultisigAddress.create([MultisigAddress.fromB58(bobAliceMultisig2of2B58), Address.fromB58(aliceB58)], 2) + }).rejects.toThrow() + }) +}) + +describe('isValid', () => { + it('returns true if the address is valid and supported', () => { + expect(MultisigAddress.isValid(bobAliceMultisig2of2B58)).toBeTruthy() + expect(MultisigAddress.isValid(bobAliceMultisig1of2B58)).toBeTruthy() + }) +}) + +describe('testnet addresses', () => { + it('decodes testnet addresses from b58', async () => { + const address = MultisigAddress.fromB58(testnetBobAliceMultisig2of2B58) + expect(address.netType).toBe(TESTNET) + }) +}) + +describe('testnet addresses', () => { + it('decodes testnet addresses from b58', async () => { + const address = MultisigAddress.fromB58(testnetBobAliceMultisig2of2B58) + expect(address.netType).toBe(TESTNET) + }) +}) + +describe('erlang interop', () => { + it('makes the same multisig key as the erlang lib ', async () => { + const keys = [ + Address.fromB58('11MJXxoWFp2bMsqKM6QZin6ync9DQ3fjjFjUrFiRCaKunmBEBhK'), + Address.fromB58('11x7jP9yAnyk5jeYywmsYDFdYq5xvKLKjP2zjhGzCwDSQtxcUDt'), + ] + const address = await MultisigAddress.create(keys, 1) + expect(address.b58).toBe('1SVRdbaAev7zSpUsMjvQrbRBGFHLXEa63SGntYCqChC4CTpqwftTPGbZ') + }) +}) \ No newline at end of file diff --git a/packages/address/src/__tests__/Utils.spec.ts b/packages/address/src/__tests__/Utils.spec.ts index 30f325a9..6f3430c9 100644 --- a/packages/address/src/__tests__/Utils.spec.ts +++ b/packages/address/src/__tests__/Utils.spec.ts @@ -22,7 +22,7 @@ describe('bs58ToBin', () => { const address = new Address(0, NetTypes.MAINNET, 1, bob.publicKey).b58 const bin = bs58.decode(address) const vPayload = bin.slice(0, -4) - const checksum = bin.slice(-4) + const checksum = Buffer.from(bin.slice(-4)) const checksumVerify = sha256(Buffer.from(sha256.digest(vPayload))) const checksumVerifyBytes = Buffer.alloc(4, checksumVerify, 'hex') expect(checksumVerifyBytes).toStrictEqual(checksum) diff --git a/packages/address/src/index.ts b/packages/address/src/index.ts index a1fc2e61..daf9299c 100644 --- a/packages/address/src/index.ts +++ b/packages/address/src/index.ts @@ -5,6 +5,7 @@ */ export { default } from './Address' +export { MultisigAddress } from './MultisigAddress' export * as NetTypes from './NetTypes' export * as KeyTypes from './KeyTypes' export * as utils from './utils' diff --git a/packages/address/src/utils.ts b/packages/address/src/utils.ts index 672d47a6..0297da9f 100644 --- a/packages/address/src/utils.ts +++ b/packages/address/src/utils.ts @@ -3,6 +3,7 @@ import { sha256 } from 'js-sha256' import bs58 from 'bs58' import { KeyType } from './KeyTypes' import { NetType } from './NetTypes' +import Address from './Address' export const bs58CheckEncode = (version: number, binary: Buffer | Uint8Array): string => { const vPayload = Buffer.concat([ @@ -63,3 +64,32 @@ export const bs58PublicKey = (bs58Address: string): Buffer => { const publicKey = Buffer.from(bin).slice(1) return publicKey } + +export const bs58M = (bs58Address: string): number => { + const bin = bs58ToBin(bs58Address) + const M = bin[1] + return M +} + +export const bs58N = (bs58Address: string): number => { + const bin = bs58ToBin(bs58Address) + const N = bin[2] + return N +} + +export const bs58MultisigPublicKey = (bs58Address: string): Buffer => { + const bin = bs58ToBin(bs58Address) + const publicKey = Buffer.from(bin).slice(3) + return publicKey +} + +export const sortAddresses = (addresses: Address[]): Address[] => { + const addressMap = addresses.map(address => { + const charCodeArray = Array.from(address.b58).map((character):number => { + return character.charCodeAt(0) + }) + return { address: address, buffer: new Uint8Array(charCodeArray)} + }) + + return addressMap.sort((a, b) => Buffer.compare(a.buffer, b.buffer)).map(obj => obj.address) +} diff --git a/packages/address/yarn.lock b/packages/address/yarn.lock index 46585c6c..ae2af5bb 100644 --- a/packages/address/yarn.lock +++ b/packages/address/yarn.lock @@ -4,31 +4,41 @@ "@types/bs58@4.0.1": version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/bs58/-/bs58-4.0.1.tgz#3d51222aab067786d3bc3740a84a7f5a0effaa37" + resolved "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz" integrity sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA== dependencies: base-x "^3.0.6" -base-x@^3.0.2, base-x@^3.0.6: +base-x@^3.0.6: version "3.0.9" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz" integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== dependencies: safe-buffer "^5.0.1" -bs58@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" - integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo= +base-x@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" + integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== + +bs58@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" + integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== dependencies: - base-x "^3.0.2" + base-x "^4.0.0" js-sha256@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== +multiformats@^9.6.4: + version "9.6.4" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.6.4.tgz#5dce1f11a407dbb69aa612cb7e5076069bb759ca" + integrity sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg== + safe-buffer@^5.0.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== diff --git a/packages/crypto/src/MultisigSignature.ts b/packages/crypto/src/MultisigSignature.ts new file mode 100644 index 00000000..6275b768 --- /dev/null +++ b/packages/crypto/src/MultisigSignature.ts @@ -0,0 +1,121 @@ +import Address, { KeyTypes, MultisigAddress } from '@helium/address' +import { sortAddresses } from '@helium/address/build/utils'; +import { verifySignature } from './utils' +const {MULTISIG_KEY_TYPE} = KeyTypes +const PUBLIC_KEY_LENGTH = 33; + + +class KeySignature { + public index!: number + + public signature!: Uint8Array + + constructor(index: number, signature: Uint8Array) { + this.index = index + this.signature = signature + } + + public static new(addresses: Address[], address: Address, signature: Uint8Array) { + if (address.keyType == MULTISIG_KEY_TYPE) { + throw new Error('invalid keytype for multisig KeySignature') + } + return new KeySignature(addresses.findIndex(addr => addr.publicKey === address.publicKey), signature) + } + + get bin(): Uint8Array { + return new Uint8Array([this.index, this.signature.length, ...this.signature]) + } +} + +export default class MultisigSignature { + public addresses!: Address[] + + public signatures!: KeySignature[] + + constructor(addresses: Address[], signatures: KeySignature[]) { + this.addresses = addresses + this.signatures = signatures + } + + public static create(multisigAddress: MultisigAddress, addresses: Address[], signatures: Map): MultisigSignature { + addresses = sortAddresses(addresses) + if (multisigAddress.M > signatures.size) { + throw new Error('insufficient signatures') + } + if (multisigAddress.N != addresses.length) { + throw new Error('wrong number of addresses') + } + let keySignatures: KeySignature[] = []; + for (const [address, signature] of signatures) { + let keySignature = KeySignature.new(addresses, address, signature) + keySignatures.push(keySignature) + } + return new MultisigSignature(addresses, keySignatures) + } + + public verify(message: Uint8Array): number { + let valid_signature_count = 0; + for (const sig of this.signatures) { + let address = this.addresses[sig.index]; + if (verifySignature(sig.signature, message, address.publicKey)){ + valid_signature_count += 1 + } + } + return valid_signature_count + } + + get bin(): Uint8Array { + return new Uint8Array([...this.serializedAddresses(), ...this.serlializedSignatures()]) + } + + public static fromBin(multisigAddress: MultisigAddress, input: Uint8Array): MultisigSignature { + let addresses = this.addressesFromBin(multisigAddress.N, input) + let signatures = this.signaturesFromBin(addresses, input.slice(PUBLIC_KEY_LENGTH * multisigAddress.N)) + return MultisigSignature.create(multisigAddress, addresses, signatures) + } + + static isValid(multisigAddress: MultisigAddress, input: Uint8Array): boolean { + try { + MultisigSignature.fromBin(multisigAddress, input) + return true + } catch (error) { + return false + } + } + + private serializedAddresses() { + let multisigPubKeysBin = new Uint8Array() + for (const address of this.addresses) { + multisigPubKeysBin = new Uint8Array([...multisigPubKeysBin, ...address.bin]) + } + return multisigPubKeysBin + } + + private serlializedSignatures() { + let multisigSignatures = new Uint8Array() + // Ensure signatures are sorted prior to serialization + for (const sig of this.signatures.sort((a, b) => a.bin > b.bin ? 1 : -1)) { + multisigSignatures = new Uint8Array([...multisigSignatures, ...sig.bin]) + } + return multisigSignatures + } + + private static addressesFromBin(N: number, input: Uint8Array): Address[] { + let addresses : Address[] = []; + for (let i = 0; i < N; i++){ + let address = Address.fromBin(Buffer.from(input.slice(0, PUBLIC_KEY_LENGTH))) + input = input.slice(PUBLIC_KEY_LENGTH) + addresses.push(address) + } + return addresses + } + + private static signaturesFromBin(addresses: Address[], input: Uint8Array): Map { + let signatures = new Map(); + do { + signatures.set(addresses[input[0]], input.slice(2, input[1] + 2)) + input = input.slice(input[1] + 2) + } while (input.length); + return signatures; + } +} diff --git a/packages/crypto/src/__tests__/MultisigSignature.spec.ts b/packages/crypto/src/__tests__/MultisigSignature.spec.ts new file mode 100644 index 00000000..b8e2b55e --- /dev/null +++ b/packages/crypto/src/__tests__/MultisigSignature.spec.ts @@ -0,0 +1,145 @@ +import { MultisigAddress } from '@helium/address' +import { MultisigSignature } from '..' +import { usersFixture } from '../../../../integration_tests/fixtures/users' + +describe('create', () => { + it('invalid if not enough signatures', async () => { + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + expect(async () => { + MultisigSignature.create(multisigAddress, [bob.address, alice.address], new Map()) + }).rejects.toThrow() + }) + + it('invalid if not enough addresses', async () => { + const { bob, alice } = await usersFixture() + const signatures = new Map([[bob.address, await bob.sign('Hello')]]) + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + expect(async () => { + MultisigSignature.create(multisigAddress, [alice.address], signatures) + }).rejects.toThrow() + }) + + it('invalid if too many (unique) addresses', async () => { + const { bob, alice } = await usersFixture() + const signatures = new Map([[bob.address, await bob.sign('Hello')]]) + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + expect(async () => { + MultisigSignature.create(multisigAddress, [bob.address, bob.address, alice.address], signatures) + }).rejects.toThrow() + }) + + it('create a multisig signature', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + + const signatures = new Map([[bob.address, await bob.sign(message)]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(multisigSig.signatures.length).toBe(1) + expect(multisigSig.signatures[0].index).toBe(0) + expect(multisigSig.addresses.length).toBe(2) + expect(multisigSig.addresses[0].b58).toBe(bob.address.b58) + expect(multisigSig.addresses[1].b58).toBe(alice.address.b58) + }) +}) + +describe('verify', () => { + it('verify one of two signature', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + const signatures = new Map([[bob.address, await bob.sign(message)]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(multisigSig.verify(message)).toBe(1) + }) + + it('verify fail one of two signature', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + const signatures = new Map([[bob.address, await bob.sign(Buffer.from('Oops'))]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(multisigSig.verify(message)).toBe(0) + }) + + it('verify two of two signature', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 2) + const signatures = new Map([[bob.address, await bob.sign(message)], [alice.address, await alice.sign(message)]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(multisigSig.verify(message)).toBe(2) + }) + + it('verify fail two of two signature', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 2) + const signatures = new Map([[bob.address, await bob.sign(message)], [alice.address, await alice.sign(Buffer.from('Oops'))]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(multisigSig.verify(message)).toBe(1) + }) +}) + +describe('bin', () => { + it('serialize appropriately', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + const signatures = new Map([[bob.address, await bob.sign(message)]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(multisigSig.bin[0]).toBe(1) + }) + + it('serialize with signatures in correct sorted order', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + + const signatures2 = new Map([[alice.address, await alice.sign(message)], [bob.address, await bob.sign(message)]]) + const multisigSig2 = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures2) + + // Signatures not sorted + expect(multisigSig2.signatures[1].index).toBe(0) + expect(multisigSig2.signatures[0].index).toBe(1) + + const multisigSignatureTest2 = MultisigSignature.fromBin(multisigAddress, multisigSig2.bin) + expect(multisigSignatureTest2.verify(message)).toBe(2) + expect(multisigSignatureTest2.signatures[1].signature).toStrictEqual(await alice.sign(message)) + + // Signatures sorted + expect(multisigSignatureTest2.signatures[0].index).toBe(0) + expect(multisigSignatureTest2.signatures[1].index).toBe(1) + }) +}) + +describe('fromBin', () => { + it('deserialize appropriately', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + const signatures = new Map([[bob.address, await bob.sign(message)]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + const multisigSignatureFromBin = MultisigSignature.fromBin(multisigAddress, multisigSig.bin) + expect(multisigSignatureFromBin.verify(message)).toBe(1) + expect(multisigSignatureFromBin.signatures[0].signature).toStrictEqual(await bob.sign(message)) + }) +}) + +describe('isValid', () => { + it('is valid if can deserialize', async () => { + const message = Buffer.from("Hello") + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + const signatures = new Map([[bob.address, await bob.sign(message)]]) + const multisigSig = MultisigSignature.create(multisigAddress, [bob.address, alice.address], signatures) + expect(MultisigSignature.isValid(multisigAddress, multisigSig.bin)).toBe(true) + }) + + it('is not valid if cannot deserialize', async () => { + const { bob, alice } = await usersFixture() + const multisigAddress = await MultisigAddress.create([bob.address, alice.address], 1) + expect(MultisigSignature.isValid(multisigAddress, new Uint8Array(Buffer.from('notavalidmultisigserialization')))).toBe(false) + }) +}) \ No newline at end of file diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index de087b59..caae7014 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -6,4 +6,5 @@ export { default as Mnemonic } from './Mnemonic' export { default as Keypair } from './Keypair' +export { default as MultisigSignature } from './MultisigSignature' export * as utils from './utils' diff --git a/packages/crypto/src/utils.ts b/packages/crypto/src/utils.ts index 6c9a960e..2b80b579 100644 --- a/packages/crypto/src/utils.ts +++ b/packages/crypto/src/utils.ts @@ -28,3 +28,7 @@ export const deriveChecksumBits = (entropyBuffer: Buffer | string) => { return bytesToBinary([].slice.call(hash)).slice(0, CS) } + +export const verifySignature = (signature: Uint8Array, message: Uint8Array, pubKey: Uint8Array) => { + return sodium.crypto_sign_verify_detached(signature, message, pubKey) +} diff --git a/packages/crypto/yarn.lock b/packages/crypto/yarn.lock index 1918082b..14436063 100644 --- a/packages/crypto/yarn.lock +++ b/packages/crypto/yarn.lock @@ -2,12 +2,13 @@ # yarn lockfile v1 -"@types/bs58@4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/bs58/-/bs58-4.0.1.tgz#3d51222aab067786d3bc3740a84a7f5a0effaa37" - integrity sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA== +"@helium/address@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@helium/address/-/address-4.0.0.tgz#041c075bf6b750e52cdbee4a45950bfbf42e085a" + integrity sha512-rkQNI98lifSTc2I9GfVYRukThUCY+9ba77EadhaiFI9SHOJ/lSFp93zHOHkcyr1AI8DpKgZQGx19hqFOL7QpcA== dependencies: - base-x "^3.0.6" + bs58 "4.0.1" + js-sha256 "^0.9.0" "@types/create-hash@1.2.2": version "1.2.2" @@ -26,14 +27,14 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== -base-x@^3.0.2, base-x@^3.0.6: +base-x@^3.0.2: version "3.0.8" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d" integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA== dependencies: safe-buffer "^5.0.1" -bs58@^4.0.1: +bs58@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo= @@ -72,6 +73,11 @@ inherits@^2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + libsodium-wrappers@^0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.6.tgz#baed4c16d4bf9610104875ad8a8e164d259d48fb"