Skip to content

Commit 29e179a

Browse files
RafaelAPBCatarinaPedreira
authored andcommitted
feat(tools): substrate test ledger
Substrate-based blockchains like Polkadot are becoming increasingly relevant. However, Cactus lacked tools to support developing Substrate-based connectors, such as a test ledger. This commit defines a Substrate test ledger that allows to programmatically instantiate the Substrate Contracts node template (https://github.com/paritytech/substrate-contracts-node). Thus, it contains a Dockerfile with the test ledger and the test-ledger file that contains the execution logic. Signed-off-by: Rafael Belchior <[email protected]> Signed-off-by: Catarina Pedreira <[email protected]> Signed-off-by: Peter Somogyvari <[email protected]>
1 parent c2cee43 commit 29e179a

File tree

9 files changed

+336
-1
lines changed

9 files changed

+336
-1
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"cids",
2222
"Corda",
2323
"Cordapp",
24+
"couchdb",
2425
"dclm",
2526
"DHTAPI",
2627
"DockerOde",

packages/cactus-test-tooling/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@
5151
"email": "[email protected]",
5252
"url": "https://example.com"
5353
},
54+
{
55+
"name": "Catarina Pedreira",
56+
"email": "[email protected]",
57+
"url": "https://github.com/CatarinaPedreira"
58+
},
59+
{
60+
"name": "Rafael Belchior",
61+
"email": "[email protected]",
62+
"url": "https://rafaelapb.github.io/"
63+
},
5464
{
5565
"name": "Peter Somogyvari",
5666
"email": "[email protected]",

packages/cactus-test-tooling/src/main/typescript/public-api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ export {
131131
RustcContainer,
132132
} from "./rustc-container/rustc-container";
133133

134+
export {
135+
ISubstrateTestLedgerOptions,
136+
SubstrateTestLedger,
137+
} from "./substrate-test-ledger/substrate-test-ledger";
138+
134139
export { RustcBuildCmd } from "./rustc-container/rustc-build-cmd";
135140

136141
export { Streams } from "./common/streams";
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import type { EventEmitter } from "events";
2+
import { Optional } from "typescript-optional";
3+
import { RuntimeError } from "run-time-error";
4+
import type { Container, ContainerInfo } from "dockerode";
5+
import Docker from "dockerode";
6+
import { Logger, Checks, Bools } from "@hyperledger/cactus-common";
7+
import type { LogLevelDesc } from "@hyperledger/cactus-common";
8+
import { LoggerProvider } from "@hyperledger/cactus-common";
9+
import { Containers } from "../common/containers";
10+
11+
export interface ISubstrateTestLedgerOptions {
12+
readonly publishAllPorts: boolean;
13+
readonly logLevel?: LogLevelDesc;
14+
readonly imageName?: string;
15+
readonly imageTag?: string;
16+
readonly emitContainerLogs?: boolean;
17+
readonly envVars?: Map<string, string>;
18+
}
19+
20+
export class SubstrateTestLedger {
21+
public static readonly CLASS_NAME = "SubstrateTestLedger";
22+
23+
public readonly logLevel: LogLevelDesc;
24+
public readonly imageName: string;
25+
public readonly imageTag: string;
26+
public readonly imageFqn: string;
27+
public readonly log: Logger;
28+
public readonly emitContainerLogs: boolean;
29+
public readonly publishAllPorts: boolean;
30+
public readonly envVars: Map<string, string>;
31+
32+
private _containerId: Optional<string>;
33+
34+
public get containerId(): Optional<string> {
35+
return this._containerId;
36+
}
37+
38+
public get container(): Optional<Container> {
39+
const docker = new Docker();
40+
return this.containerId.isPresent()
41+
? Optional.ofNonNull(docker.getContainer(this.containerId.get()))
42+
: Optional.empty();
43+
}
44+
45+
public get className(): string {
46+
return SubstrateTestLedger.CLASS_NAME;
47+
}
48+
49+
constructor(public readonly opts: ISubstrateTestLedgerOptions) {
50+
const fnTag = `${this.className}#constructor()`;
51+
Checks.truthy(opts, `${fnTag} arg options`);
52+
53+
this.publishAllPorts = opts.publishAllPorts;
54+
this._containerId = Optional.empty();
55+
this.imageName =
56+
opts.imageName || "ghcr.io/hyperledger/cactus-substrate-all-in-one";
57+
this.imageTag = opts.imageTag || "2021-09-24---feat-1274";
58+
this.imageFqn = `${this.imageName}:${this.imageTag}`;
59+
this.envVars = opts.envVars || new Map();
60+
this.emitContainerLogs = Bools.isBooleanStrict(opts.emitContainerLogs)
61+
? (opts.emitContainerLogs as boolean)
62+
: true;
63+
64+
this.logLevel = opts.logLevel || "INFO";
65+
66+
const level = this.logLevel;
67+
const label = this.className;
68+
this.log = LoggerProvider.getOrCreate({ level, label });
69+
70+
this.log.debug(`Created instance of ${this.className} OK`);
71+
}
72+
public getContainerImageName(): string {
73+
return `${this.imageName}:${this.imageTag}`;
74+
}
75+
public async start(omitPull = false): Promise<Container> {
76+
const docker = new Docker();
77+
if (this.containerId.isPresent()) {
78+
this.log.debug(`Container ID provided. Will not start new one.`);
79+
const container = docker.getContainer(this.containerId.get());
80+
return container;
81+
}
82+
if (!omitPull) {
83+
this.log.debug(`Pulling image ${this.imageFqn}...`);
84+
await Containers.pullImage(this.imageFqn);
85+
this.log.debug(`Pulled image ${this.imageFqn} OK`);
86+
}
87+
88+
const dockerEnvVars: string[] = new Array(...this.envVars).map(
89+
(pairs) => `${pairs[0]}=${pairs[1]}`,
90+
);
91+
92+
// TODO: dynamically expose ports for custom port mapping
93+
const createOptions = {
94+
Env: dockerEnvVars,
95+
Healthcheck: {
96+
Test: [
97+
"CMD-SHELL",
98+
`rustup --version && rustc --version && cargo --version`,
99+
],
100+
Interval: 1000000000, // 1 second
101+
Timeout: 3000000000, // 3 seconds
102+
Retries: 10,
103+
StartPeriod: 1000000000, // 1 second
104+
},
105+
ExposedPorts: {
106+
"9944/tcp": {}, // OpenSSH Server - TCP
107+
},
108+
HostConfig: {
109+
AutoRemove: true,
110+
PublishAllPorts: this.publishAllPorts,
111+
Privileged: false,
112+
PortBindings: {
113+
"9944/tcp": [{ HostPort: "9944" }],
114+
},
115+
},
116+
};
117+
118+
this.log.debug(`Starting ${this.imageFqn} with options: `, createOptions);
119+
120+
return new Promise<Container>((resolve, reject) => {
121+
const eventEmitter: EventEmitter = docker.run(
122+
this.imageFqn,
123+
[],
124+
[],
125+
createOptions,
126+
{},
127+
(err: Error) => {
128+
if (err) {
129+
const errorMessage = `Failed to start container ${this.imageFqn}`;
130+
const exception = new RuntimeError(errorMessage, err);
131+
this.log.error(exception);
132+
reject(exception);
133+
}
134+
},
135+
);
136+
137+
eventEmitter.once("start", async (container: Container) => {
138+
const { id } = container;
139+
this.log.debug(`Started ${this.imageFqn} successfully. ID=${id}`);
140+
this._containerId = Optional.ofNonNull(id);
141+
142+
if (this.emitContainerLogs) {
143+
const logOptions = { follow: true, stderr: true, stdout: true };
144+
const logStream = await container.logs(logOptions);
145+
logStream.on("data", (data: Buffer) => {
146+
const fnTag = `[${this.imageFqn}]`;
147+
this.log.debug(`${fnTag} %o`, data.toString("utf-8"));
148+
});
149+
}
150+
this.log.debug(`Registered container log stream callbacks OK`);
151+
152+
try {
153+
this.log.debug(`Starting to wait for healthcheck... `);
154+
await Containers.waitForHealthCheck(this.containerId.get());
155+
this.log.debug(`Healthcheck passed OK`);
156+
resolve(container);
157+
} catch (ex) {
158+
this.log.error(ex);
159+
reject(ex);
160+
}
161+
});
162+
});
163+
}
164+
165+
public async stop(): Promise<unknown> {
166+
return Containers.stop(this.container.get());
167+
}
168+
169+
public async destroy(): Promise<unknown> {
170+
return this.container.get().remove();
171+
}
172+
173+
public async getContainerIpAddress(): Promise<string> {
174+
const containerInfo = await this.getContainerInfo();
175+
return Containers.getContainerInternalIp(containerInfo);
176+
}
177+
178+
protected async getContainerInfo(): Promise<ContainerInfo> {
179+
const fnTag = "FabricTestLedgerV1#getContainerInfo()";
180+
const docker = new Docker();
181+
const image = this.getContainerImageName();
182+
const containerInfos = await docker.listContainers({});
183+
184+
let aContainerInfo;
185+
if (this.containerId !== undefined) {
186+
aContainerInfo = containerInfos.find(
187+
(ci) => ci.Id == this.containerId.toString(),
188+
);
189+
}
190+
191+
if (aContainerInfo) {
192+
return aContainerInfo;
193+
} else {
194+
throw new Error(`${fnTag} no image "${image}"`);
195+
}
196+
}
197+
198+
// ./scripts/docker_run.sh ./target/release/node-template purge-chain --dev
199+
protected async purgeDevChain(): Promise<void> {
200+
throw new Error("TODO");
201+
}
202+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import test, { Test } from "tape-promise/tape";
2+
import { LogLevelDesc } from "@hyperledger/cactus-common";
3+
import { SubstrateTestLedger } from "../../../../main/typescript/substrate-test-ledger/substrate-test-ledger";
4+
import { pruneDockerAllIfGithubAction } from "../../../../main/typescript/github-actions/prune-docker-all-if-github-action";
5+
6+
const testCase = "Instantiate plugin";
7+
const logLevel: LogLevelDesc = "TRACE";
8+
9+
test("BEFORE " + testCase, async (t: Test) => {
10+
const pruning = pruneDockerAllIfGithubAction({ logLevel });
11+
await t.doesNotReject(pruning, "Pruning didn't throw OK");
12+
t.end();
13+
});
14+
15+
test(testCase, async (t: Test) => {
16+
const options = {
17+
publishAllPorts: true,
18+
logLevel: logLevel,
19+
emitContainerLogs: true,
20+
envVars: new Map([
21+
["WORKING_DIR", "/var/www/node-template"],
22+
["CONTAINER_NAME", "contracts-node-template-cactus"],
23+
["PORT", "9944"],
24+
["DOCKER_PORT", "9944"],
25+
["CARGO_HOME", "/var/www/node-template/.cargo"],
26+
]),
27+
};
28+
29+
const ledger = new SubstrateTestLedger(options);
30+
const tearDown = async () => {
31+
await ledger.stop();
32+
await pruneDockerAllIfGithubAction({ logLevel });
33+
};
34+
35+
test.onFinish(tearDown);
36+
await ledger.start();
37+
t.ok(ledger);
38+
39+
t.end();
40+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
FROM paritytech/ci-linux:production
2+
LABEL AUTHORS="Rafael Belchior, Catarina Pedreira"
3+
LABEL VERSION="2021-09-10"
4+
LABEL org.opencontainers.image.source=https://github.com/hyperledger/cactus
5+
6+
WORKDIR /
7+
ARG WORKING_DIR=/var/www/node-template
8+
ARG CONTAINER_NAME=contracts-node-template-cactus
9+
ARG PORT=9944
10+
ARG DOCKER_PORT=9944
11+
ARG CARGO_HOME=/var/www/node-template/.cargo
12+
13+
ENV CARGO_HOME=${CARGO_HOME}
14+
ENV CACTUS_CFG_PATH=/etc/hyperledger/cactus
15+
VOLUME .:/var/www/node-template
16+
17+
RUN apt update
18+
19+
# Get ubuntu and rust packages
20+
RUN apt install -y build-essential pkg-config git clang curl libssl-dev llvm libudev-dev
21+
22+
ENV CACTUS_CFG_PATH=/etc/hyperledger/cactus
23+
RUN mkdir -p $CACTUS_CFG_PATH
24+
25+
RUN set -e
26+
27+
RUN echo "*** Instaling Rust environment ***"
28+
RUN curl https://sh.rustup.rs -y -sSf | sh
29+
RUN echo 'source $HOME/.cargo/env' >> $HOME/.bashrc
30+
RUN rustup default nightly
31+
32+
RUN echo "*** Initializing WASM build environment"
33+
RUN rustup target add wasm32-unknown-unknown --toolchain nightly
34+
35+
RUN echo "*** Installing Substrate node environment ***"
36+
RUN cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git --force --locked
37+
38+
RUN echo "*** Start Substrate node template ***"
39+
CMD [ "/var/www/node-template/.cargo/bin/substrate-contracts-node", "--dev"]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# @hyperledger/cactus-substrate-all-in-one<!-- omit in toc -->
2+
3+
A container image that can holds the default Substrate test ledger (and the corresponding front-end).
4+
This image can be used for development of Substrate-based chains (including but not limited to pallets, smart contracts) and connectors.
5+
6+
## Table of Contents<!-- omit in toc -->
7+
8+
- [Usage](#usage)
9+
- [Build](#build)
10+
11+
## Usage
12+
```sh
13+
docker run -t -p 9944:9944 --name substrate-contracts-node saio:latest
14+
```
15+
16+
## Build
17+
18+
```sh
19+
DOCKER_BUILDKIT=1 docker build -f ./tools/docker/substrate-all-in-one/Dockerfile . --tag saio
20+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
3+
4+
SHORTHASH="$(git rev-parse --short HEAD)"
5+
TODAYS_DATE="$(date +%F)"
6+
7+
#
8+
# We tag every image with today's date and also the git short hash
9+
# Today's date helps humans quickly intuit which version is older/newer
10+
# And the short hash helps identify the exact git revision that the image was
11+
# built from in case you are chasing some exotic bug that requires this sort of
12+
# rabbithole diving where you are down to comparing the images at this level.
13+
#
14+
DOCKER_TAG="$TODAYS_DATE-$SHORTHASH"
15+
16+
17+
docker tag $IMAGE_NAME $DOCKER_REPO:$DOCKER_TAG
18+
docker push $DOCKER_REPO:$DOCKER_TAG

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24505,4 +24505,4 @@ [email protected]:
2450524505
zone.js@~0.10.3:
2450624506
version "0.10.3"
2450724507
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16"
24508-
integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==
24508+
integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==

0 commit comments

Comments
 (0)