Browse Source

Merge branch 'feat_postmix_change_address_types' into 'develop'

Feat postmix change address types

See merge request dojo/samourai-dojo!242
Pavel Ševčík 3 years ago
parent
commit
4d746d029e
  1. 10
      accounts/wallet-rest-api.js
  2. 2
      docker/my-dojo/.env
  3. 73
      lib/bitcoin/hd-accounts-helper.js
  4. 41
      lib/bitcoin/hd-accounts-service.js
  5. 5
      lib/bitcoin/parallel-address-derivation.js
  6. 9
      lib/http-server/http-server.js
  7. 13
      lib/util.js
  8. 38
      test/lib/bitcoin/hd-accounts-helper-test.js
  9. 3
      tracker/transaction.js

10
accounts/wallet-rest-api.js

@ -11,10 +11,10 @@ const walletService = require('../lib/wallet/wallet-service')
const authMgr = require('../lib/auth/authorizations-manager')
const HttpServer = require('../lib/http-server/http-server')
const apiHelper = require('./api-helper')
const hdaService = require('../lib/bitcoin/hd-accounts-service')
const debugApi = process.argv.indexOf('api-debug') > -1
/**
* Wallet API endpoints
*/
@ -60,6 +60,10 @@ class WalletRestApi {
// Parse params
const entities = apiHelper.parseEntitiesParams(req.query)
if (req.query.importPostmixLikeTypeChange) {
await hdaService.importPostmixLikeTypeChange(entities.active.xpubs)
}
const result = await walletService.getFullWalletInfo(
entities.active,
entities.legacy,
@ -102,6 +106,10 @@ class WalletRestApi {
// Parse params
const entities = apiHelper.parseEntitiesParams(req.body)
if (req.body.importPostmixLikeTypeChange) {
await hdaService.importPostmixLikeTypeChange(entities.active.xpubs)
}
const result = await walletService.getFullWalletInfo(
entities.active,
entities.legacy,

2
docker/my-dojo/.env

@ -13,7 +13,7 @@ COMPOSE_CONVERT_WINDOWS_PATHS=1
DOJO_VERSION_TAG=1.10.1
DOJO_DB_VERSION_TAG=1.3.0
DOJO_BITCOIND_VERSION_TAG=1.12.0
DOJO_NODEJS_VERSION_TAG=1.10.0
DOJO_NODEJS_VERSION_TAG=1.10.1
DOJO_NGINX_VERSION_TAG=1.6.0
DOJO_TOR_VERSION_TAG=1.10.0
DOJO_EXPLORER_VERSION_TAG=1.7.0

73
lib/bitcoin/hd-accounts-helper.js

@ -16,6 +16,7 @@ const activeNet = network.network
const keys = require('../../keys/')[network.key]
const addrHelper = require('./addresses-helper')
const MAX_SAFE_INT_32 = Math.pow(2, 31) - 1;
/**
* A singleton providing HD Accounts helper functions
@ -32,6 +33,12 @@ class HDAccountsHelper {
this.BIP84 = 2
this.LOCKED = 1<<7
// known HD accounts
this.RICOCHET_ACCT = MAX_SAFE_INT_32;
this.POSTMIX_ACCT = MAX_SAFE_INT_32 - 1;
this.PREMIX_ACCT = MAX_SAFE_INT_32 - 2;
this.BADBANK_ACCT = MAX_SAFE_INT_32 - 3;
// Magic numbers
this.MAGIC_XPUB = 0x0488b21e
this.MAGIC_TPUB = 0x043587cf
@ -276,7 +283,7 @@ class HDAccountsHelper {
/**
* Get the hd node associated to an hd account
* @param {string} xpub - hd account
* @returns {bip32}
* @returns {[bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface]}
*/
getNode(xpub) {
if (this.isValid(xpub))
@ -289,10 +296,10 @@ class HDAccountsHelper {
* Derives an address for an hd account
* @param {number} chain - chain to be derived
* must have a value on [0,1] for BIP44/BIP49/BIP84 derivation
* @param {bip32} chainNode - Parent bip32 used for derivation
* @param {bitcoin.bip32.BIP32Interface} chainNode - Parent bip32 used for derivation
* @param {number} index - index to be derived
* @param {number} type - type of derivation
* @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>}
* @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>, publicKey: <Buffer>, address: string }
*/
async deriveAddress(chain, chainNode, index, type) {
// Derive M/chain/index
@ -300,7 +307,8 @@ class HDAccountsHelper {
const addr = {
chain: chain,
index: index
index: index,
publicKey: indexNode.publicKey
}
switch (type) {
@ -325,7 +333,7 @@ class HDAccountsHelper {
* must have a value on [0,1] for BIP44/BIP49/BIP84 derivation
* @param {number[]} indices - array of indices to be derived
* @param {number} type - type of derivation
* @returns {Promise<object[]>} array of {address: '...', chain: <int>, index: <int>}
* @returns {Promise<{ chain: number, index: number, publicKey: Buffer, address: string }[]>} array of address objects
*/
async deriveAddresses(xpub, chain, indices, type) {
const ret = []
@ -355,10 +363,18 @@ class HDAccountsHelper {
) {
// Few addresses to be derived or external derivation deactivated
// Let's do it here
let promises = indices.map(index => {
const addresses = await Promise.all(indices.map(index => {
return this.deriveAddress(chain, chainNode, index, info.type)
})
return Promise.all(promises)
}))
// Generate additional change address types for postmix account
if (this.isPostmixAcct(node) && chain === 1) {
const additionalPostmixAddresses = this._generateAdditionalChangeAddresses(addresses)
addresses.push(...additionalPostmixAddresses)
}
return addresses;
} else {
// Many addresses to be derived
@ -375,6 +391,13 @@ class HDAccountsHelper {
const msg = await this.derivationPool.exec('deriveAddresses', [data])
if (msg.status === 'ok') {
// Generate additional change address types for postmix account
if (this.isPostmixAcct(node) && chain === 1) {
const additionalPostmixAddresses = this._generateAdditionalChangeAddresses(msg.addresses)
msg.addresses.push(...additionalPostmixAddresses)
}
resolve(msg.addresses)
} else {
Logger.error(null, 'HdAccountsHelper : A problem was met during parallel addresses derivation')
@ -393,6 +416,40 @@ class HDAccountsHelper {
}
}
/**
* @description Derive additional change addresses (P2PKH & P2SH) for postmix account
* @param addresses {{ chain: number, index: number, publicKey: Buffer, address: string }[]} - list of derived addresses
* @returns {{ chain: number, index: number, publicKey: Buffer, address: string }[]} - array of additional address types
* @private
*/
_generateAdditionalChangeAddresses(addresses) {
const additionalPostmixAddresses = addresses.map((address) => {
const newP2PKHAddress = {...address}
const newP2SHAddress = {...address}
newP2PKHAddress.address = addrHelper.p2pkhAddress(address.publicKey)
newP2SHAddress.address = addrHelper.p2wpkhP2shAddress(address.publicKey)
return [newP2PKHAddress, newP2SHAddress];
})
return additionalPostmixAddresses.flat()
}
/**
* @description Detect postmix account
* @param {[bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface, bitcoin.bip32.BIP32Interface]} node - array of BIP32 node interfaces
* @returns {boolean}
*/
isPostmixAcct(node) {
const index = node[2].index
const threshold = Math.pow(2,31)
const hardened = (index >= threshold)
const account = hardened ? (index - threshold) : index
return account === this.POSTMIX_ACCT
}
}
module.exports = new HDAccountsHelper()

41
lib/bitcoin/hd-accounts-service.js

@ -4,7 +4,7 @@
*/
'use strict'
const _ = require('lodash')
const util = require('../util')
const errors = require('../errors')
const Logger = require('../logger')
const db = require('../db/mysql-db-wrapper')
@ -141,10 +141,10 @@ class HDAccountsService {
Logger.info(`HdAccountsService : Created HD Account: ${xpub}${segwit}`)
const externalPrm = hdaHelper.deriveAddresses(xpub, 0, _.range(gap.external), scheme)
const internalPrm = hdaHelper.deriveAddresses(xpub, 1, _.range(gap.internal), scheme)
const externalPrm = hdaHelper.deriveAddresses(xpub, 0, util.range(0, gap.external), scheme)
const internalPrm = hdaHelper.deriveAddresses(xpub, 1, util.range(0, gap.internal), scheme)
const addresses = _.flatten(await Promise.all([externalPrm, internalPrm]))
const addresses = (await Promise.all([externalPrm, internalPrm])).flat()
return db.addAddressesToHDAccount(xpub, addresses)
}
@ -152,8 +152,8 @@ class HDAccountsService {
/**
* Rescan the blockchain for a hd account
* @param {string} xpub - xpub
* @param {number} gapLimit - (optional) gap limit for derivation
* @param {number} startIndex - (optional) rescan shall start from this index
* @param {number=} gapLimit - (optional) gap limit for derivation
* @param {number=} startIndex - (optional) rescan shall start from this index
* @returns {Promise}
*/
async rescan(xpub, gapLimit, startIndex) {
@ -252,6 +252,35 @@ class HDAccountsService {
}
}
/**
* @description
* @param {string[]} xpubs - array of xpubs
* @returns {Promise<void>}
*/
async importPostmixLikeTypeChange(xpubs) {
const postmixAcct = xpubs.find((xpub) => {
const node = hdaHelper.getNode(xpub)
return hdaHelper.isPostmixAcct(node)
})
if (!postmixAcct) return Promise.resolve()
const postmixNode = hdaHelper.getNode(postmixAcct)
const [externalUnused, internalUnused] = await db.getHDAccountNextUnusedIndices(postmixAcct)
const deriveRange = util.range(Math.max(0, internalUnused - 50), internalUnused + gap.internal)
const likeTypeChangeAddresses = await Promise.all(deriveRange.map((index) => {
return [
hdaHelper.deriveAddress(1, postmixNode[1], index, hdaHelper.BIP44),
hdaHelper.deriveAddress(1, postmixNode[1], index, hdaHelper.BIP49)
]
}).flat())
await db.addAddressesToHDAccount(postmixAcct, likeTypeChangeAddresses)
}
}
module.exports = new HDAccountsService()

5
lib/bitcoin/parallel-address-derivation.js

@ -25,7 +25,7 @@ const BIP84 = 2
* @param {bip32} chainNode - Parent bip32 used for derivation
* @param {number} index - index to be derived
* @param {number} type - type of derivation
* @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>}
* @returns {Promise<object>} returns an object {address: '...', chain: <int>, index: <int>, publicKey: <Buffer>}
*/
async function deriveAddress(chain, chainNode, index, type) {
// Derive M/chain/index
@ -33,7 +33,8 @@ async function deriveAddress(chain, chainNode, index, type) {
const addr = {
chain: chain,
index: index
index: index,
publicKey: indexNode.publicKey
}
switch (type) {

9
lib/http-server/http-server.js

@ -9,7 +9,9 @@ const sirv = require('sirv')
const helmet = require('helmet')
const nocache = require('nocache')
const Logger = require('../logger')
const errors = require('../errors');
const errors = require('../errors')
const network = require("../../lib/bitcoin/network")
const keys = require('../../keys')[network.key]
/**
@ -57,6 +59,11 @@ class HttpServer {
this.app.use('/static', sirv('../static'));
this.app.use((req, res, next) => {
res.append('X-Dojo-Version', keys.dojoVersion)
next()
})
this.app.use(HttpServer.setJSONResponse)
this.app.use(HttpServer.setConnection)
}

13
lib/util.js

@ -351,6 +351,19 @@ class Util {
}
}
/**
* @description Generate array of sequential numbers from start to stop
* @param {number} start
* @param {number} stop
* @param {number=} step
* @returns {number[]}
*/
static range(start, stop, step = 1) {
return Array(Math.ceil((stop - start) / step))
.fill(start)
.map((x, y) => x + y * step)
}
}
/**

38
test/lib/bitcoin/hd-accounts-helper-test.js

@ -19,6 +19,8 @@ const XPUB = 'tpubDDDAe7GgFT4fzEzKwWVA4BWo8fiJXQeGEYDTexzo2w6CK1iDoLPYkpEisXo623
const YPUB = 'upub5ELkCsSF68UnAZE7zF9CDztvHeBJiAAhwa4VxEFzZ1CfQRbpy93mkBbUZsqYVpoeEHFwY3fGh9bfftH79ZwbhjUEUBAxQj551TMxVyny4UX'
const ZPUB = 'vpub5ZB1WY7AEp2G1rREpbvpS5zRTcKkenACrgaijd9sw1aYTXR4DoDLNFFcb5o8VjTZdvNkHXFq9oxDZAtfsGMcVy9qLWsNzdtZHBRbtXe87LB'
const POSTMIX_ZPUB = 'vpub5Y6cjg7GbwSLRu33XB76n3EoJZscmYSVEToLSMqD6ugAcm4rof8E9yvDiaFfhGEuyL95P9VD4A9W3JrBTZhzWSXiRyYvWFnUBAZc67X32wh'
const BIP44_VECTORS = [
[0, 0, 'mmZ5FRccGAkwfKme4JkrsmurnimDLdfmNL'],
[0, 1, 'n3yomLicyrSULiNWFKHsK8erntSpJZEPV6'],
@ -61,6 +63,27 @@ const BIP84_VECTORS = [
[1, 4, 'tb1qjrnw8u2pvspm6hq3aa83ff93wevq2zyxqczewy']
]
const POSTMIX_VECTORS = [
[1, 0, 'tb1qv3laps2vues6nh9fkxpds3wxd0cttd9jnr0772'],
[1, 1, 'tb1qz538rwwchv2unf97g4pugv3wjwxxjaypnwz8sk'],
[1, 2, 'tb1qdm3hfvw3knzujxx24g05e30kpe7vk0ez3dk0h8'],
[1, 3, 'tb1qxn4jgg5hgl3eggvt4alvraladpwq9pj30fy5ze'],
[1, 4, 'tb1qw2ghyxhqv5ysyehq9p9xwux4zqaf0mcwm29agh'],
[1, 0, 'mpgLz1YXDU9buy7Zn8w9w9mJtrGghiXotH'],
[1, 1, 'mhShkJxHHgzJd2WcqeaKL4spqBMe1wcaK5'],
[1, 2, 'mqdH74foDiN8hV2mmFSHnceCm7vgErd4A2'],
[1, 3, 'mkLm7vUy1rij3YicskkQJxGovnGDG6G2oj'],
[1, 4, 'mqxjZfjdSdUmecTVALzhoQBPFRNvLViMBr'],
[1, 0, '2N5UxwLfWexxHDm5MKHoyitRLWEK8x25tiA'],
[1, 1, '2N8wnnGoJujWGrM5YLs1nC1TFuszx2vJVA9'],
[1, 2, '2NA6Ja6PM6YMuQpSQdeWofKRV9pcBbz4aii'],
[1, 3, '2NFLd63BqGzh5BtfxobuU4dpoThg9sxMPth'],
[1, 4, '2NEeziC2dc3nbf9k3fyUWBzLWbn4MTrR2mm']
]
const HD_TYPES_VECTORS = [
// unlocked
[0, hdaHelper.BIP44, false],
@ -74,7 +97,7 @@ const HD_TYPES_VECTORS = [
describe('HdAccountsHelper', function() {
describe('isXpub()', function() {
it('should successfully detect a XPUB', function() {
assert(hdaHelper.isXpub(XPUB))
@ -117,7 +140,7 @@ describe('HdAccountsHelper', function() {
const ret = hdaHelper.classify(v[0])
assert(ret.type == v[1])
assert(ret.locked == v[2])
}
}
})
})
@ -127,7 +150,7 @@ describe('HdAccountsHelper', function() {
for (const v of HD_TYPES_VECTORS) {
const ret = hdaHelper.makeType(v[1], v[2])
assert(ret == v[0])
}
}
})
})
@ -153,6 +176,15 @@ describe('HdAccountsHelper', function() {
assert(addresses[0].address == v[2])
}
})
it('should successfully derive additional change address types for postmix account', async () => {
const addresses = await hdaHelper.deriveAddresses(POSTMIX_ZPUB, 1, [0, 1, 2, 3, 4], hdaHelper.BIP84)
POSTMIX_VECTORS.forEach((vector) => {
assert(addresses.find((addr) => addr.index === vector[1]))
assert(addresses.find((addr) => addr.address === vector[2]))
})
})
})

3
tracker/transaction.js

@ -365,7 +365,8 @@ class Transaction {
const indices = _.range(minIdx, maxIdx)
const derived = await hdaHelper.deriveAddresses(xpub, chain, indices, hdType)
Array.prototype.push.apply(newAddresses, derived)
newAddresses.push(...derived)
Logger.info(`Tracker : Derived hdID(${hdAccount.hdID}) M/${chain}/${indices.join(',')}`)

Loading…
Cancel
Save