Skip to content

Commit 26c7a93

Browse files
committed
feat(sim-ln): update docker compose setup for simulation
1 parent c9f423a commit 26c7a93

File tree

6 files changed

+247
-5
lines changed

6 files changed

+247
-5
lines changed

src/lib/docker/composeFile.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
litdCredentials,
1515
} from 'utils/constants';
1616
import { getContainerName, getDefaultCommand } from 'utils/network';
17-
import { bitcoind, clightning, eclair, litd, lnd, tapd } from './nodeTemplates';
17+
import { bitcoind, clightning, eclair, litd, lnd, simln, tapd } from './nodeTemplates';
1818

1919
export interface ComposeService {
2020
image: string;
@@ -51,6 +51,7 @@ class ComposeFile {
5151
environment: {
5252
USERID: '${USERID:-1000}',
5353
GROUPID: '${GROUPID:-1000}',
54+
...service.environment,
5455
},
5556
stop_grace_period: '30s',
5657
...service,
@@ -193,6 +194,17 @@ class ComposeFile {
193194
this.addService(svc);
194195
}
195196

197+
addSimLn(networkId: number) {
198+
const svc: ComposeService = simln(
199+
dockerConfigs['simln'].name,
200+
`polar-n${networkId}-simln`,
201+
dockerConfigs['simln'].imageName,
202+
dockerConfigs['simln'].command,
203+
{ ...dockerConfigs['simln'].env },
204+
);
205+
this.addService(svc);
206+
}
207+
196208
private mergeCommand(command: string, variables: Record<string, string>) {
197209
let merged = command;
198210
Object.keys(variables).forEach(key => {

src/lib/docker/dockerService.ts

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ import { migrateNetworksFile } from 'utils/migrations';
2626
import { isLinux, isMac } from 'utils/system';
2727
import ComposeFile from './composeFile';
2828

29+
type SimulationNode = {
30+
id: string;
31+
address: string;
32+
macaroon?: string;
33+
cert?: string;
34+
ca_cert?: string;
35+
client_cert?: string;
36+
client_key?: string;
37+
};
38+
39+
type SimulationActivity = {
40+
source: string;
41+
destination: string;
42+
interval_secs: number;
43+
amount_msat: number;
44+
};
45+
2946
let dockerInst: Dockerode | undefined;
3047
/**
3148
* Creates a new Dockerode instance by detecting the docker socket
@@ -160,6 +177,10 @@ class DockerService implements DockerLibrary {
160177
}
161178
});
162179

180+
if (network.simulationActivities.length > 0) {
181+
file.addSimLn(network.id);
182+
}
183+
163184
const yml = yaml.dump(file.content);
164185
const path = join(network.path, 'docker-compose.yml');
165186
await write(path, yml);
@@ -176,8 +197,37 @@ class DockerService implements DockerLibrary {
176197

177198
info(`Starting docker containers for ${network.name}`);
178199
info(` - path: ${network.path}`);
179-
const result = await this.execute(compose.upAll, this.getArgs(network));
180-
info(`Network started:\n ${result.out || result.err}`);
200+
201+
// we don't want to start the simln service when starting the network
202+
// because it depends on the running lightning nodes and the simulation
203+
// activity should be started separately based on user preference
204+
const servicesToStart = this.getServicesToStart(
205+
[...bitcoin, ...lightning, ...tap],
206+
['simln'],
207+
);
208+
209+
for (const service of servicesToStart) {
210+
const result = await this.execute(compose.upOne, service, this.getArgs(network));
211+
info(`Network started: ${service}\n ${result.out || result.err}`);
212+
}
213+
}
214+
215+
/**
216+
* Filter out services based on exclude list and return a list of service names to start
217+
* @param nodes Array of all nodes
218+
* @param exclude Array of container names to exclude
219+
*/
220+
private getServicesToStart(
221+
nodes:
222+
| CommonNode[]
223+
| {
224+
name: 'simln';
225+
}[],
226+
exclude: string[],
227+
): string[] {
228+
return nodes
229+
.map(node => node.name)
230+
.filter(serviceName => !exclude.includes(serviceName));
181231
}
182232

183233
/**
@@ -317,6 +367,112 @@ class DockerService implements DockerLibrary {
317367
}
318368
}
319369

370+
/**
371+
* Constructs the contents of sim.json file for the simulation activity
372+
* @param network the network to start
373+
*/
374+
constructSimJson(network: Network) {
375+
const simJson: {
376+
nodes: SimulationNode[];
377+
activity: SimulationActivity[];
378+
} = {
379+
nodes: [],
380+
activity: [],
381+
};
382+
383+
network.simulationActivities.forEach(activity => {
384+
const { source, destination } = activity;
385+
const nodeArray = [source, destination];
386+
387+
for (const node of nodeArray) {
388+
let simNode: SimulationNode;
389+
390+
// split the macaroon and cert path at "volumes/" to get the relative path
391+
// to the docker volume. This is necessary because the docker volumes are
392+
// mounted as a different path in the container.
393+
switch (node.implementation) {
394+
case 'LND':
395+
const lnd = node as LndNode;
396+
simNode = {
397+
id: lnd.name,
398+
macaroon: `/home/simln/.${lnd.paths.adminMacaroon.split('volumes/').pop()}`,
399+
address: `https://host.docker.internal:${lnd.ports.grpc}`,
400+
cert: `/home/simln/.${lnd.paths.tlsCert.split('volumes/').pop()}`,
401+
};
402+
break;
403+
404+
case 'c-lightning':
405+
const cln = node as CLightningNode;
406+
simNode = {
407+
id: cln.name,
408+
address: `https://host.docker.internal:${cln.ports.grpc}`,
409+
ca_cert: `/home/simln/.${cln.paths?.tlsCert?.split('volumes/').pop()}`,
410+
client_cert: `/home/simln/.${cln.paths?.tlsClientCert
411+
?.split('volumes/')
412+
.pop()}`,
413+
client_key: `/home/simln/.${cln.paths?.tlsClientKey
414+
?.split('volumes/')
415+
.pop()}`,
416+
};
417+
break;
418+
419+
default:
420+
throw new Error(`unsupported node type ${node.implementation}`);
421+
}
422+
423+
// console.log(`simNode >> \n ${JSON.stringify(simNode)}`);
424+
// Add the node to the nodes Set (duplicates are automatically handled)
425+
simJson.nodes.push(simNode);
426+
}
427+
428+
// Add the activity
429+
const simActivity: SimulationActivity = {
430+
source: activity.source.name,
431+
destination: activity.destination.name,
432+
interval_secs: activity.intervalSecs,
433+
amount_msat: activity.amountMsat * 1000,
434+
};
435+
436+
// console.log(`simActivity >> \n ${JSON.stringify(simActivity)}`);
437+
// Add the activity to the activity Set (duplicates are automatically handled)
438+
simJson.activity.push(simActivity);
439+
});
440+
return {
441+
nodes: [...new Map(simJson.nodes.map(node => [node['id'], node])).values()],
442+
activity: [
443+
...new Map(
444+
simJson.activity.map(activity => [
445+
`${activity.source}-${activity.destination}`,
446+
activity,
447+
]),
448+
).values(),
449+
],
450+
};
451+
}
452+
453+
/**
454+
* Start a simulation activity in the network using docker compose
455+
* @param network the network containing the simulation activity
456+
*/
457+
async startSimulationActivity(network: Network) {
458+
const simJson = this.constructSimJson(network);
459+
// console.log(`simJson >> \n ${JSON.stringify(simJson)}`);
460+
await this.ensureDirs(network, [
461+
...network.nodes.bitcoin,
462+
...network.nodes.lightning,
463+
...network.nodes.tap,
464+
]);
465+
const simjsonPath = nodePath(network, 'simln', 'sim.json');
466+
await write(simjsonPath, JSON.stringify(simJson));
467+
console.log(`simjsonPath >> \n ${JSON.stringify(simjsonPath)}`);
468+
const result = await this.execute(compose.upOne, 'simln', this.getArgs(network));
469+
info(`Simulation activity started:\n ${result.out || result.err}`);
470+
}
471+
472+
async stopSimulationActivity(network: Network) {
473+
info(`[stopSimulationActivity] \n ${network}`);
474+
}
475+
320476
/**
321477
* Helper method to trap and format exceptions thrown and
322478
* @param cmd the compose function to call

src/lib/docker/nodeTemplates.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,25 @@ export const litd = (
181181
`${webPort}:8443`, // web
182182
],
183183
});
184+
185+
export const simln = (
186+
name: string,
187+
container: string,
188+
image: string,
189+
command: string,
190+
environment: Record<string, string>,
191+
): ComposeService => ({
192+
image,
193+
container_name: container,
194+
hostname: name,
195+
command: trimInside(command),
196+
environment,
197+
restart: 'always',
198+
volumes: [
199+
`./volumes/${name}:/home/simln/.simln`,
200+
`./volumes/${dockerConfigs.LND.volumeDirName}:/home/simln/.lnd`,
201+
`./volumes/${dockerConfigs['c-lightning'].volumeDirName}:/home/simln/.clightning`,
202+
],
203+
expose: [],
204+
ports: [],
205+
});

src/types/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface Network {
2626
status: Status;
2727
path: string;
2828
autoMineMode: AutoMineMode;
29+
simulationActivities: SimulationActivity[];
2930
nodes: {
3031
bitcoin: BitcoinNode[];
3132
lightning: LightningNode[];
@@ -100,6 +101,7 @@ export interface DockerConfig {
100101
variables: string[];
101102
dataDir?: string;
102103
apiDir?: string;
104+
env?: Record<string, string>;
103105
}
104106

105107
export interface DockerRepoImage {
@@ -136,6 +138,8 @@ export interface DockerLibrary {
136138
saveNetworks: (networks: NetworksFile) => Promise<void>;
137139
loadNetworks: () => Promise<NetworksFile>;
138140
renameNodeDir: (network: Network, node: AnyNode, newName: string) => Promise<void>;
141+
startSimulationActivity: (network: Network) => Promise<void>;
142+
stopSimulationActivity: (network: Network) => Promise<void>;
139143
}
140144

141145
export interface RepoServiceInjection {
@@ -281,3 +285,35 @@ export enum AutoMineMode {
281285
Auto5m = 300,
282286
Auto10m = 600,
283287
}
288+
289+
/**
290+
* A running lightning node that is used in the simulation activity
291+
*/
292+
export interface SimulationActivityNode {
293+
id: string;
294+
label: string;
295+
type: LightningNode['implementation'];
296+
address: string;
297+
macaroon: string;
298+
tlsCert: string;
299+
clientCert?: string;
300+
clientKey?: string;
301+
}
302+
303+
/**
304+
* A simulation activity is a payment from one node to another at a given interval and amount
305+
*/
306+
export interface SimulationActivity {
307+
networkId: number;
308+
source: LightningNode;
309+
destination: LightningNode;
310+
intervalSecs: number;
311+
amountMsat: number;
312+
}
313+
314+
export interface ActivityInfo {
315+
sourceNode: LightningNode | undefined;
316+
targetNode: LightningNode | undefined;
317+
amount: number;
318+
frequency: number;
319+
}

src/utils/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const networksPath = join(dataPath, 'networks');
3232
*/
3333
export const nodePath = (
3434
network: Network,
35-
implementation: NodeImplementation,
35+
implementation: NodeImplementation | 'simln',
3636
name: string,
3737
): string =>
3838
join(network.path, 'volumes', dockerConfigs[implementation].volumeDirName, name);

src/utils/constants.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export const litdCredentials = {
9797
pass: 'polarpass',
9898
};
9999

100-
export const dockerConfigs: Record<NodeImplementation, DockerConfig> = {
100+
export const dockerConfigs: Record<NodeImplementation | 'simln', DockerConfig> = {
101101
LND: {
102102
name: 'LND',
103103
imageName: 'polarlightning/lnd',
@@ -125,6 +125,8 @@ export const dockerConfigs: Record<NodeImplementation, DockerConfig> = {
125125
'--bitcoind.rpcpass={{rpcPass}}',
126126
'--bitcoind.zmqpubrawblock=tcp://{{backendName}}:28334',
127127
'--bitcoind.zmqpubrawtx=tcp://{{backendName}}:28335',
128+
'--accept-keysend',
129+
'--accept-amp',
128130
].join('\n '),
129131
// if vars are modified, also update composeFile.ts & the i18n strings for cmps.nodes.CommandVariables
130132
variables: ['name', 'containerName', 'backendName', 'rpcUser', 'rpcPass'],
@@ -318,6 +320,20 @@ export const dockerConfigs: Record<NodeImplementation, DockerConfig> = {
318320
// if vars are modified, also update composeFile.ts & the i18n strings for cmps.nodes.CommandVariables
319321
variables: ['name', 'containerName', 'backendName', 'rpcUser', 'rpcPass'],
320322
},
323+
simln: {
324+
name: 'simln',
325+
imageName: 'bitcoindevproject/simln:0.2.0',
326+
logo: '',
327+
platforms: ['mac', 'linux', 'windows'],
328+
volumeDirName: 'simln',
329+
env: {
330+
SIMFILE_PATH: '/home/simln/.simln/sim.json',
331+
DATA_DIR: '/home/simln/.simln',
332+
LOG_LEVEL: 'info',
333+
},
334+
command: '',
335+
variables: ['DEFAULT_SIMFILE_PATH', 'LOG_LEVEL', 'DATA_DIR'],
336+
},
321337
};
322338

323339
/**

0 commit comments

Comments
 (0)