Skip to content

Commit

Permalink
feat: token identifiers for currencies
Browse files Browse the repository at this point in the history
At a high level, this adds a concept of a token identifier for each
currency supported by xud that is consistent across the entire network.

"BTC" can mean mainnet tokens or testnet tokens (or simnet, etc...) and
can even be misconfigured to point to the LTC lightning network.
Meanwhile, Raiden ERC20 tokens refer to a particular token contract, and
there's no guarantee that peers won't be using different (and therefore
incompatible) contracts for the same currency such as WETH.

Here, we use an identifier to ensure peers are referring to the same
token as we are. For currencies that use lnd, the token identifier is
determined dynamically using the `chain` and `network` returned by lnd
on a `GetInfo` call. For example, BTC mainnet would be "bitcoin-mainnet"
and LTC testnet would be "litecoin-testnet". For currencies that use
Raiden, we use the token contract address as the identifier.

These token identifiers are shared with peers during the handshake and
on any subsequent node state updates. The p2p messaging format is
changed to accommodate this. This also drops static node values like
the `nodePubKey` and `version` from the node state p2p message, as these
values can not change mid-session for a peer.

A future improvement may be to detect and support discrepancies in the
currency symbol used by a peer for the same token. For example, a
peer that advertises "XBT" with a token identifier of "bitcoin-mainnet"
should be compatible with our "BTC" that also is identified as
"bitcoin-mainnet". This is not included in this commit as it would
require further refactoring and complexity.

Another follow-up may be creating a test suite for the `Peer` class or
refactoring it to move logic elsewhere (along with tests for that logic).
There is some logic around handling node state updates where information
regarding the peer's supported currencies may change that is currently
not tested.

Closes #910.

BREAKING CHANGE: Changed p2p messaging structure for `SessionInit`
and `NodeStateUpdate` packets.
  • Loading branch information
sangaman committed Jun 28, 2019
1 parent 477b591 commit 44a74cf
Show file tree
Hide file tree
Showing 23 changed files with 619 additions and 449 deletions.
18 changes: 18 additions & 0 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ class LndClient extends SwapClient {
private meta!: grpc.Metadata;
private uri!: string;
private credentials!: ChannelCredentials;
/** The identity pub key for this lnd instance. */
private identityPubKey?: string;
/** The identifier for the chain this lnd instance is using in the format [chain]-[network] like "bitcoin-testnet" */
private chainIdentifier?: string;
private channelSubscription?: ClientReadableStream<lndrpc.ChannelEventUpdate>;
private invoiceSubscriptions = new Map<string, ClientReadableStream<lndrpc.Invoice>>();

Expand Down Expand Up @@ -96,6 +99,10 @@ class LndClient extends SwapClient {
return this.identityPubKey;
}

public get chain() {
return this.chainIdentifier;
}

private unaryCall = <T, U>(methodName: string, params: T): Promise<U> => {
return new Promise((resolve, reject) => {
if (this.isDisabled()) {
Expand Down Expand Up @@ -204,8 +211,19 @@ class LndClient extends SwapClient {
let newPubKey: string | undefined;
if (this.identityPubKey !== getInfoResponse.getIdentityPubkey()) {
newPubKey = getInfoResponse.getIdentityPubkey();
this.logger.debug(`pubkey is ${newPubKey}`);
this.identityPubKey = newPubKey;
}
const chain = getInfoResponse.getChainsList()[0];
const chainIdentifier = `${chain.getChain()}-${chain.getNetwork()}`;
if (!this.chainIdentifier) {
this.chainIdentifier = chainIdentifier;
this.logger.debug(`chain is ${chainIdentifier}`);
} else if (this.chainIdentifier !== chainIdentifier) {
// we switched chains for this lnd client while xud was running which is not supported
this.logger.error(`chain switched from ${this.chainIdentifier} to ${chainIdentifier}`);
await this.setStatus(ClientStatus.Disabled);
}
this.emit('connectionVerified', newPubKey);

this.invoices = new InvoicesClient(this.uri, this.credentials);
Expand Down
70 changes: 46 additions & 24 deletions lib/orderbook/OrderBook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,26 +121,42 @@ class OrderBook extends EventEmitter {
return outgoingOrder ;
}

/**
* Checks that a currency advertised by a peer are known to us, have a swap client identifier,
* and that their token identifier matches ours.
*/
private isPeerCurrencySupported = (peer: Peer, currency: string) => {
const currencyAttributes = this.getCurrencyAttributes(currency);
if (!currencyAttributes) {
return false; // we don't know about this currency
}

if (!peer.getIdentifier(currencyAttributes.swapClient, currency)) {
return false; // peer did not provide a swap client identifier for this currency
}

// ensure that our token identifiers match
const ourTokenIdentifier = this.pool.getTokenIdentifier(currency);
const peerTokenIdentifier = peer.getTokenIdentifier(currency);
return ourTokenIdentifier === peerTokenIdentifier;
}

private bindPool = () => {
this.pool.on('packet.order', this.addPeerOrder);
this.pool.on('packet.orderInvalidation', this.handleOrderInvalidation);
this.pool.on('packet.getOrders', this.sendOrders);
this.pool.on('packet.swapRequest', this.handleSwapRequest);
this.pool.on('peer.close', this.removePeerOrders);
this.pool.on('peer.pairDropped', this.removePeerPair);
this.pool.on('peer.pairsAdvertised', this.verifyPeerPairs);
this.pool.on('peer.verifyPairs', this.verifyPeerPairs);
this.pool.on('peer.nodeStateUpdate', (peer) => {
// remove any trading pairs for which we no longer have both swap client identifiers
peer.forEachActivePair((activePairId) => {
const [baseCurrency, quoteCurrency] = activePairId.split('/');
const isCurrencySupported = (currency: string) => {
const currencyAttributes = this.getCurrencyAttributes(currency);
return currencyAttributes && peer.getIdentifier(currencyAttributes.swapClient, currency);
};

if (!isCurrencySupported(baseCurrency) || !isCurrencySupported(quoteCurrency)) {
// this peer's node state no longer supports at least one of the currencies for this trading pair
peer.deactivatePair(activePairId);
const advertisedCurrencies = peer.getAdvertisedCurrencies();

advertisedCurrencies.forEach((advertisedCurrency) => {
if (!this.isPeerCurrencySupported(peer, advertisedCurrency)) {
peer.disableCurrency(advertisedCurrency);
} else {
peer.enableCurrency(advertisedCurrency);
}
});
});
Expand Down Expand Up @@ -170,10 +186,7 @@ class OrderBook extends EventEmitter {

/** Loads the supported pairs and currencies from the database. */
public init = async () => {
const promises: PromiseLike<any>[] = [this.repository.getPairs(), this.repository.getCurrencies()];
const results = await Promise.all(promises);
const pairs = results[0] as PairInstance[];
const currencies = results[1] as CurrencyInstance[];
const [pairs, currencies] = await Promise.all([this.repository.getPairs(), this.repository.getCurrencies()]);

currencies.forEach(currency => this.currencyInstances.set(currency.id, currency));
pairs.forEach((pair) => {
Expand All @@ -184,8 +197,8 @@ class OrderBook extends EventEmitter {
this.pool.updatePairs(this.pairIds);
}

public getCurrencyAttributes(symbol: string) {
const currencyInstance = this.currencyInstances.get(symbol);
public getCurrencyAttributes(currency: string) {
const currencyInstance = this.currencyInstances.get(currency);
return currencyInstance ? currencyInstance.toJSON() : undefined;
}

Expand Down Expand Up @@ -693,22 +706,31 @@ class OrderBook extends EventEmitter {
}

/**
* Verifies a list of trading pair tickers for a peer. Checks that the peer has advertised
* Verifies the advertised trading pairs of a peer. Checks that the peer has advertised
* lnd pub keys for both the base and quote currencies for each pair, and also attempts a
* "sanity swap" for each currency which is a 1 satoshi for 1 satoshi swap of a given currency
* that demonstrates that we can both accept and receive payments for this peer.
* @param pairIds the list of trading pair ids to verify
*/
private verifyPeerPairs = async (peer: Peer, pairIds: string[]) => {
private verifyPeerPairs = async (peer: Peer) => {
/** An array of inactive trading pair ids that don't involve a disabled currency for this peer. */
const pairIdsToVerify = peer.advertisedPairs.filter((pairId) => {
if (peer.isPairActive(pairId)) {
return false; // don't verify a pair that is already active
}
const [baseCurrency, quoteCurrency] = pairId.split('/');
return !peer.disabledCurrencies.has(baseCurrency) && !peer.disabledCurrencies.has(quoteCurrency);
});

if (this.nosanityswaps) {
// we have disabled sanity swaps, so assume all pairs should be activated
pairIds.forEach(peer.activatePair);
// we have disabled sanity checks, so assume all pairs should be activated
pairIdsToVerify.forEach(peer.activatePair);
return;
}

// identify the unique currencies we need to verify for specified trading pairs
const currenciesToVerify = new Set<string>();
pairIds.forEach((pairId) => {
pairIdsToVerify.forEach((pairId) => {
const [baseCurrency, quoteCurrency] = pairId.split('/');
if (!peer.verifiedCurrencies.has(baseCurrency)) {
currenciesToVerify.add(baseCurrency);
Expand Down Expand Up @@ -743,7 +765,7 @@ class OrderBook extends EventEmitter {

// activate pairs that have had both currencies verified
const activationPromises: Promise<void>[] = [];
pairIds.forEach(async (pairId) => {
pairIdsToVerify.forEach(async (pairId) => {
const [baseCurrency, quoteCurrency] = pairId.split('/');
if (peer.verifiedCurrencies.has(baseCurrency) && peer.verifiedCurrencies.has(quoteCurrency)) {
activationPromises.push(peer.activatePair(pairId));
Expand Down
Loading

0 comments on commit 44a74cf

Please sign in to comment.