Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ mount
**temp
*key*
*keys*
test

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not ignore every test path.

A slashless test pattern recursively ignores any file or directory with that name, which can silently exclude test source. Remove it or scope it to the intended generated artifact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore at line 13, Update the .gitignore entry containing the slashless
test pattern so it no longer recursively ignores every file or directory named
test. Remove the broad pattern or scope it specifically to the intended
generated artifact, preserving visibility of test source files.

2 changes: 1 addition & 1 deletion container-manager/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use the official Node.js 18.15 image as the base
FROM node:18.15
FROM node:24

# Install Docker CLI (required to interact with the Docker API)
RUN apt-get update && apt-get install -y \
Expand Down
1 change: 1 addition & 0 deletions container-manager/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm run dev
40 changes: 40 additions & 0 deletions container-manager/src/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,46 @@ app.use(
);
app.use(express.json());

// NON_SUBNET mode: when NON_SUBNET=true, block every subnet-management route
// (including the wizard landing page "/") so only the XDPoS private-network
// generator (/gen_xdpos, /submit_xdpos) remains reachable.
const NON_SUBNET = process.env.NON_SUBNET === "true";
const SUBNET_ROUTES = [
"/",
"/debug",
"/state",
"/deploy_csc_lite",
"/deploy_csc_full",
"/deploy_csc_reversefull",
"/deploy_zero",
"/deploy_subswap",
"/start_subnet",
"/stop_subnet",
"/start_subnet_slow",
"/start_services",
"/stop_services",
"/start_subswap_frontend",
"/stop_subswap_frontend",
"/start_explorer",
"/stop_explorer",
"/remove_subnet",
"/gen",
"/submit",
"/submit_preconfig",
"/faucet",
"/faucet_subnet",
];

app.use((req, res, next) => {
if (NON_SUBNET && SUBNET_ROUTES.includes(req.path)) {
console.log(`NON_SUBNET: blocked ${req.path}`);
return res
.status(403)
.json({ error: "Subnet routes are disabled (NON_SUBNET mode)" });
}
next();
Comment on lines +55 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching express.js:\n'
fd -a 'express\.js$' . || true

printf '\nRelevant file excerpts:\n'
for f in $(fd 'express\.js$' .); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,140p' "$f" | cat -n
done

printf '\nSubnet route declarations/usages:\n'
rg -n "SUBNET_ROUTES|/submit|submit|strictRouting|strict routing" container-manager/src || true

printf '\nPackage Express version if available:\n'
fd 'package\.json$' -d 5 . | xargs -r -n1 sh -c 'echo "--- $0"; jq ".dependencies.express, .devDependencies.express" "$0"'

Repository: XinFinOrg/Subnet-Deployment

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Package.json/lock Express entries (if present):\n'
for f in $(fd 'package(-lock)?\.json$' -d 4 .); do
  echo "--- $f"
  jq -r '(.dependencies.express?, .devDependencies.express? // keys[]?|select(startswith("express@"))? // "none", (.["express@*"] // {})|keys[]?|select(startswith("express@"))? // "none")|select(type=="string")' "$f" 2>/dev/null || true
done

printf '\nRest of express.js route handlers and strict settings:\n'
sed -n '140,225p' container-manager/src/express.js | cat -n
grep -En "strictRouting|app\(strict|strict\(\)" container-manager/src || true

printf '\nProgrammatic simulation of current middleware vs Express-style non-strict GET routes for /submit and /submit/.submit variants:\n'
python3 - <<'PY'
BLOCKED = ["/submit", "/submit_preconfig", "/debug", "/"]
def current_checks(path):
    return path in BLOCKED
def express_like_matches(handler_path, req_path):
    if handler_path.endswith("/"):
        return req_path == handler_path or (handler_path[:-1] == req_path)
    return req_path == handler_path or req_path == handler_path + "/"

handler = "/submit"
for req_path in ["/submit", "/submit/", "/submit.submit", "/submit/submit", "/debug/"]:
    print(f"{req_path!r}: blocked={current_checks(req_path)} matching={express_like_matches(handler, req_path)}")
PY

Repository: XinFinOrg/Subnet-Deployment

Length of output: 3664


🌐 Web query:

Express.js 4.x 5.x strictRouting trailing slashes /foo and /foo/ route matching

💡 Result:

In Express.js, both version 4.x and 5.x provide a strict routing option that controls how the router handles trailing slashes in request paths [1][2]. By default, the strict routing option is disabled (false) [1][2]. When it is disabled, Express treats paths with and without a trailing slash (e.g., /foo and /foo/) as identical, meaning they will match the same route [1][2]. When the strict routing option is enabled (true), the router treats /foo and /foo/ as distinct, different routes [1][3]. To enable this behavior, you can pass the strict: true option when creating a router instance [1][2]: const router = express.Router({ strict: true }); This setting applies to the specific router instance. If you are using the main application object as a router, note that Express applications do not typically expose a direct configuration option for strict routing at the app level in the same way, though you can use a mounted router to enforce this behavior for specific sub-paths [1][2].

Citations:


Normalize paths before checking blocked routes.

Exact matching lets trailing-slash variants such as /submit/ bypass the middleware while Express still dispatches them to the /submit handler. Strip a single trailing slash from every non-root path before SUBNET_ROUTES.includes(...) and add coverage for the blocked route variants.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 56-56: Logging request-derived input unsanitized allows log forging (CRLF injection); strip newlines / encode the value before logging.
Context: console.log(NON_SUBNET: blocked ${req.path})
Note: [CWE-117] Improper Output Neutralization for Logs. OWASP A09:2021 Security Logging and Monitoring Failures.

(log-injection-javascript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@container-manager/src/express.js` around lines 55 - 62, Update the middleware
registered with app.use so it normalizes req.path before SUBNET_ROUTES.includes
by removing one trailing slash from non-root paths, ensuring variants like
/submit/ remain blocked while / is unchanged. Add coverage for blocked routes
with and without the trailing slash.

});

app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "views", "index.html"));
});
Expand Down
2 changes: 2 additions & 0 deletions container-manager/src/gen/config_gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config = {
deployment_path: process.env.CONFIG_PATH || "",
num_machines: parseInt(process.env.NUM_MACHINE),
num_subnet: parseInt(process.env.NUM_SUBNET),
num_nethermind: parseInt(process.env.NUM_NETHERMIND) || 0,
ip_1: process.env.MAIN_IP || "",
public_ip: process.env.PUBLIC_IP || process.env.MAIN_IP,
network_name: process.env.NETWORK_NAME,
Expand Down Expand Up @@ -56,6 +57,7 @@ const config = {
},
xdpos: {
xdposnode: process.env.VERSION_NODE_IMAGE || "xinfinorg/devnet:dev-upgrade-53e5601",
nethermind: process.env.VERSION_NETHERMIND_IMAGE || "nethermindeth/nethermind:master-857da8f",
stake_threshold: parseInt(process.env.MASTERNODE_MINIMUM_STAKE) || "",
reward_yield: parseInt(process.env.REWARDS_YIELD) || "",
foundation_addr: "",
Expand Down
4 changes: 3 additions & 1 deletion container-manager/src/gen/gen_compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ function injectNetworkConfig(compose_object) {
Object.entries(compose_object["services"]).forEach((entry) => {
const [key, value] = entry;
let component_ip;
if (key.startsWith("subnet")) {
if (key === "bootnode") {
component_ip = ip_string_base + "254"; // fixed, predictable bootnode IP
} else if (key.startsWith("subnet")) {
component_ip = ip_string_base + parseInt(start_ip_subnet);
start_ip_subnet += 1;
} else {
Expand Down
139 changes: 115 additions & 24 deletions container-manager/src/gen/gen_xdpos.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,26 @@ doc["services"]["bootnode"] = {

// checkpoint smartcontract deployment config
doc, (ip_record = gen_compose.injectNetworkConfig(doc));

// bootnode enode for bootnodes.list — set here so it is populated even when
// every masternode runs Nethermind (and genXdposNodeConfig is never called).
const bootnode_ip =
config.num_machines === 1 ? ip_record["bootnode"] : config.ip_1;
bootnode = `enode://cc566d1033f21c7eb0eb9f403bb651f3949b5f63b40683917765c343f9c0c596e9cd021e2e8416908cbc3ab7d6f6671a83c85f7b121c1872f8be50a591723a5d@${bootnode_ip}:20301\n`;

subnetconf = [];
for (let i = 1; i <= config.num_subnet; i++) {
subnetconf.push(genXdposNodeConfig(i, keys, ip_record));
if (isNethermindNode(i)) {
subnetconf.push({
filename: `masternode${i}nmc.env`,
content: genNethermindNodeConfig(i, keys, ip_record),
});
} else {
subnetconf.push({
filename: `masternode${i}.env`,
content: genXdposNodeConfig(i, keys, ip_record),
});
}
}

const compose_content = yaml.dump(doc, {});
Expand Down Expand Up @@ -123,8 +140,8 @@ function writeGenerated(output_dir) {

for (let i = 1; i <= config.num_subnet; i++) {
fs.writeFileSync(
`${output_dir}/masternode${i}.env`,
subnetconf[i - 1],
`${output_dir}/${subnetconf[i - 1].filename}`,
subnetconf[i - 1].content,
(err) => {
if (err) {
console.error(err);
Expand Down Expand Up @@ -169,6 +186,14 @@ function copyScripts(output_dir) {
`${__dirname}/scripts/docker-down.sh`,
`${output_dir}/docker-down.sh`
);
if (config.num_nethermind > 0) {
// shared Nethermind config mounted by every nmc node (chainspec.json is
// produced separately from genesis.json after puppeth runs)
fs.copyFileSync(
`${__dirname}/scripts/xdc-nmc.json`,
`${output_dir}/xdc-nmc.json`
);
}
}

function initConfig(config) {
Expand All @@ -182,6 +207,11 @@ function initConfig(config) {
process.exit(1);
}

if (config.num_nethermind < 0 || config.num_nethermind > config.num_subnet) {
console.log("NUM_NETHERMIND must be between 0 and NUM_SUBNET");
process.exit(1);
}

if (net.isIP(config.main_ip) != 0) {
console.log("MAIN_IP Invalid IP address");
process.exit(1);
Expand Down Expand Up @@ -334,12 +364,46 @@ GC_MODE=archive
PORT=${port}
RPC_PORT=${rpcport}
WS_PORT=${wsport}
LOG_LEVEL=2
LOG_LEVEL=4
`;

return config_env;
}

// The last `num_nethermind` masternodes run the Nethermind client instead of
// the Go client. They stay validators and reuse the same key/port/IP slot.
function isNethermindNode(subnet_id) {
return subnet_id > config.num_subnet - config.num_nethermind;
}

// Per-node env file (masternode<i>nmc.env) — Nethermind reads NETHERMIND_* env
// vars (format NETHERMIND_<CATEGORY>CONFIG_<PROPERTY>). Shared/static settings
// live in xdc-nmc.json; only per-node values are emitted here.
function genNethermindNodeConfig(subnet_id, key, ip_record) {
const private_key = key[`key${subnet_id}`]["PrivateKey"]; // 0x-prefixed
const port = 20302 + subnet_id; // P2P + discovery
const rpcport = 8544 + subnet_id; // JSON-RPC
const ip = ip_record[`masternode${subnet_id}`];
const config_env = `
NETHERMIND_JSONRPCCONFIG_ENABLED=true
NETHERMIND_JSONRPCCONFIG_HOST=0.0.0.0
NETHERMIND_JSONRPCCONFIG_PORT=${rpcport}
NETHERMIND_NETWORKCONFIG_P2PPORT=${port}
NETHERMIND_NETWORKCONFIG_DISCOVERYPORT=${port}
NETHERMIND_NETWORKCONFIG_EXTERNALIP=${ip}
NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP=false
NETHERMIND_NETWORKCONFIG_BOOTNODES=${bootnode.trim()}
NETHERMIND_INITCONFIG_DISCOVERYENABLED=true
NETHERMIND_MININGCONFIG_ENABLED=true
NETHERMIND_KEYSTORECONFIG_TESTNODEKEY=${private_key}
NETHERMIND_HEALTHCHECKSCONFIG_ENABLED=true
NETHERMIND_METRICSCONFIG_ENABLED=true
NETHERMIND_METRICSCONFIG_EXPOSEPORT=8009
NO_COLOR=1
`;
return config_env;
}


function genXdposCompose(machine_id, num, start_num = 1) {
let nodes = {};
Expand All @@ -350,26 +414,53 @@ function genXdposCompose(machine_id, num, start_num = 1) {
const port = 20302 + i;
const rpcport = 8544 + i;
const wsport = 9554 + i;

imageName = `${config.xdpos.xdposnode}`;
config_path = "masternode" + i.toString() + ".env";

nodes[node_name] = {
image: imageName,
volumes: [volume, "./genesis.json:/work/genesis.json", "./bootnodes.list:/work/bootnodes.list"],
restart: "always",
network_mode: "host",
env_file: [config_path],
profiles: [compose_profile],
ports: [
`${port}:${port}/tcp`,
`${port}:${port}/udp`,
`${rpcport}:${rpcport}/tcp`,
`${rpcport}:${rpcport}/udp`,
`${wsport}:${wsport}/tcp`,
`${wsport}:${wsport}/udp`,
],
};
const port_mappings = [
`${port}:${port}/tcp`,
`${port}:${port}/udp`,
`${rpcport}:${rpcport}/tcp`,
`${rpcport}:${rpcport}/udp`,
`${wsport}:${wsport}/tcp`,
`${wsport}:${wsport}/udp`,
];

if (isNethermindNode(i)) {
// Nethermind validator: config via masternode<i>nmc.env (NETHERMIND_*)
// + shared xdc-nmc.json; uses chainspec.json instead of genesis.json.
// injectNetworkConfig() adds the bridge networks/ipv4_address block.
nodes[node_name] = {
image: `${config.xdpos.nethermind}`,
volumes: [
volume,
"./chainspec.json:/work/chainspec.json",
"./xdc-nmc.json:/work/xdc-nmc.json",
"./bootnodes.list:/work/bootnodes.list",
],
restart: "always",
env_file: [`masternode${i}nmc.env`],
command: [
"--config=/work/xdc-nmc.json",
"--datadir=/work/xdcchain",
"--log=debug",
],
profiles: [compose_profile],
ports: port_mappings,
};
} else {
// Go XDPoS masternode (default).
nodes[node_name] = {
image: `${config.xdpos.xdposnode}`,
volumes: [
volume,
"./genesis.json:/work/genesis.json",
"./bootnodes.list:/work/bootnodes.list",
],
restart: "always",
network_mode: "host",
env_file: [`masternode${i}.env`],
profiles: [compose_profile],
ports: port_mappings,
};
}
}
return nodes;
}
Expand Down
33 changes: 33 additions & 0 deletions container-manager/src/gen/scripts/xdc-nmc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://raw.githubusercontent.com/NethermindEth/core-scripts/refs/heads/main/schemas/config.json",
"Init": {
"ChainSpecPath": "/work/chainspec.json",
"BaseDbPath": "nethermind_db/xdc-nmc",
"LogFileName": "xdc.log",
"StateDbKeyScheme": "Hash",
"AutoDump": "All"
},
"TxPool": {
"BlobsSupport": "Disabled"
},
"Sync": {
"FastSync": true,
"NeedToWaitForHeader": true,
"VerifyTrieOnStateSyncFinished": true
},
"Mining": {
"Enabled": true
},
"JsonRpc": {
"Enabled": true
},
"Blocks": {
"TargetBlockGasLimit": 50000000
},
"Merge": {
"Enabled": false
},
"TraceStore": {
"Enabled": false
}
}
35 changes: 35 additions & 0 deletions container-manager/src/libs/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,33 @@ function generateXdpos(params) {
command = `cd ${mountPath}; docker run -v ${config.hostPath}:/app/generated/ --entrypoint 'bash' ${versionGenesisFullname} /work/puppeth.sh`;
console.log(command);
const [result2, out2] = callExec(command);
if (!result2) {
return [result2, out2];
}

//step 3: convert genesis.json -> chainspec.json (needed by Nethermind nodes)
const nethermindCount =
"customversion-checkbox" in params &&
params["customversion-checkbox"] != ""
? parseInt(params["customversion-xdpos-nethermind-count"]) || 0
: 0;
Comment on lines +188 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file sizes =="
wc -l container-manager/src/libs/exec.js container-manager/src/views/xdpos_generator/index.pug

echo "== relevant exec.js sections =="
sed -n '160,210p' container-manager/src/libs/exec.js
sed -n '380,420p' container-manager/src/libs/exec.js

echo "== relevant pug section =="
sed -n '80,100p' container-manager/src/views/xdpos_generator/index.pug

echo "== JS parse/runtime behavior probe =="
node - <<'JS'
const input = { "customversion-checkbox": "on", "customversion-xdpos-nethermind-count": "1e3" };
const nethermindCount =
  "customversion-checkbox" in input &&
  input["customversion-checkbox"] != ""
    ? parseInt(input["customversion-xdpos-nethermind-count"]) || 0
    : 0;

const emissionCondition = "customversion-checkbox" in input && input["customversion-checkbox"] != "" && "customversion-xdpos-nethermind-count" in input && input["customversion-xdpos-nethermind-count"] != "";
console.log({
  parseIntValue: nethermindCount,
  emissionCondition,
  emittedLine: emissionCondition ? `NUM_NETHERMIND=${input["customversion-xdpos-nethermind-count"]}` : "",
  isNaNNegative: isNaN(parseInt(input["customversion-xdpos-nethermind-count"])),
});

for (const value of ["-5", "", "abc", "3.7", "9007199254740991", "9007199254740992"]) {
  console.log(value, parseInt(value, 10));
}
JS

echo "== search for Nethermind count handling =="
rg -n "customversion-xdpos-nethermind-count|NUM_NETHERMIND|nethermind" container-manager/src

Repository: XinFinOrg/Subnet-Deployment

Length of output: 8224


Validate and canonicalize the Nethermind count before storing/updating it.

parseInt(params["...-count"]) can disagree with the raw NUM_NETHERMIND value, and negative values bypass chainspec generation while still being written to the environment. Server-side normalize the count to a non-negative safe integer (or reject invalid values), write that normalized value to NUM_NETHERMIND, and add min='0'/step='1' to the form input.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: require("child_process")
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)

📍 Affects 2 files
  • container-manager/src/libs/exec.js#L188-L192 (this comment)
  • container-manager/src/libs/exec.js#L404-L406
  • container-manager/src/views/xdpos_generator/index.pug#L90-L91
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@container-manager/src/libs/exec.js` around lines 188 - 192, The Nethermind
count is parsed inconsistently and can allow negative or unsafe values into
configuration. In container-manager/src/libs/exec.js at lines 188-192, normalize
or reject the count as a non-negative safe integer before storing it; at lines
404-406, write that same normalized value to NUM_NETHERMIND and use it for
chainspec generation. In container-manager/src/views/xdpos_generator/index.pug
at lines 90-91, add min='0' and step='1' to the count input.

if (nethermindCount > 0) {
try {
const { translate } = require("./genesis-to-chainspec");
const genesis = JSON.parse(
fs.readFileSync(path.join(mountPath, "genesis.json"), "utf-8")
);
const chainspec = translate(genesis, {});
fs.writeFileSync(
path.join(mountPath, "chainspec.json"),
JSON.stringify(chainspec, null, 2) + "\n"
);
console.log("chainspec.json generated");
} catch (e) {
console.error("chainspec generation failed:", e.message);
return [false, `chainspec generation failed: ${e.message}`];
}
}
return [result2, out2];
}

Expand Down Expand Up @@ -374,6 +401,14 @@ function genGenXdposEnv(input){
if ("customversion-checkbox" in input && input["customversion-checkbox"] != "" && "customversion-xdpos-node-fullname" in input && input["customversion-xdpos-node-fullname"] != "") {
content_version += `\nVERSION_NODE_IMAGE=${input["customversion-xdpos-node-fullname"]}`;
}
if ("customversion-checkbox" in input && input["customversion-checkbox"] != "" && "customversion-xdpos-nethermind-count" in input && input["customversion-xdpos-nethermind-count"] != "") {
content_version += `\nNUM_NETHERMIND=${input["customversion-xdpos-nethermind-count"]}`;
}
if ("customversion-checkbox" in input && input["customversion-checkbox"] != "" && "customversion-xdpos-nethermind-version" in input && input["customversion-xdpos-nethermind-version"] != "") {
content_version += `\nVERSION_NETHERMIND_IMAGE=${input["customversion-xdpos-nethermind-version"]}`;
}



let content_rewards = "";
if ("customrewards-checkbox" in input && input["customrewards-checkbox"] != "") {
Expand Down
Loading