Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.

Commit 310d8b1

Browse files
authored
Merge pull request #1005 from lightninglabs/bos-scores
Bos scores
2 parents daf15f6 + fbf8318 commit 310d8b1

File tree

10 files changed

+213
-10
lines changed

10 files changed

+213
-10
lines changed

public/lnd-child-process.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ module.exports.startLndProcess = async function({
6565
'--autopilot.private',
6666
'--autopilot.minconfs=0',
6767
'--autopilot.allocation=0.95',
68+
'--autopilot.heuristic=externalscore:1',
69+
'--autopilot.heuristic=preferential:0',
6870
lndPort ? `--rpclisten=localhost:${lndPort}` : '',
6971
lndPeerPort ? `--listen=localhost:${lndPeerPort}` : '',
7072
lndRestPort ? `--restlisten=localhost:${lndRestPort}` : '',

src/action/autopilot.js

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@
33
* whether autopilot should open channels.
44
*/
55

6+
import { ATPL_DELAY } from '../config';
7+
import { poll, checkHttpStatus } from '../helper';
8+
import * as log from './log';
9+
610
class AtplAction {
7-
constructor(store, grpc, db, notification) {
11+
constructor(store, grpc, info, db, notification) {
812
this._store = store;
913
this._grpc = grpc;
14+
this._info = info;
1015
this._db = db;
1116
this._notification = notification;
1217
}
1318

1419
/**
1520
* Initialize autopilot from the stored settings and enable it via grpc
16-
* depending on if the user has enabled it in the last session.
21+
* depending on if the user has enabled it in the last session. Fetch node
22+
* scores are fetched from an api to inform channel selection.
1723
* @return {Promise<undefined>}
1824
*/
1925
async init() {
26+
await this.updateNodeScores();
2027
if (this._store.settings.autopilot) {
2128
await this._setStatus(true);
2229
}
30+
await poll(() => this.updateNodeScores(), ATPL_DELAY);
2331
}
2432

2533
/**
@@ -48,6 +56,65 @@ class AtplAction {
4856
this._notification.display({ msg: 'Error toggling autopilot', err });
4957
}
5058
}
59+
60+
/**
61+
* Update node scores to get better channels via autopilot.
62+
* @return {Promise<undefined>}
63+
*/
64+
async updateNodeScores() {
65+
try {
66+
await this._setNetwork();
67+
const scores = await this._readNodeScores();
68+
await this._setNodeScores(scores);
69+
} catch (err) {
70+
log.error('Updating autopilot scores failed', err);
71+
}
72+
}
73+
74+
async _setNetwork() {
75+
await this._info.getInfo();
76+
if (!this._store.network) {
77+
throw new Error('Could not read network');
78+
}
79+
}
80+
81+
async _readNodeScores() {
82+
const { network, settings } = this._store;
83+
try {
84+
settings.nodeScores[network] = await this._fetchNodeScores(network);
85+
this._db.save();
86+
} catch (err) {
87+
log.error('Fetching node scores failed', err);
88+
}
89+
return settings.nodeScores[network];
90+
}
91+
92+
async _fetchNodeScores(network) {
93+
const baseUri = 'https://nodes.lightning.computer/availability/v1';
94+
const uri = `${baseUri}/btc${network === 'testnet' ? 'testnet' : ''}.json`;
95+
const response = checkHttpStatus(await fetch(uri));
96+
return this._formatNodesScores((await response.json()).scores);
97+
}
98+
99+
_formatNodesScores(jsonScores) {
100+
return jsonScores.reduce((map, { public_key, score }) => {
101+
if (typeof public_key !== 'string' || !Number.isInteger(score)) {
102+
throw new Error('Invalid node score format!');
103+
}
104+
map[public_key] = score / 100000000.0;
105+
return map;
106+
}, {});
107+
}
108+
109+
async _setNodeScores(scores) {
110+
if (!scores) {
111+
throw new Error('Node scores are emtpy');
112+
}
113+
await this._grpc.sendAutopilotCommand('setScores', {
114+
heuristic: 'externalscore',
115+
scores,
116+
});
117+
}
51118
}
52119

53120
export default AtplAction;

src/action/index-mobile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const auth = new AuthAction(
6363
LocalAuthentication,
6464
Alert
6565
);
66-
export const autopilot = new AtplAction(store, grpc, db, notify);
66+
export const autopilot = new AtplAction(store, grpc, info, db, notify);
6767

6868
payment.listenForUrlMobile(Linking); // enable incoming url handler
6969

src/action/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const channel = new ChannelAction(store, grpc, nav, notify);
4242
export const invoice = new InvoiceAction(store, grpc, nav, notify, Clipboard);
4343
export const payment = new PaymentAction(store, grpc, nav, notify, Clipboard);
4444
export const setting = new SettingAction(store, wallet, db, ipc);
45-
export const autopilot = new AtplAction(store, grpc, db, notify);
45+
export const autopilot = new AtplAction(store, grpc, info, db, notify);
4646

4747
payment.listenForUrl(ipc); // enable incoming url handler
4848

src/action/info.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class InfoAction {
2828
this._store.pubKey = response.identityPubkey;
2929
this._store.syncedToChain = response.syncedToChain;
3030
this._store.blockHeight = response.blockHeight;
31+
this._store.network = response.chains[0].network;
3132
if (this.startingSyncTimestamp === undefined) {
3233
this.startingSyncTimestamp = response.bestHeaderTimestamp || 0;
3334
}

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports.RETRY_DELAY = 1000;
66
module.exports.LND_INIT_DELAY = 5000;
77
module.exports.NOTIFICATION_DELAY = 5000;
88
module.exports.RATE_DELAY = 15 * 60 * 1000;
9+
module.exports.ATPL_DELAY = 60 * 60 * 1000;
910
module.exports.PAYMENT_TIMEOUT = 60 * 1000;
1011
module.exports.POLL_STORE_TIMEOUT = 100;
1112

src/store.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class Store {
3535
unconfirmedBalanceSatoshis: 0,
3636
pendingBalanceSatoshis: 0,
3737
channelBalanceSatoshis: 0,
38+
network: null,
3839
pubKey: null,
3940
walletAddress: null,
4041
displayCopied: false,
@@ -91,6 +92,7 @@ export class Store {
9192
exchangeRate: {},
9293
restoring: false,
9394
autopilot: true,
95+
nodeScores: {},
9496
},
9597
});
9698
}

test/integration/action/action-integration.spec.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('Action Integration Tests', function() {
167167
channels1 = new ChannelAction(store1, grpc1, nav1, notify1);
168168
invoice1 = new InvoiceAction(store1, grpc1, nav1, notify1);
169169
payments1 = new PaymentAction(store1, grpc1, nav1, notify1);
170-
autopilot1 = new AtplAction(store1, grpc1, db1, notify1);
170+
autopilot1 = new AtplAction(store1, grpc1, info1, db1, notify1);
171171

172172
db2 = sinon.createStubInstance(AppStorage);
173173
nav2 = sinon.createStubInstance(NavAction);
@@ -180,7 +180,10 @@ describe('Action Integration Tests', function() {
180180
channels2 = new ChannelAction(store2, grpc2, nav2, notify2);
181181
invoice2 = new InvoiceAction(store2, grpc2, nav2, notify2);
182182
payments2 = new PaymentAction(store2, grpc2, nav2, notify2);
183-
autopilot2 = new AtplAction(store2, grpc2, db2, notify2);
183+
autopilot2 = new AtplAction(store2, grpc2, info2, db2, notify2);
184+
185+
sandbox.stub(autopilot1, 'updateNodeScores').resolves(true);
186+
sandbox.stub(autopilot2, 'updateNodeScores').resolves(true);
184187
});
185188

186189
after(async () => {
@@ -298,6 +301,7 @@ describe('Action Integration Tests', function() {
298301
it('should get public key node1', async () => {
299302
await info1.pollInfo();
300303
expect(store1.pubKey, 'to be ok');
304+
expect(store1.network, 'to equal', 'simnet');
301305
});
302306

303307
it('should wait until node1 is synced to chain', async () => {

test/unit/action/autopilot.spec.js

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,56 @@ import { Store } from '../../../src/store';
22
import GrpcAction from '../../../src/action/grpc';
33
import NotificationAction from '../../../src/action/notification';
44
import AppStorage from '../../../src/action/app-storage';
5+
import InfoAction from '../../../src/action/info';
56
import AtplAction from '../../../src/action/autopilot';
67
import * as logger from '../../../src/action/log';
8+
import nock from 'nock';
9+
import 'isomorphic-fetch';
710

811
describe('Action Autopilot Unit Test', () => {
912
let store;
1013
let db;
1114
let grpc;
1215
let notify;
16+
let info;
1317
let autopilot;
1418
let sandbox;
1519

1620
beforeEach(() => {
1721
sandbox = sinon.createSandbox({});
1822
sandbox.stub(logger);
1923
store = new Store();
24+
require('../../../src/config').ATPL_DELAY = 1;
2025
grpc = sinon.createStubInstance(GrpcAction);
2126
db = sinon.createStubInstance(AppStorage);
2227
notify = sinon.createStubInstance(NotificationAction);
23-
autopilot = new AtplAction(store, grpc, db, notify);
28+
info = sinon.createStubInstance(InfoAction);
29+
autopilot = new AtplAction(store, grpc, info, db, notify);
2430
});
2531

2632
afterEach(() => {
2733
sandbox.restore();
2834
});
2935

3036
describe('init()', () => {
31-
it('should enable autopilot by default', async () => {
37+
beforeEach(() => {
38+
sandbox.stub(autopilot, 'updateNodeScores').resolves();
39+
autopilot.updateNodeScores.onThirdCall().resolves(true);
40+
});
41+
42+
it('should enable autopilot and fetch scores by default', async () => {
3243
await autopilot.init();
3344
expect(grpc.sendAutopilotCommand, 'was called with', 'modifyStatus', {
3445
enable: true,
3546
});
47+
expect(autopilot.updateNodeScores, 'was called thrice');
3648
});
3749

38-
it('should not enable autopilot if disabled', async () => {
50+
it('should not enable autopilot if disabled but fetch scores', async () => {
3951
store.settings.autopilot = false;
4052
await autopilot.init();
4153
expect(grpc.sendAutopilotCommand, 'was not called');
54+
expect(autopilot.updateNodeScores, 'was called thrice');
4255
});
4356
});
4457

@@ -59,4 +72,110 @@ describe('Action Autopilot Unit Test', () => {
5972
expect(db.save, 'was not called');
6073
});
6174
});
75+
76+
describe('updateNodeScores()', async () => {
77+
let scoresJson;
78+
79+
beforeEach(() => {
80+
scoresJson = {
81+
last_updated: '2019-03-21T04:39:41.031Z',
82+
scores: [
83+
{
84+
alias: 'some-alias',
85+
public_key: 'some-pubkey',
86+
score: 14035087,
87+
},
88+
],
89+
};
90+
info.getInfo.resolves();
91+
store.network = 'testnet';
92+
});
93+
94+
it('should fail if network cannot be read', async () => {
95+
store.network = null;
96+
await autopilot.updateNodeScores();
97+
expect(logger.error, 'was called once');
98+
expect(grpc.sendAutopilotCommand, 'was not called');
99+
expect(db.save, 'was not called');
100+
expect(store.settings.nodeScores, 'to equal', {});
101+
});
102+
103+
it('should read scores for testnet from empty cache', async () => {
104+
nock('https://nodes.lightning.computer')
105+
.get('/availability/v1/btctestnet.json')
106+
.reply(500, 'Boom!');
107+
108+
await autopilot.updateNodeScores();
109+
expect(grpc.sendAutopilotCommand, 'was not called');
110+
expect(logger.error, 'was called twice');
111+
expect(db.save, 'was not called');
112+
expect(store.settings.nodeScores, 'to equal', {});
113+
});
114+
115+
it('should handle invalid score format', async () => {
116+
delete scoresJson.scores[0].public_key;
117+
nock('https://nodes.lightning.computer')
118+
.get('/availability/v1/btctestnet.json')
119+
.reply(200, scoresJson);
120+
121+
await autopilot.updateNodeScores();
122+
expect(logger.error, 'was called twice');
123+
expect(grpc.sendAutopilotCommand, 'was not called');
124+
expect(db.save, 'was not called');
125+
expect(store.settings.nodeScores, 'to equal', {});
126+
});
127+
128+
it('should read scores for testnet from cache', async () => {
129+
nock('https://nodes.lightning.computer')
130+
.get('/availability/v1/btctestnet.json')
131+
.reply(500, 'Boom!');
132+
store.settings.nodeScores = {
133+
testnet: { 'some-pubkey': 0.14035087 },
134+
};
135+
136+
await autopilot.updateNodeScores();
137+
expect(grpc.sendAutopilotCommand, 'was called with', 'setScores', {
138+
heuristic: 'externalscore',
139+
scores: { 'some-pubkey': 0.14035087 },
140+
});
141+
expect(logger.error, 'was called once');
142+
expect(db.save, 'was not called');
143+
expect(store.settings.nodeScores, 'to equal', {
144+
testnet: { 'some-pubkey': 0.14035087 },
145+
});
146+
});
147+
148+
it('should set scores for testnet', async () => {
149+
nock('https://nodes.lightning.computer')
150+
.get('/availability/v1/btctestnet.json')
151+
.reply(200, scoresJson);
152+
153+
await autopilot.updateNodeScores();
154+
expect(grpc.sendAutopilotCommand, 'was called with', 'setScores', {
155+
heuristic: 'externalscore',
156+
scores: { 'some-pubkey': 0.14035087 },
157+
});
158+
expect(db.save, 'was called once');
159+
expect(store.settings.nodeScores, 'to equal', {
160+
testnet: { 'some-pubkey': 0.14035087 },
161+
});
162+
});
163+
164+
it('should set scores for mainnet', async () => {
165+
store.network = 'mainnet';
166+
nock('https://nodes.lightning.computer')
167+
.get('/availability/v1/btc.json')
168+
.reply(200, scoresJson);
169+
170+
await autopilot.updateNodeScores();
171+
expect(grpc.sendAutopilotCommand, 'was called with', 'setScores', {
172+
heuristic: 'externalscore',
173+
scores: { 'some-pubkey': 0.14035087 },
174+
});
175+
expect(db.save, 'was called once');
176+
expect(store.settings.nodeScores, 'to equal', {
177+
mainnet: { 'some-pubkey': 0.14035087 },
178+
});
179+
});
180+
});
62181
});

0 commit comments

Comments
 (0)