UNPKG

bmultisig

Version:

Bcoin wallet plugin for multi signature transaction proposals

463 lines (377 loc) 12.5 kB
/*! * cosigner.js - Cosigner * Copyright (c) 2018, The Bcoin Developers (MIT License). * https://github.com/bcoin-org/bmultisig */ 'use strict'; const assert = require('bsert'); const bufio = require('bufio'); const {Struct} = bufio; const wcommon = require('bcoin/lib/wallet/common'); const HDPublicKey = require('bcoin/lib/hd/public'); const secp256k1 = require('bcrypto/lib/secp256k1'); const sigUtils = require('../utils/sig'); const common = require('../utils/common'); const ZERO_SIG = Buffer.alloc(65); const ZERO_KEY = Buffer.alloc(33); const NULL_TOKEN = Buffer.alloc(32); const EMPTY = Buffer.alloc(0); /** * Cosigner for proposal * @alias module:primitives.Cosigner * @extends {bufio.Struct} * @property {Number} id * @property {String} name * @property {String} path * @property {Number} tokenDepth * @property {Buffer} token * @property {Number} purpose * @property {Number} fingerPrint - uint32be * @property {Buffer} data - data up to 100 bytes * @property {Buffer} authPubKey - compressed public key * @property {Buffer} joinSignature * @property {bcoin.HDPublicKey?} key */ class Cosigner extends Struct { /** * Create Cosigner object * @constructor * @param {Object} [options] - Options * @param {Number} options.id - index of the cosigner * @param {String} options.name - name of cosigner * @param {Buffer} options.authPubKey - compressed public key * @param {Buffer} options.joinSignature * @param {String?} options.path - bip32 path if user wants to store * @param {Buffer?} options.data - data up to 100 bytes. * @param {Number} [options.tokenDepth = 0] - token change counter * @param {Number} [options.purpose=0] */ constructor(options) { super(); this.id = 0; this.name = ''; this.purpose = 0; this.fingerPrint = 0; this.data = EMPTY; this.tokenDepth = 0; this.token = NULL_TOKEN; this.key = new HDPublicKey(); this.authPubKey = ZERO_KEY; this.joinSignature = ZERO_SIG; if (options) this.fromOptions(options); } /** * Inject options to Cosigner * @override * @param {Object} options * @returns {Cosigner} */ fromOptions(options) { if (!options) return this; assert(wcommon.isName(options.name), 'Bad cosigner name.'); assert(HDPublicKey.isHDPublicKey(options.key), 'Account key is required.'); assert(Buffer.isBuffer(options.authPubKey), 'authPubKey must be a buffer.'); assert(options.authPubKey.length === 33, 'Bad authPubKey length.'); assert(secp256k1.publicKeyVerify(options.authPubKey), 'Bad authPubKey.'); assert(Buffer.isBuffer(options.joinSignature), 'joinSignature must be a buffer.'); assert(options.joinSignature.length === 65, 'Bad joinSignature length.'); this.name = options.name; this.key = options.key; this.authPubKey = options.authPubKey; this.joinSignature = options.joinSignature; if (options.id != null) { assert((options.id & 0xff) === options.id, 'ID must be uint8.'); this.id = options.id; } if (options.tokenDepth != null) { assert((options.tokenDepth >>> 0) === options.tokenDepth, 'tokenDepth must be a uint32.'); this.tokenDepth = options.tokenDepth; } if (options.token != null) { assert(Buffer.isBuffer(options.token), 'token must be a buffer.'); assert(options.token.length === 32, 'token must be 32 bytes long.'); this.token = options.token; } if (options.purpose != null) { assert((options.purpose >>> 0) === options.purpose, 'Purpose must be a uint32.'); this.purpose = options.purpose; } if (options.fingerPrint != null) { assert((options.fingerPrint >>> 0) === options.fingerPrint, 'fingerPrint must be a uint32.'); this.fingerPrint = options.fingerPrint; } if (options.data != null) { assert(Buffer.isBuffer(options.data), 'data must be a buffer.'); assert(options.data.length <= 100, 'data must be less than 100 bytes.'); this.data = options.data; } return this; } /** * Get http options from cosigner. * NOTE: This will return token. * @param {Network} network * @returns {Object} */ toHTTPOptions(network) { return { cosigner: { name: this.name, purpose: this.purpose, fingerPrint: this.fingerPrint, data: this.data.toString('hex'), accountKey: this.key.xpubkey(network), token: this.token.toString('hex'), authPubKey: this.authPubKey.toString('hex') }, joinSignature: this.joinSignature.toString('hex') }; } format() { return this.getJSON(true); } /** * Make serializable object from cosigner * @param {Boolean} showDetails * @param {Network} network * @returns {Object} */ toJSON() { return this.getJSON(false); } /** * Make serializable object from cosigner * @override * @param {Boolean} showDetails * @param {Network} network * @returns {Object} */ getJSON(showDetails, network) { const json = { id: this.id, name: this.name, data: this.data.toString('hex'), purpose: this.purpose, fingerPrint: this.fingerPrint, accountKey: this.key.xpubkey(network), authPubKey: this.authPubKey.toString('hex'), joinSignature: this.joinSignature.toString('hex') }; if (showDetails) { json.token = this.token.toString('hex'); json.tokenDepth = this.tokenDepth; } return json; } /** * Inject properties from JSON object * @param {Object} json * @param {Boolean} details * @param {Network} network */ fromJSON(json, details, network) { assert((json.id & 0xff) === json.id, 'id must be an u8.'); assert(wcommon.isName(json.name), 'Bad cosigner name.'); assert(typeof json.authPubKey === 'string', 'Bad authPubKey.'); assert(typeof json.joinSignature === 'string', 'Bad joinSignature.'); assert(typeof json.accountKey === 'string', 'Bad accountKey.'); const authPubKey = Buffer.from(json.authPubKey, 'hex'); const joinSignature = Buffer.from(json.joinSignature, 'hex'); const key = HDPublicKey.fromBase58(json.accountKey, network); assert(authPubKey.length === 33, 'Bad authPubKey length.'); assert(secp256k1.publicKeyVerify(authPubKey), 'Bad authPubKey.'); assert(joinSignature.length === 65, 'Bad joinSignature length.'); this.id = json.id; this.name = json.name; this.authPubKey = authPubKey; this.joinSignature = joinSignature; this.key = key; if (!details) return this; assert((json.tokenDepth >>> 0) === json.tokenDepth, 'tokenDepth must be an u32.'); this.tokenDepth = json.tokenDepth; assert((json.purpose >>> 0) === json.purpose); assert(json.purpose >= 0); this.purpose = json.purpose; assert((json.fingerPrint >>> 0) === json.fingerPrint); this.fingerPrint = json.fingerPrint; assert(typeof json.data === 'string', 'data must be a hex string.'); const data = Buffer.from(json.data, 'hex'); assert(data.length <= 100, 'Bad data length.'); this.data = data; const token = Buffer.from(json.token, 'hex'); assert(token.length === 32, 'token must be 32 bytes long.'); this.token = token; return this; } /** * Calculate serialization size * @returns {Number} */ getSize() { let size = 0; size += 1; // id size += 4; // tokenDepth size += 32; // token size += 1; // name length size += this.name.length; // name size += 4; // purpose size += 4; // fingerPrint size += 1; // data length size += this.data.length; // data size += 82; // key size size += 33; // authPubKey size += 65; // joinSignature return size; } /** * Serialize to reader * @override * @param {bufio.BufferWriter} bw * @param {bcoin.Network} network * @returns {bufio.BufferWriter} */ write(bw, network) { bw.writeU8(this.id); bw.writeU32(this.tokenDepth); bw.writeBytes(this.token); bw.writeU8(this.name.length); bw.writeString(this.name, 'latin1'); bw.writeU32BE(this.purpose); bw.writeU32BE(this.fingerPrint); bw.writeU8(this.data.length); bw.writeBytes(this.data); // this.key.toWriter(bw, network) -- will cause hash256 digest // to be calculated for whole cosigner buffer. bw.writeBytes(this.key.toRaw(network)); bw.writeBytes(this.authPubKey); bw.writeBytes(this.joinSignature); return bw; } /** * Deserialize from reader * @override * @param {bufio.BufferReader} br * @param {bcoin.Network} network * @returns {Cosigner} */ read(br, network) { this.id = br.readU8(); this.tokenDepth = br.readU32(); this.token = br.readBytes(32); const nameSize = br.readU8(); this.name = br.readBytes(nameSize).toString('utf8'); this.purpose = br.readU32BE(); this.fingerPrint = br.readU32BE(); const dataLength = br.readU8(); this.data = br.readBytes(dataLength); const key = br.readBytes(this.key.getSize()); this.key = HDPublicKey.fromRaw(key, network); this.authPubKey = br.readBytes(33); this.joinSignature = br.readBytes(65); return this; } /** * Check cosigner equality * @param {Cosigner} cosigner * @param {Boolean} details * @returns {Boolean} */ equals(cosigner, details = false) { if (!details) return this.id === cosigner.id && this.name === cosigner.name && this.authPubKey.equals(cosigner.authPubKey) && this.joinSignature.equals(cosigner.joinSignature) && this.key.equals(cosigner.key); return this.id === cosigner.id && this.name === cosigner.name && this.purpose === cosigner.purpose && this.fingerPrint === cosigner.fingerPrint && this.data.equals(cosigner.data) && this.tokenDepth === cosigner.tokenDepth && this.token.equals(cosigner.token) && this.authPubKey.equals(cosigner.authPubKey) && this.joinSignature.equals(cosigner.joinSignature) && this.key.equals(cosigner.key); } /** * Inject properties from cosigner * @param {Cosigner} cosigner * @returns {Cosigner} */ inject(cosigner) { this.id = cosigner.id; this.name = cosigner.name; this.purpose = cosigner.purpose; this.fingerPrint = cosigner.fingerPrint; this.tokenDepth = cosigner.tokenDepth; this.token = cosigner.token.slice(); this.data = cosigner.data.slice(); this.authPubKey = cosigner.authPubKey.slice(); this.joinSignature = cosigner.joinSignature.slice(); this.key = common.cloneHDPublicKey(cosigner.key); return this; } /** * Verify account key proof * @param {Buffer} signature * @param {String} id - walletName * @param {Network} [network=main] * @returns {Boolean} */ verifyProof(signature, id, network) { assert(Buffer.isBuffer(signature), 'Signature must be a buffer.'); assert(signature.length === 65, 'Signature must be 65 bytes long.'); const proofKey = this.key.derive(sigUtils.PROOF_INDEX).derive(0); const hash = this.getJoinHash(id, network); return sigUtils.verifyHash(hash, signature, proofKey.publicKey); } /** * Verify join signature * @param {Buffer} joinPubKey * @param {String} id - wallet id * @returns {Boolean} */ verifyJoinSignature(joinPubKey, id, network) { assert(Buffer.isBuffer(joinPubKey), 'joinPubKey must be a buffer.'); assert(joinPubKey.length === 33, 'joinPubKey must be 33 bytes long.'); const hash = this.getJoinHash(id, network); return sigUtils.verifyHash(hash, this.joinSignature, joinPubKey); } /** * Get join message hash * @param {String} id - wallet id * @returns {Buffer} */ getJoinHash(id, network) { assert(typeof id === 'string', 'walletName must be a string.'); return sigUtils.getJoinHash(id, this, network); } /** * Derive pubkey for cosigner * @param {Number} branch * @param {Number} index * @returns {Buffer} - Public key */ deriveKey(branch, index) { assert(this.key); return this.key.derive(branch).derive(index); } /** * Test whether an object is a Cosigner. * @param {Object} obj * @returns {Boolean} */ static isCosigner(obj) { return obj instanceof Cosigner; } } module.exports = Cosigner;