diff --git a/lib/Xud.ts b/lib/Xud.ts index cb601e62e..aeec157f9 100644 --- a/lib/Xud.ts +++ b/lib/Xud.ts @@ -130,6 +130,12 @@ class Xud extends EventEmitter { nodeKey = await NodeKey.generate(); await nodeKey.toFile(nodeKeyPath); } + + // we need to initialize connext every time xud starts, even in noencrypt mode + // the call below is in lieu of the UnlockNode/CreateNode call flow + await this.swapClientManager.initConnext( + nodeKey.childSeed(SwapClientType.Connext), + ); } else if (this.rpcServer) { this.rpcServer.grpcService.locked = true; const initService = new InitService(this.swapClientManager, nodeKeyPath, nodeKeyExists, this.config.dbpath); @@ -194,12 +200,6 @@ class Xud extends EventEmitter { // wait for components to initialize in parallel await Promise.all(initPromises); - // We initialize Connext separately because it - // requires a NodeKey. - await this.swapClientManager.initConnext( - nodeKey.childSeed(SwapClientType.Connext), - ); - // initialize pool and start listening/connecting only once other components are initialized await this.pool.init(); diff --git a/lib/connextclient/ConnextClient.ts b/lib/connextclient/ConnextClient.ts index eb6d054d5..624a83689 100644 --- a/lib/connextclient/ConnextClient.ts +++ b/lib/connextclient/ConnextClient.ts @@ -96,6 +96,7 @@ class ConnextClient extends SwapClient { public address?: string; /** A map of currency symbols to token addresses. */ public tokenAddresses = new Map(); + public userIdentifier?: string; /** * A map of expected invoices by hash. * This is equivalent to invoices of lnd with the difference @@ -371,9 +372,9 @@ class ConnextClient extends SwapClient { this.subscribeIncomingTransfer(), this.subscribeDeposit(), ]); - const { userIdentifier } = config; + this.userIdentifier = config.userIdentifier; this.emit('connectionVerified', { - newIdentifier: userIdentifier, + newIdentifier: this.userIdentifier, }); this.setStatus(ClientStatus.ConnectionVerified); } catch (err) { diff --git a/lib/service/InitService.ts b/lib/service/InitService.ts index ecd585867..4f2834f6f 100644 --- a/lib/service/InitService.ts +++ b/lib/service/InitService.ts @@ -44,6 +44,7 @@ class InitService extends EventEmitter { // use this seed to init any lnd wallets that are uninitialized const initWalletResult = await this.swapClientManager.initWallets({ seedMnemonic, + nodeKey, walletPassword: password, }); @@ -73,6 +74,7 @@ class InitService extends EventEmitter { this.emit('nodekey', nodeKey); return this.swapClientManager.unlockWallets({ + nodeKey, walletPassword: password, connextSeed: nodeKey.childSeed(SwapClientType.Connext), }); @@ -115,6 +117,7 @@ class InitService extends EventEmitter { // use the seed and database backups to restore any swap clients' wallets // that are uninitialized const initWalletResult = await this.swapClientManager.initWallets({ + nodeKey, lndBackups: lndBackupsMap, walletPassword: password, seedMnemonic: seedMnemonicList, diff --git a/lib/service/Service.ts b/lib/service/Service.ts index bdb0e9887..f0b8f3f61 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -1,3 +1,4 @@ +import { EventEmitter } from 'events'; import { fromEvent, merge, Observable } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ProvidePreimageEvent, TransferReceivedEvent } from '../connextclient/types'; @@ -18,7 +19,6 @@ import { checkDecimalPlaces, sortOrders, toEip55Address } from '../utils/utils'; import commitHash from '../Version'; import errors from './errors'; import { NodeIdentifier, ServiceComponents, ServiceOrder, ServiceOrderSidesArrays, ServicePlaceOrderEvent, ServiceTrade, XudInfo } from './types'; -import { EventEmitter } from 'events'; /** Functions to check argument validity and throw [[INVALID_ARGUMENT]] when invalid. */ const argChecks = { diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index 139642a98..5edb62afc 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -8,8 +8,10 @@ import lndErrors from '../lndclient/errors'; import LndClient from '../lndclient/LndClient'; import { LndInfo } from '../lndclient/types'; import { Level, Loggers } from '../Logger'; +import NodeKey from '../nodekey/NodeKey'; import { Currency, OwnLimitOrder } from '../orderbook/types'; import Peer from '../p2p/Peer'; +import { encipher } from '../utils/seedutil'; import { UnitConverter } from '../utils/UnitConverter'; import errors from './errors'; import SwapClient, { ClientStatus } from './SwapClient'; @@ -119,12 +121,21 @@ class SwapClientManager extends EventEmitter { } } - // Initializes Connext client with a seed + /** + * Initializes Connext client with a seed + * @returns true if Connext + */ public initConnext = async (seed: string) => { - if (!this.config.connext.disable && this.connextClient) { - this.connextClient.setSeed(seed); - await this.connextClient.init(); + try { + if (!this.config.connext.disable && this.connextClient) { + this.connextClient.setSeed(seed); + await this.connextClient.init(); + return true; + } + } catch (err) { + this.loggers.connext.error('could not initialize connext', err); } + return false; } /** @@ -272,11 +283,12 @@ class SwapClientManager extends EventEmitter { /** * Initializes wallets with seed and password. */ - public initWallets = async ({ walletPassword, seedMnemonic, restore, lndBackups }: { + public initWallets = async ({ walletPassword, seedMnemonic, restore, lndBackups, nodeKey }: { walletPassword: string, seedMnemonic: string[], restore?: boolean, lndBackups?: Map, + nodeKey: NodeKey, }) => { this.walletPassword = walletPassword; @@ -285,24 +297,26 @@ class SwapClientManager extends EventEmitter { const initializedLndWallets: string[] = []; let initializedConnext = false; - for (const client of this.swapClients.values()) { - if (isLndClient(client)) { - if (client.isWaitingUnlock()) { - const initWalletPromise = client.initWallet( + for (const swapClient of this.swapClients.values()) { + if (isLndClient(swapClient)) { + if (swapClient.isWaitingUnlock()) { + const initWalletPromise = swapClient.initWallet( walletPassword, seedMnemonic, restore, - lndBackups ? lndBackups.get(client.currency) : undefined, + lndBackups ? lndBackups.get(swapClient.currency) : undefined, ).then(() => { - initializedLndWallets.push(client.currency); + initializedLndWallets.push(swapClient.currency); }).catch((err) => { - client.logger.error(`could not initialize lnd wallet: ${err.message}`); - throw errors.SWAP_CLIENT_WALLET_NOT_CREATED(`could not initialize lnd-${client.currency}: ${err.message}`); + swapClient.logger.error('could not initialize lnd wallet', err.message); + throw errors.SWAP_CLIENT_WALLET_NOT_CREATED(`could not initialize lnd-${swapClient.currency}: ${err.message}`); }); initWalletPromises.push(initWalletPromise); } - } else if (isConnextClient(client)) { - initializedConnext = true; + } else if (isConnextClient(swapClient)) { + initializedConnext = await this.initConnext( + nodeKey.childSeed(SwapClientType.Connext), + ); } } @@ -318,8 +332,9 @@ class SwapClientManager extends EventEmitter { * Unlocks wallets with a password. * @returns an array of currencies for each lnd client that was unlocked */ - public unlockWallets = async ({ walletPassword }: { + public unlockWallets = async ({ walletPassword, nodeKey }: { walletPassword: string, + nodeKey: NodeKey, connextSeed: string, }) => { this.walletPassword = walletPassword; @@ -334,22 +349,42 @@ class SwapClientManager extends EventEmitter { if (swapClient.isWaitingUnlock()) { const unlockWalletPromise = swapClient.unlockWallet(walletPassword).then(() => { unlockedLndClients.push(swapClient.currency); - }).catch((err) => { - lockedLndClients.push(swapClient.currency); - swapClient.logger.debug(`could not unlock wallet: ${err.message}`); + }).catch(async (err) => { + let walletCreated = false; + if (err.details === 'wallet not found') { + // this wallet hasn't been initialized, so we will try to initialize it now + const decipheredSeed = nodeKey.privKey.slice(0, 19); + const decipheredSeedHex = decipheredSeed.toString('hex'); + const seedMnemonic = await encipher(decipheredSeedHex); + + try { + await swapClient.initWallet(this.walletPassword ?? '', seedMnemonic); + walletCreated = true; + } catch (err) { + swapClient.logger.error('could not initialize lnd wallet', err); + } + } + + if (!walletCreated) { + lockedLndClients.push(swapClient.currency); + swapClient.logger.debug(`could not unlock wallet: ${err.message}`); + } }); unlockWalletPromises.push(unlockWalletPromise); } else if (swapClient.isDisconnected() || swapClient.isMisconfigured() || swapClient.isNotInitialized()) { // if the swap client is not connected, we treat it as locked since lnd will likely be locked when it comes online lockedLndClients.push(swapClient.currency); } + } else if (isConnextClient(swapClient)) { + // TODO(connext): unlock Connext using connextSeed + await this.initConnext( + nodeKey.childSeed(SwapClientType.Connext), + ); } } await Promise.all(unlockWalletPromises); - // TODO(connext): unlock Connext using connextSeed - return { unlockedLndClients, lockedLndClients }; } diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index 513767c45..eecb0c09b 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -143,7 +143,7 @@ class Swaps extends EventEmitter { } public init = async () => { - // update pool with lnd pubkeys + // update pool with current lnd & connext pubkeys this.swapClientManager.getLndClientsMap().forEach(({ pubKey, chain, currency, uris }) => { if (pubKey && chain) { this.pool.updateLndState({ @@ -154,6 +154,9 @@ class Swaps extends EventEmitter { }); } }); + if (this.swapClientManager.connextClient) { + this.pool.updateConnextState(this.swapClientManager.connextClient.tokenAddresses, this.swapClientManager.connextClient.userIdentifier); + } this.swapRecovery.beginTimer(); const swapDealInstances = await this.repository.getSwapDeals(); diff --git a/lib/swaps/errors.ts b/lib/swaps/errors.ts index c5196d278..8e5e747fe 100644 --- a/lib/swaps/errors.ts +++ b/lib/swaps/errors.ts @@ -23,8 +23,8 @@ const errors = { message: `swapClient for currency ${currency} not found`, code: errorCodes.SWAP_CLIENT_NOT_FOUND, }), - SWAP_CLIENT_NOT_CONFIGURED: (currency: string) => ({ - message: `swapClient for currency ${currency} is not configured`, + SWAP_CLIENT_NOT_CONFIGURED: (currencyOrClientType: string) => ({ + message: `swap client for ${currencyOrClientType} is not configured`, code: errorCodes.SWAP_CLIENT_NOT_CONFIGURED, }), PAYMENT_HASH_NOT_FOUND: (rHash: string) => ({ diff --git a/lib/utils/seedutil.ts b/lib/utils/seedutil.ts index 56f3f49cb..691e7bd1e 100644 --- a/lib/utils/seedutil.ts +++ b/lib/utils/seedutil.ts @@ -28,18 +28,19 @@ async function keystore(mnemonic: string[], password: string, pathVal: string) { } /** - * Executes the seedutil tool to encipher a seed mnemonic into bytes. - * @param mnemonic the 24 seed recovery mnemonic + * Executes the seedutil tool to encipher a deciphered seed hex string into a mnemonic + * @param decipheredSeedHex the deciphered seed in hex format */ -async function encipher(mnemonic: string[]) { - const { stdout, stderr } = await exec(`${seedutilPath} encipher ${mnemonic.join(' ')}`); +async function encipher(decipheredSeedHex: string) { + const { stdout, stderr } = await exec(`${seedutilPath} encipher ${decipheredSeedHex}`); if (stderr) { throw new Error(stderr); } - const encipheredSeed = stdout.trim(); - return Buffer.from(encipheredSeed, 'hex'); + const mnemonic = stdout.trim().split(' '); + assert.equal(mnemonic.length, 24, 'seedutil did not encipher mnemonic of exactly 24 words'); + return mnemonic; } async function decipher(mnemonic: string[]) { diff --git a/seedutil/SeedUtil.spec.ts b/seedutil/SeedUtil.spec.ts index adec48150..fbe6ac5ac 100644 --- a/seedutil/SeedUtil.spec.ts +++ b/seedutil/SeedUtil.spec.ts @@ -47,6 +47,8 @@ const ERRORS = { INVALID_AEZEED: 'invalid aezeed', KEYSTORE_FILE_ALREADY_EXISTS: 'account already exists', INVALID_PASSPHRASE: 'invalid passphrase', + INVALID_HEX_LENGTH: 'invalid hex length', + MISSING_HEX_STRING: 'missing hex string', }; const PASSWORD = 'wasspord'; @@ -75,31 +77,42 @@ const VALID_SEED_NO_PASS = { const DEFAULT_KEYSTORE_PATH = `${process.cwd()}/seedutil/keystore`; describe('SeedUtil encipher', () => { + const decipheredSeedHex = '000f4b90d9f9720bfac78aaea09a5193b34811'; test('it errors with no arguments', async () => { await expect(executeCommand('./seedutil/seedutil encipher')) - .rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH); + .rejects.toThrow(ERRORS.MISSING_HEX_STRING); }); - test('it errors with 23 words', async () => { - const cmd = `./seedutil/seedutil encipher ${VALID_SEED.seedWords.slice(0, 23).join(' ')}`; + test('it errors with insufficient hex length', async () => { + const cmd = './seedutil/seedutil encipher 000f4b90d9f9720bfac78aaea09a5193'; await expect(executeCommand(cmd)) - .rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH); + .rejects.toThrow(ERRORS.INVALID_HEX_LENGTH); }); - test('it errors with 24 words and invalid aezeed password', async () => { - const cmd = `./seedutil/seedutil encipher ${VALID_SEED.seedWords.join(' ')}`; + test('it errors with excess hex length', async () => { + const cmd = './seedutil/seedutil encipher 000f4b90d9f9720bfac78aaea09a5193b34811aabbcc'; await expect(executeCommand(cmd)) - .rejects.toThrow(ERRORS.INVALID_AEZEED); + .rejects.toThrow(ERRORS.INVALID_HEX_LENGTH); }); - test('it succeeds with 24 words, valid aezeed password', async () => { - const cmd = `./seedutil/seedutil encipher -aezeedpass=${VALID_SEED.seedPassword} ${VALID_SEED.seedWords.join(' ')}`; - await expect(executeCommand(cmd)).resolves.toMatchSnapshot(); + test('it enciphers with valid aezeed password and deciphers back to same seed', async () => { + const cmd = `./seedutil/seedutil encipher -aezeedpass=${VALID_SEED.seedPassword} ${decipheredSeedHex}`; + + const mnemonic = await executeCommand(cmd); + + const decipherCmd = `./seedutil/seedutil decipher -aezeedpass=${VALID_SEED.seedPassword} ${mnemonic}`; + const decipherOutput = await executeCommand(decipherCmd); + expect(decipherOutput.trim()).toEqual(decipheredSeedHex); }); - test('it succeeds with 24 words, no aezeed password', async () => { - const cmd = `./seedutil/seedutil encipher ${VALID_SEED_NO_PASS.seedWords.join(' ')}`; - await expect(executeCommand(cmd)).resolves.toMatchSnapshot(); + test('it enciphers with no aezeed password and deciphers back to same seed', async () => { + const cmd = `./seedutil/seedutil encipher ${decipheredSeedHex}`; + + const mnemonic = await executeCommand(cmd); + + const decipherCmd = `./seedutil/seedutil decipher ${mnemonic}`; + const decipherOutput = await executeCommand(decipherCmd); + expect(decipherOutput.trim()).toEqual(decipheredSeedHex); }); }); diff --git a/seedutil/__snapshots__/SeedUtil.spec.ts.snap b/seedutil/__snapshots__/SeedUtil.spec.ts.snap index 5c1805510..47fd4155b 100644 --- a/seedutil/__snapshots__/SeedUtil.spec.ts.snap +++ b/seedutil/__snapshots__/SeedUtil.spec.ts.snap @@ -19,13 +19,3 @@ exports[`SeedUtil derivechild it succeeds with 24 words, valid aezeed password 1 "000ecdef333ecf9054ccb4fb843a3dbbf4ac6a " `; - -exports[`SeedUtil encipher it succeeds with 24 words, no aezeed password 1`] = ` -"00738860374692022c462027a35aaaef3c3289aa0a057e2600000000002cad2e2b -" -`; - -exports[`SeedUtil encipher it succeeds with 24 words, valid aezeed password 1`] = ` -"00a71642b0ace8f523a977950005c71220ea460b423a0a9f000000000079ef937c -" -`; diff --git a/seedutil/main.go b/seedutil/main.go index b247d6493..a9000515e 100644 --- a/seedutil/main.go +++ b/seedutil/main.go @@ -3,6 +3,7 @@ package main import ( "crypto/hmac" "crypto/sha512" + "encoding/binary" "encoding/hex" "flag" "fmt" @@ -115,11 +116,42 @@ func main() { encipherCommand.Parse(os.Args[2:]) args = encipherCommand.Args() - mnemonic := parseMnemonic(args) - cipherSeed := mnemonicToCipherSeed(mnemonic, aezeedPassphrase) + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "missing hex string") + os.Exit(1) + } + decipheredSeedHex := args[0] + decipheredSeed, err := hex.DecodeString(decipheredSeedHex) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if len(decipheredSeed) != 19 { + fmt.Fprintf(os.Stderr, "\nerror: invalid hex length of %v bytes\n", len(decipheredSeed)) + os.Exit(1) + } + + internalVersion := decipheredSeed[0] + birthday := binary.BigEndian.Uint16(decipheredSeed[1:3]) + var entropy [16]byte + copy(entropy[:], decipheredSeed[3:19]) + + genesisTime := time.Date(2009, time.January, 3, 18, 15, 5, 0, time.UTC) - encipheredSeed, _ := cipherSeed.Encipher([]byte(*aezeedPassphrase)) - fmt.Println(hex.EncodeToString(encipheredSeed[:])) + cipherSeed, err := aezeed.New(internalVersion, &entropy, genesisTime.AddDate(0, 0, int(birthday))) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + mnemonic, err := cipherSeed.ToMnemonic([]byte(*aezeedPassphrase)) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + fmt.Println(strings.Join([]string(mnemonic[:]), " ")) case "decipher": aezeedPassphrase := decipherCommand.String("aezeedpass", defaultAezeedPassphrase, "aezeed passphrase") decipherCommand.Parse(os.Args[2:])