Skip to content

Commit

Permalink
Multisig (#1)
Browse files Browse the repository at this point in the history
* Add Multisig Support

Co-authored-by: syuan100 <syuan100@gmail.com>
Co-authored-by: Joe <joe@cryptoballoon.net>
  • Loading branch information
3 people authored Apr 10, 2022
1 parent d1c427a commit e1964ee
Show file tree
Hide file tree
Showing 15 changed files with 647 additions and 19 deletions.
3 changes: 3 additions & 0 deletions integration_tests/fixtures/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
117 changes: 117 additions & 0 deletions packages/address/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/address/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions packages/address/src/KeyTypes.ts
Original file line number Diff line number Diff line change
@@ -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
102 changes: 102 additions & 0 deletions packages/address/src/MultisigAddress.ts
Original file line number Diff line number Diff line change
@@ -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<MultisigAddress> {
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
85 changes: 85 additions & 0 deletions packages/address/src/__tests__/MultisigAddress.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
2 changes: 1 addition & 1 deletion packages/address/src/__tests__/Utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/address/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
30 changes: 30 additions & 0 deletions packages/address/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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)
}
Loading

0 comments on commit e1964ee

Please sign in to comment.