Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multisig key type #288

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/walletconnect/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
ApiRequestArguments,
NetworkId,
networkIds,
keyTypes,
EnableOptionsBase
} from '@alephium/web3'

Expand Down Expand Up @@ -433,12 +434,13 @@ export function formatAccount(permittedChain: string, account: Account): string
}

export function parseAccount(account: string): Account & { networkId: NetworkId } {
const [_namespace, networkId, _group, publicKey, keyType] = account.replace(/\//g, ':').split(':')
const address = addressFromPublicKey(publicKey)
const group = groupOfAddress(address)
if (keyType !== 'default' && keyType !== 'bip340-schnorr') {
throw Error(`Invalid key type: ${keyType}`)
const [_namespace, networkId, _group, publicKey, _keyType] = account.replace(/\//g, ':').split(':')
const keyType = keyTypes.find((tpe) => tpe === _keyType)
if (keyType === undefined) {
throw Error(`Invalid key type: ${_keyType}`)
}
const address = addressFromPublicKey(publicKey, keyType)
const group = groupOfAddress(address)
return { address, group, publicKey, keyType, networkId: networkId as NetworkId }
}

Expand Down
5 changes: 3 additions & 2 deletions packages/web3/src/signer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export interface Destination {
lockTime?: number
message?: string
}
assertType<Eq<keyof Destination, keyof node.Destination>>
assertType<Eq<keyof Destination, keyof node.Destination>>()

export type KeyType = 'default' | 'bip340-schnorr'
export const keyTypes = ['default', 'bip340-schnorr', 'multisig'] as const
export type KeyType = (typeof keyTypes)[number]

export interface Account {
keyType: KeyType
Expand Down
79 changes: 79 additions & 0 deletions packages/web3/src/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,92 @@ describe('utils', function () {
)
})

it('should encode multisig public keys', () => {
expect(() => utils.encodeMultisigPublicKeys([], 2)).toThrow('Public key array is empty')
expect(() =>
utils.encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'], 2)
).toThrow('Invalid m in m-of-n multisig, m: 2, n: 1')
expect(() =>
utils.encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc', '0011'], 1)
).toThrow('Invalid public key: 0011')
expect(
utils.encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'], 1)
).toEqual('0101030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc')
expect(
utils.encodeMultisigPublicKeys(
[
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc',
'03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19'
],
1
)
).toEqual(
'0102030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19'
)
expect(
utils.encodeMultisigPublicKeys(
[
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc',
'03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19',
'03c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
],
2
)
).toEqual(
'0203030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be1903c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
)
expect(
utils.encodeMultisigPublicKeys(
[
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc',
'03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19',
'03c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
],
3
)
).toEqual(
'0303030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be1903c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
)
expect(() =>
utils.encodeMultisigPublicKeys(
Array(32).fill('030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'),
2
)
).toThrow('The length of public key array exceeds maximum limit')
})

it('should compute address from public key', () => {
expect(utils.publicKeyFromPrivateKey('91411e484289ec7e8b3058697f53f9b26fa7305158b4ef1a81adfbabcf090e45')).toBe(
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'
)
expect(utils.addressFromPublicKey('030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc')).toBe(
'1ACCkgFfmTif46T3qK12znuWjb5Bk9jXpqaeWt2DXx8oc'
)
const publicKeys = [
'043ed1a15fa4b9c92d5f0b712c238cc26e16bfe8a359d6dc0aeffed983c02e800b',
'bcdfb4cbd7555f8df4b66414f17e81eaa108dea382dabb01a63ced575d1824b37e',
'94438313828b1b17e5d7f2c9d773d44a81af6c3ef67446fbf350497ff3b06c3741'
]
expect(() => utils.addressFromPublicKey('0100', 'multisig')).toThrow('Invalid n in m-of-n multisig, m: 1, n: 0')
expect(() => utils.addressFromPublicKey('0130', 'multisig')).toThrow('Invalid n in m-of-n multisig, m: 1, n: 48')
expect(() =>
utils.addressFromPublicKey('04' + utils.encodeMultisigPublicKeys(publicKeys, 3).slice(2), 'multisig')
).toThrow('Invalid m in m-of-n multisig, m: 4, n: 3')
expect(() =>
utils.addressFromPublicKey('00' + utils.encodeMultisigPublicKeys(publicKeys, 3).slice(2), 'multisig')
).toThrow('Invalid m in m-of-n multisig, m: 0, n: 3')
expect(() =>
utils.addressFromPublicKey(utils.encodeMultisigPublicKeys(publicKeys, 3).slice(0, -2), 'multisig')
).toThrow('Invalid public key size')
expect(utils.addressFromPublicKey(utils.encodeMultisigPublicKeys(publicKeys, 3), 'multisig')).toEqual(
'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEMG'
)
expect(utils.addressFromPublicKey(utils.encodeMultisigPublicKeys(publicKeys, 2), 'multisig')).toEqual(
'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEMF'
)
expect(utils.addressFromPublicKey(utils.encodeMultisigPublicKeys(publicKeys, 1), 'multisig')).toEqual(
'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEME'
)
})

it('should convert between contract id and address', () => {
Expand Down
56 changes: 55 additions & 1 deletion packages/web3/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const networkIds = ['mainnet', 'testnet', 'devnet'] as const
export type NetworkId = (typeof networkIds)[number]

const ec = new EC('secp256k1')
const PublicKeyBytesLength = 33
const MaxKeySize = 32
Lbqds marked this conversation as resolved.
Show resolved Hide resolved

export function encodeSignature(signature: EC.Signature | { r: BN; s: BN }): string {
let sNormalized = signature.s
Expand Down Expand Up @@ -194,10 +196,62 @@ export function addressFromPublicKey(publicKey: string, _keyType?: KeyType): str
const hash = Buffer.from(blake.blake2b(Buffer.from(publicKey, 'hex'), undefined, 32))
const bytes = Buffer.concat([addressType, hash])
return bs58.encode(bytes)
} else {
} else if (keyType === 'bip340-schnorr') {
const lockupScript = Buffer.from(`0101000000000458144020${publicKey}8685`, 'hex')
return addressFromScript(lockupScript)
} else {
return multisigAddressFromPublicKey(publicKey)
}
}

function multisigAddressFromPublicKey(publicKey: string): string {
try {
const bytes = hexToBinUnsafe(publicKey)
const m = bytes[0]
const n = bytes[1]
if (n <= 0 || n >= MaxKeySize) {
throw new Error(`Invalid n in m-of-n multisig, m: ${m}, n: ${n}`)
}
if (m <= 0 || m > n) {
throw new Error(`Invalid m in m-of-n multisig, m: ${m}, n: ${n}`)
}
if (bytes.length !== PublicKeyBytesLength * n + 2) {
throw new Error('Invalid public key size')
}

const publicKeyHashes: Uint8Array[] = []
for (let i = 2; i < bytes.length; i += 33) {
const publicKey = bytes.slice(i, i + 33)
publicKeyHashes.push(blake.blake2b(publicKey, undefined, 32))
}
const encoded = Buffer.concat([
Buffer.from([AddressType.P2MPKH]),
Buffer.from([n]),
...publicKeyHashes,
Buffer.from([m])
])
return bs58.encode(encoded)
} catch (err) {
throw new Error(`Invalid multisig public key, error: ${err}`)
}
}

export function encodeMultisigPublicKeys(publicKeys: string[], m: number): string {
if (publicKeys.length === 0) {
throw new Error('Public key array is empty')
}
if (publicKeys.length >= MaxKeySize) {
throw new Error('The length of public key array exceeds maximum limit')
}
if (m <= 0 || m > publicKeys.length) {
throw new Error(`Invalid m in m-of-n multisig, m: ${m}, n: ${publicKeys.length}`)
}
publicKeys.forEach((publicKey) => {
if (!isHexString(publicKey) || publicKey.length !== PublicKeyBytesLength * 2) {
throw new Error(`Invalid public key: ${publicKey}`)
}
})
return Buffer.from([m, publicKeys.length]).toString('hex') + publicKeys.join('')
}

export function addressFromScript(script: Uint8Array): string {
Expand Down
Loading