From 1ddde82bca24543542456920872bfa94abf7558b Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 6 Jul 2026 04:57:18 +0530 Subject: [PATCH 01/59] genesis-to-chainspec v1 --- .../src/libs/genesis-to-chainspec.js | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 container-manager/src/libs/genesis-to-chainspec.js diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js new file mode 100644 index 0000000..b61cc52 --- /dev/null +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -0,0 +1,278 @@ +#!/usr/bin/env node +/** + * genesis-to-chainspec.js + * + * Translate a go-ethereum / XDC style `genesis.json` into the OpenEthereum / + * Erigon style `chainspec.json` consumed by this network. + * + * Usage: + * node genesis-to-chainspec.js [chainspec.json] [--name ] + * + * node genesis-to-chainspec.js ../genesis.json ../chainspec.json --name xdc-mine + * + * If the output path is omitted the result is printed to stdout. + * + * Self-contained: no external dependencies. + * + * -------------------------------------------------------------------------- + * Mapping summary (see README at bottom of file for the full diff rationale): + * + * genesis.config.chainId -> params.chainId + * genesis.config.homesteadBlock -> params.homesteadBlock + * genesis.config.eip150Block -> params.eip150Transition + * genesis.config.eip155Block -> params.eip155Transition + * genesis.config.eip158Block -> params.eip158Transition + * genesis.config.byzantiumBlock -> params.byzantiumBlock + * (all remaining EIP transitions come from DEFAULT_PARAMS) + * + * genesis.config.XDPoS.period -> engine.XDPoS.params.period + * genesis.config.XDPoS.epoch -> engine.XDPoS.params.epoch + * genesis.config.XDPoS.reward -> engine.XDPoS.params.reward + * genesis.config.XDPoS.rewardCheckpoint -> engine.XDPoS.params.rewardCheckpoint + * genesis.config.XDPoS.gap -> engine.XDPoS.params.gap + * genesis.config.XDPoS.foudationWalletAddr-> engine.XDPoS.params.foundationWalletAddr (typo fixed, lowercased) + * genesis.config.XDPoS.v2.switchEpoch -> engine.XDPoS.params.switchEpoch (also accepts "SwitchEpoch") + * genesis.config.XDPoS.v2.switchBlock -> engine.XDPoS.params.switchBlock (also accepts "SwitchBlock") + * genesis.config.XDPoS.v2.allConfigs.* -> engine.XDPoS.params.v2Configs[] (expTimeoutConfig dropped) + * (the engine constants — contract addresses, mergeSignRange, blacklist, etc. — come from DEFAULT_ENGINE) + * + * genesis.{nonce,timestamp,extraData,gasLimit,difficulty,mixHash, + * coinbase,number,gasUsed,parentHash} -> genesis.* (verbatim) + * genesis.baseFeePerGas (null) -> genesis.baseFeePerGas (DEFAULT_BASE_FEE_PER_GAS) + * + * genesis.alloc -> accounts (verbatim: code / storage / balance) + * -------------------------------------------------------------------------- + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/* ------------------------------------------------------------------ * + * Defaults — values present in chainspec.json that are NOT derivable + * from genesis.json. Override here if the target network differs. + * ------------------------------------------------------------------ */ + +const DEFAULT_CHAIN_NAME = 'xdc-mine'; + +// chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has null. +const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; + +// All EIP transitions in chainspec.params that don't come straight from +// genesis.config. Values copied from the reference chainspec.json. +const DEFAULT_PARAMS = { + eip160Transition: 0, + eip145Transition: 0, + eip1014Transition: 0, + eip1052Transition: 0, + eip1234Transition: 0, + eip1283Transition: 0, + eip152Transition: 0, + eip1108Transition: 0, + eip1344Transition: 0, + eip1884Transition: 0, + eip2028Transition: 999999999999, + eip2200Transition: 0, + eip2565Transition: 0, + eip2718Transition: 0, + eip2930Transition: 0, + eip1559Transition: 0, + eip2929Transition: 0, + eip3198Transition: 0, + eip3529Transition: 0, + eip3541Transition: 0, + eip3554Transition: 0, + eip4399Transition: 0, + eip3651Transition: 0, + eip3855Transition: 0, + eip3860Transition: 0, + eip6049Transition: 0, + eip1153Transition: 0, + eip4844Transition: 0, + eip5656Transition: 0, + eip6780Transition: 0, + eip7516Transition: 0, + eip1559ElasticityMultiplier: '0x1', +}; + +// Engine constants not present in genesis.json (copied from chainspec.json). +const DEFAULT_ENGINE = { + mergeSignRange: 15, + tip2019Block: 1, + DynamicGasLimitBlock: 0, + blackListHFNumber: 0, + blackListedAddresses: ['0x0000000000000000000000000000000000000011'], + masternodeVotingContract: '0x0000000000000000000000000000000000000088', + randomizeSMCBinary: '0x0000000000000000000000000000000000000090', + blockSignerContract: '0x0000000000000000000000000000000000000089', +}; + +/* ------------------------------------------------------------------ * + * Address normalization + * ------------------------------------------------------------------ */ + +// Lowercase, 0x-prefixed. Clients accept this fine; we don't bother with the +// cosmetic EIP-55 mixed-case checksum. +function normalizeAddress(addr) { + return '0x' + String(addr).toLowerCase().replace(/^0x/, ''); +} + +/* ------------------------------------------------------------------ * + * Translation + * ------------------------------------------------------------------ */ + +function translate(genesis, opts = {}) { + const cfg = genesis.config || {}; + const xdpos = cfg.XDPoS || {}; + const v2 = xdpos.v2 || {}; + + // --- engine.XDPoS.params --- + const v2Configs = Object.keys(v2.allConfigs || {}) + .sort((a, b) => Number(a) - Number(b)) + .map((round) => { + const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; // drop expTimeoutConfig + return rest; + }); + + const enginePadms = { + period: xdpos.period, + epoch: xdpos.epoch, + reward: xdpos.reward, + rewardCheckpoint: xdpos.rewardCheckpoint, + gap: xdpos.gap, + // note: genesis spells the key "foudationWalletAddr" (sic) + foundationWalletAddr: normalizeAddress(xdpos.foudationWalletAddr || xdpos.foundationWalletAddr), + // genesis spells this either "switchEpoch" (newer) or "SwitchEpoch" (older). + // Use ?? not || — the valid value is 0, which is falsy. + switchEpoch: v2.switchEpoch ?? v2.SwitchEpoch, + switchBlock: v2.switchBlock ?? v2.SwitchBlock, + v2Configs, + mergeSignRange: DEFAULT_ENGINE.mergeSignRange, + tip2019Block: DEFAULT_ENGINE.tip2019Block, + DynamicGasLimitBlock: DEFAULT_ENGINE.DynamicGasLimitBlock, + blackListHFNumber: DEFAULT_ENGINE.blackListHFNumber, + blackListedAddresses: DEFAULT_ENGINE.blackListedAddresses, + masternodeVotingContract: DEFAULT_ENGINE.masternodeVotingContract, + randomizeSMCBinary: DEFAULT_ENGINE.randomizeSMCBinary, + blockSignerContract: DEFAULT_ENGINE.blockSignerContract, + }; + + // --- params (chain rules / EIP transitions) --- + const params = { + chainId: cfg.chainId, + homesteadBlock: cfg.homesteadBlock, + eip150Transition: cfg.eip150Block, + eip155Transition: cfg.eip155Block, + eip158Transition: cfg.eip158Block, + eip160Transition: DEFAULT_PARAMS.eip160Transition, + eip145Transition: DEFAULT_PARAMS.eip145Transition, + eip1014Transition: DEFAULT_PARAMS.eip1014Transition, + eip1052Transition: DEFAULT_PARAMS.eip1052Transition, + eip1234Transition: DEFAULT_PARAMS.eip1234Transition, + eip1283Transition: DEFAULT_PARAMS.eip1283Transition, + eip152Transition: DEFAULT_PARAMS.eip152Transition, + eip1108Transition: DEFAULT_PARAMS.eip1108Transition, + eip1344Transition: DEFAULT_PARAMS.eip1344Transition, + eip1884Transition: DEFAULT_PARAMS.eip1884Transition, + eip2028Transition: DEFAULT_PARAMS.eip2028Transition, + eip2200Transition: DEFAULT_PARAMS.eip2200Transition, + eip2565Transition: DEFAULT_PARAMS.eip2565Transition, + eip2718Transition: DEFAULT_PARAMS.eip2718Transition, + eip2930Transition: DEFAULT_PARAMS.eip2930Transition, + eip1559Transition: DEFAULT_PARAMS.eip1559Transition, + eip2929Transition: DEFAULT_PARAMS.eip2929Transition, + eip3198Transition: DEFAULT_PARAMS.eip3198Transition, + eip3529Transition: DEFAULT_PARAMS.eip3529Transition, + eip3541Transition: DEFAULT_PARAMS.eip3541Transition, + eip3554Transition: DEFAULT_PARAMS.eip3554Transition, + eip4399Transition: DEFAULT_PARAMS.eip4399Transition, + eip3651Transition: DEFAULT_PARAMS.eip3651Transition, + eip3855Transition: DEFAULT_PARAMS.eip3855Transition, + eip3860Transition: DEFAULT_PARAMS.eip3860Transition, + eip6049Transition: DEFAULT_PARAMS.eip6049Transition, + eip1153Transition: DEFAULT_PARAMS.eip1153Transition, + eip4844Transition: DEFAULT_PARAMS.eip4844Transition, + eip5656Transition: DEFAULT_PARAMS.eip5656Transition, + eip6780Transition: DEFAULT_PARAMS.eip6780Transition, + eip7516Transition: DEFAULT_PARAMS.eip7516Transition, + byzantiumBlock: cfg.byzantiumBlock, + eip1559ElasticityMultiplier: DEFAULT_PARAMS.eip1559ElasticityMultiplier, + }; + + // --- genesis block header --- + const block = { + nonce: genesis.nonce, + timestamp: genesis.timestamp, + extraData: genesis.extraData, + gasLimit: genesis.gasLimit, + difficulty: genesis.difficulty, + mixHash: genesis.mixHash, + coinbase: genesis.coinbase, + number: genesis.number, + gasUsed: genesis.gasUsed, + parentHash: genesis.parentHash, + baseFeePerGas: + genesis.baseFeePerGas == null + ? (opts.baseFeePerGas || DEFAULT_BASE_FEE_PER_GAS) + : genesis.baseFeePerGas, + }; + + return { + name: opts.name || DEFAULT_CHAIN_NAME, + engine: { XDPoS: { params: enginePadms } }, + params, + genesis: block, + nodes: opts.nodes || [], + accounts: genesis.alloc || {}, + }; +} + +/* ------------------------------------------------------------------ * + * CLI + * ------------------------------------------------------------------ */ + +function main(argv) { + const args = argv.slice(2); + const positional = []; + const opts = {}; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--name') { + opts.name = args[++i]; + } else if (args[i] === '--base-fee') { + opts.baseFeePerGas = args[++i]; + } else if (args[i] === '-h' || args[i] === '--help') { + console.log('Usage: node genesis-to-chainspec.js [chainspec.json] [--name ] [--base-fee <0x..>]'); + return 0; + } else { + positional.push(args[i]); + } + } + + if (positional.length < 1) { + console.error('Error: missing input genesis.json path.'); + console.error('Usage: node genesis-to-chainspec.js [chainspec.json] [--name ]'); + return 1; + } + + const inPath = path.resolve(positional[0]); + const outPath = positional[1] ? path.resolve(positional[1]) : null; + + const genesis = JSON.parse(fs.readFileSync(inPath, 'utf8')); + const chainspec = translate(genesis, opts); + const json = JSON.stringify(chainspec, null, 2) + '\n'; + + if (outPath) { + fs.writeFileSync(outPath, json); + console.error(`Wrote ${outPath} (${chainspec.params.chainId ? 'chainId ' + chainspec.params.chainId : ''}).`); + } else { + process.stdout.write(json); + } + return 0; +} + +if (require.main === module) { + process.exit(main(process.argv)); +} + +module.exports = { translate, normalizeAddress }; From d8a61d54b4038258c2b8c93cca78820a6b0bc031 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 00:43:44 +0700 Subject: [PATCH 02/59] bump nodejs to 24 --- container-manager/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-manager/Dockerfile b/container-manager/Dockerfile index 32c8cf9..e20a72a 100644 --- a/container-manager/Dockerfile +++ b/container-manager/Dockerfile @@ -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 \ From d7f6d025b8d099459eaf9954b189539b2be7de64 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 00:43:49 +0700 Subject: [PATCH 03/59] add readme --- container-manager/src/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 container-manager/src/README.md diff --git a/container-manager/src/README.md b/container-manager/src/README.md new file mode 100644 index 0000000..b896a08 --- /dev/null +++ b/container-manager/src/README.md @@ -0,0 +1 @@ +npm run dev \ No newline at end of file From 3172f54126f231bd203fb9cfbba5a512cbc1f623 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 00:44:11 +0700 Subject: [PATCH 04/59] add xdpos only mode to prevent going to the wrong url --- container-manager/src/express.js | 40 ++++++++++++++++++++++++++++++++ container-manager/start_xdpos.sh | 1 + 2 files changed, 41 insertions(+) diff --git a/container-manager/src/express.js b/container-manager/src/express.js index 76beaae..a11631a 100644 --- a/container-manager/src/express.js +++ b/container-manager/src/express.js @@ -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(); +}); + app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "views", "index.html")); }); diff --git a/container-manager/start_xdpos.sh b/container-manager/start_xdpos.sh index 867b04d..0079439 100644 --- a/container-manager/start_xdpos.sh +++ b/container-manager/start_xdpos.sh @@ -27,6 +27,7 @@ docker run -d \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $current_dir/generated:/mount/generated \ -e HOSTPWD=$current_dir/generated \ + -e NON_SUBNET=true \ xinfinorg/subnet-generator:$version \ && \ echo '' && \ From 9a2f06b40dc808cd51aa790f8c3c93e7f011ca1a Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 00:44:37 +0700 Subject: [PATCH 05/59] update genesis to chainspec converter --- .../src/libs/genesis-to-chainspec.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index b61cc52..344bad2 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -99,13 +99,22 @@ const DEFAULT_PARAMS = { // Engine constants not present in genesis.json (copied from chainspec.json). const DEFAULT_ENGINE = { mergeSignRange: 15, + RangeReturnSigner: 150, tip2019Block: 1, DynamicGasLimitBlock: 0, + TipXDCX: 0, blackListHFNumber: 0, + TipTrc21Fee: 0, + TIPXDCXMinerDisable: 0, + TIPXDCXReceiverDisable: 0, blackListedAddresses: ['0x0000000000000000000000000000000000000011'], masternodeVotingContract: '0x0000000000000000000000000000000000000088', - randomizeSMCBinary: '0x0000000000000000000000000000000000000090', blockSignerContract: '0x0000000000000000000000000000000000000089', + randomizeSMCBinary: '0x0000000000000000000000000000000000000090', + XDCXAddressBinary: '0x0000000000000000000000000000000000000091', + TradingStateAddressBinary: '0x0000000000000000000000000000000000000092', + XDCXLendingAddressBinary: '0x0000000000000000000000000000000000000093', + XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', }; /* ------------------------------------------------------------------ * @@ -149,13 +158,22 @@ function translate(genesis, opts = {}) { switchBlock: v2.switchBlock ?? v2.SwitchBlock, v2Configs, mergeSignRange: DEFAULT_ENGINE.mergeSignRange, + RangeReturnSigner: DEFAULT_ENGINE.RangeReturnSigner, tip2019Block: DEFAULT_ENGINE.tip2019Block, DynamicGasLimitBlock: DEFAULT_ENGINE.DynamicGasLimitBlock, + TipXDCX: DEFAULT_ENGINE.TipXDCX, blackListHFNumber: DEFAULT_ENGINE.blackListHFNumber, + TipTrc21Fee: DEFAULT_ENGINE.TipTrc21Fee, + TIPXDCXMinerDisable: DEFAULT_ENGINE.TIPXDCXMinerDisable, + TIPXDCXReceiverDisable: DEFAULT_ENGINE.TIPXDCXReceiverDisable, blackListedAddresses: DEFAULT_ENGINE.blackListedAddresses, masternodeVotingContract: DEFAULT_ENGINE.masternodeVotingContract, - randomizeSMCBinary: DEFAULT_ENGINE.randomizeSMCBinary, blockSignerContract: DEFAULT_ENGINE.blockSignerContract, + randomizeSMCBinary: DEFAULT_ENGINE.randomizeSMCBinary, + XDCXAddressBinary: DEFAULT_ENGINE.XDCXAddressBinary, + TradingStateAddressBinary: DEFAULT_ENGINE.TradingStateAddressBinary, + XDCXLendingAddressBinary: DEFAULT_ENGINE.XDCXLendingAddressBinary, + XDCXLendingFinalizedTradeAddressBinary: DEFAULT_ENGINE.XDCXLendingFinalizedTradeAddressBinary, }; // --- params (chain rules / EIP transitions) --- From 842279222f3a080bb38cd35c01d31a29858526b0 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 00:44:55 +0700 Subject: [PATCH 06/59] fix bootnode ip to .254 --- container-manager/src/gen/gen_compose.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/container-manager/src/gen/gen_compose.js b/container-manager/src/gen/gen_compose.js index 2e66da3..18a43b3 100644 --- a/container-manager/src/gen/gen_compose.js +++ b/container-manager/src/gen/gen_compose.js @@ -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 { From 3d43669542f2e52f75fb61a4932eba75f7fa0c5b Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 01:02:09 +0700 Subject: [PATCH 07/59] add nethermind ui option and parse to configs --- container-manager/src/gen/config_gen.js | 2 ++ container-manager/src/gen/gen_xdpos.js | 5 +++++ container-manager/src/libs/exec.js | 8 ++++++++ .../src/views/xdpos_generator/index.pug | 14 ++++++++++---- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index f1360a2..db4bc48 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -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, @@ -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: "", diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 1a5a33a..1eeb0ed 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -182,6 +182,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); diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index c7f8abe..a7e8339 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -374,6 +374,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"] != "") { diff --git a/container-manager/src/views/xdpos_generator/index.pug b/container-manager/src/views/xdpos_generator/index.pug index 9ff202d..3cfdf18 100644 --- a/container-manager/src/views/xdpos_generator/index.pug +++ b/container-manager/src/views/xdpos_generator/index.pug @@ -1,6 +1,6 @@ head meta(charset='utf-8') - title XDPoS Privatechain Generator + title XDPoS Privatechain Generator V3 style include ./pure-min.css include ./custom.css @@ -8,7 +8,7 @@ head body center - h1 XDPoS Privatechain Generator + h1 XDPoS Privatechain Generator V3 //- form.pure-form.pure-form-aligned#myForm(onsubmit="return validateForm()") form.pure-form.pure-form-aligned#myForm(action="/submit_xdpos" method="post" onsubmit="return validateForm()") @@ -81,11 +81,17 @@ body #customversion.content center .pure-control-group - label() Full Node Version + label() Node Version input#customversion-xdpos-node-fullname.pure-input(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-node-fullname") .pure-control-group - label() Full Genesis Version + label() Genesis Version input#customversion-xdpos-genesis-fullname.pure-input(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-genesis-fullname") + .pure-control-group + label() Number of Nethermind Nodes + input#customversion-xdpos-nethermind-count.pure-input(type='number' placeholder='0' name="customversion-xdpos-nethermind-count") + .pure-control-group + label() Nethermind Node Version + input#customversion-xdpos-nethermind-version.pure-input(type='text' placeholder='nethermindeth/nethermind:master-857da8f' name="customversion-xdpos-nethermind-version") .pure-control-group .info-container From 6e162f9f852be376b21c2f95b61a5a0188c56817 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 27 Jul 2026 11:02:53 +0700 Subject: [PATCH 08/59] integrate nethermind node in generator --- .gitignore | 1 + container-manager/src/gen/gen_xdpos.js | 134 ++++++++++++++---- .../src/gen/scripts/xdc-nmc.json | 33 +++++ container-manager/src/libs/exec.js | 27 ++++ .../src/libs/genesis-to-chainspec.js | 8 +- container-manager/start_xdpos.sh | 2 +- 6 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 container-manager/src/gen/scripts/xdc-nmc.json diff --git a/.gitignore b/.gitignore index ce60899..98504e1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ mount **temp *key* *keys* +test \ No newline at end of file diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 1eeb0ed..6e3b4e2 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -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, {}); @@ -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); @@ -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) { @@ -339,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 (masternodenmc.env) — Nethermind reads NETHERMIND_* env +// vars (format NETHERMIND_CONFIG_). 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 = {}; @@ -355,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 masternodenmc.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; } diff --git a/container-manager/src/gen/scripts/xdc-nmc.json b/container-manager/src/gen/scripts/xdc-nmc.json new file mode 100644 index 0000000..598b68f --- /dev/null +++ b/container-manager/src/gen/scripts/xdc-nmc.json @@ -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 + } +} diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index a7e8339..8f580e6 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -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; + 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]; } diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 344bad2..74741eb 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -54,7 +54,7 @@ const path = require('path'); * from genesis.json. Override here if the target network differs. * ------------------------------------------------------------------ */ -const DEFAULT_CHAIN_NAME = 'xdc-mine'; +const DEFAULT_CHAIN_NAME = 'xdpos-chain'; // chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has null. const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; @@ -141,6 +141,12 @@ function translate(genesis, opts = {}) { .sort((a, b) => Number(a) - Number(b)) .map((round) => { const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; // drop expTimeoutConfig + // PLACEHOLDER: per-round reward amounts zeroed until XDC reward economics + // is finalized. Also avoids fractional puppeth defaults (e.g. + // protectorReward: 45.6) that Nethermind's UInt256 parser rejects. + rest.masternodeReward = 0; + rest.protectorReward = 0; + rest.observerReward = 0; return rest; }); diff --git a/container-manager/start_xdpos.sh b/container-manager/start_xdpos.sh index 0079439..db6df76 100644 --- a/container-manager/start_xdpos.sh +++ b/container-manager/start_xdpos.sh @@ -1,5 +1,5 @@ #!/bin/bash -version="v2.1.0" +version="generator-v3.0.0" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" From 88d6325f5ebbc7cff908f325f566a1a6d2bca14c Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Wed, 29 Jul 2026 01:13:37 +0700 Subject: [PATCH 09/59] remove old source files path --- deployment-generator/docker/Dockerfile | 13 - deployment-generator/docker/README | 14 - deployment-generator/docker/start.sh | 3 - deployment-generator/scripts/add-node.sh | 195 - deployment-generator/scripts/check-mining.sh | 29 - deployment-generator/scripts/check-peer.sh | 9 - deployment-generator/scripts/csc.sh | 16 - deployment-generator/scripts/docker-down.sh | 16 - deployment-generator/scripts/docker-up.sh | 16 - deployment-generator/scripts/docker.env | 26 - .../scripts/docker.env.example | 23 - deployment-generator/scripts/faucet-server.sh | 15 - deployment-generator/scripts/faucet.sh | 17 - deployment-generator/scripts/generate.sh | 27 - deployment-generator/scripts/upgrade.sh | 1 - deployment-generator/src/config_gen.js | 221 -- deployment-generator/src/faucet.js | 176 - deployment-generator/src/gen.env | 11 - deployment-generator/src/gen.js | 213 - deployment-generator/src/gen_compose.js | 184 - deployment-generator/src/gen_env.js | 103 - deployment-generator/src/gen_other.js | 265 -- .../src/img/xdc-network-primary-logo.png | Bin 186098 -> 0 bytes .../src/img/xdc-primary-icon.png | Bin 76017 -> 0 bytes deployment-generator/src/package-lock.json | 3461 ----------------- deployment-generator/src/package.json | 18 - deployment-generator/src/ui.js | 198 - deployment-generator/src/views/about.pug | 5 - deployment-generator/src/views/custom.css | 155 - deployment-generator/src/views/index.pug | 381 -- deployment-generator/src/views/pure-min.css | 11 - deployment-generator/src/views/submit.pug | 6 - .../src/views_faucet/custom.css | 26 - .../src/views_faucet/index.pug | 95 - .../src/views_faucet/pure-min.css | 11 - 35 files changed, 5960 deletions(-) delete mode 100644 deployment-generator/docker/Dockerfile delete mode 100644 deployment-generator/docker/README delete mode 100755 deployment-generator/docker/start.sh delete mode 100755 deployment-generator/scripts/add-node.sh delete mode 100755 deployment-generator/scripts/check-mining.sh delete mode 100755 deployment-generator/scripts/check-peer.sh delete mode 100755 deployment-generator/scripts/csc.sh delete mode 100755 deployment-generator/scripts/docker-down.sh delete mode 100755 deployment-generator/scripts/docker-up.sh delete mode 100755 deployment-generator/scripts/docker.env delete mode 100755 deployment-generator/scripts/docker.env.example delete mode 100755 deployment-generator/scripts/faucet-server.sh delete mode 100755 deployment-generator/scripts/faucet.sh delete mode 100755 deployment-generator/scripts/generate.sh delete mode 100755 deployment-generator/scripts/upgrade.sh delete mode 100644 deployment-generator/src/config_gen.js delete mode 100755 deployment-generator/src/faucet.js delete mode 100644 deployment-generator/src/gen.env delete mode 100644 deployment-generator/src/gen.js delete mode 100644 deployment-generator/src/gen_compose.js delete mode 100644 deployment-generator/src/gen_env.js delete mode 100644 deployment-generator/src/gen_other.js delete mode 100644 deployment-generator/src/img/xdc-network-primary-logo.png delete mode 100644 deployment-generator/src/img/xdc-primary-icon.png delete mode 100644 deployment-generator/src/package-lock.json delete mode 100644 deployment-generator/src/package.json delete mode 100644 deployment-generator/src/ui.js delete mode 100644 deployment-generator/src/views/about.pug delete mode 100644 deployment-generator/src/views/custom.css delete mode 100644 deployment-generator/src/views/index.pug delete mode 100644 deployment-generator/src/views/pure-min.css delete mode 100644 deployment-generator/src/views/submit.pug delete mode 100644 deployment-generator/src/views_faucet/custom.css delete mode 100644 deployment-generator/src/views_faucet/index.pug delete mode 100644 deployment-generator/src/views_faucet/pure-min.css diff --git a/deployment-generator/docker/Dockerfile b/deployment-generator/docker/Dockerfile deleted file mode 100644 index eae5add..0000000 --- a/deployment-generator/docker/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:18.15 - -WORKDIR /app - -COPY ./src /app/src -COPY ./scripts /app/scripts -COPY ./docker/start.sh /app/start.sh - -RUN chmod +x /app/start.sh - -RUN cd src && npm install - -ENTRYPOINT ["bash","/app/start.sh"] diff --git a/deployment-generator/docker/README b/deployment-generator/docker/README deleted file mode 100644 index 24d02d7..0000000 --- a/deployment-generator/docker/README +++ /dev/null @@ -1,14 +0,0 @@ -#for local builds - -run from upper directory (under deployment-generator): - docker build --target builder -f docker/Dockerfile --build-arg SUBNET_BRANCH=master --no-cache . - docker build -t generator -f docker/Dockerfile --build-arg SUBNET_BRANCH=master --no-cache . - #gen configs - docker run --env-file docker.env -v $(pwd)/generated:/app/generated generator - #deploy csc (after subnet launched) - docker run \ - --env-file docker.env \ - -v $(pwd)/generated/deployment.config.json:/app/generated/deployment.config.json \ - --entrypoint 'bash' generator ./deploy_csc.sh - - diff --git a/deployment-generator/docker/start.sh b/deployment-generator/docker/start.sh deleted file mode 100755 index 4df360b..0000000 --- a/deployment-generator/docker/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -node /app/src/ui.js -# node /app/src/gen.js \ No newline at end of file diff --git a/deployment-generator/scripts/add-node.sh b/deployment-generator/scripts/add-node.sh deleted file mode 100755 index a79fe23..0000000 --- a/deployment-generator/scripts/add-node.sh +++ /dev/null @@ -1,195 +0,0 @@ -#!/bin/bash - -set -eo pipefail -cd "$(dirname "$0")" -FILE="../docker-compose.yml" - -# Detect OS type -OS_TYPE=$(uname) - -# Function to handle grep on macOS and Linux differently -function grep_command() { - if [[ "$OS_TYPE" == "Darwin" ]]; then - grep -oE "$1" "$2" - else - grep -oP "$1" "$2" - fi -} - -# Function to handle sed on macOS and Linux differently -function sed_command() { - if [[ "$OS_TYPE" == "Darwin" ]]; then - sed -i '' "$1" "$2" - else - sed -i "$1" "$2" - fi -} - -# Read the current maximum subnet ID (for incrementing) -MAX_SUBNET_ID=$(grep_command 'subnet[0-9]+' "$FILE" | sed 's/subnet//' | sort -n | tail -n 1) - -if [ -z "$MAX_SUBNET_ID" ]; then - NEW_SUBNET_ID=1 -else - NEW_SUBNET_ID=$((MAX_SUBNET_ID + 1)) -fi - -# Calculate the port numbers and IPv4 address -PORT=$((20302 + NEW_SUBNET_ID)) -RPCPORT=$((8544 + NEW_SUBNET_ID)) -WSPORT=$((9554 + NEW_SUBNET_ID)) -IPV4_ADDRESS=$((10+4 + NEW_SUBNET_ID)) - -# Function to add new subnet configuration to docker-compose.yml -function add_docker_compose_linux() { - local service_indent="" - local found_subnet=0 - local subnet_template="" - - # Pick the first subnet entry - while IFS= read -r line; do - # Check for services start - if [[ $line =~ ^[[:space:]]*services: ]]; then - service_indent=$(echo "$line" | grep -o '^[[:space:]]*') - fi - - # Check for the first subnet entry - if [[ $line =~ ^[[:space:]]*subnet[0-9]+: ]]; then - found_subnet=1 - SUBNET_INDENT=$(echo "$line" | grep -o '^[[:space:]]*') - fi - - # If in a subnet, save the content as a template - if [[ $found_subnet -eq 1 ]]; then - subnet_template+="$line\n" - if [[ $line =~ ^${SUBNET_INDENT}.*ipv4_address: ]]; then - found_subnet=0 - break - fi - fi - done < "$FILE" - - # Exit if no subnet is found - if [[ -z $subnet_template ]]; then - echo "Error: No subnet entries found in $FILE" - exit 1 - fi - - # -e "s/subnet[0-9]+/subnet$NEW_SUBNET_ID/" \ - # Modify the template to generate a new entry - NEW_SUBNET=$(echo -e "$subnet_template" | sed \ - -e "s/subnet[0-9]\+/subnet$NEW_SUBNET_ID/" \ - -e "s/xdcchain[0-9]\+/xdcchain$NEW_SUBNET_ID/" \ - -e "s/2030[0-9]\+:2030[0-9]\+/$PORT:$PORT/" \ - -e "s/854[0-9]\+:854[0-9]\+/$RPCPORT:$RPCPORT/" \ - -e "s/955[0-9]\+:955[0-9]\+/$WSPORT:$WSPORT/" \ - -e "s/192\.168\.25\.[0-9]\+/192.168.25.$IPV4_ADDRESS/") - - # Use sed to insert the new subnet configuration after "services:" - if grep -q "^services:" "$FILE"; then - # Escape backslashes and newlines for macOS compatibility - ESCAPED_SUBNET=$(echo "$NEW_SUBNET" | sed 's/[\/&]/\\&/g' | sed ':a;N;$!ba;s/\n/\\n/g') - sed_command="/^services:/a \\ -$ESCAPED_SUBNET" - sed -i.bak -e "$sed_command" "$FILE" - echo "Added new subnet${NEW_SUBNET_ID} configuration to docker-compose.yml!" - else - echo "Error: 'services:' not found in $FILE!" - exit 1 - fi -} - -function add_docker_compose_mac() { - local service_indent="" - local found_subnet=0 - local subnet_template="" - - # Pick the first subnet entry - while IFS= read -r line; do - # Check for services start - if [[ $line =~ ^[[:space:]]*services: ]]; then - service_indent=$(echo "$line" | grep -o '^[[:space:]]*') - fi - - # Check for the first subnet entry - if [[ $line =~ ^[[:space:]]*subnet[0-9]+: ]]; then - found_subnet=1 - SUBNET_INDENT=$(echo "$line" | grep -o '^[[:space:]]*') - fi - - # If in a subnet, save the content as a template - if [[ $found_subnet -eq 1 ]]; then - subnet_template+="$line\n" - if [[ $line =~ ^${SUBNET_INDENT}.*ipv4_address: ]]; then - found_subnet=0 - break - fi - fi - done < "$FILE" - - # Exit if no subnet is found - if [[ -z $subnet_template ]]; then - echo "Error: No subnet entries found in $FILE" - exit 1 - fi - - # Modify the template to generate a new entry - NEW_SUBNET=$(echo -e "$subnet_template" | sed -E \ - -e "s/subnet[0-9]+/subnet$NEW_SUBNET_ID/" \ - -e "s/xdcchain[0-9]+/xdcchain$NEW_SUBNET_ID/" \ - -e "s/2030[0-9]+:2030[0-9]+/$PORT:$PORT/" \ - -e "s/854[0-9]+:854[0-9]+/$RPCPORT:$RPCPORT/" \ - -e "s/955[0-9]+:955[0-9]+/$WSPORT:$WSPORT/" \ - -e "s/192.168.25.[0-9]+/192.168.25.$IPV4_ADDRESS/") - - # Use sed to insert the new subnet configuration after "services:" - if grep -q "^services:" "$FILE"; then - # Escape backslashes and newlines for macOS compatibility - ESCAPED_SUBNET=$(echo "$NEW_SUBNET" | sed 's/[\/&]/\\&/g') - ESCAPED_SUBNET="${ESCAPED_SUBNET//$'\n'/*}*" - sed_command="/^services:/a\\ -$ESCAPED_SUBNET" - cp ../docker-compose.yml ../docker-compose.yml.bak - sed -i '' "$sed_command" "$FILE" - sed -i '' "s/*/\n/g" "$FILE" - echo "Added new subnet${NEW_SUBNET_ID} configuration to docker-compose.yml!" - else - echo "Error: 'services:' not found in $FILE!" - exit 1 - fi -} - -# Function to create a new .env file and modify its content -function create_new_env_file() { - read -p "Input private_key: " private_key - local source_file="../subnet1.env" - local new_file="../subnet${NEW_SUBNET_ID}.env" - - # Check if the source file exists - if [ ! -f "$source_file" ]; then - echo "Source file $source_file does not exist!" - exit 1 - fi - - # Read the source file content and modify the specified entry - cp "$source_file" "$new_file" - - # Modify the specified entries - sed_command "s/^INSTANCE_NAME=.*$/INSTANCE_NAME=${NEW_SUBNET_ID}/" "$new_file" - sed_command "s/^PRIVATE_KEY=.*$/PRIVATE_KEY=${private_key}/" "$new_file" - sed_command "s/^PORT=.*$/PORT=${PORT}/" "$new_file" - sed_command "s/^RPCPORT=.*$/RPCPORT=${RPCPORT}/" "$new_file" - sed_command "s/^WSPORT=.*$/WSPORT=${WSPORT}/" "$new_file" - sed_command "s/^LOG_LEVEL=.*$/LOG_LEVEL=2/" "$new_file" - - echo "New subnet${NEW_SUBNET_ID}.env file created!" -} - -# Execute functions -if [[ "$OS_TYPE" == "Darwin" ]]; then - add_docker_compose_mac -else - add_docker_compose_linux -fi - -create_new_env_file diff --git a/deployment-generator/scripts/check-mining.sh b/deployment-generator/scripts/check-mining.sh deleted file mode 100755 index fd7345f..0000000 --- a/deployment-generator/scripts/check-mining.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -echo "getting latest block" -resp=$(curl -s --location 'http://localhost:8545' \ ---header 'Content-Type: application/json' \ ---data '{"jsonrpc":"2.0","method":"XDPoS_getV2BlockByNumber","params":["latest"],"id":1}') - -num=$(echo $resp | grep -o '"Number":[0-9]*' | cut -d':' -f2 | tr -d ' ') -echo $num - -if [[ $num == "null" ]] || [[ $num == "" ]]; then - echo "no block has been mined, please check if nodes are peering properly" - exit -else - for i in 2 3 4 - do - sleep 3 - echo "getting latest block $i" - resp=$(curl -s --location 'http://localhost:8545' \ - --header 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","method":"XDPoS_getV2BlockByNumber","params":["latest"],"id":1}') - nextnum=$(echo $resp | grep -o '"Number":[0-9]*' | cut -d':' -f2 | tr -d ' ') - echo $nextnum - done -fi - -if [[ $nextnum > $num ]]; then - echo "subnet successfully running and mining blocks" -fi diff --git a/deployment-generator/scripts/check-peer.sh b/deployment-generator/scripts/check-peer.sh deleted file mode 100755 index 1366183..0000000 --- a/deployment-generator/scripts/check-peer.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -resp=$(curl -s --location "http://localhost:8545" \ ---header 'Content-Type: application/json' \ ---data '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}') -echo $resp -num_peers=$(echo $resp | grep -o '"result":"[^"]*"' | cut -d'"' -f4) -num_peers_dec=$(printf "%d\n" $num_peers) -echo "peers: $num_peers_dec" \ No newline at end of file diff --git a/deployment-generator/scripts/csc.sh b/deployment-generator/scripts/csc.sh deleted file mode 100755 index acec461..0000000 --- a/deployment-generator/scripts/csc.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")" - -if [[ -z $1 ]] || [[ -z $2 ]]; then - echo "Missing argument" - echo "Usage: csc.sh VERSION MODE" - echo "Version: latest or check documentation for available versions" - echo "Mode: lite, full, or reverse" - exit -fi - - -docker pull xinfinorg/csc:$1 -docker run --env-file ../contract_deploy.env --network generated_docker_net xinfinorg/csc:$1 $2 - diff --git a/deployment-generator/scripts/docker-down.sh b/deployment-generator/scripts/docker-down.sh deleted file mode 100755 index e7d6c2b..0000000 --- a/deployment-generator/scripts/docker-down.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")" - - -if [[ -z $1 ]]; then - echo "Missing argument" - echo "Usage: docker-down.sh PROFILE" - echo "Profile:" - echo " machine1 - subnet nodes" - echo " services - relayer, backend, frontend" - exit -fi - -docker compose --profile $1 down - diff --git a/deployment-generator/scripts/docker-up.sh b/deployment-generator/scripts/docker-up.sh deleted file mode 100755 index f0f3801..0000000 --- a/deployment-generator/scripts/docker-up.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")" - -if [[ -z $1 ]]; then - echo "Missing argument" - echo "Usage: docker-up.sh PROFILE" - echo "Profile:" - echo " machine1 - subnet nodes" - echo " services - relayer, backend, frontend" - exit -fi - -docker compose --env-file --profile $1 pull -docker compose --env-file --profile $1 up -d - diff --git a/deployment-generator/scripts/docker.env b/deployment-generator/scripts/docker.env deleted file mode 100755 index 840295d..0000000 --- a/deployment-generator/scripts/docker.env +++ /dev/null @@ -1,26 +0,0 @@ -#current directory -CONFIG_PATH=/Users/wp/Git/self/self/testgen -#subnet config -NETWORK_NAME=localsubnet -NUM_MACHINE=1 -NUM_SUBNET=3 -MAIN_IP=192.168.1.1 -# optional -# PUBLIC_IP - -#parentchain config -PARENTNET=testnet -PARENTNET_WALLET_PK=0xb2c9249d28dc78f2f4be8567f7573854a2bc3ce2ace8ad886f5dd40fdb36ae1b - -#optional -GRANDMASTER_PK=0xdfc35b23e7afb972c7f9b8600e3fd58d15b4f7e2c50d4bffa60b955a9f03350b - - -OS=mac - -# docker image versions, default to stable -# VERSION_SUBNET=v0.2.2 -# VERSION_BOOTNODE=v0.2.2 -# VERSION_RELAYER -# VERSION_STATS -# VERSION_FRONTEND \ No newline at end of file diff --git a/deployment-generator/scripts/docker.env.example b/deployment-generator/scripts/docker.env.example deleted file mode 100755 index ed95e31..0000000 --- a/deployment-generator/scripts/docker.env.example +++ /dev/null @@ -1,23 +0,0 @@ -#subnet config -NETWORK_NAME=localsubnet -NUM_MACHINE=1 -NUM_SUBNET=3 -MAIN_IP=192.168.1.1 -# optional -# PUBLIC_IP - -#parentchain config -PARENTNET=testnet -PARENTNET_WALLET_PK=0x1111111111111111111111111111111111111111111111111111111111111111 - - - - -#OS=mac - -# docker image versions, default to stable -# VERSION_SUBNET -# VERSION_BOOTNODE -# VERSION_RELAYER -# VERSION_STATS -# VERSION_FRONTEND \ No newline at end of file diff --git a/deployment-generator/scripts/faucet-server.sh b/deployment-generator/scripts/faucet-server.sh deleted file mode 100755 index 63a0b17..0000000 --- a/deployment-generator/scripts/faucet-server.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -VERSION_GENERATOR="v1.0.0" - -cd "$(dirname "$0")" -cd .. - -read -p "Input Grandmaster PK (or any source wallet with funds): " SOURCE_PK - -extra="" -if [[ "$OSTYPE" == "darwin"* ]]; then - extra="--network generated_docker_net" -fi - -docker pull xinfinorg/subnet-generator:$VERSION_GENERATOR -docker run --env-file common.env --entrypoint node -p 5211:5211 $extra xinfinorg/subnet-generator:$VERSION_GENERATOR /app/src/faucet.js server $SOURCE_PK diff --git a/deployment-generator/scripts/faucet.sh b/deployment-generator/scripts/faucet.sh deleted file mode 100755 index a50ffc9..0000000 --- a/deployment-generator/scripts/faucet.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -VERSION_GENERATOR="v1.0.0" - -cd "$(dirname "$0")" -cd .. - -read -p "Input Grandmaster PK (or any source wallet with funds): " SOURCE_PK -read -p "Input destination wallet address: " DEST_ADDR -read -p "Input transfer amount: " AMOUNT - -extra="" -if [[ "$OSTYPE" == "darwin"* ]]; then - extra="--network generated_docker_net" -fi - -docker pull xinfinorg/subnet-generator:$VERSION_GENERATOR -docker run --env-file common.env --entrypoint node $extra xinfinorg/subnet-generator:$VERSION_GENERATOR /app/src/faucet.js $SOURCE_PK $DEST_ADDR $AMOUNT \ No newline at end of file diff --git a/deployment-generator/scripts/generate.sh b/deployment-generator/scripts/generate.sh deleted file mode 100755 index 2214f11..0000000 --- a/deployment-generator/scripts/generate.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -VERSION_GENERATOR="v1.1.0" -VERSION_GENESIS="v0.3.1" - -current_dir="$(cd "$(dirname "$0")" && pwd)" - -echo 'pull docker images' -docker pull xinfinorg/subnet-generator:$VERSION_GENERATOR -docker pull xinfinorg/xdcsubnets:$VERSION_GENESIS - - -echo '' -echo 'go to http://localhost:5210 to access Subnet Configuration Generator UI' -echo 'or use ssh tunnel if this is running on your server (if using VSCode to SSH, the Remote Explorer will automatically deploy this tunnel)' -echo 'ssh -N -L localhost:5210:localhost:5210 @ -i ' -mkdir -p generated/scripts -docker run -p 5210:5210 -v $current_dir/generated:/app/generated xinfinorg/subnet-generator:$VERSION_GENERATOR - - -echo 'generating genesis.json' -docker run -v $current_dir/generated/:/app/generated/ --entrypoint 'bash' xinfinorg/xdcsubnets:$VERSION_GENESIS /work/puppeth.sh || pup_success=false -if [[ $pup_success == false ]]; then - echo 'genesis.json generation failed' - exit 1 -fi - -echo 'subnet generation successful' diff --git a/deployment-generator/scripts/upgrade.sh b/deployment-generator/scripts/upgrade.sh deleted file mode 100755 index 62b7e0a..0000000 --- a/deployment-generator/scripts/upgrade.sh +++ /dev/null @@ -1 +0,0 @@ -wip \ No newline at end of file diff --git a/deployment-generator/src/config_gen.js b/deployment-generator/src/config_gen.js deleted file mode 100644 index 581191a..0000000 --- a/deployment-generator/src/config_gen.js +++ /dev/null @@ -1,221 +0,0 @@ -const crypto = require("crypto"); -const net = require("net"); -const dotenv = require("dotenv"); -const ethers = require("ethers"); -dotenv.config({ path: `${__dirname}/gen.env` }); -// console.log(__dirname) - -const config = { - deployment_path: process.env.CONFIG_PATH || "", - num_machines: parseInt(process.env.NUM_MACHINE), - num_subnet: parseInt(process.env.NUM_SUBNET), - ip_1: process.env.MAIN_IP || "", - public_ip: process.env.PUBLIC_IP || process.env.MAIN_IP, - network_name: process.env.NETWORK_NAME, - network_id: parseInt( - process.env.NETWORK_ID || Math.floor(Math.random() * (65536 - 1) + 1) - ), - secret_string: - process.env.SERVICES_SECRET || crypto.randomBytes(10).toString("hex"), - relayer_mode: process.env.RELAYER_MODE || "full", - docker_image_name: - process.env.IMAGE_NAME || "xinfinorg/subnet-generator:latest", - version: { - subnet: process.env.VERSION_SUBNET || "v0.3.1", - bootnode: process.env.VERSION_BOOTNODE || "v0.3.1", - relayer: process.env.VERSION_RELAYER || "v0.3.1", - stats: process.env.VERSION_STATS || "v0.1.11", - frontend: process.env.VERSION_FRONTEND || "v0.1.12", - // csc: process.env.VERSION_CSC || "v0.3.0", - csc: process.env.VERSION_CSC || "feature-v0.3.0", - // zero: process.env.VERSION_ZERO || "v0.3.0", - zero: process.env.VERSION_ZERO || "feature-v0.3.0", - subswap_frontend: - process.env.VERSION_SUBSWAP_FRONTEND || "feature-dockerize", - explorer: process.env.VERSION_EXPLORER || "v0.1.0", - }, - parentnet: { - network: process.env.PARENTNET || "testnet", - url: "", - pubkey: "", - privatekey: process.env.PARENTNET_WALLET_PK || "", - }, - keys: { - subnets_addr: [], - subnets_pk: process.env.SUBNETS_PK || "", - grandmaster_addr: "", - grandmaster_pk: process.env.GRANDMASTER_PK || "", - }, - zero: { - zero_mode: process.env.XDC_ZERO || "", - subnet_wallet_pk: process.env.SUBNET_WALLET_PK || "", - subnet_zero_wallet_pk: process.env.SUBNET_ZERO_WALLET_PK || "", - parentnet_zero_wallet_pk: process.env.PARENTNET_ZERO_WALLET_PK || "", - subswap: process.env.SUBSWAP || "", - }, - generator: { - output_path: `${__dirname}/../generated/`, - }, -}; - -if (configSanityCheck(config) === true) { - module.exports = config; -} else { - console.log("bad config init file, please check again"); - process.exit(); -} - -function validatePK(private_key) { - let wallet = new ethers.Wallet(private_key); - return [wallet.address, wallet.privateKey]; -} - -function configSanityCheck(config) { - if (!config.num_machines || !config.num_subnet || !config.network_name) { - console.log("NUM_MACHINE and NUM_SUBNET and NETWORK_NAME must be set"); - process.exit(1); - } - - if (config.num_machines < 1 || config.num_subnet < 1) { - console.log("NUM_MACHINE and NUM_SUBNET must be 1 or more"); - process.exit(1); - } - - if (!net.isIP(config.ip_1)) { - console.log("MAIN_IP Invalid IP address"); - process.exit(1); - } - - if (!net.isIP(config.public_ip)) { - console.log("PUBLIC_IP Invalid IP address"); - process.exit(1); - } - - if (!config.network_name || config.network_name === "") { - console.log("NETWORK_NAME cannot be empty"); - process.exit(1); - } - - if (config.network_id < 1 || config.network_id >= 65536) { - console.log("NETWORK_ID should be in range of 1 to 65536"); - process.exit(1); - } - - if (config.secret_string === "") { - console.log("SERVICES_SECRET cannot be empty string"); - process.exit(1); - } - - if (!(config.relayer_mode === "full" || config.relayer_mode === "lite")) { - console.log("RELAYER_MODE only accepts 'full' or 'lite' (default full)"); - process.exit(1); - } - - if ( - config.parentnet.network === "devnet" || - config.parentnet.network === "testnet" || - config.parentnet.network === "mainnet" - ) { - let official_urls = { - devnet: "https://devnetstats.apothem.network/devnet", - testnet: "https://erpc.apothem.network/", - mainnet: "https://rpc.xdc.org", - }; - config.parentnet.url = official_urls[config.parentnet.network]; - } else { - console.log("PARENTNET must be devnet or testnet"); - process.exit(1); - } - - if (config.parentnet.privatekey !== "") { - try { - config.parentnet.pubkey = validatePK(config.parentnet.privatekey)[0]; - } catch { - console.log("Invalid PARENTNET_WALLET_PK"); - process.exit(1); - } - } - - if (config.keys.grandmaster_pk !== "") { - try { - [config.keys.grandmaster_addr, config.keys.grandmaster_pk] = validatePK( - config.keys.grandmaster_pk - ); - } catch { - console.log("Invalid GRANDMASTER_PK"); - process.exit(1); - } - } else { - const privatekey = crypto.randomBytes(32).toString("hex"); - [config.keys.grandmaster_addr, config.keys.grandmaster_pk] = - validatePK(privatekey); - } - - if (config.keys.subnets_pk !== "") { - try { - let output_pk = []; - let output_wallet = []; - let pks = config.keys.subnets_pk.split(","); - pks.forEach((pk) => { - const [address, private_key] = validatePK(pk); - output_wallet.push(address); - output_pk.push(private_key); - }); - config.keys.subnets_pk = output_pk; - config.keys.subnets_addr = output_wallet; - } catch { - console.log( - "Invalid SUBNETS_PK please make sure keys are correct length, comma separated with no whitespace or invalid characters" - ); - process.exit(1); - } - - if (config.keys.subnets_addr.length !== config.num_subnet) { - console.log( - `number of keys in SUBNETS_PK (${config.keys.subnets_addr.length}) does not match NUM_SUBNET (${config.num_subnet})` - ); - process.exit(1); - } - - const setPK = new Set(config.keys.subnets_pk); - if (setPK.size !== config.keys.subnets_pk.length) { - console.log("found duplicate keys in SUBNETS_PK"); - process.exit(1); - } - } else { - let output_pk = []; - let output_wallet = []; - for (let i = 0; i < config.num_subnet; i++) { - const privatekey = crypto.randomBytes(32).toString("hex"); - const [address, private_key] = validatePK(privatekey); - output_wallet.push(address); - output_pk.push(private_key); - } - config.keys.subnets_pk = output_pk; - config.keys.subnets_addr = output_wallet; - } - - if (config.zero.zero_mode == "one-directional") { - try { - validatePK(config.zero.parentnet_zero_wallet_pk); - } catch { - console.log("Invalid PARENTNET_ZERO_WALLET_PK"); - process.exit(1); - } - } - - if (config.zero.zero_mode === "bi-directional") { - try { - validatePK(config.zero.subnet_wallet_pk); - validatePK(config.zero.subnet_zero_wallet_pk); - validatePK(config.zero.parentnet_zero_wallet_pk); - } catch { - console.log( - "Invalid SUBNET_WALLET_PK or SUBNET_ZERO_WALLET_PK or PARENTNET_ZERO_WALLET_PK" - ); - process.exit(1); - } - } - - return true; -} diff --git a/deployment-generator/src/faucet.js b/deployment-generator/src/faucet.js deleted file mode 100755 index 3113b14..0000000 --- a/deployment-generator/src/faucet.js +++ /dev/null @@ -1,176 +0,0 @@ -process.chdir(__dirname); -const fs = require("fs"); -const ethers = require("ethers"); -const path = require("path"); -const { error } = require("console"); -const express = require("express"); -const app = express(); -const router = express.Router(); - -const usage = - "Usage: SUBNET_URL= node faucet.js "; -const subnetURL = process.env.SUBNET_URL || ""; -if (subnetURL == "") { - console.log("Invalid SUBNET_URL"); - console.log(usage); - process.exit(); -} -console.log("RPC url:", subnetURL); - -const inputs = process.argv; -// console.log("inputs", inputs) - -if (inputs.length == 5) { - try { - processTransfer(inputs).then((output) => { - // console.log(output) - process.exit(); - }); - } catch (error) { - console.log(error); - console.log(usage); - process.exit(); - } -} else if (inputs.length == 4) { - faucetServer(inputs); -} else { - console.log("Invalid input length"); - console.log(usage); - process.exit(); -} - -async function processTransfer(inputs) { - fromPK = inputs[2]; - toAddress = inputs[3]; - amount = inputs[4]; - const provider = new ethers.JsonRpcProvider(subnetURL); - const fromWallet = new ethers.Wallet(fromPK, provider); - let tx = { - to: toAddress, - value: ethers.parseEther(amount), - }; - - try { - await provider._detectNetwork(); - } catch (error) { - throw Error("Cannot connect to RPC"); - } - - let sendPromise = fromWallet.sendTransaction(tx); - txHash = await sendPromise.then((tx) => { - return tx.hash; - }); - console.log("TX submitted, confirming TX execution, txhash:", txHash); - - let receipt; - let count = 0; - while (!receipt) { - count++; - // console.log("tx receipt check loop", count); - if (count > 60) { - throw Error("Timeout: transaction did not execute after 60 seconds"); - } - await sleep(1000); - let receipt = await provider.getTransactionReceipt(txHash); - if (receipt && receipt.status == 1) { - console.log("Successfully transferred", amount, "subnet token"); - let fromBalance = await provider.getBalance(fromWallet.address); - fromBalance = ethers.formatEther(fromBalance); - let toBalance = await provider.getBalance(toAddress); - toBalance = ethers.formatEther(toBalance); - console.log("Current balance"); - console.log(`${fromWallet.address}: ${fromBalance}`); - console.log(`${toAddress}: ${toBalance}`); - return { - fromBalance: fromBalance, - toBalance: toBalance, - txHash: txHash, - }; - } - } -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function faucetServer(inputs) { - try { - command = inputs[2]; - fromPK = inputs[3]; - fromWallet = new ethers.Wallet(fromPK); - - if (command != "server") { - throw Error("Invalid command"); - } - } catch (error) { - console.log(error); - console.log( - "Usage: SUBNET_URL= node faucet.js server " - ); - process.exit(); - } - app.set("view engine", "pug"); - app.set("views", path.join(__dirname, "views_faucet")); - router.use( - express.urlencoded({ - extended: true, - }) - ); - app.use(express.static("css")); - - router.get("/", (req, res) => { - res.render("index", {}); - }); - - router.get("/address", (req, res) => { - const randomWallet = ethers.Wallet.createRandom(); - res.json({ - publicKey: randomWallet.address, - privateKey: randomWallet.privateKey, - }); - }); - - router.get("/source", (req, res) => { - res.json({ - source: fromWallet.address, - }); - }); - - router.get("/faucet", async function (req, res) { - console.log(req.query); - try { - toAddress = req.query.dest; - amount = req.query.amount; - if (!ethers.isAddress(toAddress)) - throw Error("Invalid destination address"); - if (isNaN(Number(amount)) || parseFloat(amount) <= 0 || amount == "") - throw Error("Invalid Amount"); - if (parseFloat(amount) > 1000000) - throw Error("Faucet request over 1,000,000 is not allowed"); - let inputs = ["", "", fromPK, toAddress, amount]; - const { fromBalance, toBalance, txHash } = await processTransfer(inputs); - res.json({ - success: true, - sourceAddress: fromWallet.address, - destAddress: toAddress, - sourceBalance: fromBalance, - destBalance: toBalance, - txHash: txHash, - }); - } catch (error) { - console.log(error); - console.log(error.message); - res.json({ - success: false, - message: error.message, - }); - } - }); - - app.use("/", router); - - app.listen(process.env.port || 5211); - console.log("Running at Port 5211"); - console.log("visit: http://127.0.0.1:5211"); -} diff --git a/deployment-generator/src/gen.env b/deployment-generator/src/gen.env deleted file mode 100644 index 2d078ac..0000000 --- a/deployment-generator/src/gen.env +++ /dev/null @@ -1,11 +0,0 @@ - -NETWORK_NAME=gg -NUM_SUBNET=3 -PARENTNET=testnet -PARENTNET_WALLET_PK=0x4da0711c4780696f725dc6ad44751b6a9e15a80a0bcb41eca6bf4510de64172f -RELAYER_MODE=full - -MAIN_IP=127.0.0.1 -NUM_MACHINE=1 - - diff --git a/deployment-generator/src/gen.js b/deployment-generator/src/gen.js deleted file mode 100644 index c43ae75..0000000 --- a/deployment-generator/src/gen.js +++ /dev/null @@ -1,213 +0,0 @@ -const fs = require("fs"); -const yaml = require("js-yaml"); -const { exit } = require("process"); -const gen_compose = require("./gen_compose"); -const gen_env = require("./gen_env"); -const gen_other = require("./gen_other"); -const configModule = require("./config_gen"); -const config = configModule.config; -Object.freeze(config); -// console.log(config) - -// const num_machines = config.num_machines -// const num_subnet = config.num_subnet -// const ip_1 = config.ip_1 -// const network_name = config.network_name -// const network_id = config.network_id -// const secret_string = config.secret_string -// const output_path = `${__dirname}/../generated/` - -const keys = gen_other.genSubnetKeys(); - -const num_per_machine = Array(config.num_machines); -// integer division -for (let i = 0; i < config.num_machines; i++) { - num_per_machine[i] = Math.floor(config.num_subnet / config.num_machines); -} -// divide up the remainder -for (let i = 0; i < config.num_subnet % config.num_machines; i++) { - num_per_machine[i]++; -} -num_per_machine.reverse(); // let first machines host services, put fewer subnets - -// gen docker-compose -let doc = { - // version: "3.7", //docker deprecated attribute - services: {}, -}; - -let start_num = 1; -for (let i = 1; i <= config.num_machines; i++) { - const subnet_nodes = gen_compose.genSubnetNodes( - (machine_id = i), - (num = num_per_machine[i - 1]), - (start_num = start_num) - ); - start_num += num_per_machine[i - 1]; - Object.entries(subnet_nodes).forEach((entry) => { - const [key, value] = entry; - doc["services"][key] = value; - }); -} - -// gen subnets configs -const subnet_services = gen_compose.genServices((machine_id = 1)); -Object.entries(subnet_services).forEach((entry) => { - const [key, value] = entry; - doc["services"][key] = value; -}); - -const subswap_compose = gen_compose.genSubswapFrontend(); -Object.entries(subswap_compose).forEach((entry) => { - const [key, value] = entry; - doc["services"][key] = value; -}); - -const explorer_compose = gen_compose.genExplorer(); -Object.entries(explorer_compose).forEach((entry) => { - const [key, value] = entry; - doc["services"][key] = value; -}); - -// checkpoint smartcontract deployment config -doc, (ip_record = gen_compose.injectNetworkConfig(doc)); -const commonconf = gen_env.genServicesConfig(ip_record); -subnetconf = []; -for (let i = 1; i <= config.num_subnet; i++) { - subnetconf.push(gen_env.genSubnetConfig(i, keys, ip_record)); -} -const deployconf = gen_env.genContractDeployEnv(ip_record); - -const compose_content = yaml.dump(doc, {}); - -// deployment commands list -const commands = gen_other.genCommands(); -const genesis_input = gen_other.genGenesisInputFile( - config.network_name, - config.network_id, - config.num_subnet, - keys -); -const genesis_input_file = yaml.dump(genesis_input, {}); - -writeGenerated(config.generator.output_path); -copyScripts(config.generator.output_path); - -console.log("gen successful, follow the instructions in command.txt"); - -function writeGenerated(output_dir) { - // writing files - // fs.rmSync(`${output_path}`, { recursive: true, force: true }); //wont work with docker mount - // fs.mkdirSync(`${output_path}`) //won't work with docker mount - fs.writeFileSync(`${output_dir}/placeholder.txt`, "-", (err) => { - if (err) { - console.error(err); - exit(); - } - }); - - fs.writeFileSync( - `${output_dir}/docker-compose.yml`, - compose_content, - (err) => { - if (err) { - console.error(err); - exit(); - } - } - ); - - fs.writeFileSync(`${output_dir}/common.env`, commonconf, (err) => { - if (err) { - console.error(err); - exit(); - } - }); - - fs.writeFileSync(`${output_dir}/contract_deploy.env`, deployconf, (err) => { - if (err) { - console.error(err); - exit(); - } - }); - - const keys_json = JSON.stringify(keys, null, 2); - fs.writeFile(`${output_dir}/keys.json`, keys_json, (err) => { - if (err) { - console.error("Error writing key file:", err); - exit(); - } - }); - - for (let i = 1; i <= config.num_subnet; i++) { - fs.writeFileSync( - `${output_dir}/subnet${i}.env`, - subnetconf[i - 1], - (err) => { - if (err) { - console.error(err); - exit(); - } - } - ); - } - - fs.writeFileSync( - `${output_dir}/genesis_input.yml`, - genesis_input_file, - (err) => { - if (err) { - console.error(err); - exit(); - } - } - ); - - fs.writeFileSync(`${output_dir}/commands.txt`, commands, (err) => { - if (err) { - console.error(err); - exit(); - } - }); -} - -function copyScripts(output_dir) { - fs.writeFileSync(`${output_dir}/scripts/placeholder.txt`, "-", (err) => { - if (err) { - console.error(err); - exit(); - } - }); - fs.copyFileSync( - `${__dirname}/../scripts/check-mining.sh`, - `${output_dir}/scripts/check-mining.sh` - ); - fs.copyFileSync( - `${__dirname}/../scripts/check-peer.sh`, - `${output_dir}/scripts/check-peer.sh` - ); - // fs.copyFileSync( - // `${__dirname}/../scripts/docker-up.sh`, - // `${output_dir}/scripts/docker-up.sh` - // ); - // fs.copyFileSync( - // `${__dirname}/../scripts/docker-down.sh`, - // `${output_dir}/scripts/docker-down.sh` - // ); - // fs.copyFileSync( - // `${__dirname}/../scripts/csc.sh`, - // `${output_dir}/scripts/csc.sh` - // ); - fs.copyFileSync( - `${__dirname}/../scripts/faucet.sh`, - `${output_dir}/scripts/faucet.sh` - ); - fs.copyFileSync( - `${__dirname}/../scripts/faucet-server.sh`, - `${output_dir}/scripts/faucet-server.sh` - ); - fs.copyFileSync( - `${__dirname}/../scripts/add-node.sh`, - `${output_dir}/scripts/add-node.sh` - ); -} diff --git a/deployment-generator/src/gen_compose.js b/deployment-generator/src/gen_compose.js deleted file mode 100644 index b2ab4fd..0000000 --- a/deployment-generator/src/gen_compose.js +++ /dev/null @@ -1,184 +0,0 @@ -const net = require("net"); -const configModule = require("./config_gen"); -const config = configModule.config; -Object.freeze(config); - -module.exports = { - genSubnetNodes, - genBootNode, - genServices, - injectNetworkConfig, - genSubswapFrontend, - genExplorer, -}; - -function genSubnetNodes(machine_id, num, start_num = 1) { - let subnet_nodes = {}; - for (let i = start_num; i < start_num + num; i++) { - const node_name = "subnet" + i.toString(); - const volume = "${PWD}/xdcchain" + i.toString() + ":/work/xdcchain"; - const config_path = "subnet" + i.toString() + ".env"; - const compose_profile = "machine" + machine_id.toString(); - const port = 20302 + i; - const rpcport = 8544 + i; - const wsport = 9554 + i; - subnet_nodes[node_name] = { - image: `xinfinorg/xdcsubnets:${config.version.subnet}`, - volumes: [volume, "${PWD}/genesis.json:/work/genesis.json"], - 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`, - ], - }; - } - return subnet_nodes; -} - -function genBootNode(machine_id) { - let config_path = "common.env"; - const machine = "machine" + machine_id.toString(); - const bootnode = { - image: `xinfinorg/xdcsubnets:${config.version.bootnode}`, - restart: "always", - env_file: config_path, - volumes: ["${PWD}/bootnodes:/work/bootnodes"], - entrypoint: ["bash", "/work/start-bootnode.sh"], - command: ["-verbosity", "6", "-nodekey", "bootnode.key"], - ports: ["20301:20301/tcp", "20301:20301/udp"], - profiles: [machine], - }; - return bootnode; -} - -function genServices(machine_id) { - const config_path = "common.env"; - const machine = "services"; - const volume_path = "${PWD}" + "/" + config_path; - const frontend = { - image: `xinfinorg/subnet-frontend:${config.version.frontend}`, - restart: "always", - env_file: config_path, // not used directly (injected via volume) but required to trigger restart if common.env changes - volumes: [`${volume_path}:/app/.env.local`], - ports: ["5214:5214"], - profiles: [machine], - }; - const relayer = { - image: `xinfinorg/xdc-relayer:${config.version.relayer}`, - restart: "always", - env_file: config_path, - ports: ["5215:5215"], - profiles: [machine], - }; - const stats = { - image: `xinfinorg/subnet-stats-service:${config.version.stats}`, - restart: "always", - env_file: config_path, - volumes: ["${PWD}/stats-service/logs:/app/logs"], - ports: ["5213:5213"], - profiles: [machine], - }; - const bootnode = genBootNode(machine_id); - // observer=genObserver(machine_id), - - const services = { - bootnode, - // observer, - relayer, - stats, - frontend, - }; - - return services; -} - -function genSubswapFrontend() { - const subswap_frontend = { - image: `xinfinorg/subswap-frontend:${config.version.subswap_frontend}`, - restart: "always", - volumes: [ - "${PWD}/subswap-frontend.config.json:/app/subswap-frontend.config.json", - ], - ports: ["5216:5216"], - profiles: ["subswap"], - }; - const subswap = { - subswap_frontend, - }; - return subswap; -} - -function genExplorer() { - const explorer_db = {}; - const explorer_ui = {}; - const explorer_indexer = {}; - - const explorer = { - // db: explorer_db, - // ui: explorer_ui, - // idx: explorer_indexer, - }; - return explorer; -} - -function injectNetworkConfig(compose_object) { - // networks: - // docker_net: - // driver: bridge - // ipam: - // config: - // - subnet: 192.168.25.0/24 - const network = { - docker_net: { - external: true, - driver: "bridge", - ipam: { - config: [{ subnet: "192.168.25.0/24" }], - }, - }, - }; - // networks: - // docker_net: - // ipv4_address: - // 192.168.25.10 - - let record_services_ip = {}; - - const ip_string_base = "192.168.25."; - let start_ip_subnet = 11; - let start_ip_service = 51; - Object.entries(compose_object["services"]).forEach((entry) => { - const [key, value] = entry; - let component_ip; - if (key.startsWith("subnet")) { - component_ip = ip_string_base + parseInt(start_ip_subnet); - start_ip_subnet += 1; - } else { - component_ip = ip_string_base + parseInt(start_ip_service); - start_ip_service += 1; - } - if (!net.isIP(component_ip)) { - console.log(`ERROR: found invalid IP assignment ${component_ip}`); - process.exit(1); - } - const component_network = { - docker_net: { - ipv4_address: component_ip, - }, - }; - compose_object["services"][key]["networks"] = component_network; - delete compose_object["services"][key]["network_mode"]; - record_services_ip[key] = component_ip; - }); - - compose_object["networks"] = network; - - return compose_object, record_services_ip; -} diff --git a/deployment-generator/src/gen_env.js b/deployment-generator/src/gen_env.js deleted file mode 100644 index 2dd9c52..0000000 --- a/deployment-generator/src/gen_env.js +++ /dev/null @@ -1,103 +0,0 @@ -const configModule = require("./config_gen"); -const config = configModule.config; -Object.freeze(config); - -module.exports = { - genSubnetConfig, - genServicesConfig, - genContractDeployEnv, -}; - -function genSubnetConfig(subnet_id, key, ip_record) { - const key_name = `key${subnet_id}`; - let private_key = key[key_name]["PrivateKey"]; - private_key = private_key.slice(2, private_key.length); // remove 0x for subnet conf - const port = 20303 + subnet_id - 1; - const rpcport = 8545 + subnet_id - 1; - const wsport = 9555 + subnet_id - 1; - const bootnode_ip = - config.num_machines === 1 ? ip_record["bootnode"] : config.ip_1; - const stats_ip = config.num_machines === 1 ? ip_record["stats"] : config.ip_1; - const config_env = ` -INSTANCE_NAME=subnet${subnet_id} -PRIVATE_KEY=${private_key} -BOOTNODES=enode://cc566d1033f21c7eb0eb9f403bb651f3949b5f63b40683917\ -765c343f9c0c596e9cd021e2e8416908cbc3ab7d6f6671a83c85f7b121c1872f8be\ -50a591723a5d@${bootnode_ip}:20301 -NETWORK_ID=${config.network_id} -SYNC_MODE=full -RPC_API=db,eth,debug,miner,net,shh,txpool,personal,web3,XDPoS -STATS_SERVICE_ADDRESS=${stats_ip}:5213 -STATS_SECRET=${config.secret_string} -PORT=${port} -RPCPORT=${rpcport} -WSPORT=${wsport} -LOG_LEVEL=2 -`; - - return config_env; -} - -function genServicesConfig() { - const url = config.parentnet.url; - const bootnode_ip = - config.num_machines === 1 ? ip_record["bootnode"] : config.ip_1; - const subnet_ip = - config.num_machines === 1 ? ip_record["subnet1"] : config.ip_1; - let config_env = ` -# Bootnode -EXTIP=${bootnode_ip} -BOOTNODE_PORT=20301 - -# Stats and relayer -PARENTNET_URL=${url} -PARENTNET_WALLET=${config.parentnet.pubkey} -SUBNET_URL=http://${subnet_ip}:8545 -RELAYER_MODE=${config.relayer_mode} -SLACK_WEBHOOK=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX -CORS_ALLOW_ORIGIN=* - -# Frontend -VITE_SUBNET_URL=http://${config.public_ip}:5213 -VITE_SUBNET_RPC=http://${config.public_ip}:8545 - -# Share Variable -STATS_SECRET=${config.secret_string} - -# CSC -PARENTNET_WALLET_PK=${config.parentnet.privatekey} - -`; - - if (config.zero.zero_mode == "one-directional") { - config_env += ` -# XDC-ZERO -PARENTNET_ZERO_WALLET_PK=${config.zero.parentnet_zero_wallet_pk} - `; - } else if (config.zero.zero_mode == "bi-directional") { - config_env += ` -# XDC-ZERO -PARENTNET_ZERO_WALLET_PK=${config.zero.parentnet_zero_wallet_pk} -SUBNET_WALLET_PK=${config.zero.subnet_wallet_pk} -SUBNET_ZERO_WALLET_PK=${config.zero.subnet_zero_wallet_pk} - `; - } - // # Parent Chain Observe Node - // PARENTNET_NODE_NAME=mainnet_observer - // PRIVATE_KEYS=11111111111111111111111111111111111111111111111111111111111111 - return config_env; -} - -function genContractDeployEnv(ip_record) { - const subnet_ip = - config.num_machines === 1 ? ip_record["subnet1"] : config.ip_1; - const config_deploy = ` -PARENTNET_URL=${config.parentnet.url} -SUBNET_URL=http://${subnet_ip}:8545 - -PARENTNET_PK=${config.parentnet.privatekey} -SUBNET_PK=${config.keys.grandmaster_pk} - -`; - return config_deploy; -} diff --git a/deployment-generator/src/gen_other.js b/deployment-generator/src/gen_other.js deleted file mode 100644 index 3062844..0000000 --- a/deployment-generator/src/gen_other.js +++ /dev/null @@ -1,265 +0,0 @@ -const configModule = require("./config_gen"); -const config = configModule.config; -Object.freeze(config); - -module.exports = { - genSubnetKeys, - genDeploymentJson, - genCommands, - genGenesisInputFile, -}; - -function genSubnetKeys() { - const keys = {}; - // const num = config.keys.subnets_addr.length - for (let i = 0; i < config.keys.subnets_addr.length; i++) { - const key = `key${i + 1}`; - const private_key = config.keys.subnets_pk[i]; - const address = config.keys.subnets_addr[i]; - keys[key] = { - PrivateKey: private_key, - "0x": address, - short: address.replace(/^0x/i, ""), - }; - } - keys["Grandmaster"] = { - PrivateKey: config.keys.grandmaster_pk, - "0x": config.keys.grandmaster_addr, - short: config.keys.grandmaster_addr.replace(/^0x/i, ""), - }; - if (Object.keys(keys).length !== config.num_subnet + 1) { - // sanity check - console.log("bad case, key length not equal number of subnets"); - console.log(Object.keys(keys).length, config.num_subnet + 1); - console.log(keys); - process.exit(); - } - - return keys; -} - -function genDeploymentJson(keys) { - const num = Object.keys(keys).length - 1; - const validators = []; - for (let i = 1; i <= num; i++) { - let key_name = `key${i}`; - let public_key = keys[key_name]["0x"]; - validators.push(public_key); - } - - const deployment = { - validators, - gap: 450, - epoch: 900, - }; - return deployment; -} - -// function genNetworkJson(){ //don't need this, config will overlap(duplicate) with other files, shell script in CSC docker can gen this -// let network = { -// "xdcsubnet": `http://${config.ip_1}:8545`, -// "xdcparentnet": config.parentnet.url -// } -// return network -// } -// function genNetworkJsonMac(){ -// let network = { -// "xdcsubnet": `http://127.0.0.1:8545`, -// "xdcparentnet": config.parentnet.url -// } -// return network -// } - -function genCommands() { - const csc_mode = config.relayer_mode === "lite" ? "lite" : "full"; - let commands = ""; - commands += "Start under generated/ directory\n"; - commands += "\n1. Deploy Subnet nodes\n"; - for (let i = 1; i <= config.num_machines; i++) { - const machine_name = "machine" + i.toString(); - if (config.num_machines > 1) { - commands += - machine_name + `: deploy subnet on machine${i}\n`; - } - if (i !== 1) { - commands += ` Prerequisite: copy docker-compose.yml, genesis.json, config/subnetX.env to ${machine_name}.\n`; - } - commands += ` docker compose --profile ${machine_name} pull\n`; - commands += ` docker compose --profile ${machine_name} up -d\n\n`; - } - commands += - "\n2. After 60 seconds, confirm the Subnet is running correctly\n"; - commands += " ./scripts/check-mining.sh\n"; - commands += "\n3. Deploy Checkpoint Smart Contract (CSC)\n"; - commands += ` docker pull xinfinorg/csc:${config.version.csc}\n`; - commands += - " docker run -v ${PWD}/:/app/cicd/mount/" + - ` --network generated_docker_net xinfinorg/csc:${config.version.csc} ${csc_mode}\n`; - commands += "\n4. Start services (relayer, backend, frontend)\n"; - commands += ` docker compose --profile services pull\n`; - commands += ` docker compose --profile services up -d\n`; - commands += "\n5. Confirm Subnet services through browser UI\n"; - commands += ` Frontend: http://${config.public_ip}:5214\n`; - commands += ` Relayer: http://${config.public_ip}:5215\n`; - - if (config.zero.zero_mode == "") { - } else if (config.zero.zero_mode == "one-directional") { - if (config.zero.subswap == "true") { - commands += "\n6. Deploy XDC-Zero and Subswap\n"; - commands += ` docker pull xinfinorg/xdc-zero:${config.version.zero}\n`; - commands += - " docker run -v ${PWD}/:/app/cicd/mount/" + - ` --network generated_docker_net xinfinorg/xdc-zero:${config.version.zero} zeroandsubswap\n`; - commands += "\n7. Run Subswap Frontend\n"; - commands += ` docker compose --profile subswap pull\n`; - commands += ` docker compose --profile subswap up -d\n`; - commands += "\n8. Confirm Subswap UI\n"; - commands += ` Subswap-Frontend: http://${config.public_ip}:5216\n`; - } else { - commands += "\n6. Deploy XDC-Zero\n"; - commands += ` docker pull xinfinorg/xdc-zero:${config.version.zero}\n`; - commands += - " docker run -v ${PWD}/:/app/cicd/mount/" + - ` --network generated_docker_net xinfinorg/xdc-zero:${config.version.zero} endpointandregisterchain\n`; - } - commands += "\nRestart Relayer\n"; - commands += ` docker compose --profile services down\n`; - commands += ` docker compose --profile services up -d\n`; - } else if (config.zero.zero_mode == "bi-directional") { - commands += "\n6. Deploy Reverse CSC\n"; - commands += ` docker pull xinfinorg/csc:${config.version.csc}\n`; - commands += - " docker run -v ${PWD}/:/app/cicd/mount/" + - ` --network generated_docker_net xinfinorg/csc:${config.version.csc} reversefull\n`; - if (config.zero.subswap == "true") { - commands += "\n7. Deploy XDC-Zero and Subswap\n"; - commands += ` docker pull xinfinorg/xdc-zero:${config.version.zero}\n`; - commands += - " docker run -v ${PWD}/:/app/cicd/mount/" + - ` --network generated_docker_net xinfinorg/xdc-zero:${config.version.zero} zeroandsubswap\n`; - commands += "\n8. Run Subswap Frontend\n"; - commands += ` docker compose --profile subswap_frontend pull\n`; - commands += ` docker compose --profile subswap_frontend up -d\n`; - commands += "\n9. Confirm Subswap UI\n"; - commands += ` Subswap-Frontend: http://${config.public_ip}:5216\n`; - } else { - commands += "\n7. Deploy XDC-Zero\n"; - commands += ` docker pull xinfinorg/xdc-zero:${config.version.zero}\n`; - commands += - " docker run -v ${PWD}/:/app/cicd/mount/" + - ` --network generated_docker_net xinfinorg/xdc-zero:${config.version.zero} endpointandregisterchain\n`; - } - commands += "\nRestart Relayer\n"; - commands += ` docker compose --profile services down\n`; - commands += ` docker compose --profile services up -d\n`; - } else { - console.log("Error: Invalid XDC-Zero mode"); - exit(); - } - - // if (config.zero.zero_mode == "") { - // } else if (config.zero.zero_mode == "one-directional") { - // commands += "\n\nDeploy XDC-Zero crosschain framework (one-directional)\n"; - // commands += "\n1. Add CSC configuration to contract_deploy.env\n"; - // commands += - // " - copy CHECKPOINT_CONTRACT from common.env to and rename it to CSC in contract_deploy.env\n"; - // commands += - // " - copy CHECKPOINT_CONTRACT from common.env to and rename it to REVERSE_CSC in contract_deploy.env\n"; - // commands += "\n2. Deploy XDC-Zero\n"; - // commands += ` docker pull xinfinorg/xdc-zero:${config.version.zero}\n`; - // commands += ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/xdc-zero:${config.version.zero} endpointandregisterchain\n`; - // if (config.zero.subswap == "true") { - // commands += " \n Deploy Subswap and Register Application to XDC-Zero\n"; - // commands += - // " - copy SUBNET_ZERO_CONTRACT and PARENTNET_ZERO_CONTRACT to contract_deploy.env\n"; - // commands += - // ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/xdc-zero:v0.1.0 subswap\n`; - // commands += - // " - copy SUBNET_APP and PARENTNET_APP to contract_deploy.env\n"; - // commands += - // ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/xdc-zero:v0.1.0 applicationregister\n`; - // } - // commands += "\n3. Add XDC-Zero Configuration to common.env\n"; - // commands += - // " - copy step 2. output SUBNET_ZERO_CONTRACT and PARENTNET_ZERO_CONTRACT to common.env\n"; - // commands += - // " - Add PARENTNET_ZERO_WALLET_PK to common.env, this should be different from PARENTNET_WALLET_PK\n"; - // commands += "\n4. Restart Relayer\n"; - // commands += ` docker compose --profile services down\n`; - // commands += ` docker compose --profile services up -d\n`; - // commands += "\n5. Confirm Relayer is running \n"; - // commands += ` Relayer: http://${config.public_ip}:5215\n`; - // commands += ` Frontend: http://${config.public_ip}:5214\n`; - - // } else if (config.zero.zero_mode == "bi-directional") { - // commands += "\n\nDeploy XDC-Zero crosschain framework (bi-directional)\n"; - // commands += "\n1. Deploy Reverse Checkpoint Smart Contract (Reverse CSC)\n"; - // commands += ` docker pull xinfinorg/csc:${config.version.csc}\n`; - // commands += ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/csc:${config.version.csc} reversefull\n`; - // commands += "\n2. Add CSC and Reverse CSC configuration\n"; - // commands += - // " - copy previous step output REVERSE_CHECKPOINT_CONTRACT to common.env\n"; - // commands += - // " - copy REVERSE_CHECKPOINT_CONTRACT from common.env and rename it to REVERSE_CSC in contract_deploy.env\n"; - // commands += - // " - copy CHECKPOINT_CONTRACT from common.env and rename it to CSC in contract_deploy.env\n"; - // commands += "\n3. Deploy XDC-Zero\n"; - // commands += ` docker pull xinfinorg/xdc-zero:${config.version.zero}\n`; - // commands += ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/xdc-zero:${config.version.zero} endpointandregisterchain\n`; - // if (config.zero.subswap) { - // commands += - // " \n Deploy Subswap and Register Application to XDC-Zero\n"; - // commands += - // " - copy SUBNET_ZERO_CONTRACT and PARENTNET_ZERO_CONTRACT to contract_deploy.env\n"; - // commands += - // ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/xdc-zero:v0.1.0 subswap\n`; - // commands += - // " - copy SUBNET_APP and PARENTNET_APP to contract_deploy.env\n"; - // commands += - // ` docker run --env-file contract_deploy.env --network generated_docker_net xinfinorg/xdc-zero:v0.1.0 applicationregister\n`; - // } - // commands += "\n4. Add XDC-Zero Configuration to common.env\n"; - // commands += - // " - copy step 3. output SUBNET_ZERO_CONTRACT and PARENTNET_ZERO_CONTRACT to common.env\n"; - // commands += - // " - Add SUBNET_WALLET_PK and SUBNET_ZERO_WALLET_PK to common.env, they should be different from each other\n"; - // commands += - // " - Add PARENTNET_ZERO_WALLET_PK to common.env, this should be different from PARENTNET_WALLET_PK\n"; - // commands += "\n5. Transfer Subnet tokens to SUBNET_WALLET_PK and SUBNET_ZERO_WALLET_PK\n"; - // commands += " ./scripts/faucet.sh\n" - // commands += " - check keys.json for the Grandmaster Key as the source wallet\n" - // commands += " - for the destination wallet you should use the public address of SUBNET_WALLET_PK and SUBNET_ZERO_WALLET_PK\n" - // commands += "\n6. Restart Relayer\n"; - // commands += ` docker compose --profile services down\n`; - // commands += ` docker compose --profile services up -d\n`; - // commands += "\n7. Confirm Relayer is running \n"; - // commands += ` Relayer: http://${config.public_ip}:5215\n`; - // commands += ` Frontend: http://${config.public_ip}:5214\n`; - // } else { - // console.log("Error: Invalid XDC-Zero mode"); - // exit(); - // } - - return commands; -} - -function genGenesisInputFile(network_name, network_id, num_subnet, keys) { - const masternodes = []; - const grandmasters = []; - Object.keys(keys).forEach(function (k) { - const v = keys[k]; - if (k === "Grandmaster") { - grandmasters.push(v["0x"]); - } else { - masternodes.push(v["0x"]); - } - }); - - const config = { - name: network_name, - grandmasters, - masternodes, - chainid: network_id, - }; - return config; -} diff --git a/deployment-generator/src/img/xdc-network-primary-logo.png b/deployment-generator/src/img/xdc-network-primary-logo.png deleted file mode 100644 index 7bd9fbd53a1a44054a26380b8fc77209c47f08d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 186098 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�F@sc;^rH_9jt#}2k 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

RNrVot)W4|F8v37XXZU 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


w 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 G?)@J@UXW1$ diff --git a/deployment-generator/src/package-lock.json b/deployment-generator/src/package-lock.json deleted file mode 100644 index 3648cae..0000000 --- a/deployment-generator/src/package-lock.json +++ /dev/null @@ -1,3461 +0,0 @@ -{ - "name": "src", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "dotenv": "^16.3.1", - "ethers": "^6.3.0", - "express": "^4.19.2", - "fs": "^0.0.1-security", - "js-yaml": "^4.1.0", - "pug": "^3.0.3", - "readline-sync": "^1.4.10" - }, - "devDependencies": { - "eslint": "^8.56.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.6.1", - "eslint-plugin-promise": "^6.1.1" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", - "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", - "dependencies": { - "@babel/types": "^7.25.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", - "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/assert-never": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", - "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-walk": { - "version": "3.0.0-canary-5", - "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "dependencies": { - "@babel/types": "^7.9.6" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/builtins/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", - "dependencies": { - "is-regex": "^1.0.3" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/constantinople": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dependencies": { - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.1" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" - }, - "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", - "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz", - "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.6.0", - "eslint-compat-utils": "^0.1.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-n": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.1.tgz", - "integrity": "sha512-M1kE5bVQRLBMDYRZwDhWzlzbp370SRRRC1MHqq4I3L2Tatey+9/2csc5mwLDPlmhJaDvkojbrNUME5/llpRyDg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.5.0", - "get-tsconfig": "^4.7.0", - "globals": "^13.24.0", - "ignore": "^5.2.4", - "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", - "minimatch": "^3.1.2", - "resolve": "^1.22.2", - "semver": "^7.5.3" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ethers": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.9.0.tgz", - "integrity": "sha512-pmfNyQzc2mseLe91FnT2vmNaTt8dDzhxZ/xItAV7uGsF4dI4ek2ufMu3rAkgQETL/TIs0GS5A+U05g9QyWnv3Q==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@adraffy/ens-normalize": "1.10.0", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "18.15.13", - "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", - "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-expression": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dependencies": { - "acorn": "^7.1.1", - "object-assign": "^4.1.1" - } - }, - "node_modules/is-expression/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jstransformer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "dependencies": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pug": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", - "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", - "dependencies": { - "pug-code-gen": "^3.0.3", - "pug-filters": "^4.0.0", - "pug-lexer": "^5.0.1", - "pug-linker": "^4.0.0", - "pug-load": "^3.0.0", - "pug-parser": "^6.0.0", - "pug-runtime": "^3.0.1", - "pug-strip-comments": "^2.0.0" - } - }, - "node_modules/pug-attrs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dependencies": { - "constantinople": "^4.0.1", - "js-stringify": "^1.0.2", - "pug-runtime": "^3.0.0" - } - }, - "node_modules/pug-code-gen": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", - "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", - "dependencies": { - "constantinople": "^4.0.1", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.2", - "pug-attrs": "^3.0.0", - "pug-error": "^2.1.0", - "pug-runtime": "^3.0.1", - "void-elements": "^3.1.0", - "with": "^7.0.0" - } - }, - "node_modules/pug-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==" - }, - "node_modules/pug-filters": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dependencies": { - "constantinople": "^4.0.1", - "jstransformer": "1.0.0", - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0", - "resolve": "^1.15.1" - } - }, - "node_modules/pug-lexer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", - "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", - "dependencies": { - "character-parser": "^2.2.0", - "is-expression": "^4.0.0", - "pug-error": "^2.0.0" - } - }, - "node_modules/pug-linker": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dependencies": { - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-load": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dependencies": { - "object-assign": "^4.1.1", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dependencies": { - "pug-error": "^2.0.0", - "token-stream": "1.0.0" - } - }, - "node_modules/pug-runtime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" - }, - "node_modules/pug-strip-comments": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dependencies": { - "pug-error": "^2.0.0" - } - }, - "node_modules/pug-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readline-sync": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", - "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/with": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dependencies": { - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", - "assert-never": "^1.2.1", - "babel-walk": "3.0.0-canary-5" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/deployment-generator/src/package.json b/deployment-generator/src/package.json deleted file mode 100644 index c293b07..0000000 --- a/deployment-generator/src/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "dependencies": { - "dotenv": "^16.3.1", - "ethers": "^6.3.0", - "express": "^4.19.2", - "fs": "^0.0.1-security", - "js-yaml": "^4.1.0", - "pug": "^3.0.3", - "readline-sync": "^1.4.10" - }, - "devDependencies": { - "eslint": "^8.56.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.6.1", - "eslint-plugin-promise": "^6.1.1" - } -} diff --git a/deployment-generator/src/ui.js b/deployment-generator/src/ui.js deleted file mode 100644 index 5106773..0000000 --- a/deployment-generator/src/ui.js +++ /dev/null @@ -1,198 +0,0 @@ -process.chdir(__dirname); -const { execSync } = require("child_process"); -const fs = require("fs"); -const ethers = require("ethers"); -const express = require("express"); -const app = express(); -const path = require("path"); -const router = express.Router(); - -app.set("view engine", "pug"); -app.set("views", path.join(__dirname, "views")); -router.use( - express.urlencoded({ - extended: true, - }) -); -app.use(express.static("css")); - -router.get("/", (req, res) => { - res.render("index", {}); -}); - -router.post("/submit", (req, res) => { - gen_env = genGenEnv(req.body); - - fs.writeFileSync(path.join(__dirname, "gen.env"), gen_env, (err) => { - if (err) { - console.error(err); - exit(); - } - }); - - [valid, genOut] = callExec(`node gen.js`); - - console.log(); - console.log(genOut); - - if (!valid) { - res.render("submit", { - message: "failed, please try again", - error: genOut, - }); - return; - } - - res.render("submit", { - message: - "Config generation success, please follow instructions in generated/commands.txt", - }); - process.exit(); -}); - -router.post("/confirm", (req, res) => { - console.log(req.body); - res.render("confirm", { title: "Hey", message: "this is submit" }); -}); - -router.get("/address", (req, res) => { - const randomWallet = ethers.Wallet.createRandom(); - res.json({ - publicKey: randomWallet.address, - privateKey: randomWallet.privateKey, - }); -}); - -app.use("/", router); -app.listen(process.env.port || 5210); - -console.log("Running at Port 5210"); - -function callExec(command) { - try { - const stdout = execSync(command, { timeout: 200000, encoding: "utf-8" }); - output = stdout.toString(); - // console.log(output); - return [true, output]; - } catch (error) { - // console.log(error) - return [false, error.stdout]; - throw Error(error.stdout); - } -} - -function genGenEnv(input) { - console.log(input); - - let content_machine = ""; - if (input["text-num-machine"] > 1) { - content_machine += `\nMAIN_IP=${input["text-private-ip"]}`; - if (input["text-public-ip"] != "") { - content_machine += `\nPUBLIC_IP=${input["text-public-ip"]}`; - } - if (input["text-num-machine"] != "") { - content_machine += `\nNUM_MACHINE=${input["text-num-machine"]}`; - } - } else { - content_machine += `\nMAIN_IP=127.0.0.1`; - content_machine += `\nNUM_MACHINE=1`; - } - - let parentnet = ""; - switch (input.pnradio) { - case "pn-radio-testnet": - parentnet = "testnet"; - break; - case "pn-radio-devnet": - parentnet = "devnet"; - break; - case "pn-radio-mainnet": - parentnet = "mainnet"; - break; - } - - let relayer_mode = ""; - switch (input.rmradio) { - case "rm-radio-full": - relayer_mode = "full"; - break; - case "rm-radio-lite": - relayer_mode = "lite"; - break; - } - - let content_custom_key = ""; - if (input["grandmaster-pk"] != "") { - content_custom_key += `\nGRANDMASTER_PK=${input["grandmaster-pk"]}`; - } - - let subnet_keys = []; - let idx = 1; - while ("subnet-key" + idx.toString() in input) { - key = "subnet-key" + idx.toString(); - subnet_keys.push(input[key]); - idx++; - } - if (subnet_keys.length > 0) { - key_string = subnet_keys.join(","); - content_custom_key += `\nSUBNETS_PK=${key_string}`; - } - - let content_version = ""; - if (input["customversion-subnet"] != "") { - content_version += `\nVERSION_SUBNET=${input["customversion-subnet"]}`; - } - if (input["customversion-bootnode"] != "") { - content_version += `\nVERSION_BOOTNODE=${input["customversion-bootnode"]}`; - } - if (input["customversion-relayer"] != "") { - content_version += `\nVERSION_RELAYER=${input["customversion-relayer"]}`; - } - if (input["customversion-stats"] != "") { - content_version += `\nVERSION_STATS=${input["customversion-stats"]}`; - } - if (input["customversion-frontend"] != "") { - content_version += `\nVERSION_FRONTEND=${input["customversion-frontend"]}`; - } - if (input["customversion-csc"] != "") { - content_version += `\nVERSION_CSC=${input["customversion-csc"]}`; - } - if (input["customversion-zero"] != "") { - content_version += `\nVERSION_ZERO=${input["customversion-zero"]}`; - } - - let content_zero = ""; - if (relayer_mode == "full" && "xdczero-checkbox" in input) { - if (input["zmradio"] == "zm-radio-one") { - content_zero += "\nXDC_ZERO=one-directional"; - } - if (input["zmradio"] == "zm-radio-bi") { - content_zero += "\nXDC_ZERO=bi-directional"; - content_zero += `\nSUBNET_WALLET_PK=${input["subnet-wallet-pk"]}`; - content_zero += `\nSUBNET_ZERO_WALLET_PK=${input["subnet-zero-wallet-pk"]}`; - } - content_zero += `\nPARENTNET_ZERO_WALLET_PK=${input["parentnet-zero-wallet-pk"]}`; - if ("subswap-checkbox" in input) { - content_zero += "\nSUBSWAP=true"; - } - } - - content = ` -NETWORK_NAME=${input["text-subnet-name"]} -NUM_SUBNET=${input["text-num-subnet"]} -PARENTNET=${parentnet} -PARENTNET_WALLET_PK=${input["parentnet-wallet-pk"]} -RELAYER_MODE=${relayer_mode} -`; - content += content_machine; - content += "\n"; - content += content_custom_key; - content += "\n"; - content += content_version; - content += "\n"; - content += content_zero; - - console.log(content); - - return content; -} diff --git a/deployment-generator/src/views/about.pug b/deployment-generator/src/views/about.pug deleted file mode 100644 index caa0b32..0000000 --- a/deployment-generator/src/views/about.pug +++ /dev/null @@ -1,5 +0,0 @@ -html - head - title= title - body - h1= message \ No newline at end of file diff --git a/deployment-generator/src/views/custom.css b/deployment-generator/src/views/custom.css deleted file mode 100644 index 2410391..0000000 --- a/deployment-generator/src/views/custom.css +++ /dev/null @@ -1,155 +0,0 @@ -#myForm { - margin-left: 15%; - /* margin-right: 200px; */ - max-width: 1100px; -} - -#helper { - margin-left: 15%; - margin-top: 50px; - max-width: 1100px; -} -#helper-title { - margin-right: 10%; -} -.helper-block { - padding: 10px; -} - -.content { - display: none; - background-color: #f0f0f0; - margin-right: 100px; - padding-top: 10px; - padding-bottom: 10px; -} - -#toggle:checked ~ .content { - display: block; -} - -#toggle2:checked ~ .grayedout { - background-color: gray disabled; -} - -#lite-mode-extra-info { - color: red; -} - -#incomplete-required-warning { - color: red; -} - -/* hover trick */ -.info-container { - position: relative; - display: inline-block; -} - -.info-icon { - display: inline-block; - width: 20px; - height: 20px; - background-color: #007bff; - color: white; - border-radius: 50%; - text-align: center; - line-height: 20px; - margin-left: 5px; - cursor: pointer; -} -.info-icon-all-gray { - color: #f0f0f0; - background-color: #f0f0f0; -} - -.tooltip { - visibility: hidden; - width: 200px; - background-color: #555; - color: #fff; - text-align: center; - border-radius: 6px; - padding: 5px; - position: absolute; - z-index: 1; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - margin-bottom: 10px; - opacity: 0; - transition: opacity 0.3s; -} - -.tooltip::after { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: #555 transparent transparent transparent; -} - -.info-icon:hover + .tooltip, -.info-icon:focus + .tooltip { - visibility: visible; - opacity: 1; -} - -#information1 { - display: none; -} -#expand1:hover + #information1 { - display: block; -} - -/* expanding after selection */ -#ipoption { - display: none; -} -#checkbox-num-machine { - display: none; -} -#checkbox-num-machine:checked + #ipoption { - display: block; -} - -#customkeys { - display: none; -} -#customkeys-checkbox:checked + #customkeys { - display: block; -} - -#customversion { - display: none; -} -#customversion-checkbox:checked + #customversion { - display: block; -} - -#xdczero { - display: none; -} -#xdczero-checkbox:checked + #xdczero { - display: block; -} - -/* #rm-radio-full:checked ~ #xdczero-upper{ - display:block; -} -#rm-radio-lite:checked ~ #xdczero-upper{ - display:block; -} */ - -#zerobidi { - display: none; -} -#zm-radio-bi:checked ~ #zerobidi { - display: block; -} -#zm-radio-one:checked ~ #zerobidi { - display: none; -} diff --git a/deployment-generator/src/views/index.pug b/deployment-generator/src/views/index.pug deleted file mode 100644 index e4a51a1..0000000 --- a/deployment-generator/src/views/index.pug +++ /dev/null @@ -1,381 +0,0 @@ -head - meta(charset='utf-8') - title Subnet Configuration Generator - style - include ./pure-min.css - include ./custom.css - - -body - center - h1 Subnet Configuration Generator - - //- form.pure-form.pure-form-aligned#myForm(onsubmit="return validateForm()") - form.pure-form.pure-form-aligned#myForm(action="/submit" method="post" onsubmit="return validateForm()") - center - h3 Subnet Configuration - fieldset - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This cannot be changed later. - label() Network Name - input#text-subnet-name(type='text' placeholder='myxdcsubnet' name="text-subnet-name") - span.pure-form-message-inline Required - - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip At least 2/3 of nodes must be online to have consensus and make the blockchain run. - label() Number of Subnet Nodes - input#text-num-subnet(type='number' placeholder='3' name="text-num-subnet") - span.pure-form-message-inline Required - - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip The generated configs will evenly spread the nodes across the machines. - label() Number of Machines - input#text-num-machine(type='number' placeholder='1' name="text-num-machine" oninput="numMachineMoreThanOne()") - - input#checkbox-num-machine(type='checkbox' name='checkbox-num-machine') - - //- .pure-control-group - //- .info-container - //- .info-icon(tabindex="0" role="button" aria-label="more info") ? - //- .tooltip - //- p - WSL is not supported yet - //- p - Mac is only suitable for testing purposes - //- label() Operating System - //- br - //- br - //- label.pure-radio(for='os-radio-mac') - //- input#os-radio-mac(type='radio' name='osradio' value='os-radio-mac' checked) - //- | Mac - //- br - //- br - //- label.pure-radio(for='os-radio-linux') - //- input#os-radio-linux(type='radio' name='osradio' value='os-radio-linux') - //- | Linux - #ipoption.content - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Private IP is used for Subnet nodes communication. - label() Private IP - input#text-private-ip(type='text' placeholder='192.168.1.1' name="text-private-ip") - span.pure-form-message-inline Required - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Public IP is used for accessing Subnet services, eg. Frontend. - label() Public IP - input#text-public-ip(type='text' placeholder='1.1.1.1' name="text-public-ip") - - - - - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Default is random - label.pure-checkbox(for='customkeys-checkbox') Custom Subnet Keys - input#customkeys-checkbox.checkbox-indent(type='checkbox' name="customkeys-checkbox") - #customkeys.content - .pure-control-group - label() Grandmaster PK - input#grandmaster-pk.pure-input-2-3(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="grandmaster-pk") - center - button.pure-button.pure-button-primary#button-custom-subnet-key(type="button" onclick="customSubnetKeyJs()") Custom Subnet Node Keys - #custom-subnet-key-placeholder - - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Default to stable - label.pure-checkbox(for='customversion-checkbox') Custom Version - input#customversion-checkbox.checkbox-indent(type='checkbox' name="customversion-checkbox") - #customversion.content - center - p Changelog: - .pure-control-group - label() Subnet Node Version - input#customversion-subnet.pure-input(type='text' placeholder='latest' name="customversion-subnet") - .pure-control-group - label() Bootnode Version - input#customversion-bootnode.pure-input(type='text' placeholder='latest' name="customversion-bootnode") - .pure-control-group - label() Relayer Version - input#customversion-relayer.pure-input(type='text' placeholder='latest' name="customversion-relayer") - .pure-control-group - label() Backend Stats Server Version - input#customversion-stats.pure-input(type='text' placeholder='latest' name="customversion-stats") - .pure-control-group - label() Frontend Version - input#customversion-frontend.pure-input(type='text' placeholder='latest' name="customversion-frontend") - .pure-control-group - label() CSC Version - input#customversion-csc.pure-input(type='text' placeholder='latest' name="customversion-csc") - .pure-control-group - label() XDC-Zero Version - input#customversion-zero.pure-input(type='text' placeholder='latest' name="customversion-zero") - - - center - h3 Cross-Chain Configuration - fieldset - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Testnet is recommended for testing. - label() Parent Network - br - br - label.pure-radio(for='pn-radio-testnet') - input#pn-radio-testnet(type='radio' name='pnradio' value='pn-radio-testnet' checked) - | XDC-Testnet/Apothem - //- | XDC-Testnet/Apothem      faucet: - br - br - label.pure-radio(for='pn-radio-devnet') - input#pn-radio-devnet(type='radio' name='pnradio' value='pn-radio-devnet') - //- | XDC-Devnet                     faucet: - | XDC-Devnet - br - br - label.pure-radio(for='pn-radio-mainnet') - input#pn-radio-mainnet(type='radio' name='pnradio' value='pn-radio-mainnet') - | XDC-Mainnet - - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This Parentchain wallet will be used to deploy the Checkpoint Smart Contract(CSC) on the Parentchain and used by the Relayer to checkpoint Subnet block headers at regular intervals. - label() Parentnet Wallet PK - input#parentnet-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="parentnet-wallet-pk") - span.pure-form-message-inline Required - - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip - p - Full: All Subnet block headers are checkpointed on the Parentchain. - p - Lite: Lite mode reduces gas cost by checkpointing only Gap block(450) and Epoch block(900) are checkpointed on the Parentchain. - label() Relayer Mode - br - br - label.pure-radio(for='rm-radio-full') - input#rm-radio-full(type='radio' name='rmradio' value='rm-radio-full' onclick="relayerFullClicked(this)" checked) - | Full - br - br - label.pure-radio(for='rm-radio-lite') - input#rm-radio-lite(type='radio' name='rmradio' value='rm-radio-lite' onclick="relayerLiteClicked(this)") - | Lite - br - br - center - #lite-mode-extra-info - #xdczero-upper - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip XDC-Zero is a Cross-Chain communication framework - label.pure-checkbox(for='xdczero-checkbox') XDC-Zero - input#xdczero-checkbox.checkbox-indent(type='checkbox' name="xdczero-checkbox") - #xdczero.content - center - p Extra Info: - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This Parentchain Zero Wallet is used for pushing cross-chain communication tx - label() Parentnet Zero Wallet PK - input#parentnet-zero-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="parentnet-zero-wallet-pk") - span.pure-form-message-inline Required - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip In Bi-Directional mode, Reverse CSC and Reverse XDC-Zero are also deployed - label() XDC-Zero Mode - br - br - label.pure-radio(for='zm-radio-one') - input#zm-radio-one(type='radio' name='zmradio' value='zm-radio-one' checked) - | One-Directional (Subnet -> Parentnet) - br - br - label.pure-radio(for='zm-radio-bi') - input#zm-radio-bi(type='radio' name='zmradio' value='zm-radio-bi') - | Bi-Directional (Subnet <-> Parentnet) - br - br - #zerobidi.content - //- center - //- p Faucet: - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This Subnet wallet will be used to checkpoint the Parentchain to Subnet. You can transfer Subnet tokens to this wallet after Subnet is launched. - label() Subnet Wallet PK - input#subnet-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="subnet-wallet-pk") - span.pure-form-message-inline Required - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip The Subnet Zero Wallet will be used to submit cross-chain tx to the Subnet. You can transfer Subnet tokens to this wallet after Subnet is launched. - label() Subnet Zero Wallet PK - input#subnet-zero-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="subnet-zero-wallet-pk") - span.pure-form-message-inline Required - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Subswap is XDC provided default cross-chain application to handle token transfers between Subnet and Parentnet - label.pure-checkbox(for='subswap-checkbox') Subswap - input#subswap-checkbox.checkbox-indent(type='checkbox' name="subswap-checkbox") - - - - - center - #incomplete-required-warning - .pure-controls - button.pure-button.pure-button-primary(type='submit') Submit - - center - h3#helper-title Helpers - .helper-block - h4 Address Generator - p#address-gen-pub Address: - p#address-gen-pk Private Key: - #disclaim-wrap - button.pure-button.pure-button-primary(type="button" onclick="genAddress()") Generate Address - h8#disclaimer *Generation is done locally on your machine, fully offline. - .helper-block - h4 XDC Faucet - p - a(href="https://faucet.apothem.network/" target="_blank") Testnet(Apothem) Faucet - p - a(href="https://faucet.blocksscan.io/ " target="_blank") Testnet and Devnet Faucet by BlocksScan - .helper-block - h4 Documentation - p - a(href="https://xinfinorg.github.io/xdc-subnet-docs/deployment/launch_subnet" target="_blank") Official Subnet Documentation - p - a(href="https://github.com/XinFinOrg/XDC-Subnet" target="_blank") XDC-Subnet Github - - -script. - function customSubnetKeyJs(){ - const parent = document.getElementById("custom-subnet-key-placeholder") - const count = document.getElementById("text-num-subnet").value - if (!count) { - parent.innerHTML = ''; - const cen = document.createElement("center") - const para = document.createElement("p") - para.innerHTML = "Please first input 'Number of Subnet Nodes'" - cen.appendChild(para) - parent.appendChild(cen) - } else { - parent.innerHTML = ''; - for (i=1;i<=count;i++){ - var d = document.createElement("div") - d.setAttribute("class", "pure-control-group") - const label = document.createElement("label") - label.innerHTML = "Subnet Node " + i + " PK" - const input = document.createElement("input") - const id = "subnet-key"+i - input.setAttribute("id", id) - input.setAttribute("name", id) - input.setAttribute("class", "pure-input-2-3") - input.setAttribute("type", "text") - input.setAttribute("placeholder", "0x1111111111111111111111111111111111111111111111111111111111111111") - - d.appendChild(label) - d.appendChild(input) - parent.appendChild(d) - } - } - } - function numMachineMoreThanOne(){ - const num = document.getElementById("text-num-machine") - const hiddenbox = document.getElementById("checkbox-num-machine") - if (num.value > 1){ - hiddenbox.checked = true; - } else { - hiddenbox.checked = false; - const text1 = document.getElementById("text-private-ip") - const text2 = document.getElementById("text-public-ip") - text1.value = "" - text2.value = "" - } - } - - function relayerLiteClicked(radio){ - const checkbox = document.getElementById("xdczero-checkbox") - checkbox.checked = false; - checkbox.disabled = true; - //- document.getElementById("lite-mode-extra-info").innerHTML="Cannot use XDC-Zero with Lite Relayer" - } - - function relayerFullClicked(radio){ - const checkbox = document.getElementById("xdczero-checkbox") - checkbox.disabled = false; - } - - function validateForm(){ - const form = document.forms["myForm"] - - const name = form["text-subnet-name"].value; - const num_subnet = form["text-num-subnet"].value; - const pn_pk = form["parentnet-wallet-pk"].value; - if (name == "" || num_subnet == "" || pn_pk == ""){ - document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" - return false - } - - const num_machines = form["text-num-machine"].value; - if (num_machines > 1){ - const private_ip = form["text-private-ip"].value; - if (private_ip == ""){ - document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" - return false - } - } - const zero = form["xdczero-checkbox"].checked; - if (zero){ - const zero_pk = form["parentnet-zero-wallet-pk"].value; - if (zero_pk == ""){ - document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" - return false - } - - const bidi = form["zm-radio-bi"].checked - if (bidi){ - const subnet_pk = form["subnet-wallet-pk"].value; - const subnet_zero_pk = form["subnet-zero-wallet-pk"].value; - if (subnet_pk == "" || subnet_zero_pk == ""){ - document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" - return false - } - } - } - - - document.getElementById("incomplete-required-warning").innerHTML="" - console.log("pass") - return true - } - - function genAddress(){ - fetch('/address') - .then(response => response.json()) - .then(data => { - document.getElementById("address-gen-pub").innerHTML="Address: "+data["publicKey"] - document.getElementById("address-gen-pk").innerHTML="Private Key: "+data["privateKey"] - }) - .catch(error => { - console.error('Error:', error); - document.getElementById("address-gen-pub").innerHTML="Error Generating Address" - document.getElementById("address-gen-pk").innerHTML="Error Generating Address" - }); - } \ No newline at end of file diff --git a/deployment-generator/src/views/pure-min.css b/deployment-generator/src/views/pure-min.css deleted file mode 100644 index acdc431..0000000 --- a/deployment-generator/src/views/pure-min.css +++ /dev/null @@ -1,11 +0,0 @@ -/*! -Pure v3.0.0 -Copyright 2013 Yahoo! -Licensed under the BSD License. -https://github.com/pure-css/pure/blob/master/LICENSE -*/ -/*! -normalize.css v | MIT License | https://necolas.github.io/normalize.css/ -Copyright (c) Nicolas Gallagher and Jonathan Neal -*/ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/deployment-generator/src/views/submit.pug b/deployment-generator/src/views/submit.pug deleted file mode 100644 index b018258..0000000 --- a/deployment-generator/src/views/submit.pug +++ /dev/null @@ -1,6 +0,0 @@ -html - head - title="Submitted" - body - h1=message - h1=error \ No newline at end of file diff --git a/deployment-generator/src/views_faucet/custom.css b/deployment-generator/src/views_faucet/custom.css deleted file mode 100644 index e217b06..0000000 --- a/deployment-generator/src/views_faucet/custom.css +++ /dev/null @@ -1,26 +0,0 @@ -#myForm { - margin-left: 15%; - /* margin-right: 200px; */ - max-width: 1100px; -} - -#helper { - margin-left: 15%; - margin-top: 50px; - max-width: 1100px; -} -#helper-title { - margin-right: 10%; -} -.helper-block { - padding: 10px; -} - -#final { - margin-left: 175px; - margin-top: 20px; - margin-bottom: 40px; -} -#error { - color: red; -} diff --git a/deployment-generator/src/views_faucet/index.pug b/deployment-generator/src/views_faucet/index.pug deleted file mode 100644 index c87fd19..0000000 --- a/deployment-generator/src/views_faucet/index.pug +++ /dev/null @@ -1,95 +0,0 @@ -head - meta(charset='utf-8') - title Subnet Faucet - style - include ./pure-min.css - include ./custom.css - - -body(onload="getSource()") - center - h1 Subnet Faucet - - form.pure-form.pure-form-aligned#myForm(action="/faucet" method="get") - fieldset - //- center - //- h4#source Source Wallet: - .pure-control-group - label() Destination Address - input.pure-input-1-3#text-dest(type='text' placeholder='0x1111111111111111111111111111111111111111' name="text-dest") - .pure-control-group - label() Amount - input#text-amount(type='number' placeholder='1000' value='1000' name="text-amount") - - center - .pure-controls - button.pure-button.pure-button-primary(type='button' onclick="submitGet()") Submit - - #final - #result - #error - - center - .helper-block - h4 Address Generator - p#address-gen-pub Address: - p#address-gen-pk Private Key: - #disclaim-wrap - button.pure-button.pure-button-primary(type="button" onclick="genAddress()") Generate Address - h8#disclaimer *Generation is done locally on your machine, fully offline. - -script. - function genAddress(){ - fetch('/address') - .then(response => response.json()) - .then(data => { - document.getElementById("address-gen-pub").innerHTML="Address: "+data["publicKey"] - document.getElementById("address-gen-pk").innerHTML="Private Key: "+data["privateKey"] - }) - .catch(error => { - console.error('Error:', error); - document.getElementById("address-gen-pub").innerHTML="Error Generating Address" - document.getElementById("address-gen-pk").innerHTML="Error Generating Address" - }); - } - - function getSource(){ - fetch('/source') - .then(response => response.json()) - .then(data => { - document.getElementById("source").innerHTML="Source Wallet: "+data["source"] - }) - .catch(error => { - console.error('Error:', error); - document.getElementById("source").innerHTML="Source Wallet: Error getting source address" - }); - } - - function submitGet(){ - var dest = document.getElementById("text-dest").value - var amount = document.getElementById("text-amount").value - var params = new URLSearchParams({dest: dest, amount: amount}) - errorDiv = document.getElementById("error") - resultDiv = document.getElementById("result") - resultDiv.innerHTML = "Submitting TX" - errorDiv.innerHTML = "" - fetch(`/faucet?${params}`) - .then(response => response.json()).then(data => { - if (data.success){ - display = - ` - Success -
-
To Wallet: ${data.destAddress} -
Balance: ${data.destBalance} -
TX Hash: ${data.txHash} - ` - resultDiv.innerHTML=display - errorDiv.innerHTML="" - - } else { - resultDiv.innerHTML = "" - errorDiv.innerHTML="Error: "+data.message - } - }) - } \ No newline at end of file diff --git a/deployment-generator/src/views_faucet/pure-min.css b/deployment-generator/src/views_faucet/pure-min.css deleted file mode 100644 index acdc431..0000000 --- a/deployment-generator/src/views_faucet/pure-min.css +++ /dev/null @@ -1,11 +0,0 @@ -/*! -Pure v3.0.0 -Copyright 2013 Yahoo! -Licensed under the BSD License. -https://github.com/pure-css/pure/blob/master/LICENSE -*/ -/*! -normalize.css v | MIT License | https://necolas.github.io/normalize.css/ -Copyright (c) Nicolas Gallagher and Jonathan Neal -*/ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file From c1b17bcbfb4fe58b83614b1c53aebc533a4badd7 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Sun, 16 Aug 2026 20:17:15 +0700 Subject: [PATCH 10/59] add back button when failed, since container hasn't shut down yet --- container-manager/src/express.js | 2 ++ container-manager/src/views/generator/submit.pug | 11 ++++++++++- .../src/views/xdpos_generator/submit.pug | 11 ++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/container-manager/src/express.js b/container-manager/src/express.js index a11631a..c80cf51 100644 --- a/container-manager/src/express.js +++ b/container-manager/src/express.js @@ -171,6 +171,7 @@ app.post("/submit", (req, res) => { res.render("generator/submit.pug", { message: "failed, please try again", error: genOut, + failed: true, }); } else { res.render("generator/submit.pug", { @@ -187,6 +188,7 @@ app.post("/submit_xdpos", (req, res) => { res.render("xdpos_generator/submit.pug", { message: "failed, please try again", error: genOut, + failed: true, }); } else { res.render("xdpos_generator/submit.pug", { diff --git a/container-manager/src/views/generator/submit.pug b/container-manager/src/views/generator/submit.pug index b018258..dcbbc6c 100644 --- a/container-manager/src/views/generator/submit.pug +++ b/container-manager/src/views/generator/submit.pug @@ -1,6 +1,15 @@ html head title="Submitted" + style + include ./pure-min.css + include ./custom.css body h1=message - h1=error \ No newline at end of file + h1=error + if failed + //- only on failure: the server is still running, so going back lets the + //- user fix the form (history.back keeps the values already filled in). + //- keyed off `failed`, not `error` -- a failing step can exit with nothing + //- on stdout, and the button still has to show. + button.pure-button(type="button" onclick="history.back()") Back to the form diff --git a/container-manager/src/views/xdpos_generator/submit.pug b/container-manager/src/views/xdpos_generator/submit.pug index b018258..dcbbc6c 100644 --- a/container-manager/src/views/xdpos_generator/submit.pug +++ b/container-manager/src/views/xdpos_generator/submit.pug @@ -1,6 +1,15 @@ html head title="Submitted" + style + include ./pure-min.css + include ./custom.css body h1=message - h1=error \ No newline at end of file + h1=error + if failed + //- only on failure: the server is still running, so going back lets the + //- user fix the form (history.back keeps the values already filled in). + //- keyed off `failed`, not `error` -- a failing step can exit with nothing + //- on stdout, and the button still has to show. + button.pure-button(type="button" onclick="history.back()") Back to the form From 02177ebb5d395f53e6c1fb4580842a04f42bd35f Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Sun, 16 Aug 2026 23:37:37 +0700 Subject: [PATCH 11/59] - allow chainspec converter to be called as a container - always generate chainspec --- container-manager/src/README.md | 26 ++++++++- container-manager/src/gen/gen_xdpos.js | 15 +++-- container-manager/src/libs/exec.js | 38 ++++++------- .../src/libs/genesis-to-chainspec.js | 55 +++++++++++++++---- container-manager/src/package.json | 3 +- 5 files changed, 93 insertions(+), 44 deletions(-) diff --git a/container-manager/src/README.md b/container-manager/src/README.md index b896a08..3199bb9 100644 --- a/container-manager/src/README.md +++ b/container-manager/src/README.md @@ -1 +1,25 @@ -npm run dev \ No newline at end of file +# container-manager + +`npm run dev` + +## genesis.json -> chainspec.json + +`npm run convert -- ` translates a go-ethereum / +XDC `genesis.json` into the `chainspec.json` the Nethermind nodes consume. Both +paths are required, there are no defaults. Optional: `--name `, +`--base-fee <0x..>`. + +The deployment normally does this automatically (see `libs/exec.js`); the command +is for converting a genesis by hand from the host, as a one-off container. Run it +from the directory holding `start.sh`: + +```bash +docker run --rm -v $PWD/generated:/mount/generated \ + xinfinorg/subnet-generator:v2.0.0 \ + npm run convert -- /mount/generated/genesis.json /mount/generated/chainspec.json +``` + +The trailing arguments replace the image's default `npm run start`, so no server +comes up and the container exits when the conversion is done. `/mount/generated` +inside the container is `./generated` on the host; relative paths resolve against +`/app`, so pass absolute container paths. diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 6e3b4e2..4ba4c56 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -186,14 +186,13 @@ 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` - ); - } + // shared Nethermind config mounted by every nmc node, copied unconditionally + // so it is always available (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) { diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 8f580e6..15baa7b 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -184,28 +184,22 @@ function generateXdpos(params) { 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; - 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}`]; - } + //step 3: convert genesis.json -> chainspec.json. Nethermind nodes mount it, + //but generate it unconditionally so it is always available alongside genesis.json. + 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]; } diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 74741eb..bd5fbef 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -6,11 +6,18 @@ * Erigon style `chainspec.json` consumed by this network. * * Usage: - * node genesis-to-chainspec.js [chainspec.json] [--name ] + * node genesis-to-chainspec.js [--name ] * * node genesis-to-chainspec.js ../genesis.json ../chainspec.json --name xdc-mine * - * If the output path is omitted the result is printed to stdout. + * Both the input and the output path are required; there are no defaults. + * + * From the host, as a one-off container (paths are container paths; + * /mount/generated is the host's ./generated directory): + * + * docker run --rm -v $PWD/generated:/mount/generated \ + * xinfinorg/subnet-generator: \ + * npm run convert -- /mount/generated/genesis.json /mount/generated/chainspec.json * * Self-contained: no external dependencies. * @@ -256,6 +263,13 @@ function translate(genesis, opts = {}) { * CLI * ------------------------------------------------------------------ */ +const USAGE = + 'Usage: node genesis-to-chainspec.js [--name ] [--base-fee <0x..>]\n' + + ' npm run convert -- [--name ] [--base-fee <0x..>]\n' + + 'Both paths are required. Relative paths resolve against the current working\n' + + 'directory (/app when invoked through npm inside the container), so prefer\n' + + 'absolute paths such as /mount/generated/genesis.json.'; + function main(argv) { const args = argv.slice(2); const positional = []; @@ -266,32 +280,49 @@ function main(argv) { } else if (args[i] === '--base-fee') { opts.baseFeePerGas = args[++i]; } else if (args[i] === '-h' || args[i] === '--help') { - console.log('Usage: node genesis-to-chainspec.js [chainspec.json] [--name ] [--base-fee <0x..>]'); + console.log(USAGE); return 0; } else { positional.push(args[i]); } } - if (positional.length < 1) { - console.error('Error: missing input genesis.json path.'); - console.error('Usage: node genesis-to-chainspec.js [chainspec.json] [--name ]'); + if (positional.length < 2) { + console.error( + positional.length === 0 + ? 'Error: missing input genesis.json and output chainspec.json paths.' + : 'Error: missing output chainspec.json path.' + ); + console.error(USAGE); + return 1; + } + if (positional.length > 2) { + console.error(`Error: unexpected extra argument "${positional[2]}".`); + console.error(USAGE); return 1; } const inPath = path.resolve(positional[0]); - const outPath = positional[1] ? path.resolve(positional[1]) : null; + const outPath = path.resolve(positional[1]); + + let genesis; + try { + genesis = JSON.parse(fs.readFileSync(inPath, 'utf8')); + } catch (e) { + console.error(`Error: cannot read genesis from ${inPath}: ${e.message}`); + return 1; + } - const genesis = JSON.parse(fs.readFileSync(inPath, 'utf8')); const chainspec = translate(genesis, opts); const json = JSON.stringify(chainspec, null, 2) + '\n'; - if (outPath) { + try { fs.writeFileSync(outPath, json); - console.error(`Wrote ${outPath} (${chainspec.params.chainId ? 'chainId ' + chainspec.params.chainId : ''}).`); - } else { - process.stdout.write(json); + } catch (e) { + console.error(`Error: cannot write chainspec to ${outPath}: ${e.message}`); + return 1; } + console.error(`Wrote ${outPath} (${chainspec.params.chainId ? 'chainId ' + chainspec.params.chainId : ''}).`); return 0; } diff --git a/container-manager/src/package.json b/container-manager/src/package.json index 078251f..db9af42 100644 --- a/container-manager/src/package.json +++ b/container-manager/src/package.json @@ -5,7 +5,8 @@ "main": "test-docker.js", "scripts": { "start": "node express.js", - "dev": "HOSTPWD=$PWD/../mount/generated nodemon express.js" + "dev": "HOSTPWD=$PWD/../mount/generated nodemon express.js", + "convert": "node libs/genesis-to-chainspec.js" }, "dependencies": { "axios": "^1.7.9", From d13b8bb973d8f2eba807d497cd28b23d20770b53 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 00:07:04 +0700 Subject: [PATCH 12/59] add chainspec check before every startup to prevent genesis.json <> chainspec.json mismatch --- container-manager/src/README.md | 25 ++ container-manager/src/gen/gen_xdpos.js | 14 + .../src/gen/scripts/check-chainspec.sh | 50 ++++ .../src/gen/scripts/docker-up.sh | 15 + container-manager/src/libs/check-chainspec.js | 273 ++++++++++++++++++ container-manager/src/package.json | 3 +- 6 files changed, 379 insertions(+), 1 deletion(-) create mode 100755 container-manager/src/gen/scripts/check-chainspec.sh create mode 100644 container-manager/src/libs/check-chainspec.js diff --git a/container-manager/src/README.md b/container-manager/src/README.md index 3199bb9..f72f21f 100644 --- a/container-manager/src/README.md +++ b/container-manager/src/README.md @@ -23,3 +23,28 @@ The trailing arguments replace the image's default `npm run start`, so no server comes up and the container exits when the conversion is done. `/mount/generated` inside the container is `./generated` on the host; relative paths resolve against `/app`, so pass absolute container paths. + +## Pre-boot check + +`check-chainspec.sh` ships in every generated deployment folder, next to +`docker-up.sh`. Run it before booting: it re-translates `genesis.json` and +compares the result with `chainspec.json`, since a stale chainspec gives the +Nethermind nodes a different genesis block than the XDC nodes. + +```bash +./check-chainspec.sh # check, and repair if they differ +./check-chainspec.sh --dry-run # report only +``` + +On a difference it lists the offending keys, moves the old file to +`archive/chainspec..json`, and writes the chainspec that +`genesis.json` translates to. Exit codes: `0` already matched, `1` differed, +`2` error — so `./check-chainspec.sh && ./docker-up.sh machine1` stops after a +repair and lets you decide whether node data directories need wiping. + +It runs on the host's `node` if there is one, otherwise inside a throwaway +`node:24-alpine` container (`CHECK_IMAGE` overrides the image). The check itself +is `scripts/check-chainspec.js`, copied into the deployment folder at generation +time along with `scripts/genesis-to-chainspec.js`, so it needs neither this +container nor a network. Inside the container manager the same check is +`npm run check-chainspec -- `. diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 4ba4c56..e821009 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -193,6 +193,20 @@ function copyScripts(output_dir) { `${__dirname}/scripts/xdc-nmc.json`, `${output_dir}/xdc-nmc.json` ); + // pre-boot check that chainspec.json still matches genesis.json; the two js + // files are the check itself, shipped so it runs without this container + fs.copyFileSync( + `${__dirname}/scripts/check-chainspec.sh`, + `${output_dir}/check-chainspec.sh` + ); + fs.copyFileSync( + `${__dirname}/../libs/check-chainspec.js`, + `${output_dir}/scripts/check-chainspec.js` + ); + fs.copyFileSync( + `${__dirname}/../libs/genesis-to-chainspec.js`, + `${output_dir}/scripts/genesis-to-chainspec.js` + ); } function initConfig(config) { diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh new file mode 100755 index 0000000..04ef775 --- /dev/null +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Pre-boot check: verify chainspec.json still describes the same chain as +# genesis.json. Run this before docker-up.sh — a stale chainspec gives the +# Nethermind nodes a different genesis block than the XDC nodes, and they +# will never sync with each other. +# +# If they differ, the old chainspec.json is moved to archive/ under a UTC +# timestamp and replaced with the one genesis.json translates to. +# +# Usage: ./check-chainspec.sh [genesis.json] [chainspec.json] [--dry-run] +# paths are relative to this deployment folder +# --dry-run reports the differences without changing any file +# +# Exit code: 0 already matched, 1 differed, 2 error. +# +# Uses the host's node when it has one, otherwise runs the check in a +# throwaway container ($CHECK_IMAGE, default node:24-alpine). + +cd "$(dirname "$0")" + +genesis="genesis.json" +chainspec="chainspec.json" +if [[ -n "$1" && "$1" != -* ]]; then + genesis="$1" + shift + if [[ -n "$1" && "$1" != -* ]]; then + chainspec="$1" + shift + fi +fi +image="${CHECK_IMAGE:-node:24-alpine}" + +if [[ ! -f "$genesis" ]]; then + echo "Error: $genesis not found in $PWD" + exit 2 +fi + +if [[ ! -f "$chainspec" ]]; then + echo "Error: $chainspec not found in $PWD" + echo "It is generated from $genesis when the deployment is generated." + exit 2 +fi + +if command -v node > /dev/null 2>&1; then + node scripts/check-chainspec.js "$genesis" "$chainspec" "$@" +else + # -u keeps the archived and rewritten files owned by the invoking user + docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$image" \ + node scripts/check-chainspec.js "$genesis" "$chainspec" "$@" +fi diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index bf7cb30..908ef37 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -25,6 +25,21 @@ else fi +# pre-check: chainspec.json must still match genesis.json, otherwise the +# Nethermind nodes boot on a different config than the XDC nodes. +bash check-chainspec.sh +check_result=$? +if [[ $check_result == 1 ]]; then + echo "" + echo "chainspec.json was updated and the old copy saved as backup in archive directory." + exit 1 +elif [[ $check_result != 0 ]]; then + echo "" + echo "chainspec pre-check failed, not booting." + exit $check_result +fi + + docker-compose --profile $1 pull docker-compose --profile $1 up -d diff --git a/container-manager/src/libs/check-chainspec.js b/container-manager/src/libs/check-chainspec.js new file mode 100644 index 0000000..6e92079 --- /dev/null +++ b/container-manager/src/libs/check-chainspec.js @@ -0,0 +1,273 @@ +#!/usr/bin/env node +/** + * check-chainspec.js + * + * Pre-boot sanity check: re-run the genesis -> chainspec translation and compare + * the result against the chainspec.json a deployment is about to hand to its + * Nethermind nodes. A mismatch means the two files describe different chains — + * booting on it gives nodes that never agree on the genesis block. + * + * On a mismatch the stale chainspec is moved into an `archive/` directory next + * to it, named with a UTC timestamp, and replaced with the one genesis.json + * translates to. Pass --dry-run to report without touching anything. + * + * Usage: + * node check-chainspec.js [--dry-run] [--name ] [--base-fee <0x..>] + * + * Both paths are required; there are no defaults. Pass --name / --base-fee when + * the chainspec was produced with those flags, otherwise the translator defaults + * are used (which is what the deployment pipeline does). + * + * Exit codes: + * 0 chainspec.json already matches genesis.json, nothing was touched + * 1 they differed (differences are listed, and the chainspec was replaced + * unless --dry-run) — worth a look before booting + * 2 a file is missing / unreadable / not valid JSON, or the replace failed + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { translate } = require('./genesis-to-chainspec'); + +const MAX_REPORTED = 20; +const MAX_VALUE_LEN = 60; + +/* ------------------------------------------------------------------ * + * Deep comparison + * ------------------------------------------------------------------ */ + +// Sentinel for "this key does not exist here at all", so a missing key reads +// differently from a key explicitly set to undefined/null. +const ABSENT = Symbol('absent'); + +function isPlainObject(v) { + return v !== null && typeof v === 'object' && !Array.isArray(v); +} + +function formatValue(v) { + if (v === ABSENT) return '(missing)'; + const s = JSON.stringify(v); + if (s === undefined) return String(v); + return s.length > MAX_VALUE_LEN ? s.slice(0, MAX_VALUE_LEN) + '…' : s; +} + +// Collect every path where `expected` (derived from genesis) and `actual` +// (chainspec on disk) disagree. +function collectDiffs(expected, actual, prefix, out) { + if (expected === ABSENT || actual === ABSENT) { + out.push({ path: prefix, expected, actual }); + return; + } + + if (Array.isArray(expected) || Array.isArray(actual)) { + if (!Array.isArray(expected) || !Array.isArray(actual)) { + out.push({ path: prefix, expected, actual }); + return; + } + const len = Math.max(expected.length, actual.length); + for (let i = 0; i < len; i++) { + collectDiffs( + i < expected.length ? expected[i] : ABSENT, + i < actual.length ? actual[i] : ABSENT, + `${prefix}[${i}]`, + out + ); + } + return; + } + + if (isPlainObject(expected) || isPlainObject(actual)) { + if (!isPlainObject(expected) || !isPlainObject(actual)) { + out.push({ path: prefix, expected, actual }); + return; + } + const keys = new Set([...Object.keys(expected), ...Object.keys(actual)]); + for (const key of keys) { + collectDiffs( + key in expected ? expected[key] : ABSENT, + key in actual ? actual[key] : ABSENT, + prefix ? `${prefix}.${key}` : key, + out + ); + } + return; + } + + if (expected !== actual) { + out.push({ path: prefix, expected, actual }); + } +} + +/** + * Compare a chainspec against the one genesis.json translates to. + * Returns { expected, diffs }; diffs is empty when they match. + */ +function compare(genesis, chainspec, opts = {}) { + // Round-trip through JSON so keys translate() leaves undefined are dropped + // the same way they are when the chainspec is written to disk. + const expected = JSON.parse(JSON.stringify(translate(genesis, opts))); + const diffs = []; + collectDiffs(expected, chainspec, '', diffs); + return { expected, diffs }; +} + +/* ------------------------------------------------------------------ * + * Archiving + * ------------------------------------------------------------------ */ + +// UTC so the ordering of archived files is unambiguous across machines. +function timestamp(date) { + const p = (n, w = 2) => String(n).padStart(w, '0'); + return ( + `${date.getUTCFullYear()}${p(date.getUTCMonth() + 1)}${p(date.getUTCDate())}` + + `-${p(date.getUTCHours())}${p(date.getUTCMinutes())}${p(date.getUTCSeconds())}Z` + ); +} + +// archive/..json, with a counter if that name is taken (two +// runs inside the same second). +function archivePath(filePath, date) { + const dir = path.join(path.dirname(filePath), 'archive'); + const ext = path.extname(filePath); + const base = path.basename(filePath, ext); + const stamp = timestamp(date); + let candidate = path.join(dir, `${base}.${stamp}${ext}`); + for (let i = 2; fs.existsSync(candidate); i++) { + candidate = path.join(dir, `${base}.${stamp}.${i}${ext}`); + } + return candidate; +} + +/** + * Move the current chainspec into archive/ and write the expected one in its + * place. Returns the path the old file was archived to. + */ +function replaceChainspec(chainspecPath, expected, date) { + const backup = archivePath(chainspecPath, date); + fs.mkdirSync(path.dirname(backup), { recursive: true }); + fs.copyFileSync(chainspecPath, backup); + fs.writeFileSync(chainspecPath, JSON.stringify(expected, null, 2) + '\n'); + return backup; +} + +/* ------------------------------------------------------------------ * + * CLI + * ------------------------------------------------------------------ */ + +const USAGE = + 'Usage: node check-chainspec.js [--dry-run] [--name ] [--base-fee <0x..>]\n' + + 'Both paths are required. A differing chainspec is archived and replaced,\n' + + 'unless --dry-run is given.\n' + + 'Exit code: 0 already matched, 1 differed, 2 error.'; + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +function main(argv) { + const args = argv.slice(2); + const positional = []; + const opts = {}; + let dryRun = false; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--name') { + opts.name = args[++i]; + } else if (args[i] === '--base-fee') { + opts.baseFeePerGas = args[++i]; + } else if (args[i] === '--dry-run') { + dryRun = true; + } else if (args[i] === '-h' || args[i] === '--help') { + console.log(USAGE); + return 0; + } else { + positional.push(args[i]); + } + } + + if (positional.length !== 2) { + console.error( + positional.length < 2 + ? 'Error: both the genesis.json and the chainspec.json path are required.' + : `Error: unexpected extra argument "${positional[2]}".` + ); + console.error(USAGE); + return 2; + } + + const genesisPath = path.resolve(positional[0]); + const chainspecPath = path.resolve(positional[1]); + + let genesis; + let chainspec; + try { + genesis = readJson(genesisPath); + } catch (e) { + console.error(`Error: cannot read genesis from ${genesisPath}: ${e.message}`); + return 2; + } + try { + chainspec = readJson(chainspecPath); + } catch (e) { + console.error(`Error: cannot read chainspec from ${chainspecPath}: ${e.message}`); + return 2; + } + + let expected; + let diffs; + try { + ({ expected, diffs } = compare(genesis, chainspec, opts)); + } catch (e) { + console.error(`Error: cannot translate ${genesisPath}: ${e.message}`); + return 2; + } + + if (diffs.length === 0) { + console.log( + `OK: ${chainspecPath} matches ${genesisPath}` + + (chainspec.params && chainspec.params.chainId ? ` (chainId ${chainspec.params.chainId})` : '') + ); + return 0; + } + + console.log( + `MISMATCH: ${chainspecPath} does not match ${genesisPath} ` + + `(${diffs.length} difference${diffs.length === 1 ? '' : 's'})` + ); + for (const d of diffs.slice(0, MAX_REPORTED)) { + console.log( + ` ${d.path || '(root)'}: from genesis ${formatValue(d.expected)}, ` + + `in chainspec ${formatValue(d.actual)}` + ); + } + if (diffs.length > MAX_REPORTED) { + console.log(` ... and ${diffs.length - MAX_REPORTED} more`); + } + + if (dryRun) { + console.log('--dry-run: nothing was changed. Drop the flag to archive and replace it.'); + return 1; + } + + let backup; + try { + backup = replaceChainspec(chainspecPath, expected, new Date()); + } catch (e) { + console.error(`Error: cannot replace ${chainspecPath}: ${e.message}`); + return 2; + } + console.log(`Archived the old chainspec to ${backup}`); + console.log(`Replaced ${chainspecPath} with the one generated from ${genesisPath}`); + console.log( + 'Nodes that already ran on the old chainspec keep chain data built from it; ' + + 'wipe their data directories before booting if the genesis block changed.' + ); + return 1; +} + +if (require.main === module) { + process.exit(main(process.argv)); +} + +module.exports = { compare }; diff --git a/container-manager/src/package.json b/container-manager/src/package.json index db9af42..cc9e616 100644 --- a/container-manager/src/package.json +++ b/container-manager/src/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node express.js", "dev": "HOSTPWD=$PWD/../mount/generated nodemon express.js", - "convert": "node libs/genesis-to-chainspec.js" + "convert": "node libs/genesis-to-chainspec.js", + "check-chainspec": "node libs/check-chainspec.js" }, "dependencies": { "axios": "^1.7.9", From 7329ad9b4f3b84443bdde310e3785ad006464bc4 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 00:33:49 +0700 Subject: [PATCH 13/59] improve version detection of chainspec checker and organize directories --- container-manager/src/README.md | 31 ++++++++-------- container-manager/src/gen/gen_xdpos.js | 14 ++------ .../src/gen/scripts/check-chainspec.sh | 36 +++++++++++-------- .../src/gen/scripts/docker-up.sh | 2 +- container-manager/src/libs/check-chainspec.js | 24 ++++++++----- container-manager/src/libs/exec.js | 19 ++++++++++ 6 files changed, 76 insertions(+), 50 deletions(-) diff --git a/container-manager/src/README.md b/container-manager/src/README.md index f72f21f..9899271 100644 --- a/container-manager/src/README.md +++ b/container-manager/src/README.md @@ -26,25 +26,26 @@ inside the container is `./generated` on the host; relative paths resolve agains ## Pre-boot check -`check-chainspec.sh` ships in every generated deployment folder, next to -`docker-up.sh`. Run it before booting: it re-translates `genesis.json` and -compares the result with `chainspec.json`, since a stale chainspec gives the -Nethermind nodes a different genesis block than the XDC nodes. +`scripts/check-chainspec.sh` ships in every generated deployment folder, and +`docker-up.sh` runs it before starting any container: it re-translates +`genesis.json` and compares the result with `chainspec.json`, since a stale +chainspec gives the Nethermind nodes a different config than the XDC nodes. ```bash -./check-chainspec.sh # check, and repair if they differ -./check-chainspec.sh --dry-run # report only +./scripts/check-chainspec.sh # check, and repair if they differ +./scripts/check-chainspec.sh --dry-run # report only ``` On a difference it lists the offending keys, moves the old file to `archive/chainspec..json`, and writes the chainspec that `genesis.json` translates to. Exit codes: `0` already matched, `1` differed, -`2` error — so `./check-chainspec.sh && ./docker-up.sh machine1` stops after a -repair and lets you decide whether node data directories need wiping. - -It runs on the host's `node` if there is one, otherwise inside a throwaway -`node:24-alpine` container (`CHECK_IMAGE` overrides the image). The check itself -is `scripts/check-chainspec.js`, copied into the deployment folder at generation -time along with `scripts/genesis-to-chainspec.js`, so it needs neither this -container nor a network. Inside the container manager the same check is -`npm run check-chainspec -- `. +`2` error. `docker-up.sh` stops on anything but `0`, so a repaired chainspec +leaves you free to wipe node data directories before running it again. + +The check runs in a throwaway subnet-generator container, which already carries +the converter — the host needs docker only, no node. The image is the one that +generated the deployment: the manager inspects its own container during genesis +generation and records it in `gen.env` as `GENERATOR_IMAGE_VERSION`, so the +check always uses the converter version the chainspec was built with. Export +`GENERATOR_IMAGE_VERSION` to override it. Inside the container manager the same +check is `npm run check-chainspec -- `. diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index e821009..a1fb23a 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -193,19 +193,11 @@ function copyScripts(output_dir) { `${__dirname}/scripts/xdc-nmc.json`, `${output_dir}/xdc-nmc.json` ); - // pre-boot check that chainspec.json still matches genesis.json; the two js - // files are the check itself, shipped so it runs without this container + // pre-boot check that chainspec.json still matches genesis.json; the check + // itself runs in a subnet-generator container, so only the wrapper is copied fs.copyFileSync( `${__dirname}/scripts/check-chainspec.sh`, - `${output_dir}/check-chainspec.sh` - ); - fs.copyFileSync( - `${__dirname}/../libs/check-chainspec.js`, - `${output_dir}/scripts/check-chainspec.js` - ); - fs.copyFileSync( - `${__dirname}/../libs/genesis-to-chainspec.js`, - `${output_dir}/scripts/genesis-to-chainspec.js` + `${output_dir}/scripts/check-chainspec.sh` ); } diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index 04ef775..fba38c7 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -1,22 +1,33 @@ #!/bin/bash # Pre-boot check: verify chainspec.json still describes the same chain as # genesis.json. Run this before docker-up.sh — a stale chainspec gives the -# Nethermind nodes a different genesis block than the XDC nodes, and they -# will never sync with each other. +# Nethermind nodes a different config than the XDC nodes, and they will never +# sync with each other. # # If they differ, the old chainspec.json is moved to archive/ under a UTC # timestamp and replaced with the one genesis.json translates to. # -# Usage: ./check-chainspec.sh [genesis.json] [chainspec.json] [--dry-run] -# paths are relative to this deployment folder +# Usage: ./scripts/check-chainspec.sh [genesis.json] [chainspec.json] [--dry-run] +# paths are relative to the deployment folder (the parent of this script) # --dry-run reports the differences without changing any file # # Exit code: 0 already matched, 1 differed, 2 error. # -# Uses the host's node when it has one, otherwise runs the check in a -# throwaway container ($CHECK_IMAGE, default node:24-alpine). +# The check runs in a throwaway subnet-generator container, which carries the +# converter; the host needs docker only. The image defaults to the generator +# that produced this deployment, recorded in gen.env at generation time; export +# GENERATOR_IMAGE_VERSION to override it. -cd "$(dirname "$0")" +cd "$(dirname "$0")/.." + +if [[ -z "$GENERATOR_IMAGE_VERSION" && -f gen.env ]]; then + GENERATOR_IMAGE_VERSION=$(grep -E '^GENERATOR_IMAGE_VERSION=' gen.env | tail -1 | cut -d '=' -f 2-) +fi +if [[ -z "$GENERATOR_IMAGE_VERSION" ]]; then + echo "Error: GENERATOR_IMAGE_VERSION is not set and gen.env does not record one" + echo "Export it to pick the subnet-generator image to run the check with." + exit 2 +fi genesis="genesis.json" chainspec="chainspec.json" @@ -28,7 +39,6 @@ if [[ -n "$1" && "$1" != -* ]]; then shift fi fi -image="${CHECK_IMAGE:-node:24-alpine}" if [[ ! -f "$genesis" ]]; then echo "Error: $genesis not found in $PWD" @@ -41,10 +51,6 @@ if [[ ! -f "$chainspec" ]]; then exit 2 fi -if command -v node > /dev/null 2>&1; then - node scripts/check-chainspec.js "$genesis" "$chainspec" "$@" -else - # -u keeps the archived and rewritten files owned by the invoking user - docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$image" \ - node scripts/check-chainspec.js "$genesis" "$chainspec" "$@" -fi +# -u keeps the archived and rewritten files owned by the invoking user +docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$GENERATOR_IMAGE_VERSION" \ + node /app/libs/check-chainspec.js "$genesis" "$chainspec" "$@" diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index 908ef37..3c77fa6 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -27,7 +27,7 @@ fi # pre-check: chainspec.json must still match genesis.json, otherwise the # Nethermind nodes boot on a different config than the XDC nodes. -bash check-chainspec.sh +bash scripts/check-chainspec.sh check_result=$? if [[ $check_result == 1 ]]; then echo "" diff --git a/container-manager/src/libs/check-chainspec.js b/container-manager/src/libs/check-chainspec.js index 6e92079..6c6ca7e 100644 --- a/container-manager/src/libs/check-chainspec.js +++ b/container-manager/src/libs/check-chainspec.js @@ -166,6 +166,14 @@ function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); } +// Report paths relative to the working directory when they sit under it. The +// check usually runs in a container with the deployment folder bind-mounted, so +// absolute paths would be the container's, not the ones the operator knows. +function display(filePath) { + const rel = path.relative(process.cwd(), filePath); + return rel && !rel.startsWith('..') ? rel : filePath; +} + function main(argv) { const args = argv.slice(2); const positional = []; @@ -204,13 +212,13 @@ function main(argv) { try { genesis = readJson(genesisPath); } catch (e) { - console.error(`Error: cannot read genesis from ${genesisPath}: ${e.message}`); + console.error(`Error: cannot read genesis from ${display(genesisPath)}: ${e.message}`); return 2; } try { chainspec = readJson(chainspecPath); } catch (e) { - console.error(`Error: cannot read chainspec from ${chainspecPath}: ${e.message}`); + console.error(`Error: cannot read chainspec from ${display(chainspecPath)}: ${e.message}`); return 2; } @@ -219,20 +227,20 @@ function main(argv) { try { ({ expected, diffs } = compare(genesis, chainspec, opts)); } catch (e) { - console.error(`Error: cannot translate ${genesisPath}: ${e.message}`); + console.error(`Error: cannot translate ${display(genesisPath)}: ${e.message}`); return 2; } if (diffs.length === 0) { console.log( - `OK: ${chainspecPath} matches ${genesisPath}` + + `OK: ${display(chainspecPath)} matches ${display(genesisPath)}` + (chainspec.params && chainspec.params.chainId ? ` (chainId ${chainspec.params.chainId})` : '') ); return 0; } console.log( - `MISMATCH: ${chainspecPath} does not match ${genesisPath} ` + + `MISMATCH: ${display(chainspecPath)} does not match ${display(genesisPath)} ` + `(${diffs.length} difference${diffs.length === 1 ? '' : 's'})` ); for (const d of diffs.slice(0, MAX_REPORTED)) { @@ -254,11 +262,11 @@ function main(argv) { try { backup = replaceChainspec(chainspecPath, expected, new Date()); } catch (e) { - console.error(`Error: cannot replace ${chainspecPath}: ${e.message}`); + console.error(`Error: cannot replace ${display(chainspecPath)}: ${e.message}`); return 2; } - console.log(`Archived the old chainspec to ${backup}`); - console.log(`Replaced ${chainspecPath} with the one generated from ${genesisPath}`); + console.log(`Archived the old chainspec to ${display(backup)}`); + console.log(`Replaced ${display(chainspecPath)} with the one generated from ${display(genesisPath)}`); console.log( 'Nodes that already ran on the old chainspec keep chain data built from it; ' + 'wipe their data directories before booting if the genesis block changed.' diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 15baa7b..83025ad 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -357,6 +357,24 @@ RELAYER_MODE=${relayer_mode} return content; } +// This manager runs inside the subnet-generator image, so ask docker which +// image that is: recorded in gen.env, it lets the deployment run the pre-boot +// chainspec check with the exact version that generated it. Falls back to the +// configured image name when not running in a container (npm run dev). +function getGeneratorImage() { + try { + const image = execSync(`docker inspect --format '{{.Config.Image}}' $(hostname)`, { + encoding: "utf-8", + }).trim(); + if (image) { + return image; + } + } catch (e) { + console.error("cannot detect the generator image:", e.message); + } + return require("../gen/config_gen.js").config.docker_image_name; +} + function genGenXdposEnv(input){ console.log(input); let content_machine = ""; @@ -401,6 +419,7 @@ function genGenXdposEnv(input){ 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"]}`; } + content_version += `\nGENERATOR_IMAGE_VERSION=${getGeneratorImage()}`; From 945c1de86319439bd328407ce78ca11c6b2d37fe Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 00:38:12 +0700 Subject: [PATCH 14/59] update default --- container-manager/src/gen/config_gen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index db4bc48..45ae418 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -19,7 +19,7 @@ const config = { process.env.SERVICES_SECRET || crypto.randomBytes(10).toString("hex"), relayer_mode: process.env.RELAYER_MODE || "full", docker_image_name: - process.env.IMAGE_NAME || "xinfinorg/subnet-generator:latest", + process.env.IMAGE_NAME || "xinfinorg/subnet-generator:v3.0.0", version: { genesis: process.env.VERSION_GENESIS || "v0.3.1", subnet: process.env.VERSION_SUBNET || "v0.3.2", From 5aa223cc87924360915b2d0434cc2d2f994275a2 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 00:46:31 +0700 Subject: [PATCH 15/59] fix bug --- container-manager/src/README.md | 8 +++++--- .../src/gen/scripts/check-chainspec.sh | 14 +++++++++++++- container-manager/src/gen/scripts/docker-up.sh | 2 +- container-manager/src/libs/check-chainspec.js | 13 +++++++++---- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/container-manager/src/README.md b/container-manager/src/README.md index 9899271..691f4c2 100644 --- a/container-manager/src/README.md +++ b/container-manager/src/README.md @@ -38,9 +38,11 @@ chainspec gives the Nethermind nodes a different config than the XDC nodes. On a difference it lists the offending keys, moves the old file to `archive/chainspec..json`, and writes the chainspec that -`genesis.json` translates to. Exit codes: `0` already matched, `1` differed, -`2` error. `docker-up.sh` stops on anything but `0`, so a repaired chainspec -leaves you free to wipe node data directories before running it again. +`genesis.json` translates to. Exit codes: `0` already matched, `3` differed, +`2` error — `3` rather than `1`, since the check runs in a container and node +exits `1` when it crashes. `docker-up.sh` stops on anything but `0`, so a +repaired chainspec leaves you free to wipe node data directories before running +it again. The check runs in a throwaway subnet-generator container, which already carries the converter — the host needs docker only, no node. The image is the one that diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index fba38c7..c318133 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -11,7 +11,7 @@ # paths are relative to the deployment folder (the parent of this script) # --dry-run reports the differences without changing any file # -# Exit code: 0 already matched, 1 differed, 2 error. +# Exit code: 0 already matched, 3 differed, 2 error. # # The check runs in a throwaway subnet-generator container, which carries the # converter; the host needs docker only. The image defaults to the generator @@ -54,3 +54,15 @@ fi # -u keeps the archived and rewritten files owned by the invoking user docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$GENERATOR_IMAGE_VERSION" \ node /app/libs/check-chainspec.js "$genesis" "$chainspec" "$@" +result=$? + +# only 0, 2 and 3 come from the check itself; anything else is docker or node +# failing before it could compare, and must not be read as a result +if [[ $result != 0 && $result != 2 && $result != 3 ]]; then + echo "" + echo "Error: the check did not run (exit $result)." + echo "$GENERATOR_IMAGE_VERSION may predate it — the image needs /app/libs/check-chainspec.js." + echo "Set GENERATOR_IMAGE_VERSION to a newer subnet-generator image." + exit 2 +fi +exit $result diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index 3c77fa6..ece1837 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -29,7 +29,7 @@ fi # Nethermind nodes boot on a different config than the XDC nodes. bash scripts/check-chainspec.sh check_result=$? -if [[ $check_result == 1 ]]; then +if [[ $check_result == 3 ]]; then echo "" echo "chainspec.json was updated and the old copy saved as backup in archive directory." exit 1 diff --git a/container-manager/src/libs/check-chainspec.js b/container-manager/src/libs/check-chainspec.js index 6c6ca7e..577bf30 100644 --- a/container-manager/src/libs/check-chainspec.js +++ b/container-manager/src/libs/check-chainspec.js @@ -20,9 +20,13 @@ * * Exit codes: * 0 chainspec.json already matches genesis.json, nothing was touched - * 1 they differed (differences are listed, and the chainspec was replaced + * 3 they differed (differences are listed, and the chainspec was replaced * unless --dry-run) — worth a look before booting * 2 a file is missing / unreadable / not valid JSON, or the replace failed + * + * 3 rather than 1 for "differed": this usually runs inside a container, where + * 1 is also what node exits with when it crashes, so a caller could not tell a + * replaced chainspec from a broken run. */ 'use strict'; @@ -31,6 +35,7 @@ const fs = require('fs'); const path = require('path'); const { translate } = require('./genesis-to-chainspec'); +const EXIT_DIFFERED = 3; const MAX_REPORTED = 20; const MAX_VALUE_LEN = 60; @@ -160,7 +165,7 @@ const USAGE = 'Usage: node check-chainspec.js [--dry-run] [--name ] [--base-fee <0x..>]\n' + 'Both paths are required. A differing chainspec is archived and replaced,\n' + 'unless --dry-run is given.\n' + - 'Exit code: 0 already matched, 1 differed, 2 error.'; + 'Exit code: 0 already matched, 3 differed, 2 error.'; function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); @@ -255,7 +260,7 @@ function main(argv) { if (dryRun) { console.log('--dry-run: nothing was changed. Drop the flag to archive and replace it.'); - return 1; + return EXIT_DIFFERED; } let backup; @@ -271,7 +276,7 @@ function main(argv) { 'Nodes that already ran on the old chainspec keep chain data built from it; ' + 'wipe their data directories before booting if the genesis block changed.' ); - return 1; + return EXIT_DIFFERED; } if (require.main === module) { From 91d1a661e4d1a8c5e14d7943f4c56226406abd6e Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 01:04:04 +0700 Subject: [PATCH 16/59] update genesis-to-chainspec.js to read values from genesis.json instead of using defaults --- .../src/gen/scripts/check-chainspec.sh | 4 +- .../src/libs/genesis-to-chainspec.js | 159 ++++++++++++------ 2 files changed, 114 insertions(+), 49 deletions(-) diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index c318133..56055c3 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -62,7 +62,9 @@ if [[ $result != 0 && $result != 2 && $result != 3 ]]; then echo "" echo "Error: the check did not run (exit $result)." echo "$GENERATOR_IMAGE_VERSION may predate it — the image needs /app/libs/check-chainspec.js." - echo "Set GENERATOR_IMAGE_VERSION to a newer subnet-generator image." + echo "A local copy of that tag can be outdated, docker will not re-pull it. Try:" + echo " docker pull $GENERATOR_IMAGE_VERSION" + echo "Or set GENERATOR_IMAGE_VERSION to a newer subnet-generator image." exit 2 fi exit $result diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index bd5fbef..e37087f 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -22,15 +22,20 @@ * Self-contained: no external dependencies. * * -------------------------------------------------------------------------- - * Mapping summary (see README at bottom of file for the full diff rationale): + * Every chainspec value that genesis.json can supply is read from it. The + * DEFAULT_* tables below are fallbacks only: they apply when genesis says + * nothing about a field. * * genesis.config.chainId -> params.chainId * genesis.config.homesteadBlock -> params.homesteadBlock * genesis.config.eip150Block -> params.eip150Transition * genesis.config.eip155Block -> params.eip155Transition - * genesis.config.eip158Block -> params.eip158Transition + * genesis.config.eip158Block -> params.eip158Transition, eip160Transition * genesis.config.byzantiumBlock -> params.byzantiumBlock - * (all remaining EIP transitions come from DEFAULT_PARAMS) + * the remaining EIP transitions come from the hardfork that ships them, see + * TRANSITION_FORKS: constantinopleBlock, istanbulBlock, berlinBlock, + * londonBlock (eip1559Block wins for eip1559Transition), mergeBlock, + * shanghaiBlock, cancunBlock * * genesis.config.XDPoS.period -> engine.XDPoS.params.period * genesis.config.XDPoS.epoch -> engine.XDPoS.params.epoch @@ -40,14 +45,32 @@ * genesis.config.XDPoS.foudationWalletAddr-> engine.XDPoS.params.foundationWalletAddr (typo fixed, lowercased) * genesis.config.XDPoS.v2.switchEpoch -> engine.XDPoS.params.switchEpoch (also accepts "SwitchEpoch") * genesis.config.XDPoS.v2.switchBlock -> engine.XDPoS.params.switchBlock (also accepts "SwitchBlock") - * genesis.config.XDPoS.v2.allConfigs.* -> engine.XDPoS.params.v2Configs[] (expTimeoutConfig dropped) - * (the engine constants — contract addresses, mergeSignRange, blacklist, etc. — come from DEFAULT_ENGINE) + * genesis.config.XDPoS.v2.allConfigs.* -> engine.XDPoS.params.v2Configs[] + * (TODO: the masternode/protector/observer reward amounts are zeroed, not + * carried — see translate()) + * genesis.config.tip2019Block -> engine.XDPoS.params.tip2019Block + * genesis.config.dynamicGasLimitBlock -> engine.XDPoS.params.DynamicGasLimitBlock + * genesis.config.tipXDCXBlock -> engine.XDPoS.params.TipXDCX + * genesis.config.denylistBlock -> engine.XDPoS.params.blackListHFNumber + * genesis.config.tipTRC21FeeBlock -> engine.XDPoS.params.TipTrc21Fee + * genesis.config.tipXDCXMinerDisableBlock -> engine.XDPoS.params.TIPXDCXMinerDisable + * genesis.config.tipXDCXReceiverDisableBlock -> engine.XDPoS.params.TIPXDCXReceiverDisable * * genesis.{nonce,timestamp,extraData,gasLimit,difficulty,mixHash, * coinbase,number,gasUsed,parentHash} -> genesis.* (verbatim) * genesis.baseFeePerGas (null) -> genesis.baseFeePerGas (DEFAULT_BASE_FEE_PER_GAS) * * genesis.alloc -> accounts (verbatim: code / storage / balance) + * + * Not derivable from genesis, so always DEFAULT_ENGINE / DEFAULT_PARAMS: + * mergeSignRange, RangeReturnSigner, blackListedAddresses, the contract + * binaries (they live in XDC's common constants, not in genesis), and + * eip1559ElasticityMultiplier. + * + * Present in genesis but with no counterpart in the chainspec schema, so + * dropped rather than invented: v2 expTimeoutConfig, maxMasternodesV2, + * SkipV1Validation, pragueBlock/osakaBlock, and the trc21IssuerSMC / + * xdcxListingSMC / relayerRegistrationSMC / lendingRegistrationSMC addresses. * -------------------------------------------------------------------------- */ @@ -124,6 +147,59 @@ const DEFAULT_ENGINE = { XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', }; +// Which hardfork block in genesis.config activates each chainspec transition. +// The EIPs of a fork all switch on at that fork's block; when genesis does not +// mention the fork, the DEFAULT_PARAMS entry stands in. Order matters: it is +// the key order of the generated params object. +const TRANSITION_FORKS = { + eip160Transition: 'eip158Block', // Spurious Dragon, same block as EIP-158 + eip145Transition: 'constantinopleBlock', + eip1014Transition: 'constantinopleBlock', + eip1052Transition: 'constantinopleBlock', + eip1234Transition: 'constantinopleBlock', + eip1283Transition: 'constantinopleBlock', + eip152Transition: 'istanbulBlock', + eip1108Transition: 'istanbulBlock', + eip1344Transition: 'istanbulBlock', + eip1884Transition: 'istanbulBlock', + eip2028Transition: 'istanbulBlock', + eip2200Transition: 'istanbulBlock', + eip2565Transition: 'berlinBlock', + eip2718Transition: 'berlinBlock', + eip2930Transition: 'berlinBlock', + eip1559Transition: 'londonBlock', // eip1559Block takes precedence, see below + eip2929Transition: 'berlinBlock', + eip3198Transition: 'londonBlock', + eip3529Transition: 'londonBlock', + eip3541Transition: 'londonBlock', + eip3554Transition: 'londonBlock', + eip4399Transition: 'mergeBlock', + eip3651Transition: 'shanghaiBlock', + eip3855Transition: 'shanghaiBlock', + eip3860Transition: 'shanghaiBlock', + eip6049Transition: 'shanghaiBlock', + eip1153Transition: 'cancunBlock', + eip4844Transition: 'cancunBlock', + eip5656Transition: 'cancunBlock', + eip6780Transition: 'cancunBlock', + eip7516Transition: 'cancunBlock', +}; + +/* ------------------------------------------------------------------ * + * Value selection + * ------------------------------------------------------------------ */ + +// First value genesis actually states wins; a default is only reached when +// every candidate is absent. Not `||` — 0 and false are real values here. +function pick(...candidates) { + for (const candidate of candidates) { + if (candidate !== undefined && candidate !== null) { + return candidate; + } + } + return undefined; +} + /* ------------------------------------------------------------------ * * Address normalization * ------------------------------------------------------------------ */ @@ -144,12 +220,15 @@ function translate(genesis, opts = {}) { const v2 = xdpos.v2 || {}; // --- engine.XDPoS.params --- + // Every per-round field is carried through as genesis states it, with two + // exceptions: expTimeoutConfig, which the chainspec schema has no key for and + // so is dropped rather than invented, and the reward amounts below. const v2Configs = Object.keys(v2.allConfigs || {}) .sort((a, b) => Number(a) - Number(b)) .map((round) => { - const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; // drop expTimeoutConfig - // PLACEHOLDER: per-round reward amounts zeroed until XDC reward economics - // is finalized. Also avoids fractional puppeth defaults (e.g. + const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; + // TODO: carry the genesis reward amounts once XDC reward economics is + // finalized. Zeroed for now: puppeth writes fractional defaults (e.g. // protectorReward: 45.6) that Nethermind's UInt256 parser rejects. rest.masternodeReward = 0; rest.protectorReward = 0; @@ -170,15 +249,20 @@ function translate(genesis, opts = {}) { switchEpoch: v2.switchEpoch ?? v2.SwitchEpoch, switchBlock: v2.switchBlock ?? v2.SwitchBlock, v2Configs, + // no genesis counterpart: node-level constants, not chain data mergeSignRange: DEFAULT_ENGINE.mergeSignRange, RangeReturnSigner: DEFAULT_ENGINE.RangeReturnSigner, - tip2019Block: DEFAULT_ENGINE.tip2019Block, - DynamicGasLimitBlock: DEFAULT_ENGINE.DynamicGasLimitBlock, - TipXDCX: DEFAULT_ENGINE.TipXDCX, - blackListHFNumber: DEFAULT_ENGINE.blackListHFNumber, - TipTrc21Fee: DEFAULT_ENGINE.TipTrc21Fee, - TIPXDCXMinerDisable: DEFAULT_ENGINE.TIPXDCXMinerDisable, - TIPXDCXReceiverDisable: DEFAULT_ENGINE.TIPXDCXReceiverDisable, + // XDC hardfork blocks, named differently on each side + tip2019Block: pick(cfg.tip2019Block, DEFAULT_ENGINE.tip2019Block), + DynamicGasLimitBlock: pick(cfg.dynamicGasLimitBlock, DEFAULT_ENGINE.DynamicGasLimitBlock), + TipXDCX: pick(cfg.tipXDCXBlock, DEFAULT_ENGINE.TipXDCX), + blackListHFNumber: pick(cfg.denylistBlock, DEFAULT_ENGINE.blackListHFNumber), + TipTrc21Fee: pick(cfg.tipTRC21FeeBlock, DEFAULT_ENGINE.TipTrc21Fee), + TIPXDCXMinerDisable: pick(cfg.tipXDCXMinerDisableBlock, DEFAULT_ENGINE.TIPXDCXMinerDisable), + TIPXDCXReceiverDisable: pick( + cfg.tipXDCXReceiverDisableBlock, + DEFAULT_ENGINE.TIPXDCXReceiverDisable + ), blackListedAddresses: DEFAULT_ENGINE.blackListedAddresses, masternodeVotingContract: DEFAULT_ENGINE.masternodeVotingContract, blockSignerContract: DEFAULT_ENGINE.blockSignerContract, @@ -196,40 +280,19 @@ function translate(genesis, opts = {}) { eip150Transition: cfg.eip150Block, eip155Transition: cfg.eip155Block, eip158Transition: cfg.eip158Block, - eip160Transition: DEFAULT_PARAMS.eip160Transition, - eip145Transition: DEFAULT_PARAMS.eip145Transition, - eip1014Transition: DEFAULT_PARAMS.eip1014Transition, - eip1052Transition: DEFAULT_PARAMS.eip1052Transition, - eip1234Transition: DEFAULT_PARAMS.eip1234Transition, - eip1283Transition: DEFAULT_PARAMS.eip1283Transition, - eip152Transition: DEFAULT_PARAMS.eip152Transition, - eip1108Transition: DEFAULT_PARAMS.eip1108Transition, - eip1344Transition: DEFAULT_PARAMS.eip1344Transition, - eip1884Transition: DEFAULT_PARAMS.eip1884Transition, - eip2028Transition: DEFAULT_PARAMS.eip2028Transition, - eip2200Transition: DEFAULT_PARAMS.eip2200Transition, - eip2565Transition: DEFAULT_PARAMS.eip2565Transition, - eip2718Transition: DEFAULT_PARAMS.eip2718Transition, - eip2930Transition: DEFAULT_PARAMS.eip2930Transition, - eip1559Transition: DEFAULT_PARAMS.eip1559Transition, - eip2929Transition: DEFAULT_PARAMS.eip2929Transition, - eip3198Transition: DEFAULT_PARAMS.eip3198Transition, - eip3529Transition: DEFAULT_PARAMS.eip3529Transition, - eip3541Transition: DEFAULT_PARAMS.eip3541Transition, - eip3554Transition: DEFAULT_PARAMS.eip3554Transition, - eip4399Transition: DEFAULT_PARAMS.eip4399Transition, - eip3651Transition: DEFAULT_PARAMS.eip3651Transition, - eip3855Transition: DEFAULT_PARAMS.eip3855Transition, - eip3860Transition: DEFAULT_PARAMS.eip3860Transition, - eip6049Transition: DEFAULT_PARAMS.eip6049Transition, - eip1153Transition: DEFAULT_PARAMS.eip1153Transition, - eip4844Transition: DEFAULT_PARAMS.eip4844Transition, - eip5656Transition: DEFAULT_PARAMS.eip5656Transition, - eip6780Transition: DEFAULT_PARAMS.eip6780Transition, - eip7516Transition: DEFAULT_PARAMS.eip7516Transition, - byzantiumBlock: cfg.byzantiumBlock, - eip1559ElasticityMultiplier: DEFAULT_PARAMS.eip1559ElasticityMultiplier, }; + for (const [key, forkBlock] of Object.entries(TRANSITION_FORKS)) { + params[key] = pick(cfg[forkBlock], DEFAULT_PARAMS[key]); + } + // genesis states EIP-1559 on its own key as well as through londonBlock + params.eip1559Transition = pick( + cfg.eip1559Block, + cfg.londonBlock, + DEFAULT_PARAMS.eip1559Transition + ); + params.byzantiumBlock = cfg.byzantiumBlock; + // no genesis counterpart + params.eip1559ElasticityMultiplier = DEFAULT_PARAMS.eip1559ElasticityMultiplier; // --- genesis block header --- const block = { From 6026e7c2ffade692e5a4bba9e39c59e0e99ffd19 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 01:16:59 +0700 Subject: [PATCH 17/59] improve wording --- container-manager/src/gen/scripts/docker-up.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index ece1837..09f4b13 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -32,6 +32,7 @@ check_result=$? if [[ $check_result == 3 ]]; then echo "" echo "chainspec.json was updated and the old copy saved as backup in archive directory." + echo "Run the previous command again to start the chain!" exit 1 elif [[ $check_result != 0 ]]; then echo "" From 971feb28fe81a19bae7b28e16ca75c7146dd6d05 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 17 Aug 2026 01:32:42 +0700 Subject: [PATCH 18/59] for a single machine setup, make docker-up.sh and docker-down.sh to not require specification of machine1 --- container-manager/src/express.js | 8 +++-- .../src/gen/scripts/docker-down.sh | 33 +++++++++++------ .../src/gen/scripts/docker-up.sh | 35 ++++++++++++------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/container-manager/src/express.js b/container-manager/src/express.js index c80cf51..026d6be 100644 --- a/container-manager/src/express.js +++ b/container-manager/src/express.js @@ -191,9 +191,13 @@ app.post("/submit_xdpos", (req, res) => { failed: true, }); } else { + // a single machine deployment needs no profile argument + const upCommand = + parseInt(req.body["text-num-machine"]) > 1 + ? "'./docker-up.sh machine1;'" + : "'./docker-up.sh;'"; res.render("xdpos_generator/submit.pug", { - message: - "Config generation success, please continue with 'cd generated;' then './docker-up.sh machine1;'", + message: `Config generation success, please continue with 'cd generated;' then ${upCommand}`, }); process.exit(0); } diff --git a/container-manager/src/gen/scripts/docker-down.sh b/container-manager/src/gen/scripts/docker-down.sh index 79054fb..dff8394 100755 --- a/container-manager/src/gen/scripts/docker-down.sh +++ b/container-manager/src/gen/scripts/docker-down.sh @@ -3,19 +3,30 @@ cd "$(dirname "$0")" -if [[ -z $1 ]]; then - echo "Missing argument" - echo "Usage: docker-down.sh PROFILE" - echo "Profile:" - echo " machine1, machine2, ... (as defined in docker-compose.yml)" - exit +compose="docker-compose" +if ! which docker-compose > /dev/null 2>&1; then + compose="docker compose" fi -which docker-compose -if [[ $? != 0 ]]; then - shopt -s expand_aliases - alias docker-compose='docker compose' +profiles=($($compose config --profiles)) + +profile="$1" +if [[ -z $profile ]]; then + if [[ ${#profiles[@]} == 1 ]]; then + # single machine deployment, nothing to choose between + profile="${profiles[0]}" + else + echo "Missing argument" + echo "Usage: docker-down.sh PROFILE" + echo "Profile:" + if [[ ${#profiles[@]} == 0 ]]; then + echo " no profile found in docker-compose.yml" + else + echo " ${profiles[*]} (as defined in docker-compose.yml)" + fi + exit + fi fi -docker-compose --profile $1 down +$compose --profile $profile down diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index 09f4b13..98b3fce 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -2,18 +2,29 @@ cd "$(dirname "$0")" -if [[ -z $1 ]]; then - echo "Missing argument" - echo "Usage: docker-up.sh PROFILE" - echo "Profile:" - echo " machine1, machine2, ... (as defined in docker-compose.yml)" - exit +compose="docker-compose" +if ! which docker-compose > /dev/null 2>&1; then + compose="docker compose" fi -which docker-compose -if [[ $? != 0 ]]; then - shopt -s expand_aliases - alias docker-compose='docker compose' +profiles=($($compose config --profiles)) + +profile="$1" +if [[ -z $profile ]]; then + if [[ ${#profiles[@]} == 1 ]]; then + # single machine deployment, nothing to choose between + profile="${profiles[0]}" + else + echo "Missing argument" + echo "Usage: docker-up.sh PROFILE" + echo "Profile:" + if [[ ${#profiles[@]} == 0 ]]; then + echo " no profile found in docker-compose.yml" + else + echo " ${profiles[*]} (as defined in docker-compose.yml)" + fi + exit + fi fi network_name="docker_net" @@ -41,6 +52,6 @@ elif [[ $check_result != 0 ]]; then fi -docker-compose --profile $1 pull -docker-compose --profile $1 up -d +$compose --profile $profile pull +$compose --profile $profile up -d From 0c6d2c6f4e4e2a141c5dffe639fc6cc5792b4192 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 24 Aug 2026 11:26:35 +0700 Subject: [PATCH 19/59] improve startup and update docs --- README.md | 21 +++++++++++++++++++-- container-manager/start.sh | 2 +- container-manager/start_xdpos.sh | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) mode change 100644 => 100755 container-manager/start.sh mode change 100644 => 100755 container-manager/start_xdpos.sh diff --git a/README.md b/README.md index 492dc40..a8a8955 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ -# Subnet Deployment Generator +## XDPoS Private Network Generator -Please refer to official docs: https://docs.xdc.network/subnet/install_guide/launch_subnet/ \ No newline at end of file +Official docs: [XDPoSChain privatenet](https://docs.xdc.network/xdcchain/developers/xdposchain_privatenet/) + +Quick start: + +```bash +cd container-manager +./start_xdpos.sh +``` + +## Subnet Deployment Generator + +Official docs: [Launch subnet](https://docs.xdc.network/subnet/install_guide/launch_subnet/) +Quick start: + +```bash +cd container-manager +./start.sh +``` diff --git a/container-manager/start.sh b/container-manager/start.sh old mode 100644 new mode 100755 index f3091ce..7a9b2e1 --- a/container-manager/start.sh +++ b/container-manager/start.sh @@ -1,5 +1,5 @@ #!/bin/bash -version="v2.0.0" +version="${GENERATOR_VERSION:v2.0.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" diff --git a/container-manager/start_xdpos.sh b/container-manager/start_xdpos.sh old mode 100644 new mode 100755 index db6df76..b6c3ab0 --- a/container-manager/start_xdpos.sh +++ b/container-manager/start_xdpos.sh @@ -1,5 +1,5 @@ #!/bin/bash -version="generator-v3.0.0" +version="${GENERATOR_VERSION:-generator-v3.0.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" From 5252c0b99796783b21dedda175022e3bdd8ad10c Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Tue, 25 Aug 2026 03:48:19 +0700 Subject: [PATCH 20/59] carry genesis reward amounts into chainspec; fix generator image refs - genesis-to-chainspec: stop zeroing masternode/protector/observer rewards, carry the values from genesis.config.XDPoS.v2.allConfigs - start_xdpos.sh: take the full image ref via GENERATOR_IMAGE_VERSION instead of building it from a GENERATOR_VERSION tag - config_gen.js: default docker_image_name to the generator-v3.0.0 tag - xdpos_generator view: widen the custom version inputs to pure-input-2-3 --- container-manager/src/gen/config_gen.js | 2 +- container-manager/src/libs/genesis-to-chainspec.js | 7 +------ container-manager/src/views/xdpos_generator/index.pug | 6 +++--- container-manager/start_xdpos.sh | 6 +++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index 45ae418..8aff160 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -19,7 +19,7 @@ const config = { process.env.SERVICES_SECRET || crypto.randomBytes(10).toString("hex"), relayer_mode: process.env.RELAYER_MODE || "full", docker_image_name: - process.env.IMAGE_NAME || "xinfinorg/subnet-generator:v3.0.0", + process.env.IMAGE_NAME || "xinfinorg/subnet-generator:generator-v3.0.0", version: { genesis: process.env.VERSION_GENESIS || "v0.3.1", subnet: process.env.VERSION_SUBNET || "v0.3.2", diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index e37087f..8566ab7 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -227,12 +227,7 @@ function translate(genesis, opts = {}) { .sort((a, b) => Number(a) - Number(b)) .map((round) => { const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; - // TODO: carry the genesis reward amounts once XDC reward economics is - // finalized. Zeroed for now: puppeth writes fractional defaults (e.g. - // protectorReward: 45.6) that Nethermind's UInt256 parser rejects. - rest.masternodeReward = 0; - rest.protectorReward = 0; - rest.observerReward = 0; + return rest; }); diff --git a/container-manager/src/views/xdpos_generator/index.pug b/container-manager/src/views/xdpos_generator/index.pug index 3cfdf18..ffd5e11 100644 --- a/container-manager/src/views/xdpos_generator/index.pug +++ b/container-manager/src/views/xdpos_generator/index.pug @@ -82,16 +82,16 @@ body center .pure-control-group label() Node Version - input#customversion-xdpos-node-fullname.pure-input(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-node-fullname") + input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-node-fullname") .pure-control-group label() Genesis Version - input#customversion-xdpos-genesis-fullname.pure-input(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-genesis-fullname") + input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-genesis-fullname") .pure-control-group label() Number of Nethermind Nodes input#customversion-xdpos-nethermind-count.pure-input(type='number' placeholder='0' name="customversion-xdpos-nethermind-count") .pure-control-group label() Nethermind Node Version - input#customversion-xdpos-nethermind-version.pure-input(type='text' placeholder='nethermindeth/nethermind:master-857da8f' name="customversion-xdpos-nethermind-version") + input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder='nethermindeth/nethermind:master-857da8f' name="customversion-xdpos-nethermind-version") .pure-control-group .info-container diff --git a/container-manager/start_xdpos.sh b/container-manager/start_xdpos.sh index b6c3ab0..2105f71 100755 --- a/container-manager/start_xdpos.sh +++ b/container-manager/start_xdpos.sh @@ -1,9 +1,9 @@ #!/bin/bash -version="${GENERATOR_VERSION:-generator-v3.0.0}" +image_version="${GENERATOR_IMAGE_VERSION:-xinfinorg/subnet-generator:generator-v3.0.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" -docker pull xinfinorg/subnet-generator:$version +docker pull $image_version # Check the exit code if [ $? -eq 0 ]; then echo "SUCCESS: Docker pull completed successfully" @@ -28,7 +28,7 @@ docker run -d \ -v $current_dir/generated:/mount/generated \ -e HOSTPWD=$current_dir/generated \ -e NON_SUBNET=true \ - xinfinorg/subnet-generator:$version \ + $image_version \ && \ echo '' && \ echo '' && \ From 298a71c33c156c4ba4840030ac2d4d01df702d07 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Tue, 25 Aug 2026 04:16:32 +0700 Subject: [PATCH 21/59] auto docker pull to make sure the generator image is updated --- container-manager/src/gen/scripts/check-chainspec.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index 56055c3..4e54546 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -51,6 +51,10 @@ if [[ ! -f "$chainspec" ]]; then exit 2 fi +# a local copy of the tag can be outdated and docker will not re-pull it on +# its own; if the pull fails, fall through and use whatever is local +docker pull "$GENERATOR_IMAGE_VERSION" + # -u keeps the archived and rewritten files owned by the invoking user docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$GENERATOR_IMAGE_VERSION" \ node /app/libs/check-chainspec.js "$genesis" "$chainspec" "$@" @@ -59,12 +63,6 @@ result=$? # only 0, 2 and 3 come from the check itself; anything else is docker or node # failing before it could compare, and must not be read as a result if [[ $result != 0 && $result != 2 && $result != 3 ]]; then - echo "" - echo "Error: the check did not run (exit $result)." - echo "$GENERATOR_IMAGE_VERSION may predate it — the image needs /app/libs/check-chainspec.js." - echo "A local copy of that tag can be outdated, docker will not re-pull it. Try:" - echo " docker pull $GENERATOR_IMAGE_VERSION" - echo "Or set GENERATOR_IMAGE_VERSION to a newer subnet-generator image." exit 2 fi exit $result From bba78f6968e05739824d989004eb7c115aa2e1b0 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Tue, 25 Aug 2026 04:57:12 +0700 Subject: [PATCH 22/59] add prague processing to genesis-to-chainspec.js --- .../src/libs/genesis-to-chainspec.js | 117 +++++++++++++++++- 1 file changed, 113 insertions(+), 4 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 8566ab7..fd1c034 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -35,7 +35,7 @@ * the remaining EIP transitions come from the hardfork that ships them, see * TRANSITION_FORKS: constantinopleBlock, istanbulBlock, berlinBlock, * londonBlock (eip1559Block wins for eip1559Transition), mergeBlock, - * shanghaiBlock, cancunBlock + * shanghaiBlock, cancunBlock, pragueBlock * * genesis.config.XDPoS.period -> engine.XDPoS.params.period * genesis.config.XDPoS.epoch -> engine.XDPoS.params.epoch @@ -69,8 +69,18 @@ * * Present in genesis but with no counterpart in the chainspec schema, so * dropped rather than invented: v2 expTimeoutConfig, maxMasternodesV2, - * SkipV1Validation, pragueBlock/osakaBlock, and the trc21IssuerSMC / - * xdcxListingSMC / relayerRegistrationSMC / lendingRegistrationSMC addresses. + * SkipV1Validation, osakaBlock, and the trc21IssuerSMC / xdcxListingSMC / + * relayerRegistrationSMC / lendingRegistrationSMC addresses. + * + * Prague (pragueBlock) maps to exactly the three EIPs XDPoSChain gates on it: + * 2935, 7623 and 7702 — see TRANSITION_FORKS for what is deliberately left + * out. Unlike every other fork here these keys have no DEFAULT_PARAMS entry, + * so a genesis that says nothing about pragueBlock (XDC mainnet, chainId 50) + * omits them from the chainspec entirely and Prague stays off, rather than + * silently switching on at block 0. + * + * Prague also needs the EIP-2935 history contract to exist in state; this + * script warns when genesis would leave it missing. See checkPrague(). * -------------------------------------------------------------------------- */ @@ -89,6 +99,26 @@ const DEFAULT_CHAIN_NAME = 'xdpos-chain'; // chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has null. const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; +// EIP-2935 history storage contract: a ring buffer of the last +// 8191 block hashes, written once per block by a system call from +// 0xfffffffffffffffffffffffffffffffffffffffe. +// +// It is a plain account with fixed bytecode, not a precompile, so the code has +// to reach state somehow, and every client on the chain has to agree on when +// it got there. XDPoSChain deploys it itself at the Prague block if the account +// has no code (core/state_processor.go, ProcessParentBlockHash). Nethermind's +// XdcBlockhashStore may or may not do the same — it does carry this bytecode as +// a literal, so do not assume either way without testing the specific build. +// +// Pre-allocating the account sidesteps the question: both clients then start +// from identical state and neither has to deploy anything. That is how +// Ethereum's Hoodi genesis and XDPoSChain's own DeveloperGenesisBlock do it. +// Leaving it out has been observed to fork a mixed XDPoSChain/Nethermind +// network at block 1 — different state roots, no quorum, nothing committed. +const HISTORY_STORAGE_ADDRESS = '0x0000f90827f1c53a10cb7a02335b175320002935'; +const HISTORY_STORAGE_CODE = + '0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500'; + // All EIP transitions in chainspec.params that don't come straight from // genesis.config. Values copied from the reference chainspec.json. const DEFAULT_PARAMS = { @@ -183,6 +213,20 @@ const TRANSITION_FORKS = { eip5656Transition: 'cancunBlock', eip6780Transition: 'cancunBlock', eip7516Transition: 'cancunBlock', + // Prague. These three are the whole of what XDPoSChain gates on IsPrague: + // 2935 (history contract), 7623 (calldata floor cost) and 7702 (setcode tx, + // the only opcode-level change — newPragueInstructionSet is Cancun plus + // enable7702). Deliberately absent, because XDPoSChain does not implement + // them and switching them on for Nethermind alone would reintroduce exactly + // the state-root divergence this mapping exists to prevent: + // 2537 (BLS precompiles) — activePrecompiledContracts has no Prague case, + // it falls through to the EIP-1559 set + // 4788 / 6110 / 7002 / 7251 / 7685 — beacon-chain EIPs; 7685 would also + // add requestsHash to the block header + // No DEFAULT_PARAMS fallback on purpose: absent pragueBlock => key omitted. + eip2935Transition: 'pragueBlock', + eip7623Transition: 'pragueBlock', + eip7702Transition: 'pragueBlock', }; /* ------------------------------------------------------------------ * @@ -210,6 +254,61 @@ function normalizeAddress(addr) { return '0x' + String(addr).toLowerCase().replace(/^0x/, ''); } +/* ------------------------------------------------------------------ * + * Prague / EIP-2935 sanity check + * ------------------------------------------------------------------ */ + +// Emitting the Prague transitions is necessary but not sufficient: every client +// on the chain must also agree on how the history contract got into state. +// Returns the lines of a warning, or [] when there is nothing to report. +// Advisory only — the chainspec is still written, because this script's job is +// to translate genesis faithfully, not to edit the chain. +function checkPrague(genesis) { + const pragueBlock = (genesis.config || {}).pragueBlock; + if (pragueBlock === undefined || pragueBlock === null) { + return []; // Prague never activates; nothing to check. + } + + const alloc = genesis.alloc || {}; + const present = Object.keys(alloc).some( + (addr) => normalizeAddress(addr) === HISTORY_STORAGE_ADDRESS + ); + if (present) { + return []; + } + + if (pragueBlock !== 0) { + // Genesis cannot help here: the contract is supposed to appear at the fork + // block, which is already in the chain's future or past. + return [ + `Prague activates mid-chain at block ${pragueBlock}, so the EIP-2935 history`, + `contract at ${HISTORY_STORAGE_ADDRESS} cannot be supplied via genesis.alloc.`, + 'XDPoSChain self-deploys it at that block. Confirm every other client on', + 'the chain does the same, or they will fork there.', + ]; + } + + return [ + 'Prague is active from block 0, but genesis.alloc has no EIP-2935 history', + `contract at ${HISTORY_STORAGE_ADDRESS}.`, + '', + 'XDPoSChain deploys it at block 1 and writes the parent hash into it. A', + 'second client that does not do exactly the same thing computes a different', + 'state root for every block, no quorum forms, and the chain stalls without', + 'ever committing a block — this has been observed against Nethermind.', + '', + 'Pre-allocating the account avoids relying on either client to deploy it.', + 'Add this to genesis.alloc, then re-run this converter so it also reaches', + 'chainspec.accounts (both files must match, or the genesis hashes differ):', + '', + ` "${HISTORY_STORAGE_ADDRESS.replace(/^0x/, '')}": {`, + ' "balance": "0x0",', + ' "nonce": "0x1",', + ` "code": "${HISTORY_STORAGE_CODE}"`, + ' }', + ]; +} + /* ------------------------------------------------------------------ * * Translation * ------------------------------------------------------------------ */ @@ -374,6 +473,16 @@ function main(argv) { const chainspec = translate(genesis, opts); const json = JSON.stringify(chainspec, null, 2) + '\n'; + const pragueWarning = checkPrague(genesis); + if (pragueWarning.length) { + console.error(''); + console.error('WARNING: Prague is enabled but the EIP-2935 history contract is missing.'); + for (const line of pragueWarning) { + console.error(line ? ` ${line}` : ''); + } + console.error(''); + } + try { fs.writeFileSync(outPath, json); } catch (e) { @@ -388,4 +497,4 @@ if (require.main === module) { process.exit(main(process.argv)); } -module.exports = { translate, normalizeAddress }; +module.exports = { translate, normalizeAddress, checkPrague }; From 61be76e20e5495ee2beb75d6d0b930e3087d39c7 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Tue, 25 Aug 2026 16:18:19 +0700 Subject: [PATCH 23/59] update forks in genesis-to-chainspec converter --- .../src/libs/genesis-to-chainspec.js | 144 ++++++------------ 1 file changed, 50 insertions(+), 94 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index fd1c034..d94bec2 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -72,15 +72,31 @@ * SkipV1Validation, osakaBlock, and the trc21IssuerSMC / xdcxListingSMC / * relayerRegistrationSMC / lendingRegistrationSMC addresses. * + * genesis.config.tipSigningBlock -> engine.XDPoS.params.TipSigningBlock + * genesis.config.tipRandomizeBlock -> engine.XDPoS.params.TipRandomizeBlock + * genesis.config.tipIncreaseMasternodesBlock -> engine.XDPoS.params.TipIncreaseMasternodesBlock + * genesis.config.tipNoHalvingMNRewardBlock -> engine.XDPoS.params.TipNoHalvingMNRewardBlock + * genesis.config.tipXDCXLendingBlock -> engine.XDPoS.params.TipXDCXLendingBlock + * genesis.config.tipXDCXCancellationFeeBlock -> engine.XDPoS.params.TipXDCXCancellationFeeBlock + * genesis.config.gas50xBlock -> engine.XDPoS.params.Gas50xBlock + * genesis.config.tipUpgradeRewardBlock -> engine.XDPoS.params.TIPUpgradeReward + * genesis.config.tipUpgradePenaltyBlock -> engine.XDPoS.params.TIPUpgradePenalty + * (see ENGINE_FORKS; key spellings, TIP casing included, are Nethermind's) + * * Prague (pragueBlock) maps to exactly the three EIPs XDPoSChain gates on it: - * 2935, 7623 and 7702 — see TRANSITION_FORKS for what is deliberately left - * out. Unlike every other fork here these keys have no DEFAULT_PARAMS entry, - * so a genesis that says nothing about pragueBlock (XDC mainnet, chainId 50) - * omits them from the chainspec entirely and Prague stays off, rather than - * silently switching on at block 0. + * 2935, 7623 and 7702 — see TRANSITION_FORKS. Nethermind's own reference spec + * for Apothem (/nethermind/chainspec/xdc-testnet.json) carries the same three + * at 83600000 and nothing else, and pre-allocates no EIP-2935 history contract, + * so Nethermind supplies that contract itself; the transitions are all that is + * needed. * - * Prague also needs the EIP-2935 history contract to exist in state; this - * script warns when genesis would leave it missing. See checkPrague(). + * Neither the Prague transitions nor ENGINE_FORKS have a DEFAULT_* fallback, + * unlike every other fork here: a fork genesis does not state is left out of + * the chainspec so it stays off, rather than silently activating at block 0. + * tipUpgradeRewardBlock is why that matters — it selects between two different + * reward formulas (eth/hooks/engine_v2_hooks.go, IsTIPUpgradeReward), so + * dropping it makes XDPoSChain and Nethermind compute different balances at the + * first reward checkpoint and the chain stalls there for good. * -------------------------------------------------------------------------- */ @@ -99,26 +115,6 @@ const DEFAULT_CHAIN_NAME = 'xdpos-chain'; // chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has null. const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; -// EIP-2935 history storage contract: a ring buffer of the last -// 8191 block hashes, written once per block by a system call from -// 0xfffffffffffffffffffffffffffffffffffffffe. -// -// It is a plain account with fixed bytecode, not a precompile, so the code has -// to reach state somehow, and every client on the chain has to agree on when -// it got there. XDPoSChain deploys it itself at the Prague block if the account -// has no code (core/state_processor.go, ProcessParentBlockHash). Nethermind's -// XdcBlockhashStore may or may not do the same — it does carry this bytecode as -// a literal, so do not assume either way without testing the specific build. -// -// Pre-allocating the account sidesteps the question: both clients then start -// from identical state and neither has to deploy anything. That is how -// Ethereum's Hoodi genesis and XDPoSChain's own DeveloperGenesisBlock do it. -// Leaving it out has been observed to fork a mixed XDPoSChain/Nethermind -// network at block 1 — different state roots, no quorum, nothing committed. -const HISTORY_STORAGE_ADDRESS = '0x0000f90827f1c53a10cb7a02335b175320002935'; -const HISTORY_STORAGE_CODE = - '0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500'; - // All EIP transitions in chainspec.params that don't come straight from // genesis.config. Values copied from the reference chainspec.json. const DEFAULT_PARAMS = { @@ -229,6 +225,26 @@ const TRANSITION_FORKS = { eip7702Transition: 'pragueBlock', }; +// XDC hardfork blocks that live in engine.XDPoS.params rather than params. +// Key = chainspec key, value = genesis.config key. The key spellings come from +// Nethermind's own reference specs (/nethermind/chainspec/xdc.json and +// xdc-testnet.json) — note TIPUpgradeReward/TIPUpgradePenalty use all-caps TIP +// and, unlike the rest, carry no "Block" suffix. Binding is case-insensitive, +// but matching the reference keeps generated and reference specs diffable. +// +// No defaults here on purpose — see the header note on ENGINE_FORKS. +const ENGINE_FORKS = { + TipSigningBlock: 'tipSigningBlock', + TipRandomizeBlock: 'tipRandomizeBlock', + TipIncreaseMasternodesBlock: 'tipIncreaseMasternodesBlock', + TipNoHalvingMNRewardBlock: 'tipNoHalvingMNRewardBlock', + TipXDCXLendingBlock: 'tipXDCXLendingBlock', + TipXDCXCancellationFeeBlock: 'tipXDCXCancellationFeeBlock', + Gas50xBlock: 'gas50xBlock', + TIPUpgradeReward: 'tipUpgradeRewardBlock', + TIPUpgradePenalty: 'tipUpgradePenaltyBlock', +}; + /* ------------------------------------------------------------------ * * Value selection * ------------------------------------------------------------------ */ @@ -254,61 +270,6 @@ function normalizeAddress(addr) { return '0x' + String(addr).toLowerCase().replace(/^0x/, ''); } -/* ------------------------------------------------------------------ * - * Prague / EIP-2935 sanity check - * ------------------------------------------------------------------ */ - -// Emitting the Prague transitions is necessary but not sufficient: every client -// on the chain must also agree on how the history contract got into state. -// Returns the lines of a warning, or [] when there is nothing to report. -// Advisory only — the chainspec is still written, because this script's job is -// to translate genesis faithfully, not to edit the chain. -function checkPrague(genesis) { - const pragueBlock = (genesis.config || {}).pragueBlock; - if (pragueBlock === undefined || pragueBlock === null) { - return []; // Prague never activates; nothing to check. - } - - const alloc = genesis.alloc || {}; - const present = Object.keys(alloc).some( - (addr) => normalizeAddress(addr) === HISTORY_STORAGE_ADDRESS - ); - if (present) { - return []; - } - - if (pragueBlock !== 0) { - // Genesis cannot help here: the contract is supposed to appear at the fork - // block, which is already in the chain's future or past. - return [ - `Prague activates mid-chain at block ${pragueBlock}, so the EIP-2935 history`, - `contract at ${HISTORY_STORAGE_ADDRESS} cannot be supplied via genesis.alloc.`, - 'XDPoSChain self-deploys it at that block. Confirm every other client on', - 'the chain does the same, or they will fork there.', - ]; - } - - return [ - 'Prague is active from block 0, but genesis.alloc has no EIP-2935 history', - `contract at ${HISTORY_STORAGE_ADDRESS}.`, - '', - 'XDPoSChain deploys it at block 1 and writes the parent hash into it. A', - 'second client that does not do exactly the same thing computes a different', - 'state root for every block, no quorum forms, and the chain stalls without', - 'ever committing a block — this has been observed against Nethermind.', - '', - 'Pre-allocating the account avoids relying on either client to deploy it.', - 'Add this to genesis.alloc, then re-run this converter so it also reaches', - 'chainspec.accounts (both files must match, or the genesis hashes differ):', - '', - ` "${HISTORY_STORAGE_ADDRESS.replace(/^0x/, '')}": {`, - ' "balance": "0x0",', - ' "nonce": "0x1",', - ` "code": "${HISTORY_STORAGE_CODE}"`, - ' }', - ]; -} - /* ------------------------------------------------------------------ * * Translation * ------------------------------------------------------------------ */ @@ -367,6 +328,12 @@ function translate(genesis, opts = {}) { XDCXLendingFinalizedTradeAddressBinary: DEFAULT_ENGINE.XDCXLendingFinalizedTradeAddressBinary, }; + // XDC hardfork blocks carried straight through. Absent in genesis => absent + // from the chainspec, so the fork stays off rather than activating at 0. + for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { + enginePadms[key] = cfg[genesisKey]; + } + // --- params (chain rules / EIP transitions) --- const params = { chainId: cfg.chainId, @@ -472,17 +439,6 @@ function main(argv) { const chainspec = translate(genesis, opts); const json = JSON.stringify(chainspec, null, 2) + '\n'; - - const pragueWarning = checkPrague(genesis); - if (pragueWarning.length) { - console.error(''); - console.error('WARNING: Prague is enabled but the EIP-2935 history contract is missing.'); - for (const line of pragueWarning) { - console.error(line ? ` ${line}` : ''); - } - console.error(''); - } - try { fs.writeFileSync(outPath, json); } catch (e) { @@ -497,4 +453,4 @@ if (require.main === module) { process.exit(main(process.argv)); } -module.exports = { translate, normalizeAddress, checkPrague }; +module.exports = { translate, normalizeAddress }; From 3deb7b543dd8ee37f53b5d29469b2e7ced443ff9 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Sun, 30 Aug 2026 04:51:36 +0700 Subject: [PATCH 24/59] - integrate Nethermind subnet client - improve subnet generator UI --- container-manager/src/express.js | 4 +- container-manager/src/gen/config_gen.js | 30 +- container-manager/src/gen/gen.js | 30 +- container-manager/src/gen/gen_compose.js | 63 +++- container-manager/src/gen/gen_env.js | 48 ++- .../src/gen/scripts/check-chainspec.sh | 14 +- .../src/gen/scripts/check-mining.sh | 91 ++++-- .../src/gen/scripts/xdc-nmc-subnet.json | 33 ++ container-manager/src/libs/check-chainspec.js | 6 +- container-manager/src/libs/exec.js | 97 +++++- .../src/libs/genesis-to-chainspec.js | 214 +++++++++---- container-manager/src/libs/state.js | 97 ++++-- container-manager/src/public/script.js | 303 +++++++++++------- container-manager/src/public/styles.css | 81 ++++- .../src/views/generator/custom.css | 39 ++- .../src/views/generator/index.pug | 227 +++++++------ container-manager/src/views/index.html | 56 ++-- 17 files changed, 1004 insertions(+), 429 deletions(-) create mode 100644 container-manager/src/gen/scripts/xdc-nmc-subnet.json diff --git a/container-manager/src/express.js b/container-manager/src/express.js index 026d6be..7434b42 100644 --- a/container-manager/src/express.js +++ b/container-manager/src/express.js @@ -175,8 +175,10 @@ app.post("/submit", (req, res) => { }); } else { res.render("generator/submit.pug", { + // no "tab": the generator is embedded in the wizard's step 1, though it + // is still reachable on its own at /gen message: - "Config generation success, please continue in the Deployment Wizard tab", + "Config generation success, please continue with the next step in the Deployment Wizard", }); } }); diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index 8aff160..6ce8830 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -2,7 +2,14 @@ const crypto = require("crypto"); const net = require("net"); const dotenv = require("dotenv"); const ethers = require("ethers"); -dotenv.config({ path: `${__dirname}/../../mount/generated/gen.env` }); +// override: gen.env is the source of truth for a generation. The manager +// process requires this module too, so its process.env holds whatever gen.env +// said when it first loaded; without override that stale value is inherited by +// the `node gen.js` child and wins over the file just written. +dotenv.config({ + path: `${__dirname}/../../mount/generated/gen.env`, + override: true, +}); const config = { deployment_path: process.env.CONFIG_PATH || "", @@ -23,6 +30,9 @@ const config = { version: { genesis: process.env.VERSION_GENESIS || "v0.3.1", subnet: process.env.VERSION_SUBNET || "v0.3.2", + // full image name; overrides the xinfinorg/xdcsubnets: default when + // set, so a node build outside that repo/tag scheme can be used + subnet_image: process.env.VERSION_SUBNET_IMAGE || "", bootnode: process.env.VERSION_BOOTNODE || "v0.3.1", relayer: process.env.VERSION_RELAYER || "v0.3.1", stats: process.env.VERSION_STATS || "v0.1.11", @@ -57,7 +67,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", + nethermind: process.env.VERSION_NETHERMIND_IMAGE || "nethermindeth/nethermind:master-d4ef4fd", stake_threshold: parseInt(process.env.MASTERNODE_MINIMUM_STAKE) || "", reward_yield: parseInt(process.env.REWARDS_YIELD) || "", foundation_addr: "", @@ -74,8 +84,17 @@ const config = { module.exports = { config, configSanityCheck, + isNethermindNode, }; +// The last `num_nethermind` nodes run the Nethermind client instead of the Go +// client. They stay validators and reuse the same key/port/IP slot, so nothing +// else in the deployment has to know which client a node runs. +// (gen_xdpos.js carries its own copy of this for the private-network path.) +function isNethermindNode(subnet_id) { + return subnet_id > config.num_subnet - config.num_nethermind; +} + function validatePK(private_key) { let wallet = new ethers.Wallet(private_key); return [wallet.address, wallet.privateKey]; @@ -92,6 +111,11 @@ function configSanityCheck(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.ip_1)) { console.log("MAIN_IP Invalid IP address"); process.exit(1); @@ -128,7 +152,7 @@ function configSanityCheck(config) { config.parentnet.network === "mainnet" ) { let official_urls = { - devnet: "https://devnetstats.apothem.network/devnet", + devnet: "https://rpc1.devnet.xinfin.org", testnet: "https://erpc.apothem.network/", mainnet: "https://rpc.xinfin.network", }; diff --git a/container-manager/src/gen/gen.js b/container-manager/src/gen/gen.js index 587f587..7d1667e 100644 --- a/container-manager/src/gen/gen.js +++ b/container-manager/src/gen/gen.js @@ -76,7 +76,17 @@ doc, (ip_record = gen_compose.injectNetworkConfig(doc)); const commonconf = gen_env.genServicesConfig(ip_record); subnetconf = []; for (let i = 1; i <= config.num_subnet; i++) { - subnetconf.push(gen_env.genSubnetConfig(i, keys, ip_record)); + if (configModule.isNethermindNode(i)) { + subnetconf.push({ + filename: `subnet${i}nmc.env`, + content: gen_env.genNethermindSubnetConfig(i, keys, ip_record), + }); + } else { + subnetconf.push({ + filename: `subnet${i}.env`, + content: gen_env.genSubnetConfig(i, keys, ip_record), + }); + } } const deployconf = gen_env.genContractDeployEnv(ip_record); @@ -146,8 +156,8 @@ function writeGenerated(output_dir) { for (let i = 1; i <= config.num_subnet; i++) { fs.writeFileSync( - `${output_dir}/subnet${i}.env`, - subnetconf[i - 1], + `${output_dir}/${subnetconf[i - 1].filename}`, + subnetconf[i - 1].content, (err) => { if (err) { console.error(err); @@ -196,4 +206,18 @@ function copyScripts(output_dir) { `${__dirname}/scripts/add-node.sh`, `${output_dir}/scripts/add-node.sh` ); + // shared Nethermind config mounted by every nmc node, copied unconditionally + // so it is always available (chainspec.json is produced separately from + // genesis.json after puppeth runs). The subnet build differs from the + // private-network one gen_xdpos.js copies, so it has its own file. + fs.copyFileSync( + `${__dirname}/scripts/xdc-nmc-subnet.json`, + `${output_dir}/xdc-nmc-subnet.json` + ); + // pre-boot check that chainspec.json still matches genesis.json; the check + // itself runs in a subnet-generator container, so only the wrapper is copied + fs.copyFileSync( + `${__dirname}/scripts/check-chainspec.sh`, + `${output_dir}/scripts/check-chainspec.sh` + ); } diff --git a/container-manager/src/gen/gen_compose.js b/container-manager/src/gen/gen_compose.js index 18a43b3..d93683e 100644 --- a/container-manager/src/gen/gen_compose.js +++ b/container-manager/src/gen/gen_compose.js @@ -22,22 +22,53 @@ function genSubnetNodes(machine_id, num, start_num = 1) { const port = 20302 + i; const rpcport = 8544 + i; const wsport = 9554 + i; - subnet_nodes[node_name] = { - image: `xinfinorg/xdcsubnets:${config.version.subnet}`, - volumes: [volume, "${HOSTPWD}/genesis.json:/work/genesis.json"], - 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 (configModule.isNethermindNode(i)) { + // Nethermind validator: per-node NETHERMIND_* vars come from + // subnetnmc.env, shared/static settings from xdc-nmc.json, and the + // chain from chainspec.json rather than genesis.json. Deliberately keeps + // the subnet service name and the same RPC port so the wizard's + // container discovery and mining check still find it (state.js keys off + // both). injectNetworkConfig() adds the networks/ipv4_address block. + subnet_nodes[node_name] = { + image: `${config.xdpos.nethermind}`, + volumes: [ + volume, + "${HOSTPWD}/chainspec.json:/work/chainspec.json", + "${HOSTPWD}/xdc-nmc-subnet.json:/work/xdc-nmc.json", + ], + restart: "always", + env_file: [`subnet${i}nmc.env`], + command: [ + "--config=/work/xdc-nmc.json", + "--datadir=/work/xdcchain", + "--log=debug", + ], + profiles: [compose_profile], + ports: port_mappings, + }; + } else { + // Go XDC subnet node (default). + subnet_nodes[node_name] = { + image: + config.version.subnet_image || + `xinfinorg/xdcsubnets:${config.version.subnet}`, + volumes: [volume, "${HOSTPWD}/genesis.json:/work/genesis.json"], + restart: "always", + network_mode: "host", + env_file: [config_path], + profiles: [compose_profile], + ports: port_mappings, + }; + } } return subnet_nodes; } diff --git a/container-manager/src/gen/gen_env.js b/container-manager/src/gen/gen_env.js index 2dd9c52..a021334 100644 --- a/container-manager/src/gen/gen_env.js +++ b/container-manager/src/gen/gen_env.js @@ -4,10 +4,22 @@ Object.freeze(config); module.exports = { genSubnetConfig, + genNethermindSubnetConfig, genServicesConfig, genContractDeployEnv, }; +// The bootnode container always starts from the same bootnode.key, so its enode +// id is fixed; only the host it is reached on changes. +const BOOTNODE_ENODE_ID = + "cc566d1033f21c7eb0eb9f403bb651f3949b5f63b40683917765c343f9c0c596e9cd021e2e8416908cbc3ab7d6f6671a83c85f7b121c1872f8be50a591723a5d"; + +function bootnodeEnode(ip_record) { + const bootnode_ip = + config.num_machines === 1 ? ip_record["bootnode"] : config.ip_1; + return `enode://${BOOTNODE_ENODE_ID}@${bootnode_ip}:20301`; +} + function genSubnetConfig(subnet_id, key, ip_record) { const key_name = `key${subnet_id}`; let private_key = key[key_name]["PrivateKey"]; @@ -15,15 +27,11 @@ function genSubnetConfig(subnet_id, key, ip_record) { const port = 20303 + subnet_id - 1; const rpcport = 8545 + subnet_id - 1; const wsport = 9555 + subnet_id - 1; - const bootnode_ip = - config.num_machines === 1 ? ip_record["bootnode"] : config.ip_1; const stats_ip = config.num_machines === 1 ? ip_record["stats"] : config.ip_1; const config_env = ` INSTANCE_NAME=subnet${subnet_id} PRIVATE_KEY=${private_key} -BOOTNODES=enode://cc566d1033f21c7eb0eb9f403bb651f3949b5f63b40683917\ -765c343f9c0c596e9cd021e2e8416908cbc3ab7d6f6671a83c85f7b121c1872f8be\ -50a591723a5d@${bootnode_ip}:20301 +BOOTNODES=${bootnodeEnode(ip_record)} NETWORK_ID=${config.network_id} SYNC_MODE=full RPC_API=db,eth,debug,miner,net,shh,txpool,personal,web3,XDPoS @@ -38,6 +46,36 @@ LOG_LEVEL=2 return config_env; } +// Per-node env file (subnetnmc.env) for a Nethermind subnet node. Nethermind +// reads NETHERMIND_CONFIG_ env vars; everything shared and +// static lives in xdc-nmc.json, so only per-node values are emitted here. +// Ports match genSubnetConfig so a node keeps its slot when its client changes. +function genNethermindSubnetConfig(subnet_id, key, ip_record) { + const private_key = key[`key${subnet_id}`]["PrivateKey"]; // 0x-prefixed, unlike the Go client's + const port = 20303 + subnet_id - 1; + const rpcport = 8545 + subnet_id - 1; + const ip = ip_record[`subnet${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=${bootnodeEnode(ip_record)} +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 genServicesConfig() { const url = config.parentnet.url; const bootnode_ip = diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index 4e54546..e030fc5 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -29,6 +29,18 @@ if [[ -z "$GENERATOR_IMAGE_VERSION" ]]; then exit 2 fi +# A Subnet chainspec names its engine block XDPoSSubnet, a standalone XDPoS +# network names it XDPoS. The converter has to be told which, or the engine +# block alone reads as a difference and the check would archive and overwrite a +# perfectly good chainspec. Recorded in gen.env when the deployment is generated. +if [[ -z "$CHAINSPEC_ENGINE" && -f gen.env ]]; then + CHAINSPEC_ENGINE=$(grep -E '^CHAINSPEC_ENGINE=' gen.env | tail -1 | cut -d '=' -f 2-) +fi +engine_flag=() +if [[ "$CHAINSPEC_ENGINE" == "XDPoSSubnet" ]]; then + engine_flag=(--subnet) +fi + genesis="genesis.json" chainspec="chainspec.json" if [[ -n "$1" && "$1" != -* ]]; then @@ -57,7 +69,7 @@ docker pull "$GENERATOR_IMAGE_VERSION" # -u keeps the archived and rewritten files owned by the invoking user docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$GENERATOR_IMAGE_VERSION" \ - node /app/libs/check-chainspec.js "$genesis" "$chainspec" "$@" + node /app/libs/check-chainspec.js "$genesis" "$chainspec" "${engine_flag[@]}" "$@" result=$? # only 0, 2 and 3 come from the check itself; anything else is docker or node diff --git a/container-manager/src/gen/scripts/check-mining.sh b/container-manager/src/gen/scripts/check-mining.sh index fd7345f..c82d1ff 100755 --- a/container-manager/src/gen/scripts/check-mining.sh +++ b/container-manager/src/gen/scripts/check-mining.sh @@ -1,29 +1,80 @@ #!/bin/bash +# Report whether the subnet is producing blocks, by sampling the head block a +# few times and checking it advances. +# +# Usage: ./scripts/check-mining.sh [url-or-port] +# defaults to http://localhost:8545 (subnet1). Pass 8547, or a full url, to +# ask a different node. -echo "getting latest block" -resp=$(curl -s --location 'http://localhost:8545' \ ---header 'Content-Type: application/json' \ ---data '{"jsonrpc":"2.0","method":"XDPoS_getV2BlockByNumber","params":["latest"],"id":1}') +target="${1:-8545}" +if [[ "$target" == http://* || "$target" == https://* ]]; then + url="$target" +else + url="http://localhost:${target}" +fi + +rpc() { + curl -s -m 10 --location "$url" \ + --header 'Content-Type: application/json' \ + --data "{\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":$2,\"id\":1}" +} + +# The two clients report the head block differently: +# Go (xdcsubnets): "Number":189 decimal, capitalised key +# Nethermind: "number":"0xbd" hex string, lowercase key +# Match either, and normalise to decimal. +parse_block_number() { + local raw + raw=$(printf '%s' "$1" \ + | grep -oiE '"number"[[:space:]]*:[[:space:]]*"?(0x[0-9a-fA-F]+|[0-9]+)"?' \ + | head -1 \ + | sed -E 's/.*:[[:space:]]*"?//; s/"$//') + [[ -z "$raw" ]] && return 1 + # bash printf reads 0x.. natively, so this covers both forms + printf '%d' "$raw" 2>/dev/null || return 1 +} -num=$(echo $resp | grep -o '"Number":[0-9]*' | cut -d':' -f2 | tr -d ' ') -echo $num +get_block() { + local resp num + # XDPoS_getV2BlockByNumber needs the Xdc namespace, which a Nethermind node + # only serves when JsonRpc.EnabledModules includes it -- fall back to the + # standard call so this still works when it does not. + resp=$(rpc XDPoS_getV2BlockByNumber '["latest"]') + if num=$(parse_block_number "$resp"); then + printf '%s' "$num" + return 0 + fi + resp=$(rpc eth_blockNumber '[]') + if num=$(parse_block_number "$(printf '%s' "$resp" | sed -E 's/"result"/"number"/')"); then + printf '%s' "$num" + return 0 + fi + return 1 +} -if [[ $num == "null" ]] || [[ $num == "" ]]; then +echo "checking $url" +echo "getting latest block" +if ! num=$(get_block); then echo "no block has been mined, please check if nodes are peering properly" - exit -else - for i in 2 3 4 - do - sleep 3 - echo "getting latest block $i" - resp=$(curl -s --location 'http://localhost:8545' \ - --header 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","method":"XDPoS_getV2BlockByNumber","params":["latest"],"id":1}') - nextnum=$(echo $resp | grep -o '"Number":[0-9]*' | cut -d':' -f2 | tr -d ' ') - echo $nextnum - done + exit 1 fi +echo "$num" -if [[ $nextnum > $num ]]; then +nextnum=$num +for i in 2 3 4; do + sleep 3 + echo "getting latest block $i" + if ! nextnum=$(get_block); then + echo "node stopped answering, please check if it is still running" + exit 1 + fi + echo "$nextnum" +done + +# numeric, not the string comparison this used to do: [[ 9 > 10 ]] is true +if [[ "$nextnum" -gt "$num" ]]; then echo "subnet successfully running and mining blocks" +else + echo "block number has not advanced ($num -> $nextnum), subnet is not mining" + exit 1 fi diff --git a/container-manager/src/gen/scripts/xdc-nmc-subnet.json b/container-manager/src/gen/scripts/xdc-nmc-subnet.json new file mode 100644 index 0000000..0f6dda8 --- /dev/null +++ b/container-manager/src/gen/scripts/xdc-nmc-subnet.json @@ -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": 420000000 + }, + "Merge": { + "Enabled": false + }, + "TraceStore": { + "Enabled": false + } +} diff --git a/container-manager/src/libs/check-chainspec.js b/container-manager/src/libs/check-chainspec.js index 577bf30..113ce58 100644 --- a/container-manager/src/libs/check-chainspec.js +++ b/container-manager/src/libs/check-chainspec.js @@ -162,7 +162,9 @@ function replaceChainspec(chainspecPath, expected, date) { * ------------------------------------------------------------------ */ const USAGE = - 'Usage: node check-chainspec.js [--dry-run] [--name ] [--base-fee <0x..>]\n' + + 'Usage: node check-chainspec.js [--subnet] [--dry-run] [--name ] [--base-fee <0x..>]\n' + + '--subnet must match how the chainspec was generated (engine XDPoSSubnet vs\n' + + 'XDPoS), otherwise the engine block alone reads as a difference.\n' + 'Both paths are required. A differing chainspec is archived and replaced,\n' + 'unless --dry-run is given.\n' + 'Exit code: 0 already matched, 3 differed, 2 error.'; @@ -189,6 +191,8 @@ function main(argv) { opts.name = args[++i]; } else if (args[i] === '--base-fee') { opts.baseFeePerGas = args[++i]; + } else if (args[i] === '--subnet') { + opts.subnet = true; } else if (args[i] === '--dry-run') { dryRun = true; } else if (args[i] === '-h' || args[i] === '--help') { diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 83025ad..4befde9 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -221,9 +221,51 @@ function generate(params) { return [result, out]; } console.log("gen success"); - command = `cd ${mountPath}; docker run -v ${config.hostPath}:/app/generated/ --entrypoint 'bash' xinfinorg/xdcsubnets:${config.version.genesis} /work/puppeth.sh`; + + //step 2: generate genesis.json + let versionGenesisFullname = `xinfinorg/xdcsubnets:${config.version.genesis}`; + if ( + "customclient-checkbox" in params && + params["customclient-checkbox"] != "" && + "customversion-xdpos-genesis-fullname" in params && + params["customversion-xdpos-genesis-fullname"] != "" + ) { + versionGenesisFullname = `${params["customversion-xdpos-genesis-fullname"]}`; + } + + 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, which the Nethermind subnet + //nodes mount instead of genesis.json. Generated unconditionally so it is + //always available alongside genesis.json, but only fatal when a Nethermind + //node was actually asked for -- an all-Go subnet never reads it. + const nethermindCount = parseInt( + ("customclient-checkbox" in params && params["customclient-checkbox"] != "" + ? params["customversion-xdpos-nethermind-count"] + : 0) || 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, { subnet: true }); + 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); + if (nethermindCount > 0) { + return [false, `chainspec generation failed: ${e.message}`]; + } + } return [result2, out2]; } @@ -241,6 +283,14 @@ function callExec(command) { } } +// Every private key in the form is optional: a blank field means "make me one". +// Generated here rather than in config_gen so the key is recorded in gen.env -- +// state.js reads the wallets back out of it to show the addresses the user has +// to fund (step 3 of the wizard). +function keyOrNew(value) { + return value != null && value !== "" ? value : ethers.Wallet.createRandom().privateKey; +} + function genGenEnv(input) { console.log(input); @@ -282,7 +332,7 @@ function genGenEnv(input) { } let content_custom_key = ""; - if (input["grandmaster-pk"] != "") { + if (input["grandmaster-pk"]) { content_custom_key += `\nGRANDMASTER_PK=${input["grandmaster-pk"]}`; } @@ -293,15 +343,15 @@ function genGenEnv(input) { subnet_keys.push(input[key]); idx++; } - if (subnet_keys.length > 0) { - key_string = subnet_keys.join(","); + // all blank means the fields were never filled in: let config_gen generate the + // whole set. Otherwise fill only the gaps, since config_gen requires exactly + // one key per node and rejects a short or empty-valued list. + if (subnet_keys.some((k) => k !== "")) { + key_string = subnet_keys.map(keyOrNew).join(","); content_custom_key += `\nSUBNETS_PK=${key_string}`; } let content_version = ""; - if (input["customversion-subnet"] != "") { - content_version += `\nVERSION_SUBNET=${input["customversion-subnet"]}`; - } if (input["customversion-bootnode"] != "") { content_version += `\nVERSION_BOOTNODE=${input["customversion-bootnode"]}`; } @@ -321,6 +371,25 @@ function genGenEnv(input) { content_version += `\nVERSION_ZERO=${input["customversion-zero"]}`; } + // Custom Client: full image names and the Nethermind node count. Gated on the + // section's own checkbox so a collapsed section never overrides anything. + let content_client = ""; + const clientOn = + "customclient-checkbox" in input && input["customclient-checkbox"] != ""; + if (clientOn && input["customversion-xdpos-node-fullname"] != "") { + content_client += `\nVERSION_SUBNET_IMAGE=${input["customversion-xdpos-node-fullname"]}`; + } + if (clientOn && input["customversion-xdpos-nethermind-count"] != "") { + content_client += `\nNUM_NETHERMIND=${input["customversion-xdpos-nethermind-count"]}`; + } + if (clientOn && input["customversion-xdpos-nethermind-version"] != "") { + content_client += `\nVERSION_NETHERMIND_IMAGE=${input["customversion-xdpos-nethermind-version"]}`; + } + // scripts/check-chainspec.sh reads these back out of gen.env: the image it + // re-runs the converter in, and which engine block the chainspec should have + content_client += `\nGENERATOR_IMAGE_VERSION=${getGeneratorImage()}`; + content_client += `\nCHAINSPEC_ENGINE=XDPoSSubnet`; + let content_zero = ""; if (relayer_mode == "full" && "xdczero-checkbox" in input) { if (input["zmradio"] == "zm-radio-one") { @@ -328,10 +397,14 @@ function genGenEnv(input) { } if (input["zmradio"] == "zm-radio-bi") { content_zero += "\nXDC_ZERO=bi-directional"; - content_zero += `\nSUBNET_WALLET_PK=${input["subnet-wallet-pk"]}`; - content_zero += `\nSUBNET_ZERO_WALLET_PK=${input["subnet-zero-wallet-pk"]}`; + content_zero += `\nSUBNET_WALLET_PK=${keyOrNew(input["subnet-wallet-pk"])}`; + content_zero += `\nSUBNET_ZERO_WALLET_PK=${keyOrNew( + input["subnet-zero-wallet-pk"] + )}`; } - content_zero += `\nPARENTNET_ZERO_WALLET_PK=${input["parentnet-zero-wallet-pk"]}`; + content_zero += `\nPARENTNET_ZERO_WALLET_PK=${keyOrNew( + input["parentnet-zero-wallet-pk"] + )}`; if ("subswap-checkbox" in input) { content_zero += "\nSUBSWAP=true"; } @@ -341,7 +414,7 @@ function genGenEnv(input) { NETWORK_NAME=${input["text-subnet-name"]} NUM_SUBNET=${input["text-num-subnet"]} PARENTNET=${parentnet} -PARENTNET_WALLET_PK=${input["parentnet-wallet-pk"]} +PARENTNET_WALLET_PK=${keyOrNew(input["parentnet-wallet-pk"])} RELAYER_MODE=${relayer_mode} `; content += content_machine; @@ -350,6 +423,8 @@ RELAYER_MODE=${relayer_mode} content += "\n"; content += content_version; content += "\n"; + content += content_client; + content += "\n"; content += content_zero; console.log(content); diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index d94bec2..830b2c0 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -118,37 +118,37 @@ const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; // All EIP transitions in chainspec.params that don't come straight from // genesis.config. Values copied from the reference chainspec.json. const DEFAULT_PARAMS = { - eip160Transition: 0, - eip145Transition: 0, - eip1014Transition: 0, - eip1052Transition: 0, - eip1234Transition: 0, - eip1283Transition: 0, - eip152Transition: 0, - eip1108Transition: 0, - eip1344Transition: 0, - eip1884Transition: 0, + eip160Transition: 999999999999, + eip145Transition: 999999999999, + eip1014Transition: 999999999999, + eip1052Transition: 999999999999, + eip1234Transition: 999999999999, + eip1283Transition: 999999999999, + eip152Transition: 999999999999, + eip1108Transition: 999999999999, + eip1344Transition: 999999999999, + eip1884Transition: 999999999999, eip2028Transition: 999999999999, - eip2200Transition: 0, - eip2565Transition: 0, - eip2718Transition: 0, - eip2930Transition: 0, - eip1559Transition: 0, - eip2929Transition: 0, - eip3198Transition: 0, - eip3529Transition: 0, - eip3541Transition: 0, - eip3554Transition: 0, - eip4399Transition: 0, - eip3651Transition: 0, - eip3855Transition: 0, - eip3860Transition: 0, - eip6049Transition: 0, - eip1153Transition: 0, - eip4844Transition: 0, - eip5656Transition: 0, - eip6780Transition: 0, - eip7516Transition: 0, + eip2200Transition: 999999999999, + eip2565Transition: 999999999999, + eip2718Transition: 999999999999, + eip2930Transition: 999999999999, + eip1559Transition: 999999999999, + eip2929Transition: 999999999999, + eip3198Transition: 999999999999, + eip3529Transition: 999999999999, + eip3541Transition: 999999999999, + eip3554Transition: 999999999999, + eip4399Transition: 999999999999, + eip3651Transition: 999999999999, + eip3855Transition: 999999999999, + eip3860Transition: 999999999999, + eip6049Transition: 999999999999, + eip1153Transition: 999999999999, + eip4844Transition: 999999999999, + eip5656Transition: 999999999999, + eip6780Transition: 999999999999, + eip7516Transition: 999999999999, eip1559ElasticityMultiplier: '0x1', }; @@ -157,12 +157,12 @@ const DEFAULT_ENGINE = { mergeSignRange: 15, RangeReturnSigner: 150, tip2019Block: 1, - DynamicGasLimitBlock: 0, - TipXDCX: 0, - blackListHFNumber: 0, - TipTrc21Fee: 0, - TIPXDCXMinerDisable: 0, - TIPXDCXReceiverDisable: 0, + DynamicGasLimitBlock: 9999999999999, + TipXDCX: 9999999999999, + blackListHFNumber: 9999999999999, + TipTrc21Fee: 9999999999999, + TIPXDCXMinerDisable: 9999999999999, + TIPXDCXReceiverDisable: 9999999999999, blackListedAddresses: ['0x0000000000000000000000000000000000000011'], masternodeVotingContract: '0x0000000000000000000000000000000000000088', blockSignerContract: '0x0000000000000000000000000000000000000089', @@ -173,6 +173,38 @@ const DEFAULT_ENGINE = { XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', }; +// The XDPoSSubnet engine plugin binds DIFFERENT property names than XDPoS, so a +// subnet chainspec is not just XDPoS with a renamed engine block: +// XDPoS XDPoSSubnet +// mergeSignRange -> MergeSignRange +// blackListHFNumber -> BlackListHFNumber +// XDCXAddressBinary -> XDCXAddrBinary (a different name, not case) +// TradingStateAddressBinary -> tradingStateAddressBinary +// TipXDCX/TipTrc21Fee/TIPXDCXMinerDisable/TIPXDCXReceiverDisable -> not used at all +// A name the engine does not bind is silently ignored and its own default +// applies, which is how a chainspec can look correct and still diverge from the +// Go nodes. Values below match a working Nethermind+Go subnet deployment. +const DEFAULT_ENGINE_SUBNET = { + MergeSignRange: 15, + RangeReturnSigner: 150, + DynamicGasLimitBlock: 99999999999999, + tip2019Block: 1, + BlackListHFNumber: 99999999999999, + blackListedAddresses: [], + masternodeVotingContract: '0x0000000000000000000000000000000000000088', + blockSignerContract: '0x0000000000000000000000000000000000000089', + randomizeSMCBinary: '0x0000000000000000000000000000000000000090', + XDCXAddrBinary: '0x0000000000000000000000000000000000000091', + tradingStateAddressBinary: '0x0000000000000000000000000000000000000092', + XDCXLendingAddressBinary: '0x0000000000000000000000000000000000000093', + XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', + // genesis allConfigs carries no maxMasternodes; this is XDC's own default and + // is what the working subnet chainspec states explicitly + maxMasternodes: 108, + // genesis states no switchEpoch on a subnet, and the engine wants it present + switchEpoch: 0, +}; + // Which hardfork block in genesis.config activates each chainspec transition. // The EIPs of a fork all switch on at that fork's block; when genesis does not // mention the fork, the DEFAULT_PARAMS entry stands in. Order matters: it is @@ -287,11 +319,17 @@ function translate(genesis, opts = {}) { .sort((a, b) => Number(a) - Number(b)) .map((round) => { const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; - - return rest; + if (!opts.subnet) { + return rest; + } + // genesis carries no maxMasternodes on a subnet, but the working subnet + // chainspec states it. Key spellings are left as genesis writes them -- + // the reference capitalises them, but binding is case-insensitive. + return { maxMasternodes: DEFAULT_ENGINE_SUBNET.maxMasternodes, ...rest }; }); - const enginePadms = { + // chain data, identical on both engines + const shared = { period: xdpos.period, epoch: xdpos.epoch, reward: xdpos.reward, @@ -304,34 +342,68 @@ function translate(genesis, opts = {}) { switchEpoch: v2.switchEpoch ?? v2.SwitchEpoch, switchBlock: v2.switchBlock ?? v2.SwitchBlock, v2Configs, - // no genesis counterpart: node-level constants, not chain data - mergeSignRange: DEFAULT_ENGINE.mergeSignRange, - RangeReturnSigner: DEFAULT_ENGINE.RangeReturnSigner, - // XDC hardfork blocks, named differently on each side - tip2019Block: pick(cfg.tip2019Block, DEFAULT_ENGINE.tip2019Block), - DynamicGasLimitBlock: pick(cfg.dynamicGasLimitBlock, DEFAULT_ENGINE.DynamicGasLimitBlock), - TipXDCX: pick(cfg.tipXDCXBlock, DEFAULT_ENGINE.TipXDCX), - blackListHFNumber: pick(cfg.denylistBlock, DEFAULT_ENGINE.blackListHFNumber), - TipTrc21Fee: pick(cfg.tipTRC21FeeBlock, DEFAULT_ENGINE.TipTrc21Fee), - TIPXDCXMinerDisable: pick(cfg.tipXDCXMinerDisableBlock, DEFAULT_ENGINE.TIPXDCXMinerDisable), - TIPXDCXReceiverDisable: pick( - cfg.tipXDCXReceiverDisableBlock, - DEFAULT_ENGINE.TIPXDCXReceiverDisable - ), - blackListedAddresses: DEFAULT_ENGINE.blackListedAddresses, - masternodeVotingContract: DEFAULT_ENGINE.masternodeVotingContract, - blockSignerContract: DEFAULT_ENGINE.blockSignerContract, - randomizeSMCBinary: DEFAULT_ENGINE.randomizeSMCBinary, - XDCXAddressBinary: DEFAULT_ENGINE.XDCXAddressBinary, - TradingStateAddressBinary: DEFAULT_ENGINE.TradingStateAddressBinary, - XDCXLendingAddressBinary: DEFAULT_ENGINE.XDCXLendingAddressBinary, - XDCXLendingFinalizedTradeAddressBinary: DEFAULT_ENGINE.XDCXLendingFinalizedTradeAddressBinary, }; + // Key names below are the ones each engine actually binds -- see the note on + // DEFAULT_ENGINE_SUBNET. Emitted in the reference specs' own order so a + // generated spec diffs cleanly against them. + const enginePadms = opts.subnet + ? { + ...shared, + switchEpoch: shared.switchEpoch ?? DEFAULT_ENGINE_SUBNET.switchEpoch, + MergeSignRange: DEFAULT_ENGINE_SUBNET.MergeSignRange, + RangeReturnSigner: DEFAULT_ENGINE_SUBNET.RangeReturnSigner, + DynamicGasLimitBlock: pick( + cfg.dynamicGasLimitBlock, + DEFAULT_ENGINE_SUBNET.DynamicGasLimitBlock + ), + tip2019Block: pick(cfg.tip2019Block, DEFAULT_ENGINE_SUBNET.tip2019Block), + BlackListHFNumber: pick(cfg.denylistBlock, DEFAULT_ENGINE_SUBNET.BlackListHFNumber), + blackListedAddresses: DEFAULT_ENGINE_SUBNET.blackListedAddresses, + masternodeVotingContract: DEFAULT_ENGINE_SUBNET.masternodeVotingContract, + blockSignerContract: DEFAULT_ENGINE_SUBNET.blockSignerContract, + randomizeSMCBinary: DEFAULT_ENGINE_SUBNET.randomizeSMCBinary, + XDCXAddrBinary: DEFAULT_ENGINE_SUBNET.XDCXAddrBinary, + tradingStateAddressBinary: DEFAULT_ENGINE_SUBNET.tradingStateAddressBinary, + XDCXLendingAddressBinary: DEFAULT_ENGINE_SUBNET.XDCXLendingAddressBinary, + XDCXLendingFinalizedTradeAddressBinary: + DEFAULT_ENGINE_SUBNET.XDCXLendingFinalizedTradeAddressBinary, + } + : { + ...shared, + // no genesis counterpart: node-level constants, not chain data + mergeSignRange: DEFAULT_ENGINE.mergeSignRange, + RangeReturnSigner: DEFAULT_ENGINE.RangeReturnSigner, + // XDC hardfork blocks, named differently on each side + tip2019Block: pick(cfg.tip2019Block, DEFAULT_ENGINE.tip2019Block), + DynamicGasLimitBlock: pick(cfg.dynamicGasLimitBlock, DEFAULT_ENGINE.DynamicGasLimitBlock), + TipXDCX: pick(cfg.tipXDCXBlock, DEFAULT_ENGINE.TipXDCX), + blackListHFNumber: pick(cfg.denylistBlock, DEFAULT_ENGINE.blackListHFNumber), + TipTrc21Fee: pick(cfg.tipTRC21FeeBlock, DEFAULT_ENGINE.TipTrc21Fee), + TIPXDCXMinerDisable: pick(cfg.tipXDCXMinerDisableBlock, DEFAULT_ENGINE.TIPXDCXMinerDisable), + TIPXDCXReceiverDisable: pick( + cfg.tipXDCXReceiverDisableBlock, + DEFAULT_ENGINE.TIPXDCXReceiverDisable + ), + blackListedAddresses: DEFAULT_ENGINE.blackListedAddresses, + masternodeVotingContract: DEFAULT_ENGINE.masternodeVotingContract, + blockSignerContract: DEFAULT_ENGINE.blockSignerContract, + randomizeSMCBinary: DEFAULT_ENGINE.randomizeSMCBinary, + XDCXAddressBinary: DEFAULT_ENGINE.XDCXAddressBinary, + TradingStateAddressBinary: DEFAULT_ENGINE.TradingStateAddressBinary, + XDCXLendingAddressBinary: DEFAULT_ENGINE.XDCXLendingAddressBinary, + XDCXLendingFinalizedTradeAddressBinary: + DEFAULT_ENGINE.XDCXLendingFinalizedTradeAddressBinary, + }; + // XDC hardfork blocks carried straight through. Absent in genesis => absent // from the chainspec, so the fork stays off rather than activating at 0. - for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { - enginePadms[key] = cfg[genesisKey]; + // These are XDPoS spellings; the subnet engine binds none of them, and the + // working subnet chainspec carries none. + if (!opts.subnet) { + for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { + enginePadms[key] = cfg[genesisKey]; + } } // --- params (chain rules / EIP transitions) --- @@ -373,9 +445,15 @@ function translate(genesis, opts = {}) { : genesis.baseFeePerGas, }; + // Subnet nodes run a different consensus plugin than a standalone XDPoS + // network, and Nethermind selects it by this key. The mapping notes above + // spell it "engine.XDPoS" throughout; with opts.subnet the whole block is + // named engine.XDPoSSubnet instead, contents unchanged. + const engineName = opts.subnet ? "XDPoSSubnet" : "XDPoS"; + return { name: opts.name || DEFAULT_CHAIN_NAME, - engine: { XDPoS: { params: enginePadms } }, + engine: { [engineName]: { params: enginePadms } }, params, genesis: block, nodes: opts.nodes || [], @@ -388,8 +466,10 @@ function translate(genesis, opts = {}) { * ------------------------------------------------------------------ */ const USAGE = - 'Usage: node genesis-to-chainspec.js [--name ] [--base-fee <0x..>]\n' + - ' npm run convert -- [--name ] [--base-fee <0x..>]\n' + + 'Usage: node genesis-to-chainspec.js [--subnet] [--name ] [--base-fee <0x..>]\n' + + ' npm run convert -- [--subnet] [--name ] [--base-fee <0x..>]\n' + + '--subnet names the engine block XDPoSSubnet instead of XDPoS; use it for a\n' + + 'Subnet deployment, leave it off for a standalone XDPoS network.\n' + 'Both paths are required. Relative paths resolve against the current working\n' + 'directory (/app when invoked through npm inside the container), so prefer\n' + 'absolute paths such as /mount/generated/genesis.json.'; @@ -403,6 +483,8 @@ function main(argv) { opts.name = args[++i]; } else if (args[i] === '--base-fee') { opts.baseFeePerGas = args[++i]; + } else if (args[i] === '--subnet') { + opts.subnet = true; } else if (args[i] === '-h' || args[i] === '--help') { console.log(USAGE); return 0; diff --git a/container-manager/src/libs/state.js b/container-manager/src/libs/state.js index 9046c39..68a7cb4 100644 --- a/container-manager/src/libs/state.js +++ b/container-manager/src/libs/state.js @@ -140,56 +140,80 @@ async function checkMining() { }; } -async function checkBlock(containerIP, containerPort) { +async function rpcCall(containerIP, containerPort, method, params = []) { let url = `http://${containerIP}:${containerPort}`; const data = { jsonrpc: "2.0", - method: "XDPoS_getV2BlockByNumber", - params: ["latest"], + method, + params, id: 1, }; const headers = { "Content-Type": "application/json", }; + let response; + try { + response = await axios.post(url, data, { headers, timeout: 2000 }); + } catch (error) { + url = `http://localhost:${containerPort}`; //fallback for local testing + response = await axios.post(url, data, { headers, timeout: 2000 }); + } + return response.data.result; +} + +// The two clients report the head block differently: +// Go (xdcsubnets): { "Number": 189 } decimal, capitalised key +// Nethermind: { "number": "0xbd" } hex string, lowercase key +// eth_blockNumber returns the hex string form on both. +function parseBlockNumber(value) { + if (typeof value === "number") { + return value; + } + if (typeof value === "string" && value !== "") { + const parsed = parseInt(value, /^0x/i.test(value) ? 16 : 10); + return Number.isNaN(parsed) ? undefined : parsed; + } + return undefined; +} + +async function checkBlock(containerIP, containerPort) { + try { + const block = await rpcCall( + containerIP, + containerPort, + "XDPoS_getV2BlockByNumber", + ["latest"] + ); + if (block != null) { + // ?? not ||, so a genuine block 0 is not mistaken for "missing" + const number = parseBlockNumber(block.Number ?? block.number); + if (number !== undefined) { + return number; + } + } + } catch (error) { + console.log(error.code); + } + + // Falls through to here when the node does not serve the XDPoS namespace at + // all (Xdc missing from JsonRpc.EnabledModules), or answered in a shape we + // could not read. try { - let response; - try { - response = await axios.post(url, data, { headers, timeout: 2000 }); - } catch (error) { - url = `http://localhost:${containerPort}`; //fallback for local testing - response = await axios.post(url, data, { headers, timeout: 2000 }); + const hex = await rpcCall(containerIP, containerPort, "eth_blockNumber"); + const number = parseBlockNumber(hex); + if (number !== undefined) { + return number; } - let block = response.data.result.Number; - if (block == null) block = 0; - return block; } catch (error) { console.log(error.code); } } async function checkPeers(containerIP, containerPort) { - let url = `http://${containerIP}:${containerPort}`; - const data = { - jsonrpc: "2.0", - method: "net_peerCount", - id: 1, - }; - const headers = { - "Content-Type": "application/json", - }; - try { - let response; - try { - response = await axios.post(url, data, { headers, timeout: 2000 }); - } catch (error) { - url = `http://localhost:${containerPort}`; //fallback for local testing - response = await axios.post(url, data, { headers, timeout: 2000 }); - } - const peerHex = response.data.result; - const peerCount = parseInt(peerHex, 16); - return peerCount; + const peerHex = await rpcCall(containerIP, containerPort, "net_peerCount"); + return parseInt(peerHex, 16); } catch (error) { console.error(error.code); } @@ -402,11 +426,20 @@ function readConfig() { let parentnet = findENVInFile("PARENTNET", filepath); parentnet = parentnet.length > 0 ? parentnet[0].split("=")[1] : ""; + const commonPath = path.join(mountPath, "common.env"); + let parentnetUrl = ""; + if (fs.existsSync(commonPath)) { + const found = findENVInFile("PARENTNET_URL", commonPath); + // slice(1).join to keep a url that contains "=" intact + parentnetUrl = found.length > 0 ? found[0].split("=").slice(1).join("=") : ""; + } + return { networkName: networkName, numSubnet: numSubnet, numMachine: numMachine, parentnet: parentnet, + parentnetUrl: parentnetUrl, }; } diff --git a/container-manager/src/public/script.js b/container-manager/src/public/script.js index 3bd8023..640a692 100644 --- a/container-manager/src/public/script.js +++ b/container-manager/src/public/script.js @@ -23,106 +23,10 @@ async function callStateApi(route, outElementId) { } catch (error) { console.error("Error:", error); outputDiv.textContent = "API call failed: " + error.message; + loadingFinished(); // never leave the page stuck behind pointer-events:none } } -async function generate1(network, subswap) { - console.log("generate1"); - loadingStart(); - const parentnetWallet = await genAddress(); - const parentnetZeroWallet = await genAddress(); - const subnetWallet = await genAddress(); - const subnetZeroWallet = await genAddress(); - const formData = { - "text-subnet-name": "myxdcsubnet", - "text-num-subnet": "1", - "text-num-machine": "1", - "text-private-ip": "", - "text-public-ip": "", - "grandmaster-pk": "", - "customversion-subnet": "", - "customversion-bootnode": "", - "customversion-relayer": "", - "customversion-stats": "", - "customversion-frontend": "", - "customversion-csc": "", - "customversion-zero": "", - pnradio: `pn-radio-${network}`, - "parentnet-wallet-pk": parentnetWallet.privateKey, - rmradio: "rm-radio-full", - "parentnet-zero-wallet-pk": "", - zmradio: "zm-radio-one", - "subnet-wallet-pk": "", - "subnet-zero-wallet-pk": "", - }; - if (subswap) { - formData["xdczero-checkbox"] = "on"; - formData["parentnet-zero-wallet-pk"] = parentnetZeroWallet.privateKey; - formData["zmradio"] = "zm-radio-bi"; - formData["subnet-wallet-pk"] = subnetWallet.privateKey; - formData["subnet-zero-wallet-pk"] = subnetZeroWallet.privateKey; - formData["subswap-checkbox"] = "on"; - } - const response = await fetch("/submit_preconfig", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }); - const outdiv = document.getElementById("output1"); - outdiv.textContent = await response.text(); -} - -async function generate3(network, subswap) { - console.log("generate3"); - loadingStart(); - const parentnetWallet = await genAddress(); - const parentnetZeroWallet = await genAddress(); - const subnetWallet = await genAddress(); - const subnetZeroWallet = await genAddress(); - const formData = { - "text-subnet-name": "myxdcsubnet", - "text-num-subnet": "3", - "text-num-machine": "1", - "text-private-ip": "", - "text-public-ip": "", - "grandmaster-pk": "", - "customversion-subnet": "", - "customversion-bootnode": "", - "customversion-relayer": "", - "customversion-stats": "", - "customversion-frontend": "", - "customversion-csc": "", - "customversion-zero": "", - pnradio: `pn-radio-${network}`, - "parentnet-wallet-pk": parentnetWallet.privateKey, - rmradio: "rm-radio-full", - }; - if (subswap) { - formData["xdczero-checkbox"] = "on"; - formData["parentnet-zero-wallet-pk"] = parentnetZeroWallet.privateKey; - formData["zmradio"] = "zm-radio-bi"; - formData["subnet-wallet-pk"] = subnetWallet.privateKey; - formData["subnet-zero-wallet-pk"] = subnetZeroWallet.privateKey; - formData["subswap-checkbox"] = "on"; - } - const response = await fetch("/submit_preconfig", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }); - const outdiv = document.getElementById("output1"); - outdiv.textContent = await response.text(); -} - -async function genAddress() { - const response = await fetch("/address", { method: "GET" }); - return response.json(); -} - async function callStreamApi(route) { // const outputElement = document.createElement('div'); // outputElement.className = 'output'; @@ -205,17 +109,28 @@ function collapseHistoryDivs() { function adjustStateDivs(data) { if (data.deployState != "NONE") { - disableButtons("gen-button"); checkSubnetStarted(data.containers.subnets); checkMiningState(data.mineInfo); checkServicesStarted(data.containers.services); checkSubswapFrontendStarted(data.containers.subswap); // checkExplorerStarted(data.containers.explorer); - showAddresses(data.requirements.addresses); - showCopyInstruction(data.requirements.subnetConfig); - showFaucet(data.requirements); - unhideContractButtons(data.requirements.requireContracts); - disableContractButtons(data.requirements.deployedContracts); + // INCOMPLETE means a generation started but never finished -- a failed + // submit leaves only some of the files behind, and /state reports no + // requirements for it. Keep the generator open in that case so the form can + // be corrected and resubmitted; only a finished config locks step 1. + if (data.requirements) { + disableGenEmbed(); + showAddresses(data.requirements.addresses); + showCopyInstruction(data.requirements.subnetConfig); + showFaucet(data.requirements); + unhideContractButtons(data.requirements.requireContracts); + disableContractButtons(data.requirements.deployedContracts); + } + } else { + // back to a clean slate (the subnet was removed): reopen the generator and + // clear what the previous config had put on screen + enableGenEmbed(); + resetParentnetInfo(); } // allowClick() @@ -223,7 +138,11 @@ function adjustStateDivs(data) { } function loadingFinished() { - document.getElementById("body-wrap").style.pointerEvents = "auto"; + // debug.html has no #body-wrap, and this is now called from an error path + const wrap = document.getElementById("body-wrap"); + if (wrap) { + wrap.style.pointerEvents = "auto"; + } } function loadingStart() { @@ -231,6 +150,92 @@ function loadingStart() { document.getElementById("state").textContent = "Status: Loading..."; } +function disableGenEmbed() { + const wrap = document.getElementById("gen-embed-wrap"); + if (!wrap || wrap.classList.contains("disabled")) { + return; + } + wrap.classList.add("disabled"); + document.getElementById("gen-embed-note").textContent = + "A subnet configuration already exists. Remove the subnet (/debug) before generating a new one."; +} + +function enableGenEmbed() { + const wrap = document.getElementById("gen-embed-wrap"); + if (!wrap || !wrap.classList.contains("disabled")) { + return; + } + wrap.classList.remove("disabled"); + document.getElementById("gen-embed-note").textContent = ""; + // the frame was left on whatever page it last showed, usually the submit + // result of the config that has just been removed + document.getElementById("gen-embed").src = "/gen"; +} + +// same-origin iframe, so the frame can be sized to the form it holds instead of +// leaving the generator inside its own scrollbar +function setupGenEmbedAutoResize() { + const iframe = document.getElementById("gen-embed"); + if (!iframe) { + return; + } + const fit = () => { + try { + const doc = iframe.contentDocument; + if (!doc || !doc.body) { + return; + } + // measure the bottom of the content, not scrollHeight: the generator + // views render without a doctype, so they are in quirks mode where both + // body.scrollHeight and documentElement.scrollHeight are at least the + // frame's own height -- feeding either back in grows the frame forever. + const scrolled = doc.documentElement.scrollTop || doc.body.scrollTop || 0; + let bottom = 0; + for (const child of doc.body.children) { + bottom = Math.max(bottom, child.getBoundingClientRect().bottom); + } + const height = Math.ceil(bottom + scrolled) + 16; + if (bottom > 0 && Math.abs(height - iframe.clientHeight) > 2) { + iframe.style.height = height + "px"; + } + } catch (error) { + // keep the CSS fallback height + console.error("Error:", error); + } + }; + let observing = false; + const attach = () => { + fit(); + if (observing) { + return; + } + // the form grows and shrinks as its optional sections are toggled, so + // measuring once is not enough + try { + const body = iframe.contentDocument.body; + const observer = new ResizeObserver(fit); + // quirks mode leaves the body box stretched to the frame, so a section + // collapsing may not resize it -- watch the children that do shrink + observer.observe(body); + for (const child of body.children) { + observer.observe(child); + } + observing = true; + } catch (error) { + // body not parsed yet; the load event below retries + console.error("Error:", error); + } + }; + iframe.addEventListener("load", () => { + // submitting the form navigates the frame, so the observed body is gone + observing = false; + attach(); + }); + // the frame is often already loaded by the time body onload runs, in which + // case the listener above never fires + attach(); +} + function disableButtons(className) { const elements = document.querySelectorAll(`.${className}`); elements.forEach((element) => { @@ -409,23 +414,97 @@ docker compose --profile machineX up -d;
} } +// Faucet links shown in step 3, keyed by parentnet. Each url becomes its own +// link. Mainnet has no faucet, so it is simply absent here and the line stays +// hidden. Edit these to change what step 3 links to -- a plain +// comma-separated string works too. +const PARENTNET_FAUCETS = { + testnet: ["https://faucet.apothem.network/", "https://faucet.blocksscan.io/"], + devnet: ["https://faucet.devnet.xinfin.org/"], +}; + +// accepts an array or a comma-separated string, so either editing style works +function faucetList(value) { + if (!value) { + return []; + } + const list = Array.isArray(value) ? value : String(value).split(","); + return list.map((url) => url.trim()).filter(Boolean); +} + +// showFaucet only ever revealed things, so whatever the last config showed stuck +// around -- after a /remove_subnet the old parentnet and its faucet stayed on +// screen. Reset the lines first so each state renders from scratch. +function resetParentnetInfo() { + document.getElementById("parentnet-name-line").style.display = "none"; + document.getElementById("parentnet-url-line").style.display = "none"; + document.getElementById("parentnet-faucet-line").style.display = "none"; + // drop the links too so a hidden line never carries the previous faucets + document.getElementById("parentnet-faucet-links").textContent = ""; +} + function showFaucet(requirements) { - if (requirements.subnetConfig.parentnet == "testnet") { - const infoDiv = document.getElementById("mainnet-testnet-info"); - infoDiv.textContent = "3. Add Testnet balance to: "; - const testnetFaucetInfo = document.getElementById("testnet-faucet-info"); - testnetFaucetInfo.style.display = "block"; + resetParentnetInfo(); + const config = requirements.subnetConfig; + const parentnet = config.parentnet; + + if (parentnet === "") { + return; } - if (requirements.subnetConfig.parentnet == "mainnet") { - const infoDiv = document.getElementById("mainnet-testnet-info"); - infoDiv.textContent = "3. Add Mainnet balance to: "; + + document.getElementById("parentnet-name").textContent = + parentnet.charAt(0).toUpperCase() + parentnet.slice(1); + document.getElementById("parentnet-name-line").style.display = "block"; + + // the RPC the relayer and the contract deploys actually talk to + if (config.parentnetUrl) { + document.getElementById("parentnet-url").textContent = config.parentnetUrl; + document.getElementById("parentnet-url-line").style.display = "block"; + } + + const faucets = faucetList(PARENTNET_FAUCETS[parentnet]); + if (faucets.length > 0) { + const container = document.getElementById("parentnet-faucet-links"); + faucets.forEach((url, i) => { + if (i > 0) { + container.appendChild(document.createTextNode(", ")); + } + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.textContent = url; + container.appendChild(link); + }); + document.getElementById("parentnet-faucet-line").style.display = "block"; } // if (requirements.addresses.subnetWallet != "" || requirements.addresses.subnetZeroWallet != ""){ // const subnetFaucetInfo = document.getElementById("subnet-faucet-info") // subnetFaucetInfo.style.display = "block" // } } +// Address Generator helper (moved here with the Helpers section from /gen). +// /address builds the wallet locally in the manager process -- nothing leaves +// the machine, which is what the disclaimer next to the button refers to. +async function genAddress() { + const pub = document.getElementById("address-gen-pub"); + const pk = document.getElementById("address-gen-pk"); + try { + const response = await fetch("/address", { method: "GET" }); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + pub.textContent = "Address: " + data.publicKey; + pk.textContent = "Private Key: " + data.privateKey; + } catch (error) { + console.error("Error:", error); + pub.textContent = "Error Generating Address"; + pk.textContent = "Error Generating Address"; + } +} + async function fetchLoop() { + setupGenEmbedAutoResize(); await callStateApi("/state", "state"); setInterval(() => { callStateApi("/state", "state"); diff --git a/container-manager/src/public/styles.css b/container-manager/src/public/styles.css index 9584b31..4630c81 100644 --- a/container-manager/src/public/styles.css +++ b/container-manager/src/public/styles.css @@ -122,10 +122,89 @@ h1 { -#testnet-faucet-info { +#parentnet-name-line, +#parentnet-url-line, +#parentnet-faucet-line { display: none; } #subnet-faucet-info { display: none; } + +#gen-embed-wrap { + margin: 10px; +} + +#gen-embed { + display: block; + width: 780px; + max-width: 100%; + /* fallback only: setupGenEmbedAutoResize() fits this to the real content + height once /gen loads, and again whenever the form expands */ + height: 900px; + border: 1px solid #ddd; + border-radius: 5px; + background-color: #fff; +} + +#gen-embed-wrap.disabled #gen-embed { + display: none; +} + +#gen-embed-note { + display: none; + margin: 0; + padding: 10px 20px; + background-color: #f4f4f4; + border: 1px solid #ddd; + border-radius: 5px; + font-family: monospace; + text-align: left; + width: 600px; +} + +#gen-embed-wrap.disabled #gen-embed-note { + display: block; +} + +#helpers { + margin-top: 20px; +} + +/* matches .helper-block's width so it centres over the blocks, not over the + whole (wider) container */ +#helpers-title { + width: 600px; + max-width: 100%; + margin-bottom: 8px; + font-size: 20px; + font-weight: bold; + text-align: center; +} + +.helper-block { + margin: 10px 0; + padding: 10px 20px; + background-color: #f4f4f4; + border: 1px solid #ddd; + border-radius: 5px; + width: 600px; + max-width: 100%; + box-sizing: border-box; +} + +.helper-title { + font-weight: bold; + margin-bottom: 5px; +} + +#address-gen-pub, +#address-gen-pk { + font-family: monospace; +} + +#disclaimer { + font-size: 12px; + color: #666; +} diff --git a/container-manager/src/views/generator/custom.css b/container-manager/src/views/generator/custom.css index 2410391..f3093ed 100644 --- a/container-manager/src/views/generator/custom.css +++ b/container-manager/src/views/generator/custom.css @@ -4,18 +4,6 @@ max-width: 1100px; } -#helper { - margin-left: 15%; - margin-top: 50px; - max-width: 1100px; -} -#helper-title { - margin-right: 10%; -} -.helper-block { - padding: 10px; -} - .content { display: none; background-color: #f0f0f0; @@ -144,12 +132,31 @@ display:block; } */ -#zerobidi { + +#customclient { display: none; } -#zm-radio-bi:checked ~ #zerobidi { +#customclient-checkbox:checked + #customclient { display: block; } -#zm-radio-one:checked ~ #zerobidi { - display: none; + +#submit-progress { + margin-top: 10px; + color: #555; +} +.spinner { + display: inline-block; + width: 14px; + height: 14px; + margin-right: 8px; + vertical-align: -2px; + border: 2px solid #ddd; + border-top-color: #0078e7; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } } diff --git a/container-manager/src/views/generator/index.pug b/container-manager/src/views/generator/index.pug index 17989a7..46126ae 100644 --- a/container-manager/src/views/generator/index.pug +++ b/container-manager/src/views/generator/index.pug @@ -8,19 +8,15 @@ head body center - h1 Subnet Configuration Generator - //- form.pure-form.pure-form-aligned#myForm(onsubmit="return validateForm()") form.pure-form.pure-form-aligned#myForm(action="/submit" method="post" onsubmit="return validateForm()") - center - h3 Subnet Configuration fieldset .pure-control-group .info-container .info-icon(tabindex="0" role="button" aria-label="more info") ? .tooltip This cannot be changed later. label() Network Name - input#text-subnet-name(type='text' placeholder='myxdcsubnet' name="text-subnet-name") + input#text-subnet-name(type='text' placeholder='myxdcsubnet' value='myxdcsubnet' name="text-subnet-name") span.pure-form-message-inline Required .pure-control-group @@ -28,7 +24,7 @@ body .info-icon(tabindex="0" role="button" aria-label="more info") ? .tooltip At least 2/3 of nodes must be online to have consensus and make the blockchain run. label() Number of Subnet Nodes - input#text-num-subnet(type='number' placeholder='3' name="text-num-subnet") + input#text-num-subnet(type='number' placeholder='3' value='3' name="text-num-subnet") span.pure-form-message-inline Required .pure-control-group @@ -36,7 +32,7 @@ body .info-icon(tabindex="0" role="button" aria-label="more info") ? .tooltip The generated configs will evenly spread the nodes across the machines. label() Number of Machines - input#text-num-machine(type='number' placeholder='1' name="text-num-machine" oninput="numMachineMoreThanOne()") + input#text-num-machine(type='number' placeholder='1' value='1' name="text-num-machine" oninput="numMachineMoreThanOne()") input#checkbox-num-machine(type='checkbox' name='checkbox-num-machine') @@ -78,13 +74,45 @@ body .pure-control-group .info-container .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip Default is random - label.pure-checkbox(for='customkeys-checkbox') Custom Subnet Keys + .tooltip Every key is optional. Leave a field blank and it is generated for you. + label.pure-checkbox(for='customkeys-checkbox') Custom Privatekey input#customkeys-checkbox.checkbox-indent(type='checkbox' name="customkeys-checkbox") #customkeys.content .pure-control-group + .info-container + .info-icon(tabindex="0" role="button" aria-label="more info") ? + .tooltip This Parentchain wallet will be used to deploy the Checkpoint Smart Contract(CSC) on the Parentchain and used by the Relayer to checkpoint Subnet block headers at regular intervals. + label() Parentnet Wallet PK + input#parentnet-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="parentnet-wallet-pk") + span.pure-form-message-inline blank = auto-generate + .pure-control-group + .info-container + .info-icon(tabindex="0" role="button" aria-label="more info") ? + .tooltip This Parentchain Zero Wallet is used for pushing cross-chain communication tx. Only used when XDC-Zero is enabled. + label() Parentnet Zero Wallet PK + input#parentnet-zero-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="parentnet-zero-wallet-pk") + span.pure-form-message-inline blank = auto-generate + .pure-control-group + .info-container + .info-icon(tabindex="0" role="button" aria-label="more info") ? + .tooltip This Subnet wallet will be used to checkpoint the Parentchain to Subnet. You can transfer Subnet tokens to this wallet after Subnet is launched. Only used in Bi-Directional mode. + label() Subnet Wallet PK + input#subnet-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="subnet-wallet-pk") + span.pure-form-message-inline blank = auto-generate + .pure-control-group + .info-container + .info-icon(tabindex="0" role="button" aria-label="more info") ? + .tooltip The Subnet Zero Wallet will be used to submit cross-chain tx to the Subnet. You can transfer Subnet tokens to this wallet after Subnet is launched. Only used in Bi-Directional mode. + label() Subnet Zero Wallet PK + input#subnet-zero-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="subnet-zero-wallet-pk") + span.pure-form-message-inline blank = auto-generate + .pure-control-group + .info-container + .info-icon(tabindex="0" role="button" aria-label="more info") ? + .tooltip Grandmaster of the Subnet. Also used as the deployer wallet for Subnet-side contracts. label() Grandmaster PK input#grandmaster-pk.pure-input-2-3(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="grandmaster-pk") + span.pure-form-message-inline blank = auto-generate center button.pure-button.pure-button-primary#button-custom-subnet-key(type="button" onclick="customSubnetKeyJs()") Custom Subnet Node Keys #custom-subnet-key-placeholder @@ -93,36 +121,52 @@ body .info-container .info-icon(tabindex="0" role="button" aria-label="more info") ? .tooltip Default to stable - label.pure-checkbox(for='customversion-checkbox') Custom Version + label.pure-checkbox(for='customclient-checkbox') Custom Node Version + input#customclient-checkbox.checkbox-indent(type='checkbox' name="customclient-checkbox") + #customclient.content + center + .pure-control-group + label() Node Version + input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/xdcsubnets:v0.3.2' value='xinfinorg/xdcsubnets:v0.3.2' name="customversion-xdpos-node-fullname") + .pure-control-group + label() Genesis Version + input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/xdcsubnets:v0.3.1' value='xinfinorg/xdcsubnets:v0.3.1' name="customversion-xdpos-genesis-fullname") + .pure-control-group + label() Number of Nethermind Nodes + input#customversion-xdpos-nethermind-count.pure-input(type='number' placeholder='0' value='0' name="customversion-xdpos-nethermind-count") + span.pure-form-message-inline The last N nodes run Nethermind + .pure-control-group + label() Nethermind Node Version + input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder='nethermindeth/nethermind:master-d4ef4fd' value='nethermindeth/nethermind:master-d4ef4fd' name="customversion-xdpos-nethermind-version") + + .pure-control-group + .info-container + .info-icon(tabindex="0" role="button" aria-label="more info") ? + .tooltip Default to stable + label.pure-checkbox(for='customversion-checkbox') Custom Services Version input#customversion-checkbox.checkbox-indent(type='checkbox' name="customversion-checkbox") #customversion.content center - p Changelog: - .pure-control-group - label() Subnet Node Version - input#customversion-subnet.pure-input(type='text' placeholder='latest' name="customversion-subnet") .pure-control-group label() Bootnode Version - input#customversion-bootnode.pure-input(type='text' placeholder='latest' name="customversion-bootnode") + input#customversion-bootnode.pure-input(type='text' placeholder='v0.3.1' value='v0.3.1' name="customversion-bootnode") .pure-control-group label() Relayer Version - input#customversion-relayer.pure-input(type='text' placeholder='latest' name="customversion-relayer") + input#customversion-relayer.pure-input(type='text' placeholder='v0.3.1' value='v0.3.1' name="customversion-relayer") .pure-control-group label() Backend Stats Server Version - input#customversion-stats.pure-input(type='text' placeholder='latest' name="customversion-stats") + input#customversion-stats.pure-input(type='text' placeholder='v0.1.11' value='v0.1.11' name="customversion-stats") .pure-control-group label() Frontend Version - input#customversion-frontend.pure-input(type='text' placeholder='latest' name="customversion-frontend") + input#customversion-frontend.pure-input(type='text' placeholder='v0.1.12' value='v0.1.12' name="customversion-frontend") .pure-control-group label() CSC Version - input#customversion-csc.pure-input(type='text' placeholder='latest' name="customversion-csc") + input#customversion-csc.pure-input(type='text' placeholder='v0.3.0' value='v0.3.0' name="customversion-csc") .pure-control-group label() XDC-Zero Version - input#customversion-zero.pure-input(type='text' placeholder='latest' name="customversion-zero") + input#customversion-zero.pure-input(type='text' placeholder='v0.3.0' value='v0.3.0' name="customversion-zero") - center - h3 Cross-Chain Configuration fieldset .pure-control-group .info-container @@ -131,30 +175,22 @@ body label() Parent Network br br - label.pure-radio(for='pn-radio-testnet') - input#pn-radio-testnet(type='radio' name='pnradio' value='pn-radio-testnet' checked) - | XDC-Testnet/Apothem - //- | XDC-Testnet/Apothem      faucet: - br - br label.pure-radio(for='pn-radio-devnet') - input#pn-radio-devnet(type='radio' name='pnradio' value='pn-radio-devnet') + input#pn-radio-devnet(type='radio' name='pnradio' value='pn-radio-devnet' checked) //- | XDC-Devnet                     faucet: | XDC-Devnet br br + label.pure-radio(for='pn-radio-testnet') + input#pn-radio-testnet(type='radio' name='pnradio' value='pn-radio-testnet') + | XDC-Testnet/Apothem + //- | XDC-Testnet/Apothem      faucet: + br + br label.pure-radio(for='pn-radio-mainnet') input#pn-radio-mainnet(type='radio' name='pnradio' value='pn-radio-mainnet') | XDC-Mainnet - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This Parentchain wallet will be used to deploy the Checkpoint Smart Contract(CSC) on the Parentchain and used by the Relayer to checkpoint Subnet block headers at regular intervals. - label() Parentnet Wallet PK - input#parentnet-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="parentnet-wallet-pk") - span.pure-form-message-inline Required - .pure-control-group .info-container .info-icon(tabindex="0" role="button" aria-label="more info") ? @@ -185,13 +221,6 @@ body #xdczero.content center p Extra Info: - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This Parentchain Zero Wallet is used for pushing cross-chain communication tx - label() Parentnet Zero Wallet PK - input#parentnet-zero-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="parentnet-zero-wallet-pk") - span.pure-form-message-inline Required .pure-control-group .info-container .info-icon(tabindex="0" role="button" aria-label="more info") ? @@ -209,23 +238,6 @@ body | Bi-Directional (Subnet <-> Parentnet) br br - #zerobidi.content - //- center - //- p Faucet: - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip This Subnet wallet will be used to checkpoint the Parentchain to Subnet. You can transfer Subnet tokens to this wallet after Subnet is launched. - label() Subnet Wallet PK - input#subnet-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="subnet-wallet-pk") - span.pure-form-message-inline Required - .pure-control-group - .info-container - .info-icon(tabindex="0" role="button" aria-label="more info") ? - .tooltip The Subnet Zero Wallet will be used to submit cross-chain tx to the Subnet. You can transfer Subnet tokens to this wallet after Subnet is launched. - label() Subnet Zero Wallet PK - input#subnet-zero-wallet-pk.pure-input-1-2(type='text' placeholder='0x1111111111111111111111111111111111111111111111111111111111111111' name="subnet-zero-wallet-pk") - span.pure-form-message-inline Required .pure-control-group .info-container .info-icon(tabindex="0" role="button" aria-label="more info") ? @@ -239,30 +251,9 @@ body center #incomplete-required-warning .pure-controls - button.pure-button.pure-button-primary(type='submit') Submit - + button#submit-button.pure-button.pure-button-primary(type='submit') Submit center - h3#helper-title Helpers - .helper-block - h4 Address Generator - p#address-gen-pub Address: - p#address-gen-pk Private Key: - #disclaim-wrap - button.pure-button.pure-button-primary(type="button" onclick="genAddress()") Generate Address - h8#disclaimer *Generation is done locally on your machine, fully offline. - .helper-block - h4 XDC Faucet - p - a(href="https://faucet.apothem.network/" target="_blank") Testnet(Apothem) Faucet - p - a(href="https://faucet.blocksscan.io/ " target="_blank") Testnet and Devnet Faucet by BlocksScan - .helper-block - h4 Documentation - p - a(href="https://xinfinorg.github.io/xdc-subnet-docs/deployment/launch_subnet" target="_blank") Official Subnet Documentation - p - a(href="https://github.com/XinFinOrg/XDC-Subnet" target="_blank") XDC-Subnet Github - + #submit-progress script. function customSubnetKeyJs(){ @@ -322,14 +313,44 @@ script. checkbox.disabled = false; } + // Generation runs synchronously on the server and can take minutes, during + // which this form is still on screen -- so the button locks itself. The flag + // is the real guard: it also covers implicit submission (Enter in a field) + // and any programmatic form.submit(). + let submitting = false + + function lockSubmit(){ + submitting = true + const button = document.getElementById("submit-button") + button.disabled = true + button.textContent = "Generating..." + document.getElementById("submit-progress").innerHTML = + 'Generating configuration, this can take a few minutes. Do not resubmit.' + } + + function unlockSubmit(){ + submitting = false + const button = document.getElementById("submit-button") + button.disabled = false + button.textContent = "Submit" + document.getElementById("submit-progress").innerHTML = "" + } + + // "Back to the form" on the failure page restores this page from history with + // the button still disabled, which would leave no way to resubmit. pageshow + // fires on that restore as well as on a normal load. + window.addEventListener("pageshow", unlockSubmit) + function validateForm(){ console.log('validateForm called') + if (submitting){ + return false + } const form = document.forms["myForm"] const name = form["text-subnet-name"].value; const num_subnet = form["text-num-subnet"].value; - const pn_pk = form["parentnet-wallet-pk"].value; - if (name == "" || num_subnet == "" || pn_pk == ""){ + if (name == "" || num_subnet == ""){ document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" return false } @@ -342,41 +363,9 @@ script. return false } } - const zero = form["xdczero-checkbox"].checked; - if (zero){ - const zero_pk = form["parentnet-zero-wallet-pk"].value; - if (zero_pk == ""){ - document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" - return false - } - - const bidi = form["zm-radio-bi"].checked - if (bidi){ - const subnet_pk = form["subnet-wallet-pk"].value; - const subnet_zero_pk = form["subnet-zero-wallet-pk"].value; - if (subnet_pk == "" || subnet_zero_pk == ""){ - document.getElementById("incomplete-required-warning").innerHTML="Please fill in all required fields" - return false - } - } - } - document.getElementById("incomplete-required-warning").innerHTML="" console.log("pass") + lockSubmit() return true } - - function genAddress(){ - fetch('/address') - .then(response => response.json()) - .then(data => { - document.getElementById("address-gen-pub").innerHTML="Address: "+data["publicKey"] - document.getElementById("address-gen-pk").innerHTML="Private Key: "+data["privateKey"] - }) - .catch(error => { - console.error('Error:', error); - document.getElementById("address-gen-pub").innerHTML="Error Generating Address" - document.getElementById("address-gen-pk").innerHTML="Error Generating Address" - }); - } \ No newline at end of file diff --git a/container-manager/src/views/index.html b/container-manager/src/views/index.html index 47a6fe2..6a62f4b 100644 --- a/container-manager/src/views/index.html +++ b/container-manager/src/views/index.html @@ -13,18 +13,12 @@

Subnet Deployment Wizard

From d5e71c640bbe3776dce02856767072246fd9e5b4 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Wed, 2 Sep 2026 13:42:39 +0700 Subject: [PATCH 25/59] update genesis-to-chainspec converter --- container-manager/src/gen/gen_env.js | 4 +- container-manager/src/libs/check-chainspec.js | 12 +- .../src/libs/genesis-to-chainspec.js | 754 +++++++++++++----- container-manager/src/package.json | 2 +- 4 files changed, 579 insertions(+), 193 deletions(-) diff --git a/container-manager/src/gen/gen_env.js b/container-manager/src/gen/gen_env.js index a021334..af10f91 100644 --- a/container-manager/src/gen/gen_env.js +++ b/container-manager/src/gen/gen_env.js @@ -40,7 +40,7 @@ STATS_SECRET=${config.secret_string} PORT=${port} RPCPORT=${rpcport} WSPORT=${wsport} -LOG_LEVEL=2 +LOG_LEVEL=4 `; return config_env; @@ -62,7 +62,7 @@ NETHERMIND_JSONRPCCONFIG_PORT=${rpcport} NETHERMIND_NETWORKCONFIG_P2PPORT=${port} NETHERMIND_NETWORKCONFIG_DISCOVERYPORT=${port} NETHERMIND_NETWORKCONFIG_EXTERNALIP=${ip} -NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP=false +NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP=true NETHERMIND_NETWORKCONFIG_BOOTNODES=${bootnodeEnode(ip_record)} NETHERMIND_INITCONFIG_DISCOVERYENABLED=true NETHERMIND_MININGCONFIG_ENABLED=true diff --git a/container-manager/src/libs/check-chainspec.js b/container-manager/src/libs/check-chainspec.js index 113ce58..b1f8cfd 100644 --- a/container-manager/src/libs/check-chainspec.js +++ b/container-manager/src/libs/check-chainspec.js @@ -12,11 +12,11 @@ * translates to. Pass --dry-run to report without touching anything. * * Usage: - * node check-chainspec.js [--dry-run] [--name ] [--base-fee <0x..>] + * node check-chainspec.js [--dry-run] [--name ] * - * Both paths are required; there are no defaults. Pass --name / --base-fee when - * the chainspec was produced with those flags, otherwise the translator defaults - * are used (which is what the deployment pipeline does). + * Both paths are required; there are no defaults. Pass --name when the chainspec + * was produced with that flag, otherwise the translator defaults are used (which + * is what the deployment pipeline does). * * Exit codes: * 0 chainspec.json already matches genesis.json, nothing was touched @@ -162,7 +162,7 @@ function replaceChainspec(chainspecPath, expected, date) { * ------------------------------------------------------------------ */ const USAGE = - 'Usage: node check-chainspec.js [--subnet] [--dry-run] [--name ] [--base-fee <0x..>]\n' + + 'Usage: node check-chainspec.js [--subnet] [--dry-run] [--name ]\n' + '--subnet must match how the chainspec was generated (engine XDPoSSubnet vs\n' + 'XDPoS), otherwise the engine block alone reads as a difference.\n' + 'Both paths are required. A differing chainspec is archived and replaced,\n' + @@ -189,8 +189,6 @@ function main(argv) { for (let i = 0; i < args.length; i++) { if (args[i] === '--name') { opts.name = args[++i]; - } else if (args[i] === '--base-fee') { - opts.baseFeePerGas = args[++i]; } else if (args[i] === '--subnet') { opts.subnet = true; } else if (args[i] === '--dry-run') { diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 830b2c0..cb35bbd 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -27,15 +27,32 @@ * nothing about a field. * * genesis.config.chainId -> params.chainId - * genesis.config.homesteadBlock -> params.homesteadBlock + * genesis.config.homesteadBlock -> params.eip7Transition * genesis.config.eip150Block -> params.eip150Transition * genesis.config.eip155Block -> params.eip155Transition - * genesis.config.eip158Block -> params.eip158Transition, eip160Transition - * genesis.config.byzantiumBlock -> params.byzantiumBlock + * genesis.config.eip158Block -> params.eip160Transition, eip161abcTransition, + * eip161dTransition, MaxCodeSizeTransition + * genesis.config.byzantiumBlock -> params.eip140Transition, eip211Transition, + * eip214Transition, eip658Transition * the remaining EIP transitions come from the hardfork that ships them, see - * TRANSITION_FORKS: constantinopleBlock, istanbulBlock, berlinBlock, - * londonBlock (eip1559Block wins for eip1559Transition), mergeBlock, - * shanghaiBlock, cancunBlock, pragueBlock + * TRANSITION_FORKS: homesteadBlock, eip150Block, eip155Block, eip158Block, + * byzantiumBlock, constantinopleBlock, istanbulBlock, londonBlock, mergeBlock, + * shanghaiBlock, eip1559Block, cancunBlock, pragueBlock + * berlinBlock and osakaBlock feed nothing -- see TRANSITION_FORKS for why + * + * params deliberately carries no homesteadBlock, byzantiumBlock or + * eip158Transition. Those are geth-genesis spellings, not chainspec ones. + * Nethermind binds params to ChainSpecParamsJson, which declares + * eip7Transition, eip161abcTransition and eip161dTransition but none of those + * three: homesteadBlock and byzantiumBlock are properties of + * GethGenesisConfigJson, a separate class that reads a geth genesis.json, and + * ChainSpecParamsJson does not derive from it; eip158Transition exists on + * neither. Emitting them only put three keys in the chainspec that the node + * silently ignored. Verified against Nethermind.Specs.dll in + * nethermindeth/nethermind:xdc-fixes -- and Nethermind's own + * chainspec/xdc-testnet.json carries all three in params, where they are just + * as dead. The forks are unaffected: each is still carried by its + * EIP-numbered keys above. * * genesis.config.XDPoS.period -> engine.XDPoS.params.period * genesis.config.XDPoS.epoch -> engine.XDPoS.params.epoch @@ -59,17 +76,21 @@ * genesis.{nonce,timestamp,extraData,gasLimit,difficulty,mixHash, * coinbase,number,gasUsed,parentHash} -> genesis.* (verbatim) * genesis.baseFeePerGas (null) -> genesis.baseFeePerGas (DEFAULT_BASE_FEE_PER_GAS) + * no flag overrides this: XDPoSChain takes the same two steps in + * core/genesis.go (g.BaseFee, else params.InitialBaseFee), and a third + * value would simply change the genesis block hash * * genesis.alloc -> accounts (verbatim: code / storage / balance) * - * Not derivable from genesis, so always DEFAULT_ENGINE / DEFAULT_PARAMS: + * Not derivable from genesis, so always DEFAULT_ENGINE* / DEFAULT_PARAMS*: * mergeSignRange, RangeReturnSigner, blackListedAddresses, the contract - * binaries (they live in XDC's common constants, not in genesis), and - * eip1559ElasticityMultiplier. + * binaries (they live in XDC's common constants, not in genesis) and + * eip1559ElasticityMultiplier. maxCodeSize is a constant too, but which one + * depends on osakaBlock — see translate(). * * Present in genesis but with no counterpart in the chainspec schema, so * dropped rather than invented: v2 expTimeoutConfig, maxMasternodesV2, - * SkipV1Validation, osakaBlock, and the trc21IssuerSMC / xdcxListingSMC / + * SkipV1Validation, and the trc21IssuerSMC / xdcxListingSMC / * relayerRegistrationSMC / lendingRegistrationSMC addresses. * * genesis.config.tipSigningBlock -> engine.XDPoS.params.TipSigningBlock @@ -83,20 +104,39 @@ * genesis.config.tipUpgradePenaltyBlock -> engine.XDPoS.params.TIPUpgradePenalty * (see ENGINE_FORKS; key spellings, TIP casing included, are Nethermind's) * - * Prague (pragueBlock) maps to exactly the three EIPs XDPoSChain gates on it: - * 2935, 7623 and 7702 — see TRANSITION_FORKS. Nethermind's own reference spec - * for Apothem (/nethermind/chainspec/xdc-testnet.json) carries the same three - * at 83600000 and nothing else, and pre-allocates no EIP-2935 history contract, - * so Nethermind supplies that contract itself; the transitions are all that is + * XDPoSChain does not follow the canonical Ethereum fork schedule, so neither + * does this mapping. A group of EIPs that Ethereum ships with Berlin, London + * and Shanghai — 2565, 2929, 2930, 3529, 3541, 3651, 3860 — is gated on + * EIP1559Block in the Go client instead, alongside 1559 itself; EIP-2028 is not + * implemented at all. TRANSITION_FORKS carries the reasoning per key, with the + * Go call site for each. + * + * pragueBlock maps to only the EIPs XDPoSChain gates on IsPrague: 2935, 7623 and + * 7702. The rest of Prague (BLS precompiles, the beacon-chain EIPs) is listed in + * TRANSITION_FORKS as null and so comes out at DEFAULT_PARAMS' 999999999999: + * present in the chainspec, visibly off, and impossible to switch on for + * Nethermind alone by accident. Nethermind's own reference spec for Apothem + * (/nethermind/chainspec/xdc-testnet.json) carries the same three transitions at + * 83600000 and nothing else, and pre-allocates no EIP-2935 history contract, so + * Nethermind supplies that contract itself; the transitions are all that is * needed. * - * Neither the Prague transitions nor ENGINE_FORKS have a DEFAULT_* fallback, - * unlike every other fork here: a fork genesis does not state is left out of - * the chainspec so it stays off, rather than silently activating at block 0. - * tipUpgradeRewardBlock is why that matters — it selects between two different - * reward formulas (eth/hooks/engine_v2_hooks.go, IsTIPUpgradeReward), so - * dropping it makes XDPoSChain and Nethermind compute different balances at the - * first reward checkpoint and the chain stalls there for good. + * osakaBlock maps to no transition at all: Nethermind's XDC build declares every + * Osaka EIP by timestamp only, with no block-numbered property to bind, so a + * block-numbered chainspec cannot enter Osaka. The one part of Osaka a chainspec + * can still carry is the code limit — maxCodeSize becomes 32768 when osakaBlock + * is at or before maxCodeSizeTransition, since 32768 is then the only limit the + * chain ever has. Everything else about Osaka is out of reach, so translate() + * warns when genesis states an osakaBlock. + * + * Every params transition has a DEFAULT_PARAMS fallback of 999999999999, a + * block no chain reaches, so a fork genesis does not state stays off rather + * than silently activating at block 0. ENGINE_FORKS has no fallback at all and + * relies on the key being absent instead. tipUpgradeRewardBlock is why that + * matters — it selects between two different reward formulas + * (eth/hooks/engine_v2_hooks.go, IsTIPUpgradeReward), so dropping it makes + * XDPoSChain and Nethermind compute different balances at the first reward + * checkpoint and the chain stalls there for good. * -------------------------------------------------------------------------- */ @@ -112,36 +152,146 @@ const path = require('path'); const DEFAULT_CHAIN_NAME = 'xdpos-chain'; -// chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has null. +// chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has +// null. Same number as XDPoSChain's params.InitialBaseFee (12.5 gwei), which is +// what its core/genesis.go puts in the genesis header in that case -- so this +// is not a preference, it is the value that makes the genesis hashes agree. const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; // All EIP transitions in chainspec.params that don't come straight from // genesis.config. Values copied from the reference chainspec.json. const DEFAULT_PARAMS = { + // Homestead + eip7Transition: 1, + + // Tangerine Whistle + eip150Transition: 999999999999, + + // Spurious Dragon eip160Transition: 999999999999, - eip145Transition: 999999999999, - eip1014Transition: 999999999999, - eip1052Transition: 999999999999, + eip161abcTransition: 999999999999, + eip161dTransition: 999999999999, + eip155Transition: 999999999999, + MaxCodeSizeTransition: 999999999999, + MaxCodeSize: 24576, + // EIP-7907's raised limit (params.MaxCodeSizeOsaka in XDPoSChain). Never + // emitted under this name -- translate() picks it as the value of maxCodeSize + // when osakaBlock makes it the only limit the chain ever has. + MaxCodeSizeOsaka: 32768, + + // Byzantium + eip140Transition: 999999999999, + eip211Transition: 999999999999, + eip214Transition: 999999999999, + eip658Transition: 999999999999, + + // Constantinople and Petersburg + eip145Transition: 0, + eip1014Transition: 0, + eip1052Transition: 0, eip1234Transition: 999999999999, - eip1283Transition: 999999999999, - eip152Transition: 999999999999, - eip1108Transition: 999999999999, - eip1344Transition: 999999999999, - eip1884Transition: 999999999999, + eip1283Transition: 0, + + // Istanbul + eip152Transition: 0, + eip1108Transition: 0, + eip1344Transition: 0, + eip1884Transition: 0, eip2028Transition: 999999999999, - eip2200Transition: 999999999999, + eip2200Transition: 0, + + // Berlin + eip2565Transition: 999999999999, + eip2718Transition: 999999999999, + eip2929Transition: 999999999999, + eip2930Transition: 999999999999, + + // London + eip1559Transition: 999999999999, + eip1559ElasticityMultiplier: '0x1', + eip3198Transition: 0, + eip3529Transition: 999999999999, + eip3541Transition: 999999999999, + eip3554Transition: 999999999999, + + // Merge + eip4399Transition: 999999999999, + + // Shanghai + eip3651Transition: 999999999999, + eip3855Transition: 0, + eip3860Transition: 999999999999, + eip6049Transition: 999999999999, + eip4895Transition: 999999999999, + + // Cancun + eip1153Transition: 999999999999, + eip4788Transition: 999999999999, + eip4844Transition: 999999999999, + eip5656Transition: 999999999999, + eip6780Transition: 999999999999, + eip7516Transition: 999999999999, + + // Prague + eip2537Transition: 999999999999, + eip2935Transition: 999999999999, + eip6110Transition: 999999999999, + eip7002Transition: 999999999999, + eip7251Transition: 999999999999, + eip7702Transition: 999999999999, + eip7623Transition: 999999999999, + + // Osaka + eip7594Transition: 999999999999, + eip7823Transition: 999999999999, + eip7825Transition: 999999999999, + eip7883Transition: 999999999999, + eip7918Transition: 999999999999, + eip7934Transition: 999999999999, + eip7939Transition: 999999999999, + eip7951Transition: 999999999999, + + +}; + + +// Same, for a Subnet. Unlike the DEFAULT_ENGINE / DEFAULT_ENGINE_SUBNET pair +// this one is a spread rather than a second full table, because the emitted key +// set and key order come from TRANSITION_FORKS, not from here -- a params table +// supplies values only. So every key stays in step with DEFAULT_PARAMS +// automatically and a subnet only has to state what it disagrees about. +// +// Nothing yet: a subnet and a standalone XDPoS network are running the same +// XDPoSChain binary, so the same EIPs are gated on the same forks. Put an entry +// here the moment a subnet needs to differ, e.g. +// eip1559Transition: 0, +const DEFAULT_PARAMS_SUBNET = { + eip150Transition: 2, + eip155Transition: 3, + eip160Transition: 3, + eip145Transition: 0, + eip1014Transition: 0, + eip1052Transition: 0, + eip1234Transition: 999999999999, + eip1283Transition: 0, + eip152Transition: 0, + eip1108Transition: 0, + eip1344Transition: 0, + eip1884Transition: 0, + eip2028Transition: 999999999999, + eip2200Transition: 0, eip2565Transition: 999999999999, eip2718Transition: 999999999999, eip2930Transition: 999999999999, eip1559Transition: 999999999999, eip2929Transition: 999999999999, - eip3198Transition: 999999999999, + eip3198Transition: 0, eip3529Transition: 999999999999, eip3541Transition: 999999999999, eip3554Transition: 999999999999, eip4399Transition: 999999999999, eip3651Transition: 999999999999, - eip3855Transition: 999999999999, + eip3855Transition: 0, eip3860Transition: 999999999999, eip6049Transition: 999999999999, eip1153Transition: 999999999999, @@ -150,9 +300,27 @@ const DEFAULT_PARAMS = { eip6780Transition: 999999999999, eip7516Transition: 999999999999, eip1559ElasticityMultiplier: '0x1', + eip7Transition: 1, + eip161abcTransition: 3, + eip161dTransition: 3, + eip140Transition: 4, + eip211Transition: 4, + eip214Transition: 4, + eip658Transition: 4, + MaxCodeSizeTransition: 3, + MaxCodeSize: 24576, + // Not in the reference spec, and not emitted under this name: pickMaxCodeSize + // reads it off whichever table is in play to decide the value of MaxCodeSize. + // Without it a subnet stating osakaBlock emits no MaxCodeSize at all. + MaxCodeSizeOsaka: 32768, }; -// Engine constants not present in genesis.json (copied from chainspec.json). + +// Engine constants for what genesis.json does not state (copied from +// chainspec.json). Spread into the engine block whole, so key order here is the +// emitted order and every key here is emitted. These are fallbacks only: +// anything genesis states wins, whether it arrives as chain data under the same +// name or through the ENGINE_GENESIS_KEYS rename -- see translate(). const DEFAULT_ENGINE = { mergeSignRange: 15, RangeReturnSigner: 150, @@ -160,7 +328,7 @@ const DEFAULT_ENGINE = { DynamicGasLimitBlock: 9999999999999, TipXDCX: 9999999999999, blackListHFNumber: 9999999999999, - TipTrc21Fee: 9999999999999, + TipTrc21Fee: 99999999999999, TIPXDCXMinerDisable: 9999999999999, TIPXDCXReceiverDisable: 9999999999999, blackListedAddresses: ['0x0000000000000000000000000000000000000011'], @@ -180,15 +348,21 @@ const DEFAULT_ENGINE = { // blackListHFNumber -> BlackListHFNumber // XDCXAddressBinary -> XDCXAddrBinary (a different name, not case) // TradingStateAddressBinary -> tradingStateAddressBinary -// TipXDCX/TipTrc21Fee/TIPXDCXMinerDisable/TIPXDCXReceiverDisable -> not used at all +// TipXDCX/TIPXDCXMinerDisable/TIPXDCXReceiverDisable -> not used at all +// TipTrc21Fee -> TipTrc21Fee (same name, and last) // A name the engine does not bind is silently ignored and its own default // applies, which is how a chainspec can look correct and still diverge from the // Go nodes. Values below match a working Nethermind+Go subnet deployment. +// +// Key order is the emitted order: translate() spreads this table into the +// engine block whole, so a key is added, removed or moved here and nowhere +// else. Fallbacks only, as with DEFAULT_ENGINE -- genesis always wins. const DEFAULT_ENGINE_SUBNET = { MergeSignRange: 15, RangeReturnSigner: 150, DynamicGasLimitBlock: 99999999999999, tip2019Block: 1, + TipTrc21Fee: 1, BlackListHFNumber: 99999999999999, blackListedAddresses: [], masternodeVotingContract: '0x0000000000000000000000000000000000000088', @@ -198,63 +372,229 @@ const DEFAULT_ENGINE_SUBNET = { tradingStateAddressBinary: '0x0000000000000000000000000000000000000092', XDCXLendingAddressBinary: '0x0000000000000000000000000000000000000093', XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', - // genesis allConfigs carries no maxMasternodes; this is XDC's own default and - // is what the working subnet chainspec states explicitly - maxMasternodes: 108, - // genesis states no switchEpoch on a subnet, and the engine wants it present + // genesis states no switchEpoch on a subnet and the engine wants it present. + // When genesis does state one (v2.switchEpoch / v2.SwitchEpoch) that wins -- + // see the merge in translate(). switchEpoch: 0, }; +// A subnet value that is NOT an engine param, so it is kept out of the table +// above: that table is spread into the engine block wholesale and anything in it +// is emitted, whereas maxMasternodes goes inside each v2Configs entry. genesis +// allConfigs carries none; this is XDC's own default and what the working subnet +// chainspec states explicitly. +const DEFAULT_SUBNET_EXTRAS = { + maxMasternodes: 108, +}; + +/* ------------------------------------------------------------------ * + * Maps — which genesis.json key feeds which chainspec key. Values come + * from the tables above when genesis states nothing. + * ------------------------------------------------------------------ */ + + // Which hardfork block in genesis.config activates each chainspec transition. -// The EIPs of a fork all switch on at that fork's block; when genesis does not -// mention the fork, the DEFAULT_PARAMS entry stands in. Order matters: it is -// the key order of the generated params object. +// One entry per DEFAULT_PARAMS transition key, same order and same fork +// headings, so the two tables read side by side. Order matters twice over: it +// is also the key order of the generated params object. +// +// 'someBlock' = genesis.config.someBlock activates the EIP; when genesis does +// not state that fork, the DEFAULT_PARAMS entry stands in. +// [a, b, ...] = more than one genesis key activates it, whichever states the +// earliest block. XDPoSChain has forks that are reached two +// ways: IsIstanbul is +// `isForked(TIPXDCXCancellationFeeBlock) || isForked(IstanbulBlock)` +// (params/config_forks.go), a compatibility path for older XDC +// configs that never set istanbulBlock -- Apothem is one, and it +// runs Istanbul from tipXDCXCancellationFeeBlock. +// null = no genesis fork may activate it. XDPoSChain does not +// implement the EIP, so the DEFAULT_PARAMS never-block +// (999999999999) always stands and the key is emitted +// explicitly off. Turning one of these on for Nethermind alone +// is exactly the state-root (or block-header) divergence this +// mapping exists to prevent: the Go nodes would compute +// something else and the chain would stall at the first block +// that touches it. +// +// The null verdicts were read off XDPoSChain itself, not off the EIP list: +// core/vm/jump_table.go for the per-fork instruction sets, core/vm/contracts.go +// for the precompile sets, and the Is call sites; then cross-checked +// against the chainspec Nethermind ships for Apothem (chainspec/xdc-testnet.json +// in the image), which is the same mapping with real block numbers on it. +// +// Watch for 'eip1559Block' below. XDPoSChain does NOT follow the canonical +// Ethereum fork schedule: core/vm/evm.go picks ONE instruction set by a switch +// on chainRules, and newEip1559InstructionSet is Shanghai plus 2929, 3529 and +// 3860 -- newBerlinInstructionSet has its enable2929 commented out. So a whole +// group of Berlin/London/Shanghai EIPs really activates at EIP1559Block +// (params/config_forks.go: IsEIP1559 is isForked(c.EIP1559Block, num), nothing +// else feeds it). Apothem is the proof: berlin = london = merge = shanghai = +// 61290000 but eip1559Block = 71550000, and xdc-testnet.json puts eip1559, +// 2929, 3529 and 3860 together on 71550000. const TRANSITION_FORKS = { - eip160Transition: 'eip158Block', // Spurious Dragon, same block as EIP-158 - eip145Transition: 'constantinopleBlock', - eip1014Transition: 'constantinopleBlock', - eip1052Transition: 'constantinopleBlock', - eip1234Transition: 'constantinopleBlock', - eip1283Transition: 'constantinopleBlock', - eip152Transition: 'istanbulBlock', - eip1108Transition: 'istanbulBlock', - eip1344Transition: 'istanbulBlock', - eip1884Transition: 'istanbulBlock', - eip2028Transition: 'istanbulBlock', - eip2200Transition: 'istanbulBlock', - eip2565Transition: 'berlinBlock', - eip2718Transition: 'berlinBlock', - eip2930Transition: 'berlinBlock', - eip1559Transition: 'londonBlock', // eip1559Block takes precedence, see below - eip2929Transition: 'berlinBlock', + // Chain identity. Not a transition, but it is read out of genesis.config + // exactly like one, so keeping it here lets params be built in a single pass + // with no hand-written head. It has no DEFAULT_PARAMS* entry: a chainspec + // should not invent a chain id, and pick() leaving it undefined drops the + // key, which is what we want. + chainId: 'chainId', + + // Homestead + eip7Transition: 'homesteadBlock', // DELEGATECALL, newHomesteadInstructionSet + + // Tangerine Whistle + eip150Transition: 'eip150Block', + + // Spurious Dragon — eip158Block carries 160, 161 and 170 as well + eip160Transition: 'eip158Block', + eip161abcTransition: 'eip158Block', + eip161dTransition: 'eip158Block', + eip155Transition: 'eip155Block', + // EIP-170. The limit itself is a constant, not a transition, so translate() + // emits maxCodeSize straight from DEFAULT_PARAMS alongside this. + MaxCodeSizeTransition: 'eip158Block', + + // Byzantium + eip140Transition: 'byzantiumBlock', + eip211Transition: 'byzantiumBlock', + eip214Transition: 'byzantiumBlock', + eip658Transition: 'byzantiumBlock', + + // Constantinople and Petersburg. The Constantinople opcodes also come in + // through Istanbul: core/vm/evm.go tries `case IsIstanbul` before + // `case IsConstantinople`, and newIstanbulInstructionSet builds on + // newConstantinopleInstructionSet, so they are live from whichever lands first. + eip145Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], + eip1014Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], + eip1052Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], + // block reward cut and difficulty-bomb delay: XDPoS pays its own rewards + // (eth/hooks/engine_v2_hooks.go) and has no bomb to postpone + eip1234Transition: null, + // net metered SSTORE, live only while IsConstantinople && !IsPetersburg + // (core/vm/gas_table.go, gasSStore). Moot once enable2200 replaces SSTORE's + // dynamic gas outright, which is why the reference spec can leave it on. + eip1283Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], + + // Istanbul + eip152Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], + eip1108Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], + eip1344Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], + eip1884Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], + // 2028 is NOT implemented: IntrinsicGas charges the flat + // params.TxDataNonZeroGas = 68, and TxDataNonZeroGasEIP2028 = 16 is declared + // in params/protocol_params.go and referenced nowhere in the tree. Nethermind + // at 16 gas per non-zero byte would misprice every call with calldata. + // xdc-testnet.json agrees: 999999999999 while the rest of Istanbul is live. + eip2028Transition: null, + eip2200Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], + + // Berlin -- none of it hangs off berlinBlock, see the eip1559Block note above + // 2565: PrecompiledContractsXDCv2 still has eip2565:false; the first set that + // turns it on is PrecompiledContractsEIP1559 (core/vm/contracts.go) + eip2565Transition: 'eip1559Block', + // 2718: same txpool gate as 2930 below -- every non-legacy tx type is + // rejected while !rules.IsEIP1559 + eip2718Transition: 'eip1559Block', + // 2929 + 2930: the access list is built in StateDB.Prepare, whose whole body + // is behind `if rules.IsEIP1559` (core/state/statedb.go), and txpool rejects + // every non-legacy tx type until IsEIP1559 (core/txpool/validation.go) + eip2929Transition: 'eip1559Block', + eip2930Transition: 'eip1559Block', + + // London. Only BASEFEE is really gated on londonBlock: enable3198 is the sole + // entry in newLondonInstructionSet. + eip1559Transition: 'eip1559Block', eip3198Transition: 'londonBlock', - eip3529Transition: 'londonBlock', - eip3541Transition: 'londonBlock', - eip3554Transition: 'londonBlock', - eip4399Transition: 'mergeBlock', - eip3651Transition: 'shanghaiBlock', + eip3529Transition: 'eip1559Block', // enable3529, newEip1559InstructionSet + eip3541Transition: 'eip1559Block', // core/vm/evm.go: ret[0]==0xEF && IsEIP1559 + eip3554Transition: null, // another bomb delay, nothing to delay + + // Merge + eip4399Transition: 'mergeBlock', // PREVRANDAO, newMergeInstructionSet + + // Shanghai. Only PUSH0 is really gated on shanghaiBlock: enable3855 is the + // sole entry in newShanghaiInstructionSet. + // 3651: the warm-coinbase AddAddress sits inside Prepare's IsEIP1559 branch + eip3651Transition: 'eip1559Block', eip3855Transition: 'shanghaiBlock', - eip3860Transition: 'shanghaiBlock', - eip6049Transition: 'shanghaiBlock', + // 3860: IntrinsicGas takes rules.IsEIP1559 as its isEIP3860 argument + eip3860Transition: 'eip1559Block', + // announces SELFDESTRUCT's deprecation and changes no rule; nothing in + // XDPoSChain reads it + eip6049Transition: null, + // withdrawals: XDPoSChain's header type has no withdrawalsRoot, so Nethermind + // must not start expecting one + eip4895Transition: null, + + // Cancun eip1153Transition: 'cancunBlock', + // beacon-roots system call at the top of every block; XDPoSChain makes no + // such call and carries no parentBeaconBlockRoot + eip4788Transition: null, eip4844Transition: 'cancunBlock', eip5656Transition: 'cancunBlock', eip6780Transition: 'cancunBlock', - eip7516Transition: 'cancunBlock', - // Prague. These three are the whole of what XDPoSChain gates on IsPrague: - // 2935 (history contract), 7623 (calldata floor cost) and 7702 (setcode tx, - // the only opcode-level change — newPragueInstructionSet is Cancun plus - // enable7702). Deliberately absent, because XDPoSChain does not implement - // them and switching them on for Nethermind alone would reintroduce exactly - // the state-root divergence this mapping exists to prevent: - // 2537 (BLS precompiles) — activePrecompiledContracts has no Prague case, - // it falls through to the EIP-1559 set - // 4788 / 6110 / 7002 / 7251 / 7685 — beacon-chain EIPs; 7685 would also - // add requestsHash to the block header - // No DEFAULT_PARAMS fallback on purpose: absent pragueBlock => key omitted. + eip7516Transition: 'cancunBlock', // BLOBBASEFEE, enable7516 in the Cancun set + + // Prague. XDPoSChain gates three EIPs on IsPrague: 2935 (history contract), + // 7623 (calldata floor cost) and 7702 (setcode tx, the only opcode-level + // change — newPragueInstructionSet is Cancun plus enable7702). + eip2537Transition: null, // BLS precompiles: activePrecompiledContracts has + // no Prague case, it falls through to the + // EIP-1559 set eip2935Transition: 'pragueBlock', - eip7623Transition: 'pragueBlock', + eip6110Transition: null, // beacon-chain EIPs; each would also put an + eip7002Transition: null, // EIP-7685 requestsHash in the block header + eip7251Transition: null, eip7702Transition: 'pragueBlock', + eip7623Transition: 'pragueBlock', + + // Osaka is unreachable from a chainspec, so the whole fork is null. XDPoSChain + // does gate five of these on IsOsaka -- 7823 and 7883 (modexp input cap and + // repricing, in PrecompiledContractsOsaka), 7825 (tx gas cap, params.MaxTxGas), + // 7934 (RLP block size cap, params.MaxBlockSize) and 7939 (CLZ opcode: + // newOsakaInstructionSet is Prague plus enable7939) -- but Nethermind's XDC + // build declares EVERY Osaka EIP as eipNNNNTransitionTimestamp only, with no + // block-numbered eipNNNNTransition property to bind (verified against + // Nethermind.Specs.dll in nethermindeth/nethermind:xdc-fixes). A block value + // under those names is an unknown JSON member and is silently ignored, so + // emitting one would read as "Osaka is on" while Nethermind ran without it. + // See the osakaBlock warning in translate(): no chainspec can follow a Go node + // into Osaka, EIP-7907's 32768-byte code limit included. + eip7594Transition: null, // also not implemented: PeerDAS, no blobs here + eip7823Transition: null, + eip7825Transition: null, + eip7883Transition: null, + eip7918Transition: null, // also not implemented: no blob header fields + eip7934Transition: null, + eip7939Transition: null, + eip7951Transition: null, // also not implemented: P256VERIFY is not in + // PrecompiledContractsOsaka +}; + +// The second way genesis reaches an engine param: a flat genesis.config key +// whose name differs from the chainspec key. (The first is chain data that keeps +// its name, which translate() merges by name.) Per engine, chainspec key -> the +// genesis.config key it comes from. The two engines bind +// different names for the same thing, so the tables are separate rather than +// one table with a rename step. A key here that is not in the matching +// DEFAULT_ENGINE* table is emitted only when genesis states it. +const ENGINE_GENESIS_KEYS = { + XDPoS: { + tip2019Block: 'tip2019Block', + DynamicGasLimitBlock: 'dynamicGasLimitBlock', + TipXDCX: 'tipXDCXBlock', + blackListHFNumber: 'denylistBlock', + TipTrc21Fee: 'tipTRC21FeeBlock', + TIPXDCXMinerDisable: 'tipXDCXMinerDisableBlock', + TIPXDCXReceiverDisable: 'tipXDCXReceiverDisableBlock', + }, + XDPoSSubnet: { + DynamicGasLimitBlock: 'dynamicGasLimitBlock', + tip2019Block: 'tip2019Block', + BlackListHFNumber: 'denylistBlock', + TipTrc21Fee: 'tipTRC21FeeBlock', + }, }; // XDC hardfork blocks that live in engine.XDPoS.params rather than params. @@ -278,9 +618,21 @@ const ENGINE_FORKS = { }; /* ------------------------------------------------------------------ * - * Value selection + * Helpers * ------------------------------------------------------------------ */ +// Earliest block any of these genesis keys states, for a fork more than one key +// can activate. Go ORs the isForked() checks, so the fork is live from the first +// one to pass; undefined when genesis states none of them. +function earliestStated(cfg, genesisKeys) { + const stated = genesisKeys + .map((key) => cfg[key]) + .filter((value) => value !== undefined && value !== null); + return stated.length + ? stated.reduce((a, b) => (Number(a) <= Number(b) ? a : b)) + : undefined; +} + // First value genesis actually states wins; a default is only reached when // every candidate is absent. Not `||` — 0 and false are real values here. function pick(...candidates) { @@ -292,10 +644,6 @@ function pick(...candidates) { return undefined; } -/* ------------------------------------------------------------------ * - * Address normalization - * ------------------------------------------------------------------ */ - // Lowercase, 0x-prefixed. Clients accept this fine; we don't bother with the // cosmetic EIP-55 mixed-case checksum. function normalizeAddress(addr) { @@ -303,19 +651,15 @@ function normalizeAddress(addr) { } /* ------------------------------------------------------------------ * - * Translation + * Builders — one per section of the chainspec * ------------------------------------------------------------------ */ -function translate(genesis, opts = {}) { - const cfg = genesis.config || {}; - const xdpos = cfg.XDPoS || {}; - const v2 = xdpos.v2 || {}; - - // --- engine.XDPoS.params --- - // Every per-round field is carried through as genesis states it, with two - // exceptions: expTimeoutConfig, which the chainspec schema has no key for and - // so is dropped rather than invented, and the reward amounts below. - const v2Configs = Object.keys(v2.allConfigs || {}) +// engine..params.v2Configs: the per-round XDPoS V2 settings, in round +// order. Every field is carried through as genesis states it, bar +// expTimeoutConfig, which the chainspec schema has no key for and so is dropped +// rather than invented. +function buildV2Configs(v2, opts) { + return Object.keys(v2.allConfigs || {}) .sort((a, b) => Number(a) - Number(b)) .map((round) => { const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; @@ -325,10 +669,18 @@ function translate(genesis, opts = {}) { // genesis carries no maxMasternodes on a subnet, but the working subnet // chainspec states it. Key spellings are left as genesis writes them -- // the reference capitalises them, but binding is case-insensitive. - return { maxMasternodes: DEFAULT_ENGINE_SUBNET.maxMasternodes, ...rest }; + return { maxMasternodes: DEFAULT_SUBNET_EXTRAS.maxMasternodes, ...rest }; }); +} - // chain data, identical on both engines +// engine.XDPoS.params, or engine.XDPoSSubnet.params with opts.subnet. Which +// default table and which genesis-key map apply is the only difference between +// the two engines here; the engine block is named by translate(). +function buildEngineParams(cfg, opts) { + const xdpos = cfg.XDPoS || {}; + const v2 = xdpos.v2 || {}; + + // chain data, read the same way for both engines const shared = { period: xdpos.period, epoch: xdpos.epoch, @@ -341,60 +693,39 @@ function translate(genesis, opts = {}) { // Use ?? not || — the valid value is 0, which is falsy. switchEpoch: v2.switchEpoch ?? v2.SwitchEpoch, switchBlock: v2.switchBlock ?? v2.SwitchBlock, - v2Configs, + v2Configs: buildV2Configs(v2, opts), }; - // Key names below are the ones each engine actually binds -- see the note on - // DEFAULT_ENGINE_SUBNET. Emitted in the reference specs' own order so a - // generated spec diffs cleanly against them. - const enginePadms = opts.subnet - ? { - ...shared, - switchEpoch: shared.switchEpoch ?? DEFAULT_ENGINE_SUBNET.switchEpoch, - MergeSignRange: DEFAULT_ENGINE_SUBNET.MergeSignRange, - RangeReturnSigner: DEFAULT_ENGINE_SUBNET.RangeReturnSigner, - DynamicGasLimitBlock: pick( - cfg.dynamicGasLimitBlock, - DEFAULT_ENGINE_SUBNET.DynamicGasLimitBlock - ), - tip2019Block: pick(cfg.tip2019Block, DEFAULT_ENGINE_SUBNET.tip2019Block), - BlackListHFNumber: pick(cfg.denylistBlock, DEFAULT_ENGINE_SUBNET.BlackListHFNumber), - blackListedAddresses: DEFAULT_ENGINE_SUBNET.blackListedAddresses, - masternodeVotingContract: DEFAULT_ENGINE_SUBNET.masternodeVotingContract, - blockSignerContract: DEFAULT_ENGINE_SUBNET.blockSignerContract, - randomizeSMCBinary: DEFAULT_ENGINE_SUBNET.randomizeSMCBinary, - XDCXAddrBinary: DEFAULT_ENGINE_SUBNET.XDCXAddrBinary, - tradingStateAddressBinary: DEFAULT_ENGINE_SUBNET.tradingStateAddressBinary, - XDCXLendingAddressBinary: DEFAULT_ENGINE_SUBNET.XDCXLendingAddressBinary, - XDCXLendingFinalizedTradeAddressBinary: - DEFAULT_ENGINE_SUBNET.XDCXLendingFinalizedTradeAddressBinary, - } - : { - ...shared, - // no genesis counterpart: node-level constants, not chain data - mergeSignRange: DEFAULT_ENGINE.mergeSignRange, - RangeReturnSigner: DEFAULT_ENGINE.RangeReturnSigner, - // XDC hardfork blocks, named differently on each side - tip2019Block: pick(cfg.tip2019Block, DEFAULT_ENGINE.tip2019Block), - DynamicGasLimitBlock: pick(cfg.dynamicGasLimitBlock, DEFAULT_ENGINE.DynamicGasLimitBlock), - TipXDCX: pick(cfg.tipXDCXBlock, DEFAULT_ENGINE.TipXDCX), - blackListHFNumber: pick(cfg.denylistBlock, DEFAULT_ENGINE.blackListHFNumber), - TipTrc21Fee: pick(cfg.tipTRC21FeeBlock, DEFAULT_ENGINE.TipTrc21Fee), - TIPXDCXMinerDisable: pick(cfg.tipXDCXMinerDisableBlock, DEFAULT_ENGINE.TIPXDCXMinerDisable), - TIPXDCXReceiverDisable: pick( - cfg.tipXDCXReceiverDisableBlock, - DEFAULT_ENGINE.TIPXDCXReceiverDisable - ), - blackListedAddresses: DEFAULT_ENGINE.blackListedAddresses, - masternodeVotingContract: DEFAULT_ENGINE.masternodeVotingContract, - blockSignerContract: DEFAULT_ENGINE.blockSignerContract, - randomizeSMCBinary: DEFAULT_ENGINE.randomizeSMCBinary, - XDCXAddressBinary: DEFAULT_ENGINE.XDCXAddressBinary, - TradingStateAddressBinary: DEFAULT_ENGINE.TradingStateAddressBinary, - XDCXLendingAddressBinary: DEFAULT_ENGINE.XDCXLendingAddressBinary, - XDCXLendingFinalizedTradeAddressBinary: - DEFAULT_ENGINE.XDCXLendingFinalizedTradeAddressBinary, - }; + const engineDefaults = opts.subnet ? DEFAULT_ENGINE_SUBNET : DEFAULT_ENGINE; + const engineGenesisKeys = opts.subnet + ? ENGINE_GENESIS_KEYS.XDPoSSubnet + : ENGINE_GENESIS_KEYS.XDPoS; + + // The whole default table is spread in rather than restated key by key: a + // value belongs to exactly one place, the DEFAULT_ENGINE* table, which also + // fixes the emitted key order. Adding a constant there is all it takes to have + // it emitted -- the older hand-copied version silently dropped any key whose + // assignment nobody remembered to write out. + // + // One rule for what follows: the table is a fallback, never a winner. + // Whatever genesis states beats it, and a table entry only shows up where + // genesis is silent. Genesis reaches a key two ways, hence two passes; both + // re-assign keys the spread already created, so key order stays the table's. + const engineParams = { ...shared, ...engineDefaults }; + + // 1. chain data off genesis.config.XDPoS, which keeps its name. A table key of + // the same name (e.g. switchEpoch) backstops it rather than overwriting it: + // the raw spread alone would let the constant win and silently replace, say, + // a switchEpoch of 900 with 0, putting the two clients on different epochs. + for (const key of Object.keys(engineDefaults)) { + if (key in shared) { + engineParams[key] = pick(shared[key], engineDefaults[key]); + } + } + // 2. flat genesis.config keys, which are named differently on each side + for (const [key, genesisKey] of Object.entries(engineGenesisKeys)) { + engineParams[key] = pick(cfg[genesisKey], engineDefaults[key]); + } // XDC hardfork blocks carried straight through. Absent in genesis => absent // from the chainspec, so the fork stays off rather than activating at 0. @@ -402,33 +733,87 @@ function translate(genesis, opts = {}) { // working subnet chainspec carries none. if (!opts.subnet) { for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { - enginePadms[key] = cfg[genesisKey]; + engineParams[key] = cfg[genesisKey]; } } - // --- params (chain rules / EIP transitions) --- - const params = { - chainId: cfg.chainId, - homesteadBlock: cfg.homesteadBlock, - eip150Transition: cfg.eip150Block, - eip155Transition: cfg.eip155Block, - eip158Transition: cfg.eip158Block, - }; + return engineParams; +} + +// params: the chain rules, which here is almost entirely EIP transitions. +function buildParams(cfg, opts) { + const paramDefaults = opts.subnet ? DEFAULT_PARAMS_SUBNET : DEFAULT_PARAMS; + + const params = {}; + for (const [key, forkBlock] of Object.entries(TRANSITION_FORKS)) { - params[key] = pick(cfg[forkBlock], DEFAULT_PARAMS[key]); + // null => no genesis fork activates it, so the DEFAULT_PARAMS never-block + // stands and the key is emitted explicitly off. An array => the earliest + // block any of those keys states. + let stated; + if (forkBlock === null) { + stated = undefined; + } else if (Array.isArray(forkBlock)) { + stated = earliestStated(cfg, forkBlock); + } else { + stated = cfg[forkBlock]; + } + params[key] = pick(stated, paramDefaults[key]); } - // genesis states EIP-1559 on its own key as well as through londonBlock - params.eip1559Transition = pick( - cfg.eip1559Block, - cfg.londonBlock, - DEFAULT_PARAMS.eip1559Transition - ); - params.byzantiumBlock = cfg.byzantiumBlock; + // no genesis counterpart - params.eip1559ElasticityMultiplier = DEFAULT_PARAMS.eip1559ElasticityMultiplier; + params.eip1559ElasticityMultiplier = paramDefaults.eip1559ElasticityMultiplier; + params.MaxCodeSize = pickMaxCodeSize(cfg, params.MaxCodeSizeTransition, paramDefaults); + + return params; +} + +// maxCodeSize, the limit maxCodeSizeTransition switches on -- the two are +// emitted together or not at all. +// +// XDPoSChain has TWO code limits (core/vm/common.go, CheckMaxCodeSize): nothing +// before eip158Block, params.MaxCodeSize (24576) from there, then +// params.MaxCodeSizeOsaka (32768) from osakaBlock. The chainspec has a single +// maxCodeSize/maxCodeSizeTransition pair, so it holds ONE limit, not a step +// between two. It still lands exactly right in the case that matters for a new +// subnet: when osakaBlock is at or before the limit's own start, 32768 is the +// only limit the chain ever has, so that is the value to emit. +function pickMaxCodeSize(cfg, maxCodeSizeTransition, paramDefaults) { + const osakaBlock = cfg.osakaBlock; + if (osakaBlock === undefined || osakaBlock === null) { + return paramDefaults.MaxCodeSize; + } + + // maxCodeSize is the one piece of Osaka a chainspec can carry at all. + console.error( + `Warning: genesis states osakaBlock ${osakaBlock}, but Nethermind's XDC ` + + 'build declares every Osaka EIP as eipNNNNTransitionTimestamp with no ' + + 'block-numbered form, so 7823, 7825, 7883, 7934 and 7939 cannot be turned ' + + 'on by block at all. The Nethermind nodes will run without the CLZ opcode, ' + + 'the 16777216 tx gas cap, the modexp repricing and the 8388608-byte block ' + + 'cap while the Go nodes apply them.' + ); + + if (Number(osakaBlock) <= Number(maxCodeSizeTransition)) { + return paramDefaults.MaxCodeSizeOsaka; + } + + // Two limits, one slot. 24576 is kept because it is right for the stretch from + // eip158Block to osakaBlock; from osakaBlock on, Nethermind is stricter than + // the Go nodes and rejects a deploy they accept. + console.error( + `Warning: genesis states osakaBlock ${osakaBlock}, later than ` + + `maxCodeSizeTransition ${maxCodeSizeTransition}. XDPoSChain raises ` + + 'its code limit to 32768 there; the chainspec holds one limit only, so it ' + + 'keeps 24576 and the Nethermind nodes stay stricter from osakaBlock on.' + ); + return paramDefaults.MaxCodeSize; +} - // --- genesis block header --- - const block = { +// genesis: the genesis block header, carried through verbatim except for the +// base fee, which genesis.json leaves null. +function buildGenesisBlock(genesis, opts) { + return { nonce: genesis.nonce, timestamp: genesis.timestamp, extraData: genesis.extraData, @@ -439,23 +824,28 @@ function translate(genesis, opts = {}) { number: genesis.number, gasUsed: genesis.gasUsed, parentHash: genesis.parentHash, - baseFeePerGas: - genesis.baseFeePerGas == null - ? (opts.baseFeePerGas || DEFAULT_BASE_FEE_PER_GAS) - : genesis.baseFeePerGas, + baseFeePerGas: pick(genesis.baseFeePerGas, DEFAULT_BASE_FEE_PER_GAS), }; +} + +/* ------------------------------------------------------------------ * + * Translation + * ------------------------------------------------------------------ */ + +function translate(genesis, opts = {}) { + const cfg = genesis.config || {}; // Subnet nodes run a different consensus plugin than a standalone XDPoS // network, and Nethermind selects it by this key. The mapping notes above // spell it "engine.XDPoS" throughout; with opts.subnet the whole block is // named engine.XDPoSSubnet instead, contents unchanged. - const engineName = opts.subnet ? "XDPoSSubnet" : "XDPoS"; + const engineName = opts.subnet ? 'XDPoSSubnet' : 'XDPoS'; return { name: opts.name || DEFAULT_CHAIN_NAME, - engine: { [engineName]: { params: enginePadms } }, - params, - genesis: block, + engine: { [engineName]: { params: buildEngineParams(cfg, opts) } }, + params: buildParams(cfg, opts), + genesis: buildGenesisBlock(genesis, opts), nodes: opts.nodes || [], accounts: genesis.alloc || {}, }; @@ -466,8 +856,8 @@ function translate(genesis, opts = {}) { * ------------------------------------------------------------------ */ const USAGE = - 'Usage: node genesis-to-chainspec.js [--subnet] [--name ] [--base-fee <0x..>]\n' + - ' npm run convert -- [--subnet] [--name ] [--base-fee <0x..>]\n' + + 'Usage: node genesis-to-chainspec.js [--subnet] [--name ]\n' + + ' npm run convert -- [--subnet] [--name ]\n' + '--subnet names the engine block XDPoSSubnet instead of XDPoS; use it for a\n' + 'Subnet deployment, leave it off for a standalone XDPoS network.\n' + 'Both paths are required. Relative paths resolve against the current working\n' + @@ -481,8 +871,6 @@ function main(argv) { for (let i = 0; i < args.length; i++) { if (args[i] === '--name') { opts.name = args[++i]; - } else if (args[i] === '--base-fee') { - opts.baseFeePerGas = args[++i]; } else if (args[i] === '--subnet') { opts.subnet = true; } else if (args[i] === '-h' || args[i] === '--help') { diff --git a/container-manager/src/package.json b/container-manager/src/package.json index cc9e616..07c16dd 100644 --- a/container-manager/src/package.json +++ b/container-manager/src/package.json @@ -5,7 +5,7 @@ "main": "test-docker.js", "scripts": { "start": "node express.js", - "dev": "HOSTPWD=$PWD/../mount/generated nodemon express.js", + "dev": "mkdir -p $PWD/../mount/generated/scripts/; HOSTPWD=$PWD/../mount/generated nodemon express.js", "convert": "node libs/genesis-to-chainspec.js", "check-chainspec": "node libs/check-chainspec.js" }, From c8854523c9635e9f6459f3af91b0a33582d65642 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Wed, 2 Sep 2026 21:56:05 +0700 Subject: [PATCH 26/59] update readme and scripts versioning --- README.md | 21 ++++++++++-- container-manager/src/README.md | 55 +++++++++++++++++++++++++++++++- container-manager/start.sh | 6 ++-- container-manager/start_xdpos.sh | 2 +- 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a8a8955..f1e7a6e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# Subnet Deployment + ## XDPoS Private Network Generator Official docs: [XDPoSChain privatenet](https://docs.xdc.network/xdcchain/developers/xdposchain_privatenet/) @@ -5,16 +7,31 @@ Official docs: [XDPoSChain privatenet](https://docs.xdc.network/xdcchain/develop Quick start: ```bash -cd container-manager +curl -O https://raw.githubusercontent.com/XinFinOrg/Subnet-Deployment/v3.1.0/container-manager/start_xdpos.sh +chmod +x start_xdpos.sh ./start_xdpos.sh ``` +Go to http://localhost:5210/gen_xdpos ## Subnet Deployment Generator Official docs: [Launch subnet](https://docs.xdc.network/subnet/install_guide/launch_subnet/) + Quick start: ```bash -cd container-manager +curl -O https://raw.githubusercontent.com/XinFinOrg/Subnet-Deployment/v3.1.0/container-manager/start.sh +chmod +x start.sh ./start.sh ``` +Go to http://localhost:5210/ + +## For Developers + +```bash +cd container-manager/src +npm run dev +``` + +- XDPoS Privatechain Generator: +- XDC Subnet Generator: diff --git a/container-manager/src/README.md b/container-manager/src/README.md index 691f4c2..26dcf02 100644 --- a/container-manager/src/README.md +++ b/container-manager/src/README.md @@ -1,6 +1,48 @@ # container-manager -`npm run dev` +## Run the generator app + +### Development mode + +```bash +cd container-manager/src +npm run dev +``` + +- XDPoS Private Network Generator: +- Subnet Deployment Generator: + +Generated files: `container-manager/mount/generated` + +### Container mode (how users run it) + +XDPoS Private Network Generator: + +```bash +GENERATOR_VERSION=generator-v3.1.0 #YOUR TESTING VERSION HERE +export GENERATOR_IMAGE_VERSION=xinfinorg/subnet-generator:$GENERATOR_VERSION + +curl -O https://raw.githubusercontent.com/XinFinOrg/Subnet-Deployment/$GENERATOR_VERSION/container-manager/start_xdpos.sh +chmod +x start_xdpos.sh +./start_xdpos.sh +``` + +Then go to + +Subnet Deployment Generator: + +```bash +GENERATOR_VERSION=generator-v3.1.0 #YOUR TESTING VERSION HERE +export GENERATOR_IMAGE_VERSION=xinfinorg/subnet-generator:$GENERATOR_VERSION + +curl -O https://raw.githubusercontent.com/XinFinOrg/Subnet-Deployment/$GENERATOR_VERSION/container-manager/start.sh +chmod +x start.sh +./start.sh +``` + +Then go to + +Generated files land in `./generated`, next to the script. ## genesis.json -> chainspec.json @@ -44,6 +86,17 @@ exits `1` when it crashes. `docker-up.sh` stops on anything but `0`, so a repaired chainspec leaves you free to wipe node data directories before running it again. +To boot without the check — a host that cannot pull the generator image, or a +mismatch you know about and mean to keep — pass `--bypass`: + +```bash +./docker-up.sh --bypass machine1 +``` + +The flag only waives the check; nothing verifies that `chainspec.json` still +matches `genesis.json`, so the Nethermind nodes may boot on a different config +than the XDC nodes and never sync with them. + The check runs in a throwaway subnet-generator container, which already carries the converter — the host needs docker only, no node. The image is the one that generated the deployment: the manager inspects its own container during genesis diff --git a/container-manager/start.sh b/container-manager/start.sh index 7a9b2e1..6f3e12b 100755 --- a/container-manager/start.sh +++ b/container-manager/start.sh @@ -1,9 +1,9 @@ #!/bin/bash -version="${GENERATOR_VERSION:v2.0.0}" +image_version="${GENERATOR_IMAGE_VERSION:-xinfinorg/subnet-generator:generator-v3.1.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" -docker pull xinfinorg/subnet-generator:$version +docker pull $image_version mkdir -p generated/scripts if ! docker network inspect "$network_name" > /dev/null 2>&1; then @@ -19,7 +19,7 @@ docker run -d \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $current_dir/generated:/mount/generated \ -e HOSTPWD=$current_dir/generated \ - xinfinorg/subnet-generator:$version \ + $image_version \ && \ echo '' && \ echo '' && \ diff --git a/container-manager/start_xdpos.sh b/container-manager/start_xdpos.sh index 2105f71..f95908b 100755 --- a/container-manager/start_xdpos.sh +++ b/container-manager/start_xdpos.sh @@ -1,5 +1,5 @@ #!/bin/bash -image_version="${GENERATOR_IMAGE_VERSION:-xinfinorg/subnet-generator:generator-v3.0.0}" +image_version="${GENERATOR_IMAGE_VERSION:-xinfinorg/subnet-generator:generator-v3.1.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" From 20fbd74e6e864d6f8de4c751bec33ab54eb8faf8 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Wed, 2 Sep 2026 21:56:12 +0700 Subject: [PATCH 27/59] add bypass chainspec-check flag to start script --- .../src/gen/scripts/docker-up.sh | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index 98b3fce..607b0d4 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -9,14 +9,35 @@ fi profiles=($($compose config --profiles)) -profile="$1" +# the chainspec pre-check needs docker to pull and run the generator image; on a +# host without that reach, or when the mismatch is known and deliberate, it can +# be waived with --bypass +bypass=false + +profile="" +for arg in "$@"; do + case "$arg" in + --bypass) + bypass=true + ;; + -*) + echo "Unknown option: $arg" + echo "Usage: docker-up.sh [--bypass] PROFILE" + exit 1 + ;; + *) + profile="$arg" + ;; + esac +done + if [[ -z $profile ]]; then if [[ ${#profiles[@]} == 1 ]]; then # single machine deployment, nothing to choose between profile="${profiles[0]}" else echo "Missing argument" - echo "Usage: docker-up.sh PROFILE" + echo "Usage: docker-up.sh [--bypass] PROFILE" echo "Profile:" if [[ ${#profiles[@]} == 0 ]]; then echo " no profile found in docker-compose.yml" @@ -38,17 +59,22 @@ fi # pre-check: chainspec.json must still match genesis.json, otherwise the # Nethermind nodes boot on a different config than the XDC nodes. -bash scripts/check-chainspec.sh -check_result=$? -if [[ $check_result == 3 ]]; then - echo "" - echo "chainspec.json was updated and the old copy saved as backup in archive directory." - echo "Run the previous command again to start the chain!" - exit 1 -elif [[ $check_result != 0 ]]; then - echo "" - echo "chainspec pre-check failed, not booting." - exit $check_result +if [[ $bypass == true ]]; then + echo "Bypassing chainspec pre-check; chainspec.json is not verified against genesis.json." +else + bash scripts/check-chainspec.sh + check_result=$? + if [[ $check_result == 3 ]]; then + echo "" + echo "chainspec.json was updated and the old copy saved as backup in archive directory." + echo "Run the previous command again to start the chain!" + exit 1 + elif [[ $check_result != 0 ]]; then + echo "" + echo "chainspec pre-check failed, not booting." + echo "Re-run with --bypass to boot anyway." + exit $check_result + fi fi From 5748422721ee90f7c027fb3e54ca5d4fc1018686 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Wed, 2 Sep 2026 21:56:36 +0700 Subject: [PATCH 28/59] enable rpc modules for nmc node --- .../src/gen/scripts/xdc-nmc-subnet.json | 15 ++++++++++++++- container-manager/src/gen/scripts/xdc-nmc.json | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/container-manager/src/gen/scripts/xdc-nmc-subnet.json b/container-manager/src/gen/scripts/xdc-nmc-subnet.json index 0f6dda8..e5672d1 100644 --- a/container-manager/src/gen/scripts/xdc-nmc-subnet.json +++ b/container-manager/src/gen/scripts/xdc-nmc-subnet.json @@ -19,7 +19,20 @@ "Enabled": true }, "JsonRpc": { - "Enabled": true + "Enabled": true, + "EnabledModules": [ + "Xdc", + "Eth", + "Subscribe", + "Trace", + "TxPool", + "Web3", + "Proof", + "Net", + "Parity", + "Health", + "Rpc" + ] }, "Blocks": { "TargetBlockGasLimit": 420000000 diff --git a/container-manager/src/gen/scripts/xdc-nmc.json b/container-manager/src/gen/scripts/xdc-nmc.json index 598b68f..50811ff 100644 --- a/container-manager/src/gen/scripts/xdc-nmc.json +++ b/container-manager/src/gen/scripts/xdc-nmc.json @@ -19,7 +19,20 @@ "Enabled": true }, "JsonRpc": { - "Enabled": true + "Enabled": true, + "EnabledModules": [ + "Xdc", + "Eth", + "Subscribe", + "Trace", + "TxPool", + "Web3", + "Proof", + "Net", + "Parity", + "Health", + "Rpc" + ] }, "Blocks": { "TargetBlockGasLimit": 50000000 From fbbf268e74697e8a1f4f6fed5959b4eee993e319 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Thu, 3 Sep 2026 15:15:30 +0700 Subject: [PATCH 29/59] update default images --- container-manager/src/gen/config_gen.js | 2 +- container-manager/src/libs/exec.js | 2 +- container-manager/src/views/xdpos_generator/index.pug | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index 6ce8830..1b58da1 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -66,7 +66,7 @@ const config = { output_path: `${__dirname}/../../mount/generated/`, }, xdpos: { - xdposnode: process.env.VERSION_NODE_IMAGE || "xinfinorg/devnet:dev-upgrade-53e5601", + xdposnode: process.env.VERSION_NODE_IMAGE || "xinfinorg/devnet:test-generator-v3", nethermind: process.env.VERSION_NETHERMIND_IMAGE || "nethermindeth/nethermind:master-d4ef4fd", stake_threshold: parseInt(process.env.MASTERNODE_MINIMUM_STAKE) || "", reward_yield: parseInt(process.env.REWARDS_YIELD) || "", diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 4befde9..2130764 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -167,7 +167,7 @@ function generateXdpos(params) { console.log("gen success"); //step 2: generate genesis.json - let versionGenesisFullname = "xinfinorg/devnet:dev-upgrade-53e5601"; + let versionGenesisFullname = "xinfinorg/devnet:test-generator-v3"; if ( "customversion-checkbox" in params && params["customversion-checkbox"] != "" && diff --git a/container-manager/src/views/xdpos_generator/index.pug b/container-manager/src/views/xdpos_generator/index.pug index ffd5e11..7f1bbd4 100644 --- a/container-manager/src/views/xdpos_generator/index.pug +++ b/container-manager/src/views/xdpos_generator/index.pug @@ -82,16 +82,16 @@ body center .pure-control-group label() Node Version - input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-node-fullname") + input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:test-generator-v3' name="customversion-xdpos-node-fullname") .pure-control-group label() Genesis Version - input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:dev-upgrade-53e5601' name="customversion-xdpos-genesis-fullname") + input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:test-generator-v3' name="customversion-xdpos-genesis-fullname") .pure-control-group label() Number of Nethermind Nodes input#customversion-xdpos-nethermind-count.pure-input(type='number' placeholder='0' name="customversion-xdpos-nethermind-count") .pure-control-group label() Nethermind Node Version - input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder='nethermindeth/nethermind:master-857da8f' name="customversion-xdpos-nethermind-version") + input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder='nethermindeth/nethermind:master-d4ef4fd' name="customversion-xdpos-nethermind-version") .pure-control-group .info-container From 79c382091d6132f0858ba1ad9f8e161139a1bd6f Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Thu, 3 Sep 2026 16:01:03 +0700 Subject: [PATCH 30/59] update default version --- container-manager/src/gen/config_gen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index 1b58da1..5c8cdd4 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -26,7 +26,7 @@ const config = { process.env.SERVICES_SECRET || crypto.randomBytes(10).toString("hex"), relayer_mode: process.env.RELAYER_MODE || "full", docker_image_name: - process.env.IMAGE_NAME || "xinfinorg/subnet-generator:generator-v3.0.0", + process.env.IMAGE_NAME || "xinfinorg/subnet-generator:generator-v3.1.0", version: { genesis: process.env.VERSION_GENESIS || "v0.3.1", subnet: process.env.VERSION_SUBNET || "v0.3.2", From 7028294c53989362974c4db3c7002cbe7c8026fb Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Tue, 8 Sep 2026 13:23:54 +0700 Subject: [PATCH 31/59] - don't run chainspec checker when not needed (all Golang nodes) - add docker pull step during generate genesis to make sure updated image is used --- container-manager/src/README.md | 4 ++ .../src/gen/scripts/docker-up.sh | 50 +++++++++++++------ container-manager/src/libs/exec.js | 30 ++++++++--- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/container-manager/src/README.md b/container-manager/src/README.md index 26dcf02..4c1c967 100644 --- a/container-manager/src/README.md +++ b/container-manager/src/README.md @@ -73,6 +73,10 @@ inside the container is `./generated` on the host; relative paths resolve agains `genesis.json` and compares the result with `chainspec.json`, since a stale chainspec gives the Nethermind nodes a different config than the XDC nodes. +Only when there is something to compare, though: `docker-up.sh` looks in +`docker compose --profile config` for a node that mounts +`chainspec.json`, and skips the check when none does. + ```bash ./scripts/check-chainspec.sh # check, and repair if they differ ./scripts/check-chainspec.sh --dry-run # report only diff --git a/container-manager/src/gen/scripts/docker-up.sh b/container-manager/src/gen/scripts/docker-up.sh index 607b0d4..dd61816 100755 --- a/container-manager/src/gen/scripts/docker-up.sh +++ b/container-manager/src/gen/scripts/docker-up.sh @@ -7,11 +7,18 @@ if ! which docker-compose > /dev/null 2>&1; then compose="docker compose" fi +banner() { + local rule="======================================================================" + echo "" + echo "$rule" + printf ' %s\n' "$@" + echo "$rule" + echo "" +} + profiles=($($compose config --profiles)) -# the chainspec pre-check needs docker to pull and run the generator image; on a -# host without that reach, or when the mismatch is known and deliberate, it can -# be waived with --bypass +# the chainspec pre-check can be waived with --bypass bypass=false profile="" @@ -48,12 +55,11 @@ if [[ -z $profile ]]; then fi fi -network_name="docker_net" -if ! docker network inspect "$network_name" > /dev/null 2>&1; then - echo "Network '$network_name' does not exist. Creating it..." - docker network create --subnet 192.168.25.0/24 "$network_name" -else - echo "Joining existing network '$network_name'" +if ! compose_config=$($compose --profile "$profile" config 2>&1); then + echo "$compose_config" + echo "" + echo "docker-compose.yml could not be read, not booting." + exit 2 fi @@ -61,22 +67,36 @@ fi # Nethermind nodes boot on a different config than the XDC nodes. if [[ $bypass == true ]]; then echo "Bypassing chainspec pre-check; chainspec.json is not verified against genesis.json." +elif ! grep -q 'target: /work/chainspec.json' <<< "$compose_config"; then + echo "No node in profile '$profile' mounts chainspec.json, skipping the pre-check." else bash scripts/check-chainspec.sh check_result=$? if [[ $check_result == 3 ]]; then - echo "" - echo "chainspec.json was updated and the old copy saved as backup in archive directory." - echo "Run the previous command again to start the chain!" + banner \ + "chainspec.json did not match genesis.json, and has been updated." \ + "The old copy is kept as a backup in the archive directory." \ + "" \ + "NOTHING WAS STARTED." \ + "Run the previous command again to start the chain!" exit 1 elif [[ $check_result != 0 ]]; then - echo "" - echo "chainspec pre-check failed, not booting." - echo "Re-run with --bypass to boot anyway." + banner \ + "chainspec pre-check failed, NOT booting." \ + "" \ + "Re-run with --bypass to boot anyway." exit $check_result fi fi +network_name="docker_net" +if ! docker network inspect "$network_name" > /dev/null 2>&1; then + echo "Network '$network_name' does not exist. Creating it..." + docker network create --subnet 192.168.25.0/24 "$network_name" +else + echo "Joining existing network '$network_name'" +fi + $compose --profile $profile pull $compose --profile $profile up -d diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 2130764..feb8b2f 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -167,7 +167,7 @@ function generateXdpos(params) { console.log("gen success"); //step 2: generate genesis.json - let versionGenesisFullname = "xinfinorg/devnet:test-generator-v3"; + let versionGenesisFullname = config.version.xdposnode; if ( "customversion-checkbox" in params && params["customversion-checkbox"] != "" && @@ -177,9 +177,17 @@ function generateXdpos(params) { versionGenesisFullname = `${params["customversion-xdpos-genesis-fullname"]}`; } - command = `cd ${mountPath}; docker run -v ${config.hostPath}:/app/generated/ --entrypoint 'bash' ${versionGenesisFullname} /work/puppeth.sh`; + // a local copy of the tag can be outdated and docker will not re-pull it on + // its own; the pull is non-fatal so a locally built image that was never + // pushed (or a host with no registry access) still runs off what is there + command = + `cd ${mountPath}; ` + + `docker pull ${versionGenesisFullname} || ` + + `echo 'docker pull failed, using local copy of ${versionGenesisFullname} if present'; ` + + `docker run -v ${config.hostPath}:/app/generated/ --entrypoint 'bash' ${versionGenesisFullname} /work/puppeth.sh`; console.log(command); - const [result2, out2] = callExec(command); + // the pull shares the exec timeout with puppeth, so allow for a cold fetch + const [result2, out2] = callExec(command, 600000); if (!result2) { return [result2, out2]; } @@ -233,9 +241,17 @@ function generate(params) { versionGenesisFullname = `${params["customversion-xdpos-genesis-fullname"]}`; } - command = `cd ${mountPath}; docker run -v ${config.hostPath}:/app/generated/ --entrypoint 'bash' ${versionGenesisFullname} /work/puppeth.sh`; + // a local copy of the tag can be outdated and docker will not re-pull it on + // its own; the pull is non-fatal so a locally built image that was never + // pushed (or a host with no registry access) still runs off what is there + command = + `cd ${mountPath}; ` + + `docker pull ${versionGenesisFullname} || ` + + `echo 'docker pull failed, using local copy of ${versionGenesisFullname} if present'; ` + + `docker run -v ${config.hostPath}:/app/generated/ --entrypoint 'bash' ${versionGenesisFullname} /work/puppeth.sh`; console.log(command); - const [result2, out2] = callExec(command); + // the pull shares the exec timeout with puppeth, so allow for a cold fetch + const [result2, out2] = callExec(command, 600000); if (!result2) { return [result2, out2]; } @@ -269,9 +285,9 @@ function generate(params) { return [result2, out2]; } -function callExec(command) { +function callExec(command, timeout = 300000) { try { - const stdout = execSync(command, { timeout: 200000, encoding: "utf-8" }); + const stdout = execSync(command, { timeout, encoding: "utf-8" }); output = stdout.toString(); // console.log(output); From 1d618df4dcc18fa61efa3dd4bf7f1fc76292c644 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Tue, 8 Sep 2026 13:31:15 +0700 Subject: [PATCH 32/59] fix config bug --- container-manager/src/libs/config.js | 3 +++ container-manager/src/libs/exec.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/container-manager/src/libs/config.js b/container-manager/src/libs/config.js index 9ec6506..cef3489 100644 --- a/container-manager/src/libs/config.js +++ b/container-manager/src/libs/config.js @@ -5,6 +5,7 @@ const path = require("path"); const mountPath = path.join(__dirname, "../../mount/generated/"); const configModule = require("../gen/config_gen.js"); const version = configModule.config.version; +const xdpos = configModule.config.xdpos; const config = {}; module.exports = config; @@ -24,6 +25,8 @@ function initModule() { } console.log("init with versions", version); config["version"] = version; + console.log("init with xdpos versions", xdpos); + config["xdpos"] = xdpos; } function checkMountPath() { diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index feb8b2f..93617b0 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -167,7 +167,7 @@ function generateXdpos(params) { console.log("gen success"); //step 2: generate genesis.json - let versionGenesisFullname = config.version.xdposnode; + let versionGenesisFullname = config.xdpos.xdposnode; if ( "customversion-checkbox" in params && params["customversion-checkbox"] != "" && From af3cb17209cad8a98d121f43bdb6b5bb17e4252d Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Wed, 9 Sep 2026 18:10:07 +0700 Subject: [PATCH 33/59] Improve start scripts: - If preexisting container, replaces the old one instead of failing - Make terminal messages easier to see and always print the URL instructions Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/start.sh | 132 ++++++++++++++++++++++------ container-manager/start_xdpos.sh | 142 ++++++++++++++++++++++++------- 2 files changed, 217 insertions(+), 57 deletions(-) diff --git a/container-manager/start.sh b/container-manager/start.sh index 6f3e12b..1a4577f 100755 --- a/container-manager/start.sh +++ b/container-manager/start.sh @@ -1,29 +1,113 @@ #!/bin/bash + image_version="${GENERATOR_IMAGE_VERSION:-xinfinorg/subnet-generator:generator-v3.1.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" +container_name="subnet-generator" +port=5210 +docker_ip=192.168.25.111 + +main() { + pull_image + mkdir -p "$current_dir/generated/scripts" + remove_existing + check_conflicts + ensure_network + + # stderr is left uncaptured: docker puts the container id on stdout and + # warnings on stderr, so merging them puts warning text where the id goes + local id + if ! id=$(docker run -d \ + --name "$container_name" \ + --network "$network_name" --ip="$docker_ip" \ + -p $port:$port \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$current_dir/generated:/mount/generated" \ + -e HOSTPWD="$current_dir/generated" \ + "$image_version"); then + # docker leaves the container in Created state, where it blocks the next run + docker rm -f "$container_name" > /dev/null 2>&1 + fail "docker run failed, see the error above" + fi + + echo "Started generator '$container_name' (${id:0:12})" + echo "" + echo "======================================================================" + echo " if this is running on your server, first use ssh tunnel:" + echo " ssh -N -L localhost:$port:localhost:$port @ -i " + echo " if you are using VSCode Remote Explorer, ssh tunnel is available by default" + echo "" + echo " http://localhost:$port to access Subnet Deployment Wizard" + echo "======================================================================" + echo "" +} + +# docker's error for a taken port or ip is a wall of endpoint ids that never +# says what is holding it, so bail out with a name instead +fail() { + echo "" + echo "======================================================================" + echo " CANNOT START: $1" + if [[ -n $2 ]]; then + echo " $2" + fi + echo "======================================================================" + echo "" + exit 1 +} + +pull_image() { + docker pull "$image_version" + local result=$? + if [[ $result != 0 ]]; then + fail "docker pull $image_version failed with exit code $result" + fi + echo "SUCCESS: Docker pull completed successfully" +} + +# every run starts a fresh container, so what runs always matches the image just +# pulled; a container left over from a previous run is removed, and said so out +# loud rather than leaving it to be guessed at +remove_existing() { + local existing status + existing=$(docker ps -aq --filter "name=^${container_name}$") + if [[ -z $existing ]]; then + return + fi + status=$(docker inspect "$existing" --format '{{.State.Status}}') + echo "Replacing previous generator '$container_name' ($status, ${existing:0:12})" + docker rm -f "$existing" > /dev/null +} + +# run after remove_existing, so our own container is never the reported culprit +check_conflicts() { + local holder + holder=$(docker ps --filter "publish=$port" --format '{{.Names}} ({{.Image}})' | head -1) + if [[ -z $holder ]] && command -v lsof > /dev/null 2>&1; then + holder=$(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | awk 'NR==2 {print $1" (pid "$2")"}') + fi + if [[ -n $holder ]]; then + fail "port $port is already in use by $holder" \ + "Free the port, then run this again." + fi + + # IPv4Address comes back as /, so match the leading "/" and + # not the bare address, which would also match e.g. .1110 + holder=$(docker network inspect "$network_name" --format '{{range .Containers}}{{.Name}} {{.IPv4Address}} +{{end}}' 2>/dev/null | awk -v want="$docker_ip/" 'index($2, want) == 1 { print $1; exit }') + if [[ -n $holder ]]; then + fail "ip $docker_ip on network '$network_name' is taken by container $holder" \ + "Stop that container, then run this again." + fi +} + +ensure_network() { + if docker network inspect "$network_name" > /dev/null 2>&1; then + echo "Joining existing network '$network_name'" + else + echo "Network '$network_name' does not exist. Creating it..." + docker network create --subnet 192.168.25.0/24 "$network_name" + fi +} -docker pull $image_version -mkdir -p generated/scripts - -if ! docker network inspect "$network_name" > /dev/null 2>&1; then - echo "Network '$network_name' does not exist. Creating it..." - docker network create --subnet 192.168.25.0/24 "$network_name" -else - echo "Joining existing network '$network_name'" -fi - -docker run -d \ - --network "docker_net" --ip=192.168.25.111 \ - -p 5210:5210 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v $current_dir/generated:/mount/generated \ - -e HOSTPWD=$current_dir/generated \ - $image_version \ - && \ -echo '' && \ -echo '' && \ -echo '' && \ -echo 'if this is running on your server, first use ssh tunnel: ssh -N -L localhost:5210:localhost:5210 @ -i ' && \ -echo 'if you are using VSCode Remote Explorer, ssh tunnel will be available by default' && \ -echo 'http://localhost:5210 to access Subnet Deployment Wizard' +main "$@" diff --git a/container-manager/start_xdpos.sh b/container-manager/start_xdpos.sh index f95908b..c6961af 100755 --- a/container-manager/start_xdpos.sh +++ b/container-manager/start_xdpos.sh @@ -1,38 +1,114 @@ #!/bin/bash + image_version="${GENERATOR_IMAGE_VERSION:-xinfinorg/subnet-generator:generator-v3.1.0}" current_dir="$(cd "$(dirname "$0")" && pwd)" network_name="docker_net" +container_name="xdpos-generator" +port=5210 +docker_ip=192.168.25.111 + +main() { + pull_image + mkdir -p "$current_dir/generated/scripts" + remove_existing + check_conflicts + ensure_network + + # stderr is left uncaptured: docker puts the container id on stdout and + # warnings on stderr, so merging them puts warning text where the id goes + local id + if ! id=$(docker run -d \ + --name "$container_name" \ + --network "$network_name" --ip="$docker_ip" \ + -p $port:$port \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$current_dir/generated:/mount/generated" \ + -e HOSTPWD="$current_dir/generated" \ + -e NON_SUBNET=true \ + "$image_version"); then + # docker leaves the container in Created state, where it blocks the next run + docker rm -f "$container_name" > /dev/null 2>&1 + fail "docker run failed, see the error above" + fi + + echo "Started generator '$container_name' (${id:0:12})" + echo "" + echo "======================================================================" + echo " if this is running on your server, first use ssh tunnel:" + echo " ssh -N -L localhost:$port:localhost:$port @ -i " + echo " if you are using VSCode Remote Explorer, ssh tunnel is available by default" + echo "" + echo " http://localhost:$port/gen_xdpos to start XDPoS private network generator" + echo "======================================================================" + echo "" +} + +# docker's error for a taken port or ip is a wall of endpoint ids that never +# says what is holding it, so bail out with a name instead +fail() { + echo "" + echo "======================================================================" + echo " CANNOT START: $1" + if [[ -n $2 ]]; then + echo " $2" + fi + echo "======================================================================" + echo "" + exit 1 +} + +pull_image() { + docker pull "$image_version" + local result=$? + if [[ $result != 0 ]]; then + fail "docker pull $image_version failed with exit code $result" + fi + echo "SUCCESS: Docker pull completed successfully" +} + +# every run starts a fresh container, so what runs always matches the image just +# pulled; a container left over from a previous run is removed, and said so out +# loud rather than leaving it to be guessed at +remove_existing() { + local existing status + existing=$(docker ps -aq --filter "name=^${container_name}$") + if [[ -z $existing ]]; then + return + fi + status=$(docker inspect "$existing" --format '{{.State.Status}}') + echo "Replacing previous generator '$container_name' ($status, ${existing:0:12})" + docker rm -f "$existing" > /dev/null +} + +# run after remove_existing, so our own container is never the reported culprit +check_conflicts() { + local holder + holder=$(docker ps --filter "publish=$port" --format '{{.Names}} ({{.Image}})' | head -1) + if [[ -z $holder ]] && command -v lsof > /dev/null 2>&1; then + holder=$(lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | awk 'NR==2 {print $1" (pid "$2")"}') + fi + if [[ -n $holder ]]; then + fail "port $port is already in use by $holder" \ + "Free the port, then run this again." + fi + + # IPv4Address comes back as /, so match the leading "/" and + # not the bare address, which would also match e.g. .1110 + holder=$(docker network inspect "$network_name" --format '{{range .Containers}}{{.Name}} {{.IPv4Address}} +{{end}}' 2>/dev/null | awk -v want="$docker_ip/" 'index($2, want) == 1 { print $1; exit }') + if [[ -n $holder ]]; then + fail "ip $docker_ip on network '$network_name' is taken by container $holder" \ + "Stop that container, then run this again." + fi +} + +ensure_network() { + if docker network inspect "$network_name" > /dev/null 2>&1; then + echo "Joining existing network '$network_name'" + else + echo "Network '$network_name' does not exist. Creating it..." + docker network create --subnet 192.168.25.0/24 "$network_name" + fi +} -docker pull $image_version -# Check the exit code -if [ $? -eq 0 ]; then - echo "SUCCESS: Docker pull completed successfully" -else - echo "FAILED: Docker pull failed with exit code $?" - exit 1 -fi - -mkdir -p generated/scripts - -if ! docker network inspect "$network_name" > /dev/null 2>&1; then - echo "Network '$network_name' does not exist. Creating it..." - docker network create --subnet 192.168.25.0/24 "$network_name" -else - echo "Joining existing network '$network_name'" -fi - -docker run -d \ - --network "docker_net" --ip=192.168.25.111 \ - -p 5210:5210 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v $current_dir/generated:/mount/generated \ - -e HOSTPWD=$current_dir/generated \ - -e NON_SUBNET=true \ - $image_version \ - && \ -echo '' && \ -echo '' && \ -echo '' && \ -echo 'if this is running on your server, first use ssh tunnel: ssh -N -L localhost:5210:localhost:5210 @ -i ' && \ -echo 'if you are using VSCode Remote Explorer, ssh tunnel will be available by default' && \ -echo -e '\n\nhttp://localhost:5210/gen_xdpos to start XDPoS private network generator' \ No newline at end of file +main "$@" From b038deb657027c25e69908a3b960c8e4f32e3bb4 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Fri, 18 Sep 2026 17:15:16 +0700 Subject: [PATCH 34/59] - remove duplicate configs and read configs from single source - update default images --- container-manager/src/express.js | 10 ++++++++-- container-manager/src/gen/config_gen.js | 4 ++-- .../src/views/generator/index.pug | 18 +++++++++--------- .../src/views/xdpos_generator/index.pug | 6 +++--- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/container-manager/src/express.js b/container-manager/src/express.js index 7434b42..6f7f2d1 100644 --- a/container-manager/src/express.js +++ b/container-manager/src/express.js @@ -2,6 +2,9 @@ process.chdir(__dirname); const fs = require("fs"); const state = require("./libs/state"); const exec = require("./libs/exec"); +// the generator form's placeholders come from the same defaults the generator +// itself uses, so an image default is written once in gen/config_gen.js +const config = require("./libs/config"); const path = require("path"); const ethers = require("ethers"); const consolidate = require("consolidate"); @@ -156,11 +159,14 @@ app.get("/remove_subnet", async (req, res) => { // generator methods (also pug instead of html) app.get("/gen", (req, res) => { - res.render("generator/index.pug", {}); + res.render("generator/index.pug", { + version: config.version, + xdpos: config.xdpos, + }); }); app.get("/gen_xdpos", (req, res) => { - res.render("xdpos_generator/index.pug", {}); + res.render("xdpos_generator/index.pug", { xdpos: config.xdpos }); }); app.post("/submit", (req, res) => { diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index 5c8cdd4..7a3484b 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -66,8 +66,8 @@ const config = { output_path: `${__dirname}/../../mount/generated/`, }, xdpos: { - xdposnode: process.env.VERSION_NODE_IMAGE || "xinfinorg/devnet:test-generator-v3", - nethermind: process.env.VERSION_NETHERMIND_IMAGE || "nethermindeth/nethermind:master-d4ef4fd", + xdposnode: process.env.VERSION_NODE_IMAGE || "xinfinorg/devnet:dev-upgrade-cdce8fc", + nethermind: process.env.VERSION_NETHERMIND_IMAGE || "nethermindeth/nethermind:master-4e36ba0", stake_threshold: parseInt(process.env.MASTERNODE_MINIMUM_STAKE) || "", reward_yield: parseInt(process.env.REWARDS_YIELD) || "", foundation_addr: "", diff --git a/container-manager/src/views/generator/index.pug b/container-manager/src/views/generator/index.pug index 46126ae..21be2b3 100644 --- a/container-manager/src/views/generator/index.pug +++ b/container-manager/src/views/generator/index.pug @@ -127,17 +127,17 @@ body center .pure-control-group label() Node Version - input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/xdcsubnets:v0.3.2' value='xinfinorg/xdcsubnets:v0.3.2' name="customversion-xdpos-node-fullname") + input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder=(version.subnet_image || 'xinfinorg/xdcsubnets:' + version.subnet) value=(version.subnet_image || 'xinfinorg/xdcsubnets:' + version.subnet) name="customversion-xdpos-node-fullname") .pure-control-group label() Genesis Version - input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/xdcsubnets:v0.3.1' value='xinfinorg/xdcsubnets:v0.3.1' name="customversion-xdpos-genesis-fullname") + input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/xdcsubnets:' + version.genesis value='xinfinorg/xdcsubnets:' + version.genesis name="customversion-xdpos-genesis-fullname") .pure-control-group label() Number of Nethermind Nodes input#customversion-xdpos-nethermind-count.pure-input(type='number' placeholder='0' value='0' name="customversion-xdpos-nethermind-count") span.pure-form-message-inline The last N nodes run Nethermind .pure-control-group label() Nethermind Node Version - input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder='nethermindeth/nethermind:master-d4ef4fd' value='nethermindeth/nethermind:master-d4ef4fd' name="customversion-xdpos-nethermind-version") + input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder=xdpos.nethermind value=xdpos.nethermind name="customversion-xdpos-nethermind-version") .pure-control-group .info-container @@ -149,22 +149,22 @@ body center .pure-control-group label() Bootnode Version - input#customversion-bootnode.pure-input(type='text' placeholder='v0.3.1' value='v0.3.1' name="customversion-bootnode") + input#customversion-bootnode.pure-input(type='text' placeholder=version.bootnode value=version.bootnode name="customversion-bootnode") .pure-control-group label() Relayer Version - input#customversion-relayer.pure-input(type='text' placeholder='v0.3.1' value='v0.3.1' name="customversion-relayer") + input#customversion-relayer.pure-input(type='text' placeholder=version.relayer value=version.relayer name="customversion-relayer") .pure-control-group label() Backend Stats Server Version - input#customversion-stats.pure-input(type='text' placeholder='v0.1.11' value='v0.1.11' name="customversion-stats") + input#customversion-stats.pure-input(type='text' placeholder=version.stats value=version.stats name="customversion-stats") .pure-control-group label() Frontend Version - input#customversion-frontend.pure-input(type='text' placeholder='v0.1.12' value='v0.1.12' name="customversion-frontend") + input#customversion-frontend.pure-input(type='text' placeholder=version.frontend value=version.frontend name="customversion-frontend") .pure-control-group label() CSC Version - input#customversion-csc.pure-input(type='text' placeholder='v0.3.0' value='v0.3.0' name="customversion-csc") + input#customversion-csc.pure-input(type='text' placeholder=version.csc value=version.csc name="customversion-csc") .pure-control-group label() XDC-Zero Version - input#customversion-zero.pure-input(type='text' placeholder='v0.3.0' value='v0.3.0' name="customversion-zero") + input#customversion-zero.pure-input(type='text' placeholder=version.zero value=version.zero name="customversion-zero") fieldset diff --git a/container-manager/src/views/xdpos_generator/index.pug b/container-manager/src/views/xdpos_generator/index.pug index 7f1bbd4..5283cd9 100644 --- a/container-manager/src/views/xdpos_generator/index.pug +++ b/container-manager/src/views/xdpos_generator/index.pug @@ -82,16 +82,16 @@ body center .pure-control-group label() Node Version - input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:test-generator-v3' name="customversion-xdpos-node-fullname") + input#customversion-xdpos-node-fullname.pure-input-2-3(type='text' placeholder=xdpos.xdposnode name="customversion-xdpos-node-fullname") .pure-control-group label() Genesis Version - input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder='xinfinorg/devnet:test-generator-v3' name="customversion-xdpos-genesis-fullname") + input#customversion-xdpos-genesis-fullname.pure-input-2-3(type='text' placeholder=xdpos.xdposnode name="customversion-xdpos-genesis-fullname") .pure-control-group label() Number of Nethermind Nodes input#customversion-xdpos-nethermind-count.pure-input(type='number' placeholder='0' name="customversion-xdpos-nethermind-count") .pure-control-group label() Nethermind Node Version - input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder='nethermindeth/nethermind:master-d4ef4fd' name="customversion-xdpos-nethermind-version") + input#customversion-xdpos-nethermind-version.pure-input-2-3(type='text' placeholder=xdpos.nethermind name="customversion-xdpos-nethermind-version") .pure-control-group .info-container From 0cc6febbbd878cd579120e9264a6fc7623b02376 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:17:30 +0700 Subject: [PATCH 35/59] fix(converter): emit XDC hardfork blocks in subnet mode too (D1) buildEngineParams() dropped the whole ENGINE_FORKS loop behind `if (!opts.subnet)`, on the premise that the subnet engine binds none of those names. It binds all of them: XdcSubnetChainSpecEngineParameters derives from XdcChainSpecEngineParameters and overrides only SealEngineType, so it inherits every property. The severe case is TIPUpgradeReward. Nethermind computes IsTipUpgradeRewardEnabled = (TipUpgradeReward ?? ulong.MaxValue) <= releaseStartBlock (Nethermind.Xdc/Spec/XdcChainSpecBasedSpecProvider.cs:85) so a dropped key does not mean "off", it means "never enabled". A genesis that states tipUpgradeRewardBlock would put the two clients on different reward formulas and stall the chain at the first reward checkpoint. Harmless on today's subnets, which state no tipUpgradeRewardBlock, but silent and unguarded. Verified: with those keys present in genesis, subnet mode now emits TIPUpgradeReward, TIPUpgradePenalty, TipXDCXCancellationFeeBlock, Gas50xBlock and TipSigningBlock; XDPoS output is byte-identical. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index cb35bbd..433c683 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -729,12 +729,15 @@ function buildEngineParams(cfg, opts) { // XDC hardfork blocks carried straight through. Absent in genesis => absent // from the chainspec, so the fork stays off rather than activating at 0. - // These are XDPoS spellings; the subnet engine binds none of them, and the - // working subnet chainspec carries none. - if (!opts.subnet) { - for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { - engineParams[key] = cfg[genesisKey]; - } + // Emitted for BOTH engines: XdcSubnetChainSpecEngineParameters derives from + // XdcChainSpecEngineParameters and overrides only SealEngineType, so it + // inherits every one of these properties and binds them the same way. + // TIPUpgradeReward is why this matters -- Nethermind reads + // IsTipUpgradeRewardEnabled = (TipUpgradeReward ?? ulong.MaxValue) <= + // releaseStartBlock, so a dropped key is not "off", it is "never enabled", + // and the two clients then disagree at the first reward checkpoint. + for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { + engineParams[key] = cfg[genesisKey]; } return engineParams; From fc1f9e383c687b26be66ee0f3911ca3ab874de8d Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:17:59 +0700 Subject: [PATCH 36/59] fix(converter): emit XDCXAddressBinary, not XDCXAddrBinary (D2) DEFAULT_ENGINE_SUBNET emitted XDCXAddrBinary on the strength of a rename table that called it "a different name, not case". No such property exists: grepping the whole Nethermind tree at master-4e36ba0 for XDCXAddrBinary returns nothing. The bound name is XDCXAddressBinary (Nethermind.Xdc/Spec/XdcChainSpecEngineParameters.cs:33), read into the release spec at XdcChainSpecBasedSpecProvider.cs:95. So the subnet XDCX address was silently discarded and the property stayed null on every subnet chainspec the generator has produced. The rest of the rename premise goes with it: the subnet engine class overrides only SealEngineType, and MergeSignRange / BlackListHFNumber / tradingStateAddressBinary bind case-insensitively. Comment block replaced with what the code actually does. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 433c683..0db75e8 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -341,18 +341,13 @@ const DEFAULT_ENGINE = { XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', }; -// The XDPoSSubnet engine plugin binds DIFFERENT property names than XDPoS, so a -// subnet chainspec is not just XDPoS with a renamed engine block: -// XDPoS XDPoSSubnet -// mergeSignRange -> MergeSignRange -// blackListHFNumber -> BlackListHFNumber -// XDCXAddressBinary -> XDCXAddrBinary (a different name, not case) -// TradingStateAddressBinary -> tradingStateAddressBinary -// TipXDCX/TIPXDCXMinerDisable/TIPXDCXReceiverDisable -> not used at all -// TipTrc21Fee -> TipTrc21Fee (same name, and last) -// A name the engine does not bind is silently ignored and its own default -// applies, which is how a chainspec can look correct and still diverge from the -// Go nodes. Values below match a working Nethermind+Go subnet deployment. +// The XDPoSSubnet engine plugin binds the SAME property names as XDPoS: +// XdcSubnetChainSpecEngineParameters derives from XdcChainSpecEngineParameters +// and overrides only SealEngineType (and the internal ResolveMinGasPrice), so +// it inherits every property. Binding is case-insensitive, so the casing +// differences below are cosmetic -- but a name that is not a property at all is +// silently ignored and the engine's own default applies, which is how a +// chainspec can look correct and still diverge from the Go nodes. // // Key order is the emitted order: translate() spreads this table into the // engine block whole, so a key is added, removed or moved here and nowhere @@ -368,7 +363,7 @@ const DEFAULT_ENGINE_SUBNET = { masternodeVotingContract: '0x0000000000000000000000000000000000000088', blockSignerContract: '0x0000000000000000000000000000000000000089', randomizeSMCBinary: '0x0000000000000000000000000000000000000090', - XDCXAddrBinary: '0x0000000000000000000000000000000000000091', + XDCXAddressBinary: '0x0000000000000000000000000000000000000091', tradingStateAddressBinary: '0x0000000000000000000000000000000000000092', XDCXLendingAddressBinary: '0x0000000000000000000000000000000000000093', XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', From 451bdd934b220c9c2270d61d6754849f0b7a99b1 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:18:39 +0700 Subject: [PATCH 37/59] fix(converter): make DEFAULT_PARAMS_SUBNET a spread, as its comment claims (D3) The comment above the table says it "is a spread rather than a second full table" and that a subnet "only has to state what it disagrees about". It was in fact ~40 restated entries, and it broke the invariant the file header states: every transition falls back to 999999999999 so a fork genesis never mentions stays off. Ten of them fell back to real blocks instead -- eip150 at 2; eip155, eip160, eip161abc, eip161d and MaxCodeSize at 3; eip140, eip211, eip214 and eip658 at 4 -- which would have switched replay protection, state clearing, the EIP-170 cap and Byzantium on while the Go nodes ran with them off. Not reachable today: a subnet genesis states eip150Block 2, eip155Block 3, eip158Block 3 and byzantiumBlock 4, and the fallbacks were a verbatim copy of those, so genesis always won. Right by transcription, with nothing keeping it right. The spread also restores the 17 keys the table omitted entirely, so both engines now emit one key set. Verified on a real subnet genesis: 17 params keys added, no value changed, genesis block and accounts byte-identical, XDPoS output untouched. Also corrects the note above the table -- a subnet runs XDC-Subnet, not the XDPoSChain binary -- and says why the table must not be restated. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 0db75e8..ab61b7a 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -261,61 +261,20 @@ const DEFAULT_PARAMS = { // supplies values only. So every key stays in step with DEFAULT_PARAMS // automatically and a subnet only has to state what it disagrees about. // -// Nothing yet: a subnet and a standalone XDPoS network are running the same -// XDPoSChain binary, so the same EIPs are gated on the same forks. Put an entry -// here the moment a subnet needs to differ, e.g. +// Nothing yet. The two run different Go clients -- a subnet runs +// XinFinOrg/XDC-Subnet (xinfinorg/xdcsubnets), a standalone network runs +// XinFinOrg/XDPoSChain (xinfinorg/devnet) -- but they gate the same EIPs on the +// same genesis forks, so no entry is needed. Put one here the moment a subnet +// needs to differ, e.g. // eip1559Transition: 0, +// Do NOT restate the whole table: the 999999999999 fallbacks are the invariant +// that keeps a fork genesis never states switched off rather than on at 0. const DEFAULT_PARAMS_SUBNET = { - eip150Transition: 2, - eip155Transition: 3, - eip160Transition: 3, - eip145Transition: 0, - eip1014Transition: 0, - eip1052Transition: 0, - eip1234Transition: 999999999999, - eip1283Transition: 0, - eip152Transition: 0, - eip1108Transition: 0, - eip1344Transition: 0, - eip1884Transition: 0, - eip2028Transition: 999999999999, - eip2200Transition: 0, - eip2565Transition: 999999999999, - eip2718Transition: 999999999999, - eip2930Transition: 999999999999, - eip1559Transition: 999999999999, - eip2929Transition: 999999999999, - eip3198Transition: 0, - eip3529Transition: 999999999999, - eip3541Transition: 999999999999, - eip3554Transition: 999999999999, - eip4399Transition: 999999999999, - eip3651Transition: 999999999999, - eip3855Transition: 0, - eip3860Transition: 999999999999, - eip6049Transition: 999999999999, - eip1153Transition: 999999999999, - eip4844Transition: 999999999999, - eip5656Transition: 999999999999, - eip6780Transition: 999999999999, - eip7516Transition: 999999999999, - eip1559ElasticityMultiplier: '0x1', - eip7Transition: 1, - eip161abcTransition: 3, - eip161dTransition: 3, - eip140Transition: 4, - eip211Transition: 4, - eip214Transition: 4, - eip658Transition: 4, - MaxCodeSizeTransition: 3, - MaxCodeSize: 24576, - // Not in the reference spec, and not emitted under this name: pickMaxCodeSize - // reads it off whichever table is in play to decide the value of MaxCodeSize. - // Without it a subnet stating osakaBlock emits no MaxCodeSize at all. - MaxCodeSizeOsaka: 32768, + ...DEFAULT_PARAMS, }; + // Engine constants for what genesis.json does not state (copied from // chainspec.json). Spread into the engine block whole, so key order here is the // emitted order and every key here is emitted. These are fallbacks only: From 65795d10e96b3834110e305faa380606ed1e6076 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:19:57 +0700 Subject: [PATCH 38/59] fix(converter): collapse the two ENGINE_GENESIS_KEYS maps into one (D4) The XDPoSSubnet map omitted TipXDCX, TIPXDCXMinerDisable and TIPXDCXReceiverDisable, with a comment calling them "not used at all" on the subnet engine. All three are inherited from XdcChainSpecEngineParameters and bound: - AddTransitions() adds each to the release-spec boundary set (XdcChainSpecEngineParameters.cs:135-142) - IsTIPXDCXMiner / IsTIPXDCXReceiver are computed from them (XdcChainSpecBasedSpecProvider.cs:80-81) With those three added the two maps are identical, so they collapse into one and the per-engine selector goes away. TipXDCXCancellationFeeBlock was covered by the D1 guard removal; it matters twice, since TRANSITION_FORKS relies on it to activate Constantinople and Istanbul on configs that never set istanbulBlock. Verified: a subnet genesis stating tipXDCXCancellationFeeBlock 5 now yields eip145Transition and eip152Transition at 5. Also aligns blackListHFNumber -> BlackListHFNumber. Collapsing the maps exposed that the two engines emitted the same property under different casing; Nethermind declares it BlackListHFNumber (XdcChainSpecEngineParameters.cs:85). Binding is case-insensitive so behaviour is unchanged, but the drift is the same class of defect as D2. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index ab61b7a..4f5435c 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -68,7 +68,7 @@ * genesis.config.tip2019Block -> engine.XDPoS.params.tip2019Block * genesis.config.dynamicGasLimitBlock -> engine.XDPoS.params.DynamicGasLimitBlock * genesis.config.tipXDCXBlock -> engine.XDPoS.params.TipXDCX - * genesis.config.denylistBlock -> engine.XDPoS.params.blackListHFNumber + * genesis.config.denylistBlock -> engine.XDPoS.params.BlackListHFNumber * genesis.config.tipTRC21FeeBlock -> engine.XDPoS.params.TipTrc21Fee * genesis.config.tipXDCXMinerDisableBlock -> engine.XDPoS.params.TIPXDCXMinerDisable * genesis.config.tipXDCXReceiverDisableBlock -> engine.XDPoS.params.TIPXDCXReceiverDisable @@ -286,7 +286,7 @@ const DEFAULT_ENGINE = { tip2019Block: 1, DynamicGasLimitBlock: 9999999999999, TipXDCX: 9999999999999, - blackListHFNumber: 9999999999999, + BlackListHFNumber: 9999999999999, TipTrc21Fee: 99999999999999, TIPXDCXMinerDisable: 9999999999999, TIPXDCXReceiverDisable: 9999999999999, @@ -528,27 +528,27 @@ const TRANSITION_FORKS = { // The second way genesis reaches an engine param: a flat genesis.config key // whose name differs from the chainspec key. (The first is chain data that keeps -// its name, which translate() merges by name.) Per engine, chainspec key -> the -// genesis.config key it comes from. The two engines bind -// different names for the same thing, so the tables are separate rather than -// one table with a rename step. A key here that is not in the matching -// DEFAULT_ENGINE* table is emitted only when genesis states it. +// its name, which translate() merges by name.) chainspec key -> the +// genesis.config key it comes from. +// +// One table for both engines: XdcSubnetChainSpecEngineParameters inherits every +// property of XdcChainSpecEngineParameters, and binding is case-insensitive, so +// there is nothing to rename. The subnet table used to omit TipXDCX, +// TIPXDCXMinerDisable and TIPXDCXReceiverDisable as "not used at all" -- all +// three are bound, AddTransitions() uses them to create release-spec +// boundaries, and IsTIPXDCXMiner / IsTIPXDCXReceiver are computed from them +// (XdcChainSpecBasedSpecProvider.cs:80-81). +// +// A key here that is not in the matching DEFAULT_ENGINE* table is emitted only +// when genesis states it. const ENGINE_GENESIS_KEYS = { - XDPoS: { - tip2019Block: 'tip2019Block', - DynamicGasLimitBlock: 'dynamicGasLimitBlock', - TipXDCX: 'tipXDCXBlock', - blackListHFNumber: 'denylistBlock', - TipTrc21Fee: 'tipTRC21FeeBlock', - TIPXDCXMinerDisable: 'tipXDCXMinerDisableBlock', - TIPXDCXReceiverDisable: 'tipXDCXReceiverDisableBlock', - }, - XDPoSSubnet: { - DynamicGasLimitBlock: 'dynamicGasLimitBlock', - tip2019Block: 'tip2019Block', - BlackListHFNumber: 'denylistBlock', - TipTrc21Fee: 'tipTRC21FeeBlock', - }, + tip2019Block: 'tip2019Block', + DynamicGasLimitBlock: 'dynamicGasLimitBlock', + TipXDCX: 'tipXDCXBlock', + BlackListHFNumber: 'denylistBlock', + TipTrc21Fee: 'tipTRC21FeeBlock', + TIPXDCXMinerDisable: 'tipXDCXMinerDisableBlock', + TIPXDCXReceiverDisable: 'tipXDCXReceiverDisableBlock', }; // XDC hardfork blocks that live in engine.XDPoS.params rather than params. @@ -651,9 +651,6 @@ function buildEngineParams(cfg, opts) { }; const engineDefaults = opts.subnet ? DEFAULT_ENGINE_SUBNET : DEFAULT_ENGINE; - const engineGenesisKeys = opts.subnet - ? ENGINE_GENESIS_KEYS.XDPoSSubnet - : ENGINE_GENESIS_KEYS.XDPoS; // The whole default table is spread in rather than restated key by key: a // value belongs to exactly one place, the DEFAULT_ENGINE* table, which also @@ -677,7 +674,7 @@ function buildEngineParams(cfg, opts) { } } // 2. flat genesis.config keys, which are named differently on each side - for (const [key, genesisKey] of Object.entries(engineGenesisKeys)) { + for (const [key, genesisKey] of Object.entries(ENGINE_GENESIS_KEYS)) { engineParams[key] = pick(cfg[genesisKey], engineDefaults[key]); } From 36669dfe995a9d1c96800a891182597b217fa396 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:21:37 +0700 Subject: [PATCH 39/59] feat(converter): reject a v2 config Nethermind would silently read as zero (D5) Nethermind binds each v2Configs entry to V2ConfigParams, whose fields are non-nullable value types with `init` accessors, and its CheckConfig only validates that a switchRound: 0 entry exists and that no round repeats (XdcChainSpecEngineParameters.cs:109-118). A field genesis omits is not unset, it is 0, and nothing downstream can tell the difference. checkV2Config() now runs per entry, after the subnet maxMasternodes default is merged in, and names every missing field. The required set is per engine, because the two Go clients declare different V2Configs: XDPoS -- XDPoSChain params/config_xdpos.go, 14 fields; a generated XDPoS genesis states all 14. Subnet -- XDC-Subnet params/config.go, 5 fields; a subnet genesis states exactly those 5. Requiring the other nine would reject every valid subnet genesis. maxMasternodes is required on top, since SubnetMasternodesCalculator reads it and the converter injects it. The nine subnet-absent fields are safe at zero, and not by luck: the subnet path reads none of them. Nethermind registers SubnetPenaltyHandler (XdcSubnetModule.cs:32), which uses the hardcoded XdcConstants.MinimumMinerBlockPerEpoch = 1 and never reads LimitPenaltyEpoch or MinimumSigningTx; XDC-Subnet hardcodes the same 1. The reward and protector/observer cap fields are read only by XdcRewardCalculator, and XDC-Subnet has no protector/observer tiers. main() now catches a translate() failure and reports it as a message and exit 1 instead of an unhandled stack trace -- this runs in a container where the trace is all an operator would see. Verified: both real genesis files still convert; a subnet genesis converted without --subnet now fails naming all nine fields, which is the forgot-the-flag case that previously produced a silently wrong chainspec. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 4f5435c..5d4d364 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -608,6 +608,59 @@ function normalizeAddress(addr) { * Builders — one per section of the chainspec * ------------------------------------------------------------------ */ +// Nethermind binds each v2Configs entry to V2ConfigParams, whose fields are +// non-nullable value types with `init` accessors: an absent key binds to 0 +// rather than erroring, and its CheckConfig only validates that a +// switchRound: 0 entry exists and that no round repeats +// (XdcChainSpecEngineParameters.cs:109-118). So a field genesis omits is not +// "unset", it is zero, and nothing downstream can tell the difference. +// +// What has to be present depends on which Go client the chainspec is paired +// with, because the two declare different V2Configs: +// +// XDPoS -- XinFinOrg/XDPoSChain params/config_xdpos.go V2Config, 14 fields. +// A generated XDPoS genesis states all 14. +// Subnet -- XinFinOrg/XDC-Subnet params/config.go V2Config, 5 fields. The +// other nine do not exist on that client at all, so requiring them +// would reject every valid subnet genesis. maxMasternodes is +// required on top of the five because Nethermind's +// SubnetMasternodesCalculator reads it (spec.MaxMasternodes) and +// the converter injects it from DEFAULT_SUBNET_EXTRAS. +// +// The nine subnet-absent fields are safe at zero, but only because nothing on +// the subnet path reads them: Nethermind registers SubnetPenaltyHandler +// (XdcSubnetModule.cs:32), which uses the hardcoded +// XdcConstants.MinimumMinerBlockPerEpoch = 1 rather than the spec value and +// never touches LimitPenaltyEpoch or MinimumSigningTx, and XDC-Subnet hardcodes +// the same 1 (common.MinimunMinerBlockPerEpoch, eth/hooks/engine_v2_hooks.go). +// The reward and protector/observer cap fields are read only by +// XdcRewardCalculator, and XDC-Subnet has no protector/observer tiers. If a +// future subnet client gains any of those fields, move it into the subnet list. +const V2_REQUIRED_FIELDS_XDPOS = [ + 'switchRound', 'maxMasternodes', 'maxProtectorNodes', 'maxObserverNodes', + 'minePeriod', 'timeoutSyncThreshold', 'timeoutPeriod', 'certificateThreshold', + 'masternodeReward', 'protectorReward', 'observerReward', + 'minimumMinerBlockPerEpoch', 'limitPenaltyEpoch', 'minimumSigningTx', +]; + +const V2_REQUIRED_FIELDS_SUBNET = [ + 'switchRound', 'maxMasternodes', 'minePeriod', 'timeoutSyncThreshold', + 'timeoutPeriod', 'certificateThreshold', +]; + +function checkV2Config(round, config, opts) { + const required = opts.subnet + ? V2_REQUIRED_FIELDS_SUBNET + : V2_REQUIRED_FIELDS_XDPOS; + const missing = required.filter((f) => config[f] === undefined); + if (missing.length) { + throw new Error( + `genesis XDPoS.v2.allConfigs[${round}] is missing ${missing.join(', ')}. ` + + 'Nethermind would read 0 for each, diverging from the Go nodes.' + ); + } +} + // engine..params.v2Configs: the per-round XDPoS V2 settings, in round // order. Every field is carried through as genesis states it, bar // expTimeoutConfig, which the chainspec schema has no key for and so is dropped @@ -617,13 +670,14 @@ function buildV2Configs(v2, opts) { .sort((a, b) => Number(a) - Number(b)) .map((round) => { const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; - if (!opts.subnet) { - return rest; - } // genesis carries no maxMasternodes on a subnet, but the working subnet // chainspec states it. Key spellings are left as genesis writes them -- // the reference capitalises them, but binding is case-insensitive. - return { maxMasternodes: DEFAULT_SUBNET_EXTRAS.maxMasternodes, ...rest }; + const config = opts.subnet + ? { maxMasternodes: DEFAULT_SUBNET_EXTRAS.maxMasternodes, ...rest } + : rest; + checkV2Config(round, config, opts); + return config; }); } @@ -861,7 +915,16 @@ function main(argv) { return 1; } - const chainspec = translate(genesis, opts); + // translate() throws on a genesis it cannot faithfully convert. Report that as + // a plain message and a non-zero exit, not an unhandled stack trace: this runs + // inside a container where the trace is all the operator would see. + let chainspec; + try { + chainspec = translate(genesis, opts); + } catch (e) { + console.error(`Error: cannot translate ${inPath}: ${e.message}`); + return 1; + } const json = JSON.stringify(chainspec, null, 2) + '\n'; try { fs.writeFileSync(outPath, json); From 121b54917bb27cb8f517df730c63cc870a28bf7f Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:21:58 +0700 Subject: [PATCH 40/59] fix(converter): fail on an absent foundation wallet instead of writing "0xundefined" (D6) normalizeAddress() returned '0x' + String(addr), so a genesis missing both spellings of the foundation wallet produced the literal foundationWalletAddr = "0xundefined", exit 0, no warning. The node then died at load with System.IO.InvalidDataException: Error when loading chainspec (hex string of odd length) which names neither the field nor the file. normalizeAddress() now passes undefined/null through, and buildEngineParams() throws naming the field. The `||` in the shared object also becomes pick(), so an address of 0x0 -- falsy as a string only if empty, but the pattern is wrong either way -- cannot fall through to the second spelling. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/libs/genesis-to-chainspec.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 5d4d364..8845288 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -601,6 +601,10 @@ function pick(...candidates) { // Lowercase, 0x-prefixed. Clients accept this fine; we don't bother with the // cosmetic EIP-55 mixed-case checksum. function normalizeAddress(addr) { + // Absent stays absent. Without this, String(undefined) makes the literal + // "0xundefined", which Nethermind rejects at load with "hex string of odd + // length" -- an error that names neither the field nor the file. + if (addr === undefined || addr === null) return undefined; return '0x' + String(addr).toLowerCase().replace(/^0x/, ''); } @@ -696,7 +700,7 @@ function buildEngineParams(cfg, opts) { rewardCheckpoint: xdpos.rewardCheckpoint, gap: xdpos.gap, // note: genesis spells the key "foudationWalletAddr" (sic) - foundationWalletAddr: normalizeAddress(xdpos.foudationWalletAddr || xdpos.foundationWalletAddr), + foundationWalletAddr: normalizeAddress(pick(xdpos.foudationWalletAddr, xdpos.foundationWalletAddr)), // genesis spells this either "switchEpoch" (newer) or "SwitchEpoch" (older). // Use ?? not || — the valid value is 0, which is falsy. switchEpoch: v2.switchEpoch ?? v2.SwitchEpoch, @@ -716,6 +720,13 @@ function buildEngineParams(cfg, opts) { // Whatever genesis states beats it, and a table entry only shows up where // genesis is silent. Genesis reaches a key two ways, hence two passes; both // re-assign keys the spread already created, so key order stays the table's. + if (shared.foundationWalletAddr === undefined) { + throw new Error( + 'genesis states no XDPoS.foudationWalletAddr (nor foundationWalletAddr); ' + + 'it has no default, and Nethermind fails to load a chainspec without it' + ); + } + const engineParams = { ...shared, ...engineDefaults }; // 1. chain data off genesis.config.XDPoS, which keeps its name. A table key of From 367e57639b51bf2d73527d68bc8b3f1cda03bf9d Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:22:14 +0700 Subject: [PATCH 41/59] fix(converter): reject a genesis whose switchBlock is not 0 (D7) Nethermind parses the genesis masternode set out of genesis.extraData only when SwitchBlock == 0; otherwise it reads the engine param genesisMasternodes, which defaults to an empty array (XdcChainSpecBasedSpecProvider.cs:101-112). The converter has no source for that key and never emitted it. Generated deployments were safe by accident, since genesis states switchBlock: 0. Set it to anything else and the node boots reporting the correct genesis hash with no masternodes and no error. buildEngineParams() now throws rather than emit a chainspec that looks right and has no validator set. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/libs/genesis-to-chainspec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 8845288..26196ea 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -720,6 +720,19 @@ function buildEngineParams(cfg, opts) { // Whatever genesis states beats it, and a table entry only shows up where // genesis is silent. Genesis reaches a key two ways, hence two passes; both // re-assign keys the spread already created, so key order stays the table's. + // Nethermind parses the genesis masternodes out of genesis.extraData only when + // SwitchBlock == 0; otherwise it reads engine params genesisMasternodes, which + // this converter has no source for and never writes + // (XdcChainSpecBasedSpecProvider.cs:101-112, defaulting to an empty array). + // The node then boots reporting the right genesis hash with no masternodes and + // no error, so this has to be caught here. + if (Number(pick(shared.switchBlock, 0)) !== 0) { + throw new Error( + `genesis states XDPoS.v2.switchBlock ${shared.switchBlock}; the converter ` + + 'has no source for genesisMasternodes, which Nethermind requires then' + ); + } + if (shared.foundationWalletAddr === undefined) { throw new Error( 'genesis states no XDPoS.foudationWalletAddr (nor foundationWalletAddr); ' + From d904f257688b46d5bc6a3f34e314c38992dcb519 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:23:48 +0700 Subject: [PATCH 42/59] fix(container-manager): surface converter warnings to the operator (D8) The Osaka warnings work, but translate() runs in-process inside the container-manager, so console.error landed in the container log while the operator's page said "Config generation success". check-chainspec.sh re-runs the converter in the foreground, which was the only way one was ever seen. translate() now takes an optional opts.warnings array and pushes into it; with no array it still writes to console.error, so the CLI and check-chainspec.sh are unchanged. generate() and generateXdpos() pass a sink and return it as a third element, and both submit templates render the warnings alongside the success message. Verified: the CLI still prints to stderr; the sink captures with the console silent; both templates render a warning list and omit the section entirely when there is none. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/express.js | 9 ++++++-- container-manager/src/libs/exec.js | 13 ++++++++---- .../src/libs/genesis-to-chainspec.js | 21 +++++++++++++------ .../src/views/generator/submit.pug | 9 ++++++++ .../src/views/xdpos_generator/submit.pug | 9 ++++++++ 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/container-manager/src/express.js b/container-manager/src/express.js index 6f7f2d1..4d32341 100644 --- a/container-manager/src/express.js +++ b/container-manager/src/express.js @@ -171,7 +171,7 @@ app.get("/gen_xdpos", (req, res) => { app.post("/submit", (req, res) => { console.log("/submit called"); - const [valid, genOut] = exec.generate(req.body); + const [valid, genOut, warnings] = exec.generate(req.body); if (!valid) { res.render("generator/submit.pug", { @@ -185,13 +185,16 @@ app.post("/submit", (req, res) => { // is still reachable on its own at /gen message: "Config generation success, please continue with the next step in the Deployment Wizard", + // the chainspec converter's warnings; without this they reach the + // container log only, and the operator sees an unqualified success + warnings, }); } }); app.post("/submit_xdpos", (req, res) => { console.log("/submit_xdpos called"); - const [valid, genOut] = exec.generateXdpos(req.body); + const [valid, genOut, warnings] = exec.generateXdpos(req.body); if (!valid) { res.render("xdpos_generator/submit.pug", { message: "failed, please try again", @@ -206,6 +209,8 @@ app.post("/submit_xdpos", (req, res) => { : "'./docker-up.sh;'"; res.render("xdpos_generator/submit.pug", { message: `Config generation success, please continue with 'cd generated;' then ${upCommand}`, + // see /submit: the converter's warnings otherwise never leave the container + warnings, }); process.exit(0); } diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 93617b0..fcab021 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -194,12 +194,15 @@ function generateXdpos(params) { //step 3: convert genesis.json -> chainspec.json. Nethermind nodes mount it, //but generate it unconditionally so it is always available alongside genesis.json. + const warnings = []; try { const { translate } = require("./genesis-to-chainspec"); const genesis = JSON.parse( fs.readFileSync(path.join(mountPath, "genesis.json"), "utf-8") ); - const chainspec = translate(genesis, {}); + // translate() writes to console.error unless given a sink, and console.error + // here lands in the container log while the operator's page says success. + const chainspec = translate(genesis, { warnings }); fs.writeFileSync( path.join(mountPath, "chainspec.json"), JSON.stringify(chainspec, null, 2) + "\n" @@ -209,7 +212,7 @@ function generateXdpos(params) { console.error("chainspec generation failed:", e.message); return [false, `chainspec generation failed: ${e.message}`]; } - return [result2, out2]; + return [result2, out2, warnings]; } function generate(params) { @@ -265,12 +268,14 @@ function generate(params) { ? params["customversion-xdpos-nethermind-count"] : 0) || 0 ); + const warnings = []; try { const { translate } = require("./genesis-to-chainspec"); const genesis = JSON.parse( fs.readFileSync(path.join(mountPath, "genesis.json"), "utf-8") ); - const chainspec = translate(genesis, { subnet: true }); + // see the note on the XDPoS path: warnings need a sink to reach the operator + const chainspec = translate(genesis, { subnet: true, warnings }); fs.writeFileSync( path.join(mountPath, "chainspec.json"), JSON.stringify(chainspec, null, 2) + "\n" @@ -282,7 +287,7 @@ function generate(params) { return [false, `chainspec generation failed: ${e.message}`]; } } - return [result2, out2]; + return [result2, out2, warnings]; } function callExec(command, timeout = 300000) { diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 26196ea..f96842a 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -773,7 +773,7 @@ function buildEngineParams(cfg, opts) { } // params: the chain rules, which here is almost entirely EIP transitions. -function buildParams(cfg, opts) { +function buildParams(cfg, opts, warn) { const paramDefaults = opts.subnet ? DEFAULT_PARAMS_SUBNET : DEFAULT_PARAMS; const params = {}; @@ -795,7 +795,7 @@ function buildParams(cfg, opts) { // no genesis counterpart params.eip1559ElasticityMultiplier = paramDefaults.eip1559ElasticityMultiplier; - params.MaxCodeSize = pickMaxCodeSize(cfg, params.MaxCodeSizeTransition, paramDefaults); + params.MaxCodeSize = pickMaxCodeSize(cfg, params.MaxCodeSizeTransition, paramDefaults, warn); return params; } @@ -810,14 +810,14 @@ function buildParams(cfg, opts) { // between two. It still lands exactly right in the case that matters for a new // subnet: when osakaBlock is at or before the limit's own start, 32768 is the // only limit the chain ever has, so that is the value to emit. -function pickMaxCodeSize(cfg, maxCodeSizeTransition, paramDefaults) { +function pickMaxCodeSize(cfg, maxCodeSizeTransition, paramDefaults, warn) { const osakaBlock = cfg.osakaBlock; if (osakaBlock === undefined || osakaBlock === null) { return paramDefaults.MaxCodeSize; } // maxCodeSize is the one piece of Osaka a chainspec can carry at all. - console.error( + warn( `Warning: genesis states osakaBlock ${osakaBlock}, but Nethermind's XDC ` + 'build declares every Osaka EIP as eipNNNNTransitionTimestamp with no ' + 'block-numbered form, so 7823, 7825, 7883, 7934 and 7939 cannot be turned ' + @@ -833,7 +833,7 @@ function pickMaxCodeSize(cfg, maxCodeSizeTransition, paramDefaults) { // Two limits, one slot. 24576 is kept because it is right for the stretch from // eip158Block to osakaBlock; from osakaBlock on, Nethermind is stricter than // the Go nodes and rejects a deploy they accept. - console.error( + warn( `Warning: genesis states osakaBlock ${osakaBlock}, later than ` + `maxCodeSizeTransition ${maxCodeSizeTransition}. XDPoSChain raises ` + 'its code limit to 32768 there; the chainspec holds one limit only, so it ' + @@ -867,6 +867,15 @@ function buildGenesisBlock(genesis, opts) { function translate(genesis, opts = {}) { const cfg = genesis.config || {}; + // Warnings go to opts.warnings when the caller supplies an array, else to + // console.error. The CLI leaves it unset and so keeps writing to stderr, which + // is what check-chainspec.sh surfaces; the container-manager passes an array, + // because in-process console.error only reaches the container log while the + // operator's page says success. + const warn = Array.isArray(opts.warnings) + ? (message) => opts.warnings.push(message) + : (message) => console.error(message); + // Subnet nodes run a different consensus plugin than a standalone XDPoS // network, and Nethermind selects it by this key. The mapping notes above // spell it "engine.XDPoS" throughout; with opts.subnet the whole block is @@ -876,7 +885,7 @@ function translate(genesis, opts = {}) { return { name: opts.name || DEFAULT_CHAIN_NAME, engine: { [engineName]: { params: buildEngineParams(cfg, opts) } }, - params: buildParams(cfg, opts), + params: buildParams(cfg, opts, warn), genesis: buildGenesisBlock(genesis, opts), nodes: opts.nodes || [], accounts: genesis.alloc || {}, diff --git a/container-manager/src/views/generator/submit.pug b/container-manager/src/views/generator/submit.pug index dcbbc6c..80b2167 100644 --- a/container-manager/src/views/generator/submit.pug +++ b/container-manager/src/views/generator/submit.pug @@ -7,6 +7,15 @@ html body h1=message h1=error + if warnings && warnings.length + //- chainspec converter warnings. The config generated, so this is not a + //- failure page, but these say where Nethermind and the Go nodes will not + //- agree and the operator has to see them before booting. + .chainspec-warnings + h2 Chainspec warnings + ul + each warning in warnings + li=warning if failed //- only on failure: the server is still running, so going back lets the //- user fix the form (history.back keeps the values already filled in). diff --git a/container-manager/src/views/xdpos_generator/submit.pug b/container-manager/src/views/xdpos_generator/submit.pug index dcbbc6c..80b2167 100644 --- a/container-manager/src/views/xdpos_generator/submit.pug +++ b/container-manager/src/views/xdpos_generator/submit.pug @@ -7,6 +7,15 @@ html body h1=message h1=error + if warnings && warnings.length + //- chainspec converter warnings. The config generated, so this is not a + //- failure page, but these say where Nethermind and the Go nodes will not + //- agree and the operator has to see them before booting. + .chainspec-warnings + h2 Chainspec warnings + ul + each warning in warnings + li=warning if failed //- only on failure: the server is still running, so going back lets the //- user fix the form (history.back keeps the values already filled in). From 3a91c1b8e0b5cf4a12edd1da89b1456a43ac1477 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:24:39 +0700 Subject: [PATCH 43/59] fix(deploy): record CHAINSPEC_ENGINE for XDPoS, and let the record win (D9) check-chainspec.sh documents that the engine "is recorded in gen.env when the deployment is generated". Only the Subnet path wrote it. The check happened to be right anyway, because no flag is the XDPoS default -- the safety was the default, not the record. That left a live failure mode: the script read the environment first, so $ CHAINSPEC_ENGINE=XDPoSSubnet ./scripts/check-chainspec.sh MISMATCH: chainspec.json does not match genesis.json # exit 3 on a working XDPoS network, and without --dry-run it archives the correct chainspec and replaces it with a subnet one -- engine renamed, fork keys stripped. An exported value left over from another deployment in the same shell is enough. Two changes: genGenXdposEnv() now records CHAINSPEC_ENGINE=XDPoS, and the script prefers the recorded value over the environment, saying so when they disagree. The environment is still the fallback for a deployment generated before this, whose gen.env records nothing. Verified all five cases: XDPoS and Subnet deployments each resolve correctly with no env, with a stray opposing env (ignored, with a note), and a legacy gen.env with no record still honours the environment. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/gen/scripts/check-chainspec.sh | 18 ++++++++++++++++-- container-manager/src/libs/exec.js | 8 ++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index e030fc5..3a18e34 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -33,8 +33,22 @@ fi # network names it XDPoS. The converter has to be told which, or the engine # block alone reads as a difference and the check would archive and overwrite a # perfectly good chainspec. Recorded in gen.env when the deployment is generated. -if [[ -z "$CHAINSPEC_ENGINE" && -f gen.env ]]; then - CHAINSPEC_ENGINE=$(grep -E '^CHAINSPEC_ENGINE=' gen.env | tail -1 | cut -d '=' -f 2-) +# +# The record wins over the environment. This deployment's own gen.env is the +# authority on what it is; an exported CHAINSPEC_ENGINE is almost always a +# leftover from another deployment in the same shell, and letting it win means a +# stray value silently rewrites a correct chainspec as the other engine's. +# Export CHAINSPEC_ENGINE only for a deployment whose gen.env predates this and +# records nothing. +if [[ -f gen.env ]]; then + recorded_engine=$(grep -E '^CHAINSPEC_ENGINE=' gen.env | tail -1 | cut -d '=' -f 2-) + if [[ -n "$recorded_engine" ]]; then + if [[ -n "$CHAINSPEC_ENGINE" && "$CHAINSPEC_ENGINE" != "$recorded_engine" ]]; then + echo "Note: ignoring CHAINSPEC_ENGINE=$CHAINSPEC_ENGINE from the environment;" \ + "gen.env records $recorded_engine for this deployment." + fi + CHAINSPEC_ENGINE="$recorded_engine" + fi fi engine_flag=() if [[ "$CHAINSPEC_ENGINE" == "XDPoSSubnet" ]]; then diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index fcab021..83dff80 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -516,8 +516,12 @@ function genGenXdposEnv(input){ content_version += `\nVERSION_NETHERMIND_IMAGE=${input["customversion-xdpos-nethermind-version"]}`; } content_version += `\nGENERATOR_IMAGE_VERSION=${getGeneratorImage()}`; - - + // scripts/check-chainspec.sh reads this back to decide whether to pass + // --subnet. Recorded explicitly rather than relying on "no value means XDPoS": + // the check reads the environment first, so an exported CHAINSPEC_ENGINE left + // over from a subnet deployment would otherwise make it archive this + // network's correct chainspec and replace it with a subnet one. + content_version += `\nCHAINSPEC_ENGINE=XDPoS`; let content_rewards = ""; if ("customrewards-checkbox" in input && input["customrewards-checkbox"] != "") { From e2f74c80b6553d50f3f57fbb110e7202fdf38153 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:25:07 +0700 Subject: [PATCH 44/59] fix(deploy): set the discovery-side IP filter and align with upstream XDC (D10) The per-node env file set NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP. Nethermind's own XDC config sets FilterDiscoveryNodesByRecentIp instead (Nethermind.Runner/configs/xdc.json). Both properties exist on NetworkConfig and both default to true, so the discovery-side filter stayed on regardless -- the setting was not doing what it looked like it did. Upstream also carries DiscoveryVersion V4, EnableEnrDiscovery false and DisableDiscV4DnsFeeder true, none of which were set here. Both flags plus the three discovery settings now live in xdc-nmc.json and xdc-nmc-subnet.json, and the per-node env var is dropped from gen_xdpos.js and gen_env.js -- it is a shared setting, not a per-node one. (The subnet path was setting it to true, i.e. the default, so it had no effect at all.) Alignment work rather than a bug fix: a 3-node run converged on discv4 anyway. TargetBlockGasLimit values are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/gen_env.js | 1 - container-manager/src/gen/gen_xdpos.js | 1 - container-manager/src/gen/scripts/xdc-nmc-subnet.json | 9 +++++++++ container-manager/src/gen/scripts/xdc-nmc.json | 9 +++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/container-manager/src/gen/gen_env.js b/container-manager/src/gen/gen_env.js index af10f91..bf378cb 100644 --- a/container-manager/src/gen/gen_env.js +++ b/container-manager/src/gen/gen_env.js @@ -62,7 +62,6 @@ NETHERMIND_JSONRPCCONFIG_PORT=${rpcport} NETHERMIND_NETWORKCONFIG_P2PPORT=${port} NETHERMIND_NETWORKCONFIG_DISCOVERYPORT=${port} NETHERMIND_NETWORKCONFIG_EXTERNALIP=${ip} -NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP=true NETHERMIND_NETWORKCONFIG_BOOTNODES=${bootnodeEnode(ip_record)} NETHERMIND_INITCONFIG_DISCOVERYENABLED=true NETHERMIND_MININGCONFIG_ENABLED=true diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index a1fb23a..07a4030 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -396,7 +396,6 @@ 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 diff --git a/container-manager/src/gen/scripts/xdc-nmc-subnet.json b/container-manager/src/gen/scripts/xdc-nmc-subnet.json index e5672d1..76a56ad 100644 --- a/container-manager/src/gen/scripts/xdc-nmc-subnet.json +++ b/container-manager/src/gen/scripts/xdc-nmc-subnet.json @@ -7,6 +7,15 @@ "StateDbKeyScheme": "Hash", "AutoDump": "All" }, + "Network": { + "FilterPeersByRecentIp": false, + "FilterDiscoveryNodesByRecentIp": false, + "EnableEnrDiscovery": false, + "DisableDiscV4DnsFeeder": true + }, + "Discovery": { + "DiscoveryVersion": "V4" + }, "TxPool": { "BlobsSupport": "Disabled" }, diff --git a/container-manager/src/gen/scripts/xdc-nmc.json b/container-manager/src/gen/scripts/xdc-nmc.json index 50811ff..06a6178 100644 --- a/container-manager/src/gen/scripts/xdc-nmc.json +++ b/container-manager/src/gen/scripts/xdc-nmc.json @@ -7,6 +7,15 @@ "StateDbKeyScheme": "Hash", "AutoDump": "All" }, + "Network": { + "FilterPeersByRecentIp": false, + "FilterDiscoveryNodesByRecentIp": false, + "EnableEnrDiscovery": false, + "DisableDiscV4DnsFeeder": true + }, + "Discovery": { + "DiscoveryVersion": "V4" + }, "TxPool": { "BlobsSupport": "Disabled" }, From 52a4dfa8bafa48c9ee4a771ecf03b504b2835359 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:25:18 +0700 Subject: [PATCH 45/59] fix(deploy): ship Nethermind nodes at --log=info, not debug (D11) Every generated Nethermind node ran at debug: tens of thousands of lines in twenty minutes, for a node that is working correctly. Applied to both generators. The review named only gen_xdpos.js, but gen_compose.js:53 carried the same flag for subnet Nethermind nodes. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/gen_compose.js | 2 +- container-manager/src/gen/gen_xdpos.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/container-manager/src/gen/gen_compose.js b/container-manager/src/gen/gen_compose.js index d93683e..80810b7 100644 --- a/container-manager/src/gen/gen_compose.js +++ b/container-manager/src/gen/gen_compose.js @@ -50,7 +50,7 @@ function genSubnetNodes(machine_id, num, start_num = 1) { command: [ "--config=/work/xdc-nmc.json", "--datadir=/work/xdcchain", - "--log=debug", + "--log=info", ], profiles: [compose_profile], ports: port_mappings, diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 07a4030..8ac98b5 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -444,7 +444,7 @@ function genXdposCompose(machine_id, num, start_num = 1) { command: [ "--config=/work/xdc-nmc.json", "--datadir=/work/xdcchain", - "--log=debug", + "--log=info", ], profiles: [compose_profile], ports: port_mappings, From 54aa36a8fe5b1e9ae9bfc980184093764cbcdb1c Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:27:44 +0700 Subject: [PATCH 46/59] fix(converter): stop emitting 26 keys nothing binds; carry the genesis header properly (D12) The file header already argued for dropping homesteadBlock, byzantiumBlock and eip158Transition because Nethermind binds none of them. The same test, run against Nethermind.Specs and Nethermind.Xdc at master-4e36ba0, fails for 26 more keys. engine (2): period, rewardCheckpoint. Neither exists on XdcChainSpecEngineParameters. The mine period comes from v2Configs[].minePeriod, which is bound. genesis (5): nonce, mixHash, coinbase, number, gasUsed. ChainSpecGenesisJson declares none of them -- ChainSpecLoader.cs:356-357 reads the nonce and mixHash from Genesis.Seal.Ethereum, and :369 reads the beneficiary from Genesis.Author. These are now emitted as seal.ethereum and author. number and gasUsed are dropped: genesis is always block 0 with no gas used, and the loader hardcodes the number. params (19): six with no ChainSpecParamsJson property in any form (eip1234, eip2718, eip3554, eip4399, eip6049, eip7516) and thirteen declared only as eipNNNNTransitionTimestamp, which a block-numbered value cannot bind. Audited by matching every emitted key against the binding class rather than by hand; the other 42 are genuinely bound. A comment records the finding so they are not re-added. The stated rationale for emitting them -- "impossible to switch on for Nethermind alone by accident" -- does not survive nothing reading them. eip1234Transition is the illustration: IsEip1234Enabled derives from the Constantinople block, i.e. eip145Transition, which this converter defaults to 0, so it was on from block 0 regardless of the 999999999999 beside it. The genesis five had teeth. It cost nothing so far only because a generated genesis has all five at their defaults -- which is why the hashes matched -- but a non-zero coinbase, nonce or mixHash would have silently given Nethermind a different genesis block. Genesis hash re-verified: the emitted nonce (0), mixHash (zero) and author (zero address) are exactly the values ChainSpecLoader was already defaulting to, so the header is byte-identical and existing node data directories stay valid. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 116 +++++++----------- 1 file changed, 47 insertions(+), 69 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index f96842a..5e6584f 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -73,8 +73,11 @@ * genesis.config.tipXDCXMinerDisableBlock -> engine.XDPoS.params.TIPXDCXMinerDisable * genesis.config.tipXDCXReceiverDisableBlock -> engine.XDPoS.params.TIPXDCXReceiverDisable * - * genesis.{nonce,timestamp,extraData,gasLimit,difficulty,mixHash, - * coinbase,number,gasUsed,parentHash} -> genesis.* (verbatim) + * genesis.{timestamp,extraData,gasLimit,difficulty,parentHash} + * -> genesis.* (verbatim) + * genesis.nonce, genesis.mixHash -> genesis.seal.ethereum.* + * genesis.coinbase -> genesis.author + * genesis.number, genesis.gasUsed -> dropped (no counterpart) * genesis.baseFeePerGas (null) -> genesis.baseFeePerGas (DEFAULT_BASE_FEE_PER_GAS) * no flag overrides this: XDPoSChain takes the same two steps in * core/genesis.go (g.BaseFee, else params.InitialBaseFee), and a third @@ -189,7 +192,6 @@ const DEFAULT_PARAMS = { eip145Transition: 0, eip1014Transition: 0, eip1052Transition: 0, - eip1234Transition: 999999999999, eip1283Transition: 0, // Istanbul @@ -202,7 +204,6 @@ const DEFAULT_PARAMS = { // Berlin eip2565Transition: 999999999999, - eip2718Transition: 999999999999, eip2929Transition: 999999999999, eip2930Transition: 999999999999, @@ -212,44 +213,25 @@ const DEFAULT_PARAMS = { eip3198Transition: 0, eip3529Transition: 999999999999, eip3541Transition: 999999999999, - eip3554Transition: 999999999999, - - // Merge - eip4399Transition: 999999999999, // Shanghai eip3651Transition: 999999999999, eip3855Transition: 0, eip3860Transition: 999999999999, - eip6049Transition: 999999999999, - eip4895Transition: 999999999999, // Cancun eip1153Transition: 999999999999, - eip4788Transition: 999999999999, eip4844Transition: 999999999999, eip5656Transition: 999999999999, eip6780Transition: 999999999999, - eip7516Transition: 999999999999, // Prague eip2537Transition: 999999999999, eip2935Transition: 999999999999, - eip6110Transition: 999999999999, - eip7002Transition: 999999999999, - eip7251Transition: 999999999999, eip7702Transition: 999999999999, eip7623Transition: 999999999999, // Osaka - eip7594Transition: 999999999999, - eip7823Transition: 999999999999, - eip7825Transition: 999999999999, - eip7883Transition: 999999999999, - eip7918Transition: 999999999999, - eip7934Transition: 999999999999, - eip7939Transition: 999999999999, - eip7951Transition: 999999999999, }; @@ -423,7 +405,6 @@ const TRANSITION_FORKS = { eip1052Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], // block reward cut and difficulty-bomb delay: XDPoS pays its own rewards // (eth/hooks/engine_v2_hooks.go) and has no bomb to postpone - eip1234Transition: null, // net metered SSTORE, live only while IsConstantinople && !IsPetersburg // (core/vm/gas_table.go, gasSStore). Moot once enable2200 replaces SSTORE's // dynamic gas outright, which is why the reference spec can leave it on. @@ -448,7 +429,6 @@ const TRANSITION_FORKS = { eip2565Transition: 'eip1559Block', // 2718: same txpool gate as 2930 below -- every non-legacy tx type is // rejected while !rules.IsEIP1559 - eip2718Transition: 'eip1559Block', // 2929 + 2930: the access list is built in StateDB.Prepare, whose whole body // is behind `if rules.IsEIP1559` (core/state/statedb.go), and txpool rejects // every non-legacy tx type until IsEIP1559 (core/txpool/validation.go) @@ -461,10 +441,6 @@ const TRANSITION_FORKS = { eip3198Transition: 'londonBlock', eip3529Transition: 'eip1559Block', // enable3529, newEip1559InstructionSet eip3541Transition: 'eip1559Block', // core/vm/evm.go: ret[0]==0xEF && IsEIP1559 - eip3554Transition: null, // another bomb delay, nothing to delay - - // Merge - eip4399Transition: 'mergeBlock', // PREVRANDAO, newMergeInstructionSet // Shanghai. Only PUSH0 is really gated on shanghaiBlock: enable3855 is the // sole entry in newShanghaiInstructionSet. @@ -473,22 +449,12 @@ const TRANSITION_FORKS = { eip3855Transition: 'shanghaiBlock', // 3860: IntrinsicGas takes rules.IsEIP1559 as its isEIP3860 argument eip3860Transition: 'eip1559Block', - // announces SELFDESTRUCT's deprecation and changes no rule; nothing in - // XDPoSChain reads it - eip6049Transition: null, - // withdrawals: XDPoSChain's header type has no withdrawalsRoot, so Nethermind - // must not start expecting one - eip4895Transition: null, // Cancun eip1153Transition: 'cancunBlock', - // beacon-roots system call at the top of every block; XDPoSChain makes no - // such call and carries no parentBeaconBlockRoot - eip4788Transition: null, eip4844Transition: 'cancunBlock', eip5656Transition: 'cancunBlock', eip6780Transition: 'cancunBlock', - eip7516Transition: 'cancunBlock', // BLOBBASEFEE, enable7516 in the Cancun set // Prague. XDPoSChain gates three EIPs on IsPrague: 2935 (history contract), // 7623 (calldata floor cost) and 7702 (setcode tx, the only opcode-level @@ -497,35 +463,39 @@ const TRANSITION_FORKS = { // no Prague case, it falls through to the // EIP-1559 set eip2935Transition: 'pragueBlock', - eip6110Transition: null, // beacon-chain EIPs; each would also put an - eip7002Transition: null, // EIP-7685 requestsHash in the block header - eip7251Transition: null, + // (6110/7002/7251, the beacon-chain EIPs, are not + // emitted at all -- see the note below) eip7702Transition: 'pragueBlock', eip7623Transition: 'pragueBlock', - // Osaka is unreachable from a chainspec, so the whole fork is null. XDPoSChain - // does gate five of these on IsOsaka -- 7823 and 7883 (modexp input cap and - // repricing, in PrecompiledContractsOsaka), 7825 (tx gas cap, params.MaxTxGas), - // 7934 (RLP block size cap, params.MaxBlockSize) and 7939 (CLZ opcode: - // newOsakaInstructionSet is Prague plus enable7939) -- but Nethermind's XDC - // build declares EVERY Osaka EIP as eipNNNNTransitionTimestamp only, with no - // block-numbered eipNNNNTransition property to bind (verified against - // Nethermind.Specs.dll in nethermindeth/nethermind:xdc-fixes). A block value - // under those names is an unknown JSON member and is silently ignored, so - // emitting one would read as "Osaka is on" while Nethermind ran without it. - // See the osakaBlock warning in translate(): no chainspec can follow a Go node + // Osaka is unreachable from a chainspec, so no Osaka key is emitted at all. + // XDPoSChain does gate five of them on IsOsaka -- 7823 and 7883 (modexp input + // cap and repricing, in PrecompiledContractsOsaka), 7825 (tx gas cap, + // params.MaxTxGas), 7934 (RLP block size cap, params.MaxBlockSize) and 7939 + // (CLZ opcode: newOsakaInstructionSet is Prague plus enable7939) -- but + // Nethermind's XDC build declares EVERY Osaka EIP as + // eipNNNNTransitionTimestamp only, with no block-numbered form to bind. See + // the osakaBlock warning in translate(): no chainspec can follow a Go node // into Osaka, EIP-7907's 32768-byte code limit included. - eip7594Transition: null, // also not implemented: PeerDAS, no blobs here - eip7823Transition: null, - eip7825Transition: null, - eip7883Transition: null, - eip7918Transition: null, // also not implemented: no blob header fields - eip7934Transition: null, - eip7939Transition: null, - eip7951Transition: null, // also not implemented: P256VERIFY is not in - // PrecompiledContractsOsaka + }; +// Nineteen EIP transitions are deliberately NOT emitted, because Nethermind +// binds no such key and a value under a name nothing reads is worse than no key +// at all -- it reads as a decision that was never taken. Verified against +// Nethermind.Specs ChainSpecParamsJson at master-4e36ba0: +// +// no property in any form (6): eip1234, eip2718, eip3554, eip4399, eip6049, +// eip7516. eip1234 is the illustration -- IsEip1234Enabled derives from the +// Constantinople block, i.e. eip145Transition, which this converter defaults +// to 0, so it is on from block 0 no matter what sat beside it. +// declared only as eipNNNNTransitionTimestamp (13): eip4788, eip4895, eip6110, +// eip7002, eip7251, eip7594, eip7823, eip7825, eip7883, eip7918, eip7934, +// eip7939, eip7951. A block-numbered value under those names is an unknown +// JSON member and is silently ignored. +// +// Before adding one back, check it is a real ChainSpecParamsJson property. + // The second way genesis reaches an engine param: a flat genesis.config key // whose name differs from the chainspec key. (The first is chain data that keeps // its name, which translate() merges by name.) chainspec key -> the @@ -693,11 +663,12 @@ function buildEngineParams(cfg, opts) { const v2 = xdpos.v2 || {}; // chain data, read the same way for both engines + // period and rewardCheckpoint are NOT carried: neither exists on + // XdcChainSpecEngineParameters, so both were dead keys. The mine period comes + // from v2Configs[].minePeriod, which is bound. const shared = { - period: xdpos.period, epoch: xdpos.epoch, reward: xdpos.reward, - rewardCheckpoint: xdpos.rewardCheckpoint, gap: xdpos.gap, // note: genesis spells the key "foudationWalletAddr" (sic) foundationWalletAddr: normalizeAddress(pick(xdpos.foudationWalletAddr, xdpos.foundationWalletAddr)), @@ -846,17 +817,24 @@ function pickMaxCodeSize(cfg, maxCodeSizeTransition, paramDefaults, warn) { // base fee, which genesis.json leaves null. function buildGenesisBlock(genesis, opts) { return { - nonce: genesis.nonce, + // Nethermind reads the nonce and mixHash out of seal.ethereum, not off the + // genesis object: ChainSpecLoader.cs:356-357 takes + // Genesis.Seal?.Ethereum?.Nonce ?? 0 and ?.MixHash ?? Keccak.Zero, and + // ChainSpecGenesisJson declares neither at the top level. Emitting them + // there meant the node silently used 0 and Keccak.Zero instead. + seal: { ethereum: { nonce: genesis.nonce, mixHash: genesis.mixHash } }, + // likewise the beneficiary: ChainSpecLoader.cs:369 reads Genesis.Author, + // and there is no "coinbase" on ChainSpecGenesisJson + author: normalizeAddress(genesis.coinbase), timestamp: genesis.timestamp, extraData: genesis.extraData, gasLimit: genesis.gasLimit, difficulty: genesis.difficulty, - mixHash: genesis.mixHash, - coinbase: genesis.coinbase, - number: genesis.number, - gasUsed: genesis.gasUsed, parentHash: genesis.parentHash, baseFeePerGas: pick(genesis.baseFeePerGas, DEFAULT_BASE_FEE_PER_GAS), + // number and gasUsed have no chainspec equivalent and are not emitted: + // genesis is always block 0 with no gas used, and ChainSpecLoader hardcodes + // the number to 0. }; } From 9aa39b5576990f38655954b88e3d4b6e4ec60bad Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:28:32 +0700 Subject: [PATCH 47/59] fix(deploy): delete dead network_mode, and stop discarding the network name (D13) Two unrelated bits of the same scaffolding. network_mode: "host" was set on every Go node in gen_xdpos.js and gen_compose.js, and injectNetworkConfig deleted it again from every service before writing the compose file, assigning a bridge IP instead. It never reached a generated deployment. Removed at the source, along with the now-redundant delete, so the next reader does not reason from it. exec.js passed no --name to translate(), so every chainspec was called "xdpos-chain" while gen.env recorded the operator's NETWORK_NAME. Both generate paths now pass it, and check-chainspec.sh reads NETWORK_NAME back out of gen.env. Both halves have to land together: verified that a chainspec generated with --name my-subnet passes the check when the name is read back, and reports a one-line name mismatch (exit 3, and a rewrite without --dry-run) when it is not. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/gen_compose.js | 4 ++-- container-manager/src/gen/gen_xdpos.js | 1 - .../src/gen/scripts/check-chainspec.sh | 14 +++++++++++++- container-manager/src/libs/exec.js | 13 +++++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/container-manager/src/gen/gen_compose.js b/container-manager/src/gen/gen_compose.js index 80810b7..7cd3733 100644 --- a/container-manager/src/gen/gen_compose.js +++ b/container-manager/src/gen/gen_compose.js @@ -63,7 +63,6 @@ function genSubnetNodes(machine_id, num, start_num = 1) { `xinfinorg/xdcsubnets:${config.version.subnet}`, volumes: [volume, "${HOSTPWD}/genesis.json:/work/genesis.json"], restart: "always", - network_mode: "host", env_file: [config_path], profiles: [compose_profile], ports: port_mappings, @@ -206,8 +205,9 @@ function injectNetworkConfig(compose_object) { ipv4_address: component_ip, }, }; + // every service joins the generated bridge network and gets a fixed IP on + // it; no service sets network_mode, which would be incompatible with that compose_object["services"][key]["networks"] = component_network; - delete compose_object["services"][key]["network_mode"]; record_services_ip[key] = component_ip; }); diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 8ac98b5..2715c9f 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -459,7 +459,6 @@ function genXdposCompose(machine_id, num, start_num = 1) { "./bootnodes.list:/work/bootnodes.list", ], restart: "always", - network_mode: "host", env_file: [`masternode${i}.env`], profiles: [compose_profile], ports: port_mappings, diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index 3a18e34..abb018e 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -55,6 +55,18 @@ if [[ "$CHAINSPEC_ENGINE" == "XDPoSSubnet" ]]; then engine_flag=(--subnet) fi +# The chainspec carries the operator's network name, which the container-manager +# passes to the converter and gen.env records. Read it back, or the re-run would +# translate to the converter's default name, see that as a difference and +# archive and rewrite every chainspec over nothing but the name. +name_flag=() +if [[ -z "$NETWORK_NAME" && -f gen.env ]]; then + NETWORK_NAME=$(grep -E '^NETWORK_NAME=' gen.env | tail -1 | cut -d '=' -f 2-) +fi +if [[ -n "$NETWORK_NAME" ]]; then + name_flag=(--name "$NETWORK_NAME") +fi + genesis="genesis.json" chainspec="chainspec.json" if [[ -n "$1" && "$1" != -* ]]; then @@ -83,7 +95,7 @@ docker pull "$GENERATOR_IMAGE_VERSION" # -u keeps the archived and rewritten files owned by the invoking user docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/data" -w /data "$GENERATOR_IMAGE_VERSION" \ - node /app/libs/check-chainspec.js "$genesis" "$chainspec" "${engine_flag[@]}" "$@" + node /app/libs/check-chainspec.js "$genesis" "$chainspec" "${engine_flag[@]}" "${name_flag[@]}" "$@" result=$? # only 0, 2 and 3 come from the check itself; anything else is docker or node diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index 83dff80..baab955 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -202,7 +202,12 @@ function generateXdpos(params) { ); // translate() writes to console.error unless given a sink, and console.error // here lands in the container log while the operator's page says success. - const chainspec = translate(genesis, { warnings }); + // gen.env records NETWORK_NAME; without passing it here every chainspec + // was called by the converter's default, "xdpos-chain". + const chainspec = translate(genesis, { + name: params["text-subnet-name"], + warnings, + }); fs.writeFileSync( path.join(mountPath, "chainspec.json"), JSON.stringify(chainspec, null, 2) + "\n" @@ -275,7 +280,11 @@ function generate(params) { fs.readFileSync(path.join(mountPath, "genesis.json"), "utf-8") ); // see the note on the XDPoS path: warnings need a sink to reach the operator - const chainspec = translate(genesis, { subnet: true, warnings }); + const chainspec = translate(genesis, { + subnet: true, + name: params["text-subnet-name"], + warnings, + }); fs.writeFileSync( path.join(mountPath, "chainspec.json"), JSON.stringify(chainspec, null, 2) + "\n" From b5c7b2fadef49ad0e2c250ff5c77dba871b5a063 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:29:18 +0700 Subject: [PATCH 48/59] docs(converter): record why the subnet table sets TipTrc21Fee and tip2019Block to 1 (D14) These two are the only entries in DEFAULT_ENGINE_SUBNET that switch a fork ON where genesis states nothing, and the only justification was "match a working Nethermind+Go subnet deployment". TipTrc21Fee is read by the transaction-execution path, so that was the weakest claim in the file. Both values are correct. Checked against XinFinOrg/XDC-Subnet, the client a subnet actually runs: tip2019Block: 1 -- Nethermind: IsTIP2019 = TIP2019Block <= releaseStartBlock, on from block 1. XDC-Subnet hardcodes common.TIP2019Block = 1, isForked() from block 1. Agree. TipTrc21Fee: 1 -- Nethermind: (TipTrc21Fee ?? MaxValue) <= releaseStartBlock, on from block 1. XDC-Subnet hardcodes common.TIPTRC21Fee = 0 but tests it strictly, `BlockNumber.Cmp(...) > 0` (core/state_transition.go:271), so live from block 1 as well. Agree. No value changes. The comment now carries the reason for each, and warns against "correcting" the 1 to 0 to match the Go constant -- the Go test is strict, so 0 would enable it a block early. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 5e6584f..c2491e3 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -290,6 +290,23 @@ const DEFAULT_ENGINE = { // silently ignored and the engine's own default applies, which is how a // chainspec can look correct and still diverge from the Go nodes. // +// Most entries here switch a fork off. The two that switch one ON were checked +// against XinFinOrg/XDC-Subnet, the client a subnet actually runs, rather than +// copied from a deployment that happened to work: +// +// tip2019Block: 1 -- Nethermind computes IsTIP2019 = TIP2019Block <= +// releaseStartBlock (XdcChainSpecBasedSpecProvider.cs:79), so on from block +// 1. XDC-Subnet hardcodes common.TIP2019Block = 1 and tests isForked() +// against it, so also from block 1. They agree. +// TipTrc21Fee: 1 -- Nethermind computes IsTipTrc21FeeEnabled = +// (TipTrc21Fee ?? ulong.MaxValue) <= releaseStartBlock (:77), so on from +// block 1. XDC-Subnet hardcodes common.TIPTRC21Fee = 0 but tests it +// STRICTLY -- core/state_transition.go:271 is +// `if st.evm.BlockNumber.Cmp(common.TIPTRC21Fee) > 0` -- so it is live from +// block 1 too. They agree. This one is read by the transaction-execution +// path (XdcTransactionProcessor), so do not "correct" the 1 to 0 to match +// the Go constant: that would enable it a block early. +// // Key order is the emitted order: translate() spreads this table into the // engine block whole, so a key is added, removed or moved here and nowhere // else. Fallbacks only, as with DEFAULT_ENGINE -- genesis always wins. From 3460d3841fd809f52a339e9a012fb90844b4f020 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:32:24 +0700 Subject: [PATCH 49/59] fix(deploy): do not pass a literal "undefined" network name to the check Follow-up to D13, which introduced this. gen.env is written as NETWORK_NAME=${input["text-subnet-name"]}, so an input without that field -- /submit_preconfig posts req.body straight through -- records the literal string "undefined". translate() meanwhile saw JS undefined and fell back to DEFAULT_CHAIN_NAME, so the chainspec is called "xdpos-chain". Now that check-chainspec.sh reads NETWORK_NAME back, those two disagree: the check re-translates with --name undefined, sees a one-line name difference, and archives and rewrites a correct chainspec on every run. That is precisely the failure the --name plumbing exists to prevent. The script now ignores an empty or "undefined" value, which also covers deployments already generated with one recorded, and exec.js writes an empty value rather than "undefined" going forward. Verified: "undefined" and "" yield no flag; a real name and a name containing spaces are both passed as a single argument. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/scripts/check-chainspec.sh | 7 ++++++- container-manager/src/libs/exec.js | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/container-manager/src/gen/scripts/check-chainspec.sh b/container-manager/src/gen/scripts/check-chainspec.sh index abb018e..42cf36c 100755 --- a/container-manager/src/gen/scripts/check-chainspec.sh +++ b/container-manager/src/gen/scripts/check-chainspec.sh @@ -63,7 +63,12 @@ name_flag=() if [[ -z "$NETWORK_NAME" && -f gen.env ]]; then NETWORK_NAME=$(grep -E '^NETWORK_NAME=' gen.env | tail -1 | cut -d '=' -f 2-) fi -if [[ -n "$NETWORK_NAME" ]]; then +# The literal "undefined" is what gen.env gets when the generator ran without a +# network name in its input; the converter saw JS undefined there and fell back +# to its own default, so the chainspec is NOT called "undefined". Passing it +# would make the check differ on the name alone and rewrite a correct chainspec +# on every run -- the exact failure this flag exists to prevent. +if [[ -n "$NETWORK_NAME" && "$NETWORK_NAME" != "undefined" ]]; then name_flag=(--name "$NETWORK_NAME") fi diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index baab955..b036e15 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -441,7 +441,7 @@ function genGenEnv(input) { } content = ` -NETWORK_NAME=${input["text-subnet-name"]} +NETWORK_NAME=${input["text-subnet-name"] || ""} NUM_SUBNET=${input["text-num-subnet"]} PARENTNET=${parentnet} PARENTNET_WALLET_PK=${keyOrNew(input["parentnet-wallet-pk"])} @@ -546,7 +546,7 @@ function genGenXdposEnv(input){ } content = ` -NETWORK_NAME=${input["text-subnet-name"]} +NETWORK_NAME=${input["text-subnet-name"] || ""} NUM_SUBNET=${input["text-num-subnet"]} `; content += content_machine; From 4dab2093147b7a7ed12b1a08cb2025ff0922c9f3 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 11:34:04 +0700 Subject: [PATCH 50/59] docs(converter): correct the header after D12 Two stale claims in the mapping block. period and rewardCheckpoint were still listed as mapped to engine params; D12 stopped emitting them because neither is a property of XdcChainSpecEngineParameters. The v2Configs TODO claimed the masternode/protector/observer reward amounts are "zeroed, not carried". They are carried verbatim: puppeth computes them (calcMasternodeRewards in cmd/puppeth/wizard_genesis.go) and writes them into V2.CurrentConfig, which aliases AllConfigs[0], and the converter passes the whole entry through. A recent genesis carries masternodeReward 63.42, minimumMinerBlockPerEpoch 5, limitPenaltyEpoch 5 and minimumSigningTx 30. Replaced with what the code does, including the new checkV2Config() guard. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/libs/genesis-to-chainspec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index c2491e3..7915a9c 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -54,17 +54,19 @@ * as dead. The forks are unaffected: each is still carried by its * EIP-numbered keys above. * - * genesis.config.XDPoS.period -> engine.XDPoS.params.period * genesis.config.XDPoS.epoch -> engine.XDPoS.params.epoch * genesis.config.XDPoS.reward -> engine.XDPoS.params.reward - * genesis.config.XDPoS.rewardCheckpoint -> engine.XDPoS.params.rewardCheckpoint * genesis.config.XDPoS.gap -> engine.XDPoS.params.gap + * genesis.config.XDPoS.period and .rewardCheckpoint are NOT carried: neither + * is a property of XdcChainSpecEngineParameters, so both were dead keys. The + * mine period comes from v2Configs[].minePeriod, which is bound. * genesis.config.XDPoS.foudationWalletAddr-> engine.XDPoS.params.foundationWalletAddr (typo fixed, lowercased) * genesis.config.XDPoS.v2.switchEpoch -> engine.XDPoS.params.switchEpoch (also accepts "SwitchEpoch") * genesis.config.XDPoS.v2.switchBlock -> engine.XDPoS.params.switchBlock (also accepts "SwitchBlock") * genesis.config.XDPoS.v2.allConfigs.* -> engine.XDPoS.params.v2Configs[] - * (TODO: the masternode/protector/observer reward amounts are zeroed, not - * carried — see translate()) + * (every field carried as genesis states it, bar expTimeoutConfig, which the + * chainspec schema has no key for; checkV2Config() rejects a config missing + * any field Nethermind would otherwise silently read as 0) * genesis.config.tip2019Block -> engine.XDPoS.params.tip2019Block * genesis.config.dynamicGasLimitBlock -> engine.XDPoS.params.DynamicGasLimitBlock * genesis.config.tipXDCXBlock -> engine.XDPoS.params.TipXDCX From 58a47598d0573049893dc209b533588f4ea1aedf Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 21:24:08 +0700 Subject: [PATCH 51/59] fix(deploy): keep FilterPeersByRecentIp on for subnet only D10 moved this setting out of the per-node env files and into the shared Nethermind configs, but set it to false in both. That silently changed subnet behaviour: the per-node env had it true there, and false only on the XDPoS path. Restores the original per-engine values, now stated where the rest of the network settings live: xdc-nmc.json (XDPoS) false -- was false in gen_xdpos.js xdc-nmc-subnet.json (Subnet) true -- was true in gen_env.js FilterDiscoveryNodesByRecentIp stays false on both. That is the flag upstream's own XDC config sets, and the one D10 was about: the peer-side filter and the discovery-side filter are separate properties, and only the discovery-side one was never being configured at all. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/scripts/xdc-nmc-subnet.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-manager/src/gen/scripts/xdc-nmc-subnet.json b/container-manager/src/gen/scripts/xdc-nmc-subnet.json index 76a56ad..1c4d6c8 100644 --- a/container-manager/src/gen/scripts/xdc-nmc-subnet.json +++ b/container-manager/src/gen/scripts/xdc-nmc-subnet.json @@ -8,7 +8,7 @@ "AutoDump": "All" }, "Network": { - "FilterPeersByRecentIp": false, + "FilterPeersByRecentIp": true, "FilterDiscoveryNodesByRecentIp": false, "EnableEnrDiscovery": false, "DisableDiscV4DnsFeeder": true From 75a90c2f28849537eac8bd49cdfaab60cc3172e7 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 21:31:48 +0700 Subject: [PATCH 52/59] docs(converter): rewrite the comments; verify every claim in them The file had accumulated a 150-line header that restated the mapping tables key by key, plus comments orphaned by the D12 key removals. The duplicated header had already drifted twice and needed fixing twice. Structural changes: - Header 150 -> 64 lines. The key-by-key mapping lists are gone; the tables are the mapping and are now pointed at rather than copied. What remains is the four things the tables cannot say for themselves: the two Go clients, XDPoSChain's non-canonical fork schedule, the two "off" conventions, and the silently-ignored-key hazard. - buildEngineParams(): the D6/D7 guards had been spliced into the middle of the comment explaining the default-table spread, cutting that explanation in half. Guards moved to the top with their own note; the spread explanation is whole again and the three merge passes are numbered. - Removed comments left dangling by D12: eip1234's rationale (sitting above eip1283) and eip2718's (above eip2929), plus an empty "// Osaka" heading and a misplaced beacon-chain aside. - Compressed the DEFAULT_ENGINE_SUBNET and checkV2Config notes roughly in half without dropping the evidence. Corrections found while checking the claims against source: - eip2028: the comment said xdc-testnet.json "agrees: 999999999999". It omits the key entirely. Corrected. - Header rule on fallbacks said every params transition falls back to 999999999999. Eleven sit at 0, for forks the chain genuinely runs from block 0. Corrected. - Reference spec path was /nethermind/chainspec/; it is src/Nethermind/Chains/. Corrected. - Dropped the stale nethermindeth/nethermind:xdc-fixes references; the binding claims are checked against master-4e36ba0, the image the deployment pins. Everything kept was re-verified against source: InitialBaseFee 12500000000, MaxCodeSize 24576/32768, MergeSignRange 15, RangeReturnSigner 150, TxDataNonZeroGas 68 with EIP2028's 16 unreferenced, IsIstanbul's OR over TIPXDCXCancellationFeeBlock, IsEIP1559, newBerlinInstructionSet's commented-out enable2929, newEip1559InstructionSet = Shanghai + 2929/3529/3860, London = +3198 only, Shanghai = +3855 only, EIP-3541's 0xEF check, 3860 via IntrinsicGas, 2929 /2930 via StateDB.Prepare and the txpool type gate, 3651's warm coinbase, 1283's Constantinople-and-not-Petersburg window, EIP-2935 via ProcessParentBlockHash, and Apothem's 61290000/71550000/83600000 splits. Comments only: converter output is byte-identical on both engines, all genesis files still convert and round-trip, and all three guards fire. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/libs/genesis-to-chainspec.js | 560 ++++++------------ 1 file changed, 197 insertions(+), 363 deletions(-) diff --git a/container-manager/src/libs/genesis-to-chainspec.js b/container-manager/src/libs/genesis-to-chainspec.js index 7915a9c..9ed7571 100644 --- a/container-manager/src/libs/genesis-to-chainspec.js +++ b/container-manager/src/libs/genesis-to-chainspec.js @@ -2,146 +2,68 @@ /** * genesis-to-chainspec.js * - * Translate a go-ethereum / XDC style `genesis.json` into the OpenEthereum / - * Erigon style `chainspec.json` consumed by this network. + * Translate a go-ethereum / XDC style `genesis.json` into the chainspec.json + * Nethermind consumes. Self-contained: no external dependencies. * * Usage: - * node genesis-to-chainspec.js [--name ] + * node genesis-to-chainspec.js [--subnet] [--name ] * - * node genesis-to-chainspec.js ../genesis.json ../chainspec.json --name xdc-mine - * - * Both the input and the output path are required; there are no defaults. - * - * From the host, as a one-off container (paths are container paths; - * /mount/generated is the host's ./generated directory): + * Both paths are required. --subnet names the engine block XDPoSSubnet instead + * of XDPoS; use it for a Subnet deployment, leave it off for a standalone XDPoS + * network. As a one-off container: * * docker run --rm -v $PWD/generated:/mount/generated \ * xinfinorg/subnet-generator: \ * npm run convert -- /mount/generated/genesis.json /mount/generated/chainspec.json * - * Self-contained: no external dependencies. - * * -------------------------------------------------------------------------- - * Every chainspec value that genesis.json can supply is read from it. The - * DEFAULT_* tables below are fallbacks only: they apply when genesis says - * nothing about a field. - * - * genesis.config.chainId -> params.chainId - * genesis.config.homesteadBlock -> params.eip7Transition - * genesis.config.eip150Block -> params.eip150Transition - * genesis.config.eip155Block -> params.eip155Transition - * genesis.config.eip158Block -> params.eip160Transition, eip161abcTransition, - * eip161dTransition, MaxCodeSizeTransition - * genesis.config.byzantiumBlock -> params.eip140Transition, eip211Transition, - * eip214Transition, eip658Transition - * the remaining EIP transitions come from the hardfork that ships them, see - * TRANSITION_FORKS: homesteadBlock, eip150Block, eip155Block, eip158Block, - * byzantiumBlock, constantinopleBlock, istanbulBlock, londonBlock, mergeBlock, - * shanghaiBlock, eip1559Block, cancunBlock, pragueBlock - * berlinBlock and osakaBlock feed nothing -- see TRANSITION_FORKS for why + * The one rule: genesis wins. Everything genesis.json can supply is read from + * it; the DEFAULT_* tables are fallbacks that apply only where genesis is + * silent. The tables below ARE the mapping -- read them, not a copy of them: * - * params deliberately carries no homesteadBlock, byzantiumBlock or - * eip158Transition. Those are geth-genesis spellings, not chainspec ones. - * Nethermind binds params to ChainSpecParamsJson, which declares - * eip7Transition, eip161abcTransition and eip161dTransition but none of those - * three: homesteadBlock and byzantiumBlock are properties of - * GethGenesisConfigJson, a separate class that reads a geth genesis.json, and - * ChainSpecParamsJson does not derive from it; eip158Transition exists on - * neither. Emitting them only put three keys in the chainspec that the node - * silently ignored. Verified against Nethermind.Specs.dll in - * nethermindeth/nethermind:xdc-fixes -- and Nethermind's own - * chainspec/xdc-testnet.json carries all three in params, where they are just - * as dead. The forks are unaffected: each is still carried by its - * EIP-numbered keys above. + * TRANSITION_FORKS genesis.config fork block -> params.eipNNNNTransition + * ENGINE_GENESIS_KEYS flat genesis.config key -> engine param + * ENGINE_FORKS XDC hardfork block -> engine param + * DEFAULT_PARAMS params values genesis cannot supply + * DEFAULT_ENGINE(_SUBNET) engine values genesis cannot supply * - * genesis.config.XDPoS.epoch -> engine.XDPoS.params.epoch - * genesis.config.XDPoS.reward -> engine.XDPoS.params.reward - * genesis.config.XDPoS.gap -> engine.XDPoS.params.gap - * genesis.config.XDPoS.period and .rewardCheckpoint are NOT carried: neither - * is a property of XdcChainSpecEngineParameters, so both were dead keys. The - * mine period comes from v2Configs[].minePeriod, which is bound. - * genesis.config.XDPoS.foudationWalletAddr-> engine.XDPoS.params.foundationWalletAddr (typo fixed, lowercased) - * genesis.config.XDPoS.v2.switchEpoch -> engine.XDPoS.params.switchEpoch (also accepts "SwitchEpoch") - * genesis.config.XDPoS.v2.switchBlock -> engine.XDPoS.params.switchBlock (also accepts "SwitchBlock") - * genesis.config.XDPoS.v2.allConfigs.* -> engine.XDPoS.params.v2Configs[] - * (every field carried as genesis states it, bar expTimeoutConfig, which the - * chainspec schema has no key for; checkV2Config() rejects a config missing - * any field Nethermind would otherwise silently read as 0) - * genesis.config.tip2019Block -> engine.XDPoS.params.tip2019Block - * genesis.config.dynamicGasLimitBlock -> engine.XDPoS.params.DynamicGasLimitBlock - * genesis.config.tipXDCXBlock -> engine.XDPoS.params.TipXDCX - * genesis.config.denylistBlock -> engine.XDPoS.params.BlackListHFNumber - * genesis.config.tipTRC21FeeBlock -> engine.XDPoS.params.TipTrc21Fee - * genesis.config.tipXDCXMinerDisableBlock -> engine.XDPoS.params.TIPXDCXMinerDisable - * genesis.config.tipXDCXReceiverDisableBlock -> engine.XDPoS.params.TIPXDCXReceiverDisable + * Four things the tables cannot say for themselves: * - * genesis.{timestamp,extraData,gasLimit,difficulty,parentHash} - * -> genesis.* (verbatim) - * genesis.nonce, genesis.mixHash -> genesis.seal.ethereum.* - * genesis.coinbase -> genesis.author - * genesis.number, genesis.gasUsed -> dropped (no counterpart) - * genesis.baseFeePerGas (null) -> genesis.baseFeePerGas (DEFAULT_BASE_FEE_PER_GAS) - * no flag overrides this: XDPoSChain takes the same two steps in - * core/genesis.go (g.BaseFee, else params.InitialBaseFee), and a third - * value would simply change the genesis block hash + * 1. Two different Go clients. A subnet runs XinFinOrg/XDC-Subnet + * (xinfinorg/xdcsubnets); a standalone network runs XinFinOrg/XDPoSChain + * (xinfinorg/devnet). They gate the same EIPs on the same genesis forks, so + * params is identical, but their v2 configs differ -- see checkV2Config(). * - * genesis.alloc -> accounts (verbatim: code / storage / balance) + * 2. XDPoSChain does not follow the canonical Ethereum fork schedule. A group + * Ethereum ships with Berlin/London/Shanghai -- 2565, 2929, 2930, 3529, + * 3541, 3651, 3860 -- is gated on EIP1559Block instead, alongside 1559 + * itself. EIP-2028 is not implemented at all. berlinBlock feeds nothing. + * TRANSITION_FORKS carries the Go call site for each. * - * Not derivable from genesis, so always DEFAULT_ENGINE* / DEFAULT_PARAMS*: - * mergeSignRange, RangeReturnSigner, blackListedAddresses, the contract - * binaries (they live in XDC's common constants, not in genesis) and - * eip1559ElasticityMultiplier. maxCodeSize is a constant too, but which one - * depends on osakaBlock — see translate(). + * 3. Two different "off" conventions. In params, a fork the chain does not run + * falls back to 999999999999, a block no chain reaches, so it is emitted + * visibly off rather than silently on at 0. (The eleven entries sitting at 0 + * are forks the chain genuinely runs from block 0.) ENGINE_FORKS has no + * fallback at all and relies on the key being absent instead -- Nethermind + * reads `(key ?? ulong.MaxValue) <= block`, so absent means never enabled. * - * Present in genesis but with no counterpart in the chainspec schema, so - * dropped rather than invented: v2 expTimeoutConfig, maxMasternodesV2, - * SkipV1Validation, and the trc21IssuerSMC / xdcxListingSMC / - * relayerRegistrationSMC / lendingRegistrationSMC addresses. + * 4. A key Nethermind does not bind is silently ignored, which is how a + * chainspec can look correct and still diverge from the Go nodes. Every key + * emitted here was checked against a real property on ChainSpecParamsJson, + * ChainSpecGenesisJson or XdcChainSpecEngineParameters; the note above + * ENGINE_GENESIS_KEYS lists what that test excluded and why. * - * genesis.config.tipSigningBlock -> engine.XDPoS.params.TipSigningBlock - * genesis.config.tipRandomizeBlock -> engine.XDPoS.params.TipRandomizeBlock - * genesis.config.tipIncreaseMasternodesBlock -> engine.XDPoS.params.TipIncreaseMasternodesBlock - * genesis.config.tipNoHalvingMNRewardBlock -> engine.XDPoS.params.TipNoHalvingMNRewardBlock - * genesis.config.tipXDCXLendingBlock -> engine.XDPoS.params.TipXDCXLendingBlock - * genesis.config.tipXDCXCancellationFeeBlock -> engine.XDPoS.params.TipXDCXCancellationFeeBlock - * genesis.config.gas50xBlock -> engine.XDPoS.params.Gas50xBlock - * genesis.config.tipUpgradeRewardBlock -> engine.XDPoS.params.TIPUpgradeReward - * genesis.config.tipUpgradePenaltyBlock -> engine.XDPoS.params.TIPUpgradePenalty - * (see ENGINE_FORKS; key spellings, TIP casing included, are Nethermind's) + * Genesis block: timestamp, extraData, gasLimit, difficulty and parentHash go + * across verbatim. nonce and mixHash become genesis.seal.ethereum, coinbase + * becomes genesis.author, and number and gasUsed are dropped -- those are the + * shapes ChainSpecLoader actually reads. baseFeePerGas is null in genesis and + * concrete here: DEFAULT_BASE_FEE_PER_GAS is the value that makes the genesis + * hashes agree, not a preference. * - * XDPoSChain does not follow the canonical Ethereum fork schedule, so neither - * does this mapping. A group of EIPs that Ethereum ships with Berlin, London - * and Shanghai — 2565, 2929, 2930, 3529, 3541, 3651, 3860 — is gated on - * EIP1559Block in the Go client instead, alongside 1559 itself; EIP-2028 is not - * implemented at all. TRANSITION_FORKS carries the reasoning per key, with the - * Go call site for each. - * - * pragueBlock maps to only the EIPs XDPoSChain gates on IsPrague: 2935, 7623 and - * 7702. The rest of Prague (BLS precompiles, the beacon-chain EIPs) is listed in - * TRANSITION_FORKS as null and so comes out at DEFAULT_PARAMS' 999999999999: - * present in the chainspec, visibly off, and impossible to switch on for - * Nethermind alone by accident. Nethermind's own reference spec for Apothem - * (/nethermind/chainspec/xdc-testnet.json) carries the same three transitions at - * 83600000 and nothing else, and pre-allocates no EIP-2935 history contract, so - * Nethermind supplies that contract itself; the transitions are all that is - * needed. - * - * osakaBlock maps to no transition at all: Nethermind's XDC build declares every - * Osaka EIP by timestamp only, with no block-numbered property to bind, so a - * block-numbered chainspec cannot enter Osaka. The one part of Osaka a chainspec - * can still carry is the code limit — maxCodeSize becomes 32768 when osakaBlock - * is at or before maxCodeSizeTransition, since 32768 is then the only limit the - * chain ever has. Everything else about Osaka is out of reach, so translate() - * warns when genesis states an osakaBlock. - * - * Every params transition has a DEFAULT_PARAMS fallback of 999999999999, a - * block no chain reaches, so a fork genesis does not state stays off rather - * than silently activating at block 0. ENGINE_FORKS has no fallback at all and - * relies on the key being absent instead. tipUpgradeRewardBlock is why that - * matters — it selects between two different reward formulas - * (eth/hooks/engine_v2_hooks.go, IsTIPUpgradeReward), so dropping it makes - * XDPoSChain and Nethermind compute different balances at the first reward - * checkpoint and the chain stalls there for good. + * Present in genesis with no chainspec counterpart, so dropped rather than + * invented: v2 expTimeoutConfig, maxMasternodesV2, SkipV1Validation, and the + * trc21IssuerSMC / xdcxListingSMC / relayerRegistrationSMC / + * lendingRegistrationSMC addresses. * -------------------------------------------------------------------------- */ @@ -157,14 +79,14 @@ const path = require('path'); const DEFAULT_CHAIN_NAME = 'xdpos-chain'; -// chainspec.genesis.baseFeePerGas is a concrete value while genesis.json has -// null. Same number as XDPoSChain's params.InitialBaseFee (12.5 gwei), which is -// what its core/genesis.go puts in the genesis header in that case -- so this -// is not a preference, it is the value that makes the genesis hashes agree. +// genesis.json has null here, the chainspec needs a concrete value. This is +// params.InitialBaseFee (12500000000, 12.5 gwei), which is what core/genesis.go +// puts in the header in that case -- not a preference, the value that makes the +// genesis hashes agree. const DEFAULT_BASE_FEE_PER_GAS = '0x2e90edd00'; -// All EIP transitions in chainspec.params that don't come straight from -// genesis.config. Values copied from the reference chainspec.json. +// Fallback values for params. Every transition sits at 999999999999 unless the +// chain genuinely runs it from block 0 -- see rule 3 in the header. const DEFAULT_PARAMS = { // Homestead eip7Transition: 1, @@ -179,9 +101,9 @@ const DEFAULT_PARAMS = { eip155Transition: 999999999999, MaxCodeSizeTransition: 999999999999, MaxCodeSize: 24576, - // EIP-7907's raised limit (params.MaxCodeSizeOsaka in XDPoSChain). Never - // emitted under this name -- translate() picks it as the value of maxCodeSize - // when osakaBlock makes it the only limit the chain ever has. + // params.MaxCodeSizeOsaka. Never emitted under this name -- pickMaxCodeSize() + // uses it as the value of MaxCodeSize when osakaBlock makes it the only limit + // the chain ever has. MaxCodeSizeOsaka: 32768, // Byzantium @@ -232,38 +154,23 @@ const DEFAULT_PARAMS = { eip2935Transition: 999999999999, eip7702Transition: 999999999999, eip7623Transition: 999999999999, - - // Osaka - - }; -// Same, for a Subnet. Unlike the DEFAULT_ENGINE / DEFAULT_ENGINE_SUBNET pair -// this one is a spread rather than a second full table, because the emitted key -// set and key order come from TRANSITION_FORKS, not from here -- a params table -// supplies values only. So every key stays in step with DEFAULT_PARAMS -// automatically and a subnet only has to state what it disagrees about. -// -// Nothing yet. The two run different Go clients -- a subnet runs -// XinFinOrg/XDC-Subnet (xinfinorg/xdcsubnets), a standalone network runs -// XinFinOrg/XDPoSChain (xinfinorg/devnet) -- but they gate the same EIPs on the -// same genesis forks, so no entry is needed. Put one here the moment a subnet -// needs to differ, e.g. -// eip1559Transition: 0, -// Do NOT restate the whole table: the 999999999999 fallbacks are the invariant -// that keeps a fork genesis never states switched off rather than on at 0. +// Same, for a Subnet. A spread, not a second table: both clients gate the same +// EIPs on the same forks, so there is nothing to differ about yet. Add only the +// keys a subnet disagrees about (e.g. `eip1559Transition: 0`) and never restate +// the rest -- the 999999999999 fallbacks are the invariant from rule 3. const DEFAULT_PARAMS_SUBNET = { ...DEFAULT_PARAMS, }; -// Engine constants for what genesis.json does not state (copied from -// chainspec.json). Spread into the engine block whole, so key order here is the -// emitted order and every key here is emitted. These are fallbacks only: -// anything genesis states wins, whether it arrives as chain data under the same -// name or through the ENGINE_GENESIS_KEYS rename -- see translate(). +// Engine constants for what genesis.json does not state. Spread into the engine +// block whole, so key order here is the emitted order and every key here is +// emitted. Fallbacks only: anything genesis states wins, whether it arrives as +// chain data under the same name or via ENGINE_GENESIS_KEYS. const DEFAULT_ENGINE = { mergeSignRange: 15, RangeReturnSigner: 150, @@ -284,34 +191,24 @@ const DEFAULT_ENGINE = { XDCXLendingFinalizedTradeAddressBinary: '0x0000000000000000000000000000000000000094', }; -// The XDPoSSubnet engine plugin binds the SAME property names as XDPoS: -// XdcSubnetChainSpecEngineParameters derives from XdcChainSpecEngineParameters -// and overrides only SealEngineType (and the internal ResolveMinGasPrice), so -// it inherits every property. Binding is case-insensitive, so the casing -// differences below are cosmetic -- but a name that is not a property at all is -// silently ignored and the engine's own default applies, which is how a -// chainspec can look correct and still diverge from the Go nodes. +// Same table for the subnet engine. XdcSubnetChainSpecEngineParameters derives +// from XdcChainSpecEngineParameters and overrides only SealEngineType (and the +// internal ResolveMinGasPrice), so it binds the same property names; the casing +// differences below are cosmetic, since binding is case-insensitive. // -// Most entries here switch a fork off. The two that switch one ON were checked -// against XinFinOrg/XDC-Subnet, the client a subnet actually runs, rather than -// copied from a deployment that happened to work: +// Most entries switch a fork off. The two that switch one ON were checked +// against XDC-Subnet, the client a subnet actually runs, not copied from a +// deployment that happened to work. Both are on from block 1 on both sides: // -// tip2019Block: 1 -- Nethermind computes IsTIP2019 = TIP2019Block <= -// releaseStartBlock (XdcChainSpecBasedSpecProvider.cs:79), so on from block -// 1. XDC-Subnet hardcodes common.TIP2019Block = 1 and tests isForked() -// against it, so also from block 1. They agree. -// TipTrc21Fee: 1 -- Nethermind computes IsTipTrc21FeeEnabled = -// (TipTrc21Fee ?? ulong.MaxValue) <= releaseStartBlock (:77), so on from -// block 1. XDC-Subnet hardcodes common.TIPTRC21Fee = 0 but tests it -// STRICTLY -- core/state_transition.go:271 is -// `if st.evm.BlockNumber.Cmp(common.TIPTRC21Fee) > 0` -- so it is live from -// block 1 too. They agree. This one is read by the transaction-execution -// path (XdcTransactionProcessor), so do not "correct" the 1 to 0 to match -// the Go constant: that would enable it a block early. +// tip2019Block: 1 Nethermind: IsTIP2019 = TIP2019Block <= releaseStartBlock +// (XdcChainSpecBasedSpecProvider.cs:79). Go: common.TIP2019Block = 1. +// TipTrc21Fee: 1 Nethermind: (TipTrc21Fee ?? MaxValue) <= releaseStartBlock +// (:77). Go: common.TIPTRC21Fee = 0, but tested STRICTLY -- +// `BlockNumber.Cmp(common.TIPTRC21Fee) > 0` (core/state_transition.go:271). +// Do NOT "correct" this to 0 to match the Go constant: the strict test means +// 0 would enable it a block early, on the transaction-execution path. // -// Key order is the emitted order: translate() spreads this table into the -// engine block whole, so a key is added, removed or moved here and nowhere -// else. Fallbacks only, as with DEFAULT_ENGINE -- genesis always wins. +// Key order is the emitted order. Fallbacks only -- genesis always wins. const DEFAULT_ENGINE_SUBNET = { MergeSignRange: 15, RangeReturnSigner: 150, @@ -333,11 +230,9 @@ const DEFAULT_ENGINE_SUBNET = { switchEpoch: 0, }; -// A subnet value that is NOT an engine param, so it is kept out of the table -// above: that table is spread into the engine block wholesale and anything in it -// is emitted, whereas maxMasternodes goes inside each v2Configs entry. genesis -// allConfigs carries none; this is XDC's own default and what the working subnet -// chainspec states explicitly. +// Kept out of the table above because that one is spread into the engine block +// wholesale, whereas maxMasternodes belongs inside each v2Configs entry. A +// subnet genesis states none; 108 is XDC's own default (common.MaxMasternodes). const DEFAULT_SUBNET_EXTRAS = { maxMasternodes: 108, }; @@ -348,50 +243,37 @@ const DEFAULT_SUBNET_EXTRAS = { * ------------------------------------------------------------------ */ -// Which hardfork block in genesis.config activates each chainspec transition. -// One entry per DEFAULT_PARAMS transition key, same order and same fork -// headings, so the two tables read side by side. Order matters twice over: it -// is also the key order of the generated params object. +// Which genesis.config fork block activates each params transition. Key order +// here is the emitted key order, and mirrors DEFAULT_PARAMS. // -// 'someBlock' = genesis.config.someBlock activates the EIP; when genesis does -// not state that fork, the DEFAULT_PARAMS entry stands in. -// [a, b, ...] = more than one genesis key activates it, whichever states the -// earliest block. XDPoSChain has forks that are reached two -// ways: IsIstanbul is -// `isForked(TIPXDCXCancellationFeeBlock) || isForked(IstanbulBlock)` -// (params/config_forks.go), a compatibility path for older XDC -// configs that never set istanbulBlock -- Apothem is one, and it -// runs Istanbul from tipXDCXCancellationFeeBlock. -// null = no genesis fork may activate it. XDPoSChain does not -// implement the EIP, so the DEFAULT_PARAMS never-block -// (999999999999) always stands and the key is emitted -// explicitly off. Turning one of these on for Nethermind alone -// is exactly the state-root (or block-header) divergence this -// mapping exists to prevent: the Go nodes would compute -// something else and the chain would stall at the first block -// that touches it. +// 'someBlock' genesis.config.someBlock activates it; if genesis does not +// state that fork, the DEFAULT_PARAMS entry stands in. +// [a, b, ...] several keys can activate it -- whichever states the earliest +// block wins, because Go ORs the isForked() checks. IsIstanbul +// is `isForked(TIPXDCXCancellationFeeBlock) || isForked( +// IstanbulBlock)` (params/config_forks.go:79), a compatibility +// path for older XDC configs that never set istanbulBlock. +// null XDPoSChain does not implement it, so the never-block stands +// and the key is emitted visibly off. Switching one of these on +// for Nethermind alone is the state-root divergence this mapping +// exists to prevent. // -// The null verdicts were read off XDPoSChain itself, not off the EIP list: -// core/vm/jump_table.go for the per-fork instruction sets, core/vm/contracts.go -// for the precompile sets, and the Is call sites; then cross-checked -// against the chainspec Nethermind ships for Apothem (chainspec/xdc-testnet.json -// in the image), which is the same mapping with real block numbers on it. +// The null verdicts come from XDPoSChain itself -- core/vm/jump_table.go for the +// instruction sets, core/vm/contracts.go for the precompiles, and the Is +// call sites -- cross-checked against the spec Nethermind ships for Apothem +// (src/Nethermind/Chains/xdc-testnet.json), the same mapping with real blocks. // -// Watch for 'eip1559Block' below. XDPoSChain does NOT follow the canonical -// Ethereum fork schedule: core/vm/evm.go picks ONE instruction set by a switch -// on chainRules, and newEip1559InstructionSet is Shanghai plus 2929, 3529 and -// 3860 -- newBerlinInstructionSet has its enable2929 commented out. So a whole -// group of Berlin/London/Shanghai EIPs really activates at EIP1559Block -// (params/config_forks.go: IsEIP1559 is isForked(c.EIP1559Block, num), nothing -// else feeds it). Apothem is the proof: berlin = london = merge = shanghai = -// 61290000 but eip1559Block = 71550000, and xdc-testnet.json puts eip1559, -// 2929, 3529 and 3860 together on 71550000. +// Watch 'eip1559Block'. core/vm/evm.go picks ONE instruction set by a switch on +// chainRules, and newEip1559InstructionSet is Shanghai plus 2929, 3529 and 3860 +// (jump_table.go:115) while newBerlinInstructionSet has its enable2929 +// commented out (:152). So that whole group really activates at EIP1559Block. +// Apothem proves it: london = shanghai = 61290000 but eip1559Block = 71550000, +// and xdc-testnet.json puts eip1559, 2929, 3529 and 3860 together on 71550000. const TRANSITION_FORKS = { - // Chain identity. Not a transition, but it is read out of genesis.config - // exactly like one, so keeping it here lets params be built in a single pass - // with no hand-written head. It has no DEFAULT_PARAMS* entry: a chainspec - // should not invent a chain id, and pick() leaving it undefined drops the - // key, which is what we want. + // Not a transition, but read out of genesis.config exactly like one, so + // keeping it here lets params be built in a single pass. No DEFAULT_PARAMS + // entry on purpose: a chainspec must not invent a chain id, and pick() + // leaving it undefined drops the key. chainId: 'chainId', // Homestead @@ -405,8 +287,8 @@ const TRANSITION_FORKS = { eip161abcTransition: 'eip158Block', eip161dTransition: 'eip158Block', eip155Transition: 'eip155Block', - // EIP-170. The limit itself is a constant, not a transition, so translate() - // emits maxCodeSize straight from DEFAULT_PARAMS alongside this. + // EIP-170. The limit itself is a constant, not a transition -- buildParams() + // emits MaxCodeSize alongside this. MaxCodeSizeTransition: 'eip158Block', // Byzantium @@ -422,8 +304,6 @@ const TRANSITION_FORKS = { eip145Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], eip1014Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], eip1052Transition: ['constantinopleBlock', 'istanbulBlock', 'tipXDCXCancellationFeeBlock'], - // block reward cut and difficulty-bomb delay: XDPoS pays its own rewards - // (eth/hooks/engine_v2_hooks.go) and has no bomb to postpone // net metered SSTORE, live only while IsConstantinople && !IsPetersburg // (core/vm/gas_table.go, gasSStore). Moot once enable2200 replaces SSTORE's // dynamic gas outright, which is why the reference spec can leave it on. @@ -436,9 +316,9 @@ const TRANSITION_FORKS = { eip1884Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], // 2028 is NOT implemented: IntrinsicGas charges the flat // params.TxDataNonZeroGas = 68, and TxDataNonZeroGasEIP2028 = 16 is declared - // in params/protocol_params.go and referenced nowhere in the tree. Nethermind - // at 16 gas per non-zero byte would misprice every call with calldata. - // xdc-testnet.json agrees: 999999999999 while the rest of Istanbul is live. + // in params/protocol_params.go and referenced nowhere else in the tree. + // Nethermind at 16 gas per non-zero byte would misprice every call with + // calldata. xdc-testnet.json omits the key entirely, with Istanbul live. eip2028Transition: null, eip2200Transition: ['istanbulBlock', 'tipXDCXCancellationFeeBlock'], @@ -446,8 +326,6 @@ const TRANSITION_FORKS = { // 2565: PrecompiledContractsXDCv2 still has eip2565:false; the first set that // turns it on is PrecompiledContractsEIP1559 (core/vm/contracts.go) eip2565Transition: 'eip1559Block', - // 2718: same txpool gate as 2930 below -- every non-legacy tx type is - // rejected while !rules.IsEIP1559 // 2929 + 2930: the access list is built in StateDB.Prepare, whose whole body // is behind `if rules.IsEIP1559` (core/state/statedb.go), and txpool rejects // every non-legacy tx type until IsEIP1559 (core/txpool/validation.go) @@ -482,54 +360,40 @@ const TRANSITION_FORKS = { // no Prague case, it falls through to the // EIP-1559 set eip2935Transition: 'pragueBlock', - // (6110/7002/7251, the beacon-chain EIPs, are not - // emitted at all -- see the note below) eip7702Transition: 'pragueBlock', eip7623Transition: 'pragueBlock', - // Osaka is unreachable from a chainspec, so no Osaka key is emitted at all. - // XDPoSChain does gate five of them on IsOsaka -- 7823 and 7883 (modexp input - // cap and repricing, in PrecompiledContractsOsaka), 7825 (tx gas cap, - // params.MaxTxGas), 7934 (RLP block size cap, params.MaxBlockSize) and 7939 - // (CLZ opcode: newOsakaInstructionSet is Prague plus enable7939) -- but - // Nethermind's XDC build declares EVERY Osaka EIP as - // eipNNNNTransitionTimestamp only, with no block-numbered form to bind. See - // the osakaBlock warning in translate(): no chainspec can follow a Go node - // into Osaka, EIP-7907's 32768-byte code limit included. - + // Osaka is unreachable from a chainspec, so no Osaka key is emitted. XDPoSChain + // does gate five EIPs on IsOsaka (7823, 7825, 7883, 7934, 7939), but + // Nethermind declares every one of them as eipNNNNTransitionTimestamp only, + // with no block-numbered form to bind. pickMaxCodeSize() warns about this; + // maxCodeSize is the only piece of Osaka a chainspec can carry at all. }; -// Nineteen EIP transitions are deliberately NOT emitted, because Nethermind -// binds no such key and a value under a name nothing reads is worse than no key -// at all -- it reads as a decision that was never taken. Verified against -// Nethermind.Specs ChainSpecParamsJson at master-4e36ba0: +// Nineteen transitions are deliberately absent: Nethermind binds no such key, +// and a value under a name nothing reads is worse than no key -- it reads as a +// decision that was never taken. Checked against ChainSpecParamsJson at +// master-4e36ba0: // // no property in any form (6): eip1234, eip2718, eip3554, eip4399, eip6049, -// eip7516. eip1234 is the illustration -- IsEip1234Enabled derives from the -// Constantinople block, i.e. eip145Transition, which this converter defaults -// to 0, so it is on from block 0 no matter what sat beside it. +// eip7516. eip1234 shows why it matters -- IsEip1234Enabled derives from the +// Constantinople block (eip145Transition, which defaults to 0 here), so it +// is on from block 0 whatever sat beside it. // declared only as eipNNNNTransitionTimestamp (13): eip4788, eip4895, eip6110, // eip7002, eip7251, eip7594, eip7823, eip7825, eip7883, eip7918, eip7934, -// eip7939, eip7951. A block-numbered value under those names is an unknown -// JSON member and is silently ignored. +// eip7939, eip7951 -- a block-numbered value there is an unknown JSON +// member and is silently ignored. // // Before adding one back, check it is a real ChainSpecParamsJson property. // The second way genesis reaches an engine param: a flat genesis.config key // whose name differs from the chainspec key. (The first is chain data that keeps -// its name, which translate() merges by name.) chainspec key -> the -// genesis.config key it comes from. +// its name, which buildEngineParams() merges by name.) // -// One table for both engines: XdcSubnetChainSpecEngineParameters inherits every -// property of XdcChainSpecEngineParameters, and binding is case-insensitive, so -// there is nothing to rename. The subnet table used to omit TipXDCX, -// TIPXDCXMinerDisable and TIPXDCXReceiverDisable as "not used at all" -- all -// three are bound, AddTransitions() uses them to create release-spec -// boundaries, and IsTIPXDCXMiner / IsTIPXDCXReceiver are computed from them -// (XdcChainSpecBasedSpecProvider.cs:80-81). -// -// A key here that is not in the matching DEFAULT_ENGINE* table is emitted only -// when genesis states it. +// One table for both engines -- XdcSubnetChainSpecEngineParameters inherits +// every property of XdcChainSpecEngineParameters and binding is +// case-insensitive, so there is nothing to rename. A key here that has no entry +// in the matching DEFAULT_ENGINE* table is emitted only when genesis states it. const ENGINE_GENESIS_KEYS = { tip2019Block: 'tip2019Block', DynamicGasLimitBlock: 'dynamicGasLimitBlock', @@ -540,14 +404,13 @@ const ENGINE_GENESIS_KEYS = { TIPXDCXReceiverDisable: 'tipXDCXReceiverDisableBlock', }; -// XDC hardfork blocks that live in engine.XDPoS.params rather than params. -// Key = chainspec key, value = genesis.config key. The key spellings come from -// Nethermind's own reference specs (/nethermind/chainspec/xdc.json and -// xdc-testnet.json) — note TIPUpgradeReward/TIPUpgradePenalty use all-caps TIP -// and, unlike the rest, carry no "Block" suffix. Binding is case-insensitive, -// but matching the reference keeps generated and reference specs diffable. +// XDC hardfork blocks, which live in the engine block rather than params. +// Spellings match Nethermind's reference specs (src/Nethermind/Chains/xdc.json) +// -- note TIPUpgradeReward and TIPUpgradePenalty take all-caps TIP and no +// "Block" suffix. Binding is case-insensitive; matching the reference just keeps +// generated and reference specs diffable. // -// No defaults here on purpose — see the header note on ENGINE_FORKS. +// No defaults here on purpose -- see rule 3 in the header. const ENGINE_FORKS = { TipSigningBlock: 'tipSigningBlock', TipRandomizeBlock: 'tipRandomizeBlock', @@ -587,12 +450,11 @@ function pick(...candidates) { return undefined; } -// Lowercase, 0x-prefixed. Clients accept this fine; we don't bother with the -// cosmetic EIP-55 mixed-case checksum. +// Lowercase, 0x-prefixed; the cosmetic EIP-55 checksum is not worth the bother. +// Absent stays absent -- without that guard String(undefined) makes the literal +// "0xundefined", which Nethermind rejects at load with "hex string of odd +// length", an error naming neither the field nor the file. function normalizeAddress(addr) { - // Absent stays absent. Without this, String(undefined) makes the literal - // "0xundefined", which Nethermind rejects at load with "hex string of odd - // length" -- an error that names neither the field nor the file. if (addr === undefined || addr === null) return undefined; return '0x' + String(addr).toLowerCase().replace(/^0x/, ''); } @@ -602,33 +464,25 @@ function normalizeAddress(addr) { * ------------------------------------------------------------------ */ // Nethermind binds each v2Configs entry to V2ConfigParams, whose fields are -// non-nullable value types with `init` accessors: an absent key binds to 0 -// rather than erroring, and its CheckConfig only validates that a -// switchRound: 0 entry exists and that no round repeats +// non-nullable value types with `init` accessors, and its CheckConfig validates +// only that a switchRound: 0 entry exists and that no round repeats // (XdcChainSpecEngineParameters.cs:109-118). So a field genesis omits is not -// "unset", it is zero, and nothing downstream can tell the difference. -// -// What has to be present depends on which Go client the chainspec is paired -// with, because the two declare different V2Configs: +// "unset", it is 0, and nothing downstream can tell the difference. // -// XDPoS -- XinFinOrg/XDPoSChain params/config_xdpos.go V2Config, 14 fields. -// A generated XDPoS genesis states all 14. -// Subnet -- XinFinOrg/XDC-Subnet params/config.go V2Config, 5 fields. The -// other nine do not exist on that client at all, so requiring them -// would reject every valid subnet genesis. maxMasternodes is -// required on top of the five because Nethermind's -// SubnetMasternodesCalculator reads it (spec.MaxMasternodes) and -// the converter injects it from DEFAULT_SUBNET_EXTRAS. +// What must be present depends on the Go client, which declare different +// V2Configs: XDPoSChain's has 14 fields (params/config_xdpos.go) and a generated +// XDPoS genesis states all 14; XDC-Subnet's has 5 (params/config.go), so +// requiring the other nine would reject every valid subnet genesis. +// maxMasternodes is required on top of those 5 because +// SubnetMasternodesCalculator reads it and the converter injects it. // -// The nine subnet-absent fields are safe at zero, but only because nothing on -// the subnet path reads them: Nethermind registers SubnetPenaltyHandler -// (XdcSubnetModule.cs:32), which uses the hardcoded -// XdcConstants.MinimumMinerBlockPerEpoch = 1 rather than the spec value and -// never touches LimitPenaltyEpoch or MinimumSigningTx, and XDC-Subnet hardcodes -// the same 1 (common.MinimunMinerBlockPerEpoch, eth/hooks/engine_v2_hooks.go). -// The reward and protector/observer cap fields are read only by -// XdcRewardCalculator, and XDC-Subnet has no protector/observer tiers. If a -// future subnet client gains any of those fields, move it into the subnet list. +// Those nine are safe at 0 on a subnet only because nothing reads them there: +// Nethermind registers SubnetPenaltyHandler (XdcSubnetModule.cs:32), which uses +// a hardcoded MinimumMinerBlockPerEpoch of 1 and never touches LimitPenaltyEpoch +// or MinimumSigningTx, and XDC-Subnet hardcodes the same 1. The reward and +// protector/observer cap fields are read only by XdcRewardCalculator, and +// XDC-Subnet has no protector/observer tiers. If a future subnet client gains +// any of them, move it into the subnet list. const V2_REQUIRED_FIELDS_XDPOS = [ 'switchRound', 'maxMasternodes', 'maxProtectorNodes', 'maxObserverNodes', 'minePeriod', 'timeoutSyncThreshold', 'timeoutPeriod', 'certificateThreshold', @@ -654,18 +508,14 @@ function checkV2Config(round, config, opts) { } } -// engine..params.v2Configs: the per-round XDPoS V2 settings, in round -// order. Every field is carried through as genesis states it, bar -// expTimeoutConfig, which the chainspec schema has no key for and so is dropped -// rather than invented. +// The per-round XDPoS V2 settings, in round order. Every field is carried as +// genesis states it, bar expTimeoutConfig, which has no chainspec key. function buildV2Configs(v2, opts) { return Object.keys(v2.allConfigs || {}) .sort((a, b) => Number(a) - Number(b)) .map((round) => { const { expTimeoutConfig, ...rest } = v2.allConfigs[round]; - // genesis carries no maxMasternodes on a subnet, but the working subnet - // chainspec states it. Key spellings are left as genesis writes them -- - // the reference capitalises them, but binding is case-insensitive. + // a subnet genesis carries no maxMasternodes, but the chainspec needs it const config = opts.subnet ? { maxMasternodes: DEFAULT_SUBNET_EXTRAS.maxMasternodes, ...rest } : rest; @@ -674,17 +524,16 @@ function buildV2Configs(v2, opts) { }); } -// engine.XDPoS.params, or engine.XDPoSSubnet.params with opts.subnet. Which -// default table and which genesis-key map apply is the only difference between -// the two engines here; the engine block is named by translate(). +// engine.XDPoS.params, or engine.XDPoSSubnet.params with opts.subnet. The +// default table is the only difference between the two engines here; translate() +// names the block. function buildEngineParams(cfg, opts) { const xdpos = cfg.XDPoS || {}; const v2 = xdpos.v2 || {}; - // chain data, read the same way for both engines - // period and rewardCheckpoint are NOT carried: neither exists on - // XdcChainSpecEngineParameters, so both were dead keys. The mine period comes - // from v2Configs[].minePeriod, which is bound. + // Chain data, read the same way for both engines. period and rewardCheckpoint + // are NOT carried: neither is a property of XdcChainSpecEngineParameters. The + // mine period comes from v2Configs[].minePeriod, which is bound. const shared = { epoch: xdpos.epoch, reward: xdpos.reward, @@ -698,24 +547,14 @@ function buildEngineParams(cfg, opts) { v2Configs: buildV2Configs(v2, opts), }; - const engineDefaults = opts.subnet ? DEFAULT_ENGINE_SUBNET : DEFAULT_ENGINE; + // Two things the converter cannot express, so it refuses rather than emit a + // chainspec that looks right and silently disagrees with the Go nodes. - // The whole default table is spread in rather than restated key by key: a - // value belongs to exactly one place, the DEFAULT_ENGINE* table, which also - // fixes the emitted key order. Adding a constant there is all it takes to have - // it emitted -- the older hand-copied version silently dropped any key whose - // assignment nobody remembered to write out. - // - // One rule for what follows: the table is a fallback, never a winner. - // Whatever genesis states beats it, and a table entry only shows up where - // genesis is silent. Genesis reaches a key two ways, hence two passes; both - // re-assign keys the spread already created, so key order stays the table's. - // Nethermind parses the genesis masternodes out of genesis.extraData only when - // SwitchBlock == 0; otherwise it reads engine params genesisMasternodes, which - // this converter has no source for and never writes + // Nethermind reads the genesis masternodes out of genesis.extraData only when + // SwitchBlock == 0; otherwise it reads the engine param genesisMasternodes, + // which this converter has no source for and never writes // (XdcChainSpecBasedSpecProvider.cs:101-112, defaulting to an empty array). - // The node then boots reporting the right genesis hash with no masternodes and - // no error, so this has to be caught here. + // The node would boot on the right genesis hash with no validator set. if (Number(pick(shared.switchBlock, 0)) !== 0) { throw new Error( `genesis states XDPoS.v2.switchBlock ${shared.switchBlock}; the converter ` + @@ -723,6 +562,8 @@ function buildEngineParams(cfg, opts) { ); } + // normalizeAddress() returns undefined for an absent address, which would + // drop the key; Nethermind has no default for it and fails to load. if (shared.foundationWalletAddr === undefined) { throw new Error( 'genesis states no XDPoS.foudationWalletAddr (nor foundationWalletAddr); ' + @@ -730,31 +571,29 @@ function buildEngineParams(cfg, opts) { ); } + // The default table is spread in whole rather than restated key by key, so a + // value lives in exactly one place and the table fixes the emitted key order. + // It is a fallback, never a winner: both passes below re-assign keys the + // spread already created, so genesis always beats it and key order holds. + const engineDefaults = opts.subnet ? DEFAULT_ENGINE_SUBNET : DEFAULT_ENGINE; const engineParams = { ...shared, ...engineDefaults }; - // 1. chain data off genesis.config.XDPoS, which keeps its name. A table key of - // the same name (e.g. switchEpoch) backstops it rather than overwriting it: - // the raw spread alone would let the constant win and silently replace, say, - // a switchEpoch of 900 with 0, putting the two clients on different epochs. + // 1. chain data, which keeps its genesis name. A table key of the same name + // (e.g. switchEpoch) backstops it rather than overwriting it -- the raw + // spread alone would let the constant replace a stated switchEpoch of 900 + // with 0, putting the two clients on different epochs. for (const key of Object.keys(engineDefaults)) { if (key in shared) { engineParams[key] = pick(shared[key], engineDefaults[key]); } } - // 2. flat genesis.config keys, which are named differently on each side + // 2. flat genesis.config keys, which are spelled differently on each side for (const [key, genesisKey] of Object.entries(ENGINE_GENESIS_KEYS)) { engineParams[key] = pick(cfg[genesisKey], engineDefaults[key]); } - // XDC hardfork blocks carried straight through. Absent in genesis => absent - // from the chainspec, so the fork stays off rather than activating at 0. - // Emitted for BOTH engines: XdcSubnetChainSpecEngineParameters derives from - // XdcChainSpecEngineParameters and overrides only SealEngineType, so it - // inherits every one of these properties and binds them the same way. - // TIPUpgradeReward is why this matters -- Nethermind reads - // IsTipUpgradeRewardEnabled = (TipUpgradeReward ?? ulong.MaxValue) <= - // releaseStartBlock, so a dropped key is not "off", it is "never enabled", - // and the two clients then disagree at the first reward checkpoint. + // 3. XDC hardfork blocks, for both engines. No fallback by design: absent in + // genesis means absent here, which is how the fork stays off (rule 3). for (const [key, genesisKey] of Object.entries(ENGINE_FORKS)) { engineParams[key] = cfg[genesisKey]; } @@ -832,18 +671,16 @@ function pickMaxCodeSize(cfg, maxCodeSizeTransition, paramDefaults, warn) { return paramDefaults.MaxCodeSize; } -// genesis: the genesis block header, carried through verbatim except for the -// base fee, which genesis.json leaves null. +// The genesis block header. Most fields go across verbatim; nonce, mixHash and +// coinbase are reshaped into the form ChainSpecLoader reads, and baseFeePerGas +// gets a concrete value where genesis.json has null. function buildGenesisBlock(genesis, opts) { return { - // Nethermind reads the nonce and mixHash out of seal.ethereum, not off the - // genesis object: ChainSpecLoader.cs:356-357 takes - // Genesis.Seal?.Ethereum?.Nonce ?? 0 and ?.MixHash ?? Keccak.Zero, and - // ChainSpecGenesisJson declares neither at the top level. Emitting them - // there meant the node silently used 0 and Keccak.Zero instead. + // ChainSpecGenesisJson declares none of nonce, mixHash or coinbase at the + // top level. ChainSpecLoader.cs:356-357 reads the first two from + // Seal.Ethereum (defaulting to 0 and Keccak.Zero) and :369 reads the + // beneficiary from Author. Emitted flat, all three were silently ignored. seal: { ethereum: { nonce: genesis.nonce, mixHash: genesis.mixHash } }, - // likewise the beneficiary: ChainSpecLoader.cs:369 reads Genesis.Author, - // and there is no "coinbase" on ChainSpecGenesisJson author: normalizeAddress(genesis.coinbase), timestamp: genesis.timestamp, extraData: genesis.extraData, @@ -851,9 +688,8 @@ function buildGenesisBlock(genesis, opts) { difficulty: genesis.difficulty, parentHash: genesis.parentHash, baseFeePerGas: pick(genesis.baseFeePerGas, DEFAULT_BASE_FEE_PER_GAS), - // number and gasUsed have no chainspec equivalent and are not emitted: - // genesis is always block 0 with no gas used, and ChainSpecLoader hardcodes - // the number to 0. + // number and gasUsed are not emitted: genesis is always block 0 with no gas + // used, and ChainSpecLoader hardcodes the number anyway. }; } @@ -873,10 +709,8 @@ function translate(genesis, opts = {}) { ? (message) => opts.warnings.push(message) : (message) => console.error(message); - // Subnet nodes run a different consensus plugin than a standalone XDPoS - // network, and Nethermind selects it by this key. The mapping notes above - // spell it "engine.XDPoS" throughout; with opts.subnet the whole block is - // named engine.XDPoSSubnet instead, contents unchanged. + // Subnet nodes run a different consensus plugin, which Nethermind selects by + // this key alone -- the block's contents are the same either way. const engineName = opts.subnet ? 'XDPoSSubnet' : 'XDPoS'; return { From b9615c605b3c7583945acd708902917a49c134ad Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 21:44:34 +0700 Subject: [PATCH 53/59] fix(container-manager): don't fail an all-Go XDPoS deployment on chainspec generation generate() has gated chainspec failure on nethermindCount since the subnet client was integrated (3deb7b5) -- an all-Go subnet never reads the chainspec, so a failure there must not block the deployment. generateXdpos() never got the same gate and failed the deployment unconditionally, even with NUM_NETHERMIND=0. This pairs with 7028294, which stopped docker-up.sh running the pre-check when no service mounts /work/chainspec.json. That half was right: verified on the two generated deployments in the tree, an all-Go compose resolves to zero chainspec targets and skips the check, while one with two Nethermind nodes resolves to two and runs it. Generation was the half still able to stop an all-Go network over a file nothing mounts. generateXdpos() now counts Nethermind nodes exactly as genGenXdposEnv records NUM_NETHERMIND -- keyed on customversion-checkbox, where the subnet path keys on customclient-checkbox -- and only fails when one was actually requested. When it is not fatal, the failure now goes into the warnings array on both paths instead of the container log alone, so the operator sees why there is no chainspec on the success page rather than discovering it when they later add a Nethermind node. Not reachable with a normally generated genesis -- every genesis in the tree still converts -- but D5, D6 and D7 added three deliberate throws to the converter, so the asymmetry had more ways to bite than before. Verified: both paths now resolve identically across five input shapes (no checkbox, checkbox with count 0, count set but checkbox off, and counts 1 and 3), and the non-fatal warning renders on both success pages. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/libs/exec.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/container-manager/src/libs/exec.js b/container-manager/src/libs/exec.js index b036e15..8b34cc3 100644 --- a/container-manager/src/libs/exec.js +++ b/container-manager/src/libs/exec.js @@ -193,7 +193,15 @@ function generateXdpos(params) { } //step 3: convert genesis.json -> chainspec.json. Nethermind nodes mount it, - //but generate it unconditionally so it is always available alongside genesis.json. + //but generate it unconditionally so it is always available alongside + //genesis.json. Only fatal when a Nethermind node was actually asked for -- an + //all-Go network never reads the chainspec, so a failure there must not block + //the deployment. Counted exactly as genGenXdposEnv records NUM_NETHERMIND. + const nethermindCount = parseInt( + ("customversion-checkbox" in params && params["customversion-checkbox"] != "" + ? params["customversion-xdpos-nethermind-count"] + : 0) || 0 + ); const warnings = []; try { const { translate } = require("./genesis-to-chainspec"); @@ -215,7 +223,15 @@ function generateXdpos(params) { console.log("chainspec.json generated"); } catch (e) { console.error("chainspec generation failed:", e.message); - return [false, `chainspec generation failed: ${e.message}`]; + if (nethermindCount > 0) { + return [false, `chainspec generation failed: ${e.message}`]; + } + // no Nethermind node to read it; surface it rather than fail the deployment + warnings.push( + `chainspec.json was not generated: ${e.message} ` + + '(no Nethermind node was requested, so nothing reads it and the ' + + 'deployment continues; add one only after fixing this)' + ); } return [result2, out2, warnings]; } @@ -295,6 +311,12 @@ function generate(params) { if (nethermindCount > 0) { return [false, `chainspec generation failed: ${e.message}`]; } + // no Nethermind node to read it; surface it rather than fail the deployment + warnings.push( + `chainspec.json was not generated: ${e.message} ` + + '(no Nethermind node was requested, so nothing reads it and the ' + + 'deployment continues; add one only after fixing this)' + ); } return [result2, out2, warnings]; } From c79e978574df8d25b97375bf7f0789ed3e85ad86 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 23:02:34 +0700 Subject: [PATCH 54/59] feat(deploy): add scripts/reset-chain.sh to wipe chain data after confirmation Stops the containers, then deletes every xdcchain* data directory, so the next docker-up.sh starts the chain from block 0. Keys, genesis.json, chainspec.json and the env files are left alone. It does nothing until the operator types exactly Y -- lowercase y, yes and everything else cancel without stopping or deleting anything. Before asking, it prints the deployment path, the network name, the chain id, the chainspec name, the current head block, and the absolute path and size of every directory it would remove, plus the chain data and whole deployment totals, so the answer is given against what will actually go. The head block is cross-checked against the chain id. RPC ports are host-wide, so a node answering on 8545 need not belong to this deployment; only nodes whose eth_chainId matches genesis are counted, and any that do not are reported as a warning to check the directory. Without that, an unrelated chain on the same machine reads as this one's height. Deleting by glob, so the target is pinned rather than assumed. The data directories are defined as exactly one level above the script: - the script's own directory is resolved with pwd -P and must be named "scripts", so a copy left anywhere else refuses outright - the deployment root is its parent and must hold both docker-compose.yml and genesis.json; never / or $HOME - targets must be direct children of that root, real directories, and still inside it once resolved -- a symlinked xdcchain* is listed under "Not touching" and never followed - everything is deleted by absolute path, not a relative glob - if stopping the containers fails, nothing is deleted So it operates on its own deployment whatever the working directory is. Works on both deployment shapes: an XDPoS deployment gets docker-down.sh at its root and that is used; a subnet one does not, so it falls back to compose directly, taking the single profile when there is one and requiring it as an argument when there are several. Copied into generated deployments by gen.js and gen_xdpos.js. Verified against fake deployments of both shapes: n / y / yes all cancel with the data intact; Y proceeds; a failing docker-down deletes nothing and exits with its code; no chain data exits 0 before prompting; a missing docker-compose.yml or a script outside scripts/ refuses; a symlink pointing outside is skipped with its target untouched; and run by absolute path from a directory that itself contained xdcchain1 and xdcchain9, it removed the deployment's directory and left both decoys. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/gen.js | 6 + container-manager/src/gen/gen_xdpos.js | 6 + .../src/gen/scripts/reset-chain.sh | 247 ++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100755 container-manager/src/gen/scripts/reset-chain.sh diff --git a/container-manager/src/gen/gen.js b/container-manager/src/gen/gen.js index 7d1667e..deb27f7 100644 --- a/container-manager/src/gen/gen.js +++ b/container-manager/src/gen/gen.js @@ -206,6 +206,12 @@ function copyScripts(output_dir) { `${__dirname}/scripts/add-node.sh`, `${output_dir}/scripts/add-node.sh` ); + // stop the containers and delete every xdcchain* data directory; prompts + // before it touches anything + fs.copyFileSync( + `${__dirname}/scripts/reset-chain.sh`, + `${output_dir}/scripts/reset-chain.sh` + ); // shared Nethermind config mounted by every nmc node, copied unconditionally // so it is always available (chainspec.json is produced separately from // genesis.json after puppeth runs). The subnet build differs from the diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 2715c9f..4370521 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -178,6 +178,12 @@ function copyScripts(output_dir) { `${__dirname}/scripts/check-peer.sh`, `${output_dir}/scripts/check-peer.sh` ); + // stop the containers and delete every xdcchain* data directory; prompts + // before it touches anything + fs.copyFileSync( + `${__dirname}/scripts/reset-chain.sh`, + `${output_dir}/scripts/reset-chain.sh` + ); fs.copyFileSync( `${__dirname}/scripts/docker-up.sh`, `${output_dir}/docker-up.sh` diff --git a/container-manager/src/gen/scripts/reset-chain.sh b/container-manager/src/gen/scripts/reset-chain.sh new file mode 100755 index 0000000..e83da44 --- /dev/null +++ b/container-manager/src/gen/scripts/reset-chain.sh @@ -0,0 +1,247 @@ +#!/bin/bash +# Reset the chain: stop the containers, then delete every node's data directory +# (xdcchain*) in this deployment. The next docker-up.sh starts again from +# block 0. +# +# Destructive and not undoable. Keys, genesis.json, chainspec.json and the env +# files are kept -- only the chain data goes -- but every block, every account +# balance and every deployed contract goes with it. +# +# Usage: ./scripts/reset-chain.sh [PROFILE] +# PROFILE is passed to docker-down.sh / docker compose, and is only needed on +# a deployment with more than one. + +# -------------------------------------------------------------------------- +# Locate the deployment, strictly. +# +# This script deletes by glob, so it must be certain which directory it is +# pointed at. The rule: the data directories are always exactly one level above +# this script, i.e. this file lives at /scripts/reset-chain.sh and +# deletes /xdcchain*. Anything that does not match that shape is +# refused rather than guessed at. +# +# pwd -P resolves symlinks, so the path checked below is the real one. +# -------------------------------------------------------------------------- +script_dir=$(cd "$(dirname "$0")" && pwd -P) || exit 2 +root=$(dirname "$script_dir") + +if [[ $(basename "$script_dir") != "scripts" ]]; then + echo "Error: this script must live in /scripts/, but it is in:" + echo " $script_dir" + echo "Refusing to guess which directory to delete from." + exit 2 +fi + +if [[ "$root" == "/" || "$root" == "$HOME" ]]; then + echo "Error: refusing to operate on $root" + exit 2 +fi + +# A generated deployment has both of these. Without them this is not one. +for required in docker-compose.yml genesis.json; do + if [[ ! -f "$root/$required" ]]; then + echo "Error: $root/$required not found, so this is not a generated deployment." + echo "Refusing to delete anything." + exit 2 + fi +done + +cd "$root" || exit 2 + +banner() { + local rule="======================================================================" + echo "" + echo "$rule" + printf ' %s\n' "$@" + echo "$rule" + echo "" +} + +compose="docker-compose" +if ! which docker-compose > /dev/null 2>&1; then + compose="docker compose" +fi + +# -------------------------------------------------------------------------- +# Collect the targets. Direct children of $root only: nullglob so an unmatched +# pattern expands to nothing rather than the literal string, real directories +# only, and each one's resolved path must still be inside $root -- a symlinked +# xdcchain* pointing elsewhere is reported and skipped, never followed. +# -------------------------------------------------------------------------- +shopt -s nullglob +candidates=(xdcchain*) +shopt -u nullglob + +datadirs=() +skipped=() +for d in "${candidates[@]}"; do + if [[ -L "$d" ]]; then + skipped+=("$d (symlink)") + continue + fi + if [[ ! -d "$d" ]]; then + skipped+=("$d (not a directory)") + continue + fi + resolved=$(cd "$d" && pwd -P) + if [[ "$resolved" != "$root/$d" ]]; then + skipped+=("$d (resolves outside the deployment: $resolved)") + continue + fi + datadirs+=("$d") +done + +if [[ ${#skipped[@]} -gt 0 ]]; then + echo "Not touching:" + printf ' %s\n' "${skipped[@]}" +fi + +if [[ ${#datadirs[@]} == 0 ]]; then + echo "No xdcchain* data directories in $root; nothing to reset." + exit 0 +fi + +# -------------------------------------------------------------------------- +# Identify the chain, so the operator can see which one they are about to wipe. +# grep/sed only: jq and python are not guaranteed on a deployment host. +# -------------------------------------------------------------------------- +chain_name=$(grep -E '^NETWORK_NAME=' gen.env 2>/dev/null | tail -1 | cut -d '=' -f 2-) +chain_id=$(grep -oE '"chainId"[[:space:]]*:[[:space:]]*[0-9]+' genesis.json 2>/dev/null \ + | head -1 | grep -oE '[0-9]+$') +spec_name=$(grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]*"' chainspec.json 2>/dev/null \ + | head -1 | sed -E 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]*)"/\1/') + +# Head block, best effort. Node i publishes RPC on 8544+i, so probe one port per +# data directory and take the highest answer. A stopped chain answers nothing, +# which is normal here -- this is a sanity check, not a precondition. +parse_block_number() { + local raw + raw=$(printf '%s' "$1" \ + | grep -oiE '"(number|result)"[[:space:]]*:[[:space:]]*"?(0x[0-9a-fA-F]+|[0-9]+)"?' \ + | head -1 | sed -E 's/.*:[[:space:]]*"?//; s/"$//') + [[ -z "$raw" ]] && return 1 + printf '%d' "$raw" 2>/dev/null || return 1 +} + +rpc() { + curl -s -m 3 --location "http://localhost:$1" \ + --header 'Content-Type: application/json' \ + --data "{\"jsonrpc\":\"2.0\",\"method\":\"$2\",\"params\":[],\"id\":1}" 2>/dev/null +} + +# Ports are host-wide, so a node answering on 8545 is not necessarily a node of +# THIS deployment -- another chain on the same machine would answer too, and +# reporting its head block here would be actively misleading. Only count a node +# whose eth_chainId matches this genesis, and say so when one does not. +head_block="" +responding=0 +foreign=0 +for ((i = 1; i <= ${#datadirs[@]}; i++)); do + port=$((8544 + i)) + node_chain=$(parse_block_number "$(rpc "$port" eth_chainId)") || continue + if [[ -n "$chain_id" && "$node_chain" != "$chain_id" ]]; then + foreign=$((foreign + 1)) + continue + fi + num=$(parse_block_number "$(rpc "$port" eth_blockNumber)") || continue + responding=$((responding + 1)) + if [[ -z "$head_block" || "$num" -gt "$head_block" ]]; then + head_block="$num" + fi +done + +if [[ -n "$head_block" ]]; then + block_line="$head_block (from $responding node(s) on chain id $chain_id)" +else + block_line="unknown - no node of this chain answered on 8545-$((8544 + ${#datadirs[@]})), it looks stopped" +fi +if [[ $foreign -gt 0 ]]; then + block_line="$block_line + NOTE: $foreign node(s) on those ports belong to a DIFFERENT chain id; + check you are in the right deployment directory" +fi + +# -------------------------------------------------------------------------- +# Show exactly what goes, then ask. +# -------------------------------------------------------------------------- +echo "" +echo "Chain to be reset" +echo " deployment : $root" +echo " name : ${chain_name:-}" +echo " chain id : ${chain_id:-}" +echo " chainspec : ${spec_name:-}" +echo " head block : $block_line" +echo "" +echo "Directories to be deleted (${#datadirs[@]}):" +for d in "${datadirs[@]}"; do + printf ' %s %s\n' "$(du -sh "$d" 2>/dev/null | cut -f1 | xargs)" "$root/$d" +done +echo "" +echo " chain data total : $(du -sch "${datadirs[@]}" 2>/dev/null | tail -1 | cut -f1 | xargs)" +echo " deployment total : $(du -sh "$root" 2>/dev/null | cut -f1 | xargs)" + +banner \ + "This deletes the chain data listed above." \ + "" \ + "Every block, balance and deployed contract is lost." \ + "Keys, genesis.json, chainspec.json and the env files are kept." \ + "" \ + "This cannot be undone." + +read -r -p "Type Y to proceed, anything else to cancel: " reply +if [[ $reply != "Y" ]]; then + echo "Cancelled. Nothing was stopped and nothing was deleted." + exit 1 +fi + +# -------------------------------------------------------------------------- +# Stop first. Deleting a running node's data directory leaves it writing into a +# path that no longer exists. +# -------------------------------------------------------------------------- +echo "" +echo "Stopping containers..." +if [[ -f docker-down.sh ]]; then + bash docker-down.sh "$@" +else + # a subnet deployment gets no docker-down.sh, so drive compose directly + profile="$1" + if [[ -z $profile ]]; then + profiles=($($compose config --profiles)) + if [[ ${#profiles[@]} == 1 ]]; then + profile="${profiles[0]}" + elif [[ ${#profiles[@]} == 0 ]]; then + echo "Error: no profile found in docker-compose.yml, cannot stop the containers." + echo "NOTHING WAS DELETED." + exit 2 + else + echo "Error: this deployment has more than one profile; name the one to reset." + echo "Usage: ./scripts/reset-chain.sh PROFILE" + echo "Profiles: ${profiles[*]}" + echo "NOTHING WAS DELETED." + exit 2 + fi + fi + $compose --profile "$profile" down +fi + +down_result=$? +if [[ $down_result != 0 ]]; then + banner \ + "Failed to stop the containers (exit $down_result)." \ + "" \ + "NOTHING WAS DELETED." \ + "Stop them by hand, then run this again." + exit $down_result +fi + +echo "" +echo "Removing chain data..." +for d in "${datadirs[@]}"; do + echo " $root/$d" + rm -rf -- "$root/$d" +done + +banner \ + "Reset complete. ${#datadirs[@]} directory(ies) removed from $root" \ + "" \ + "Bring the chain back up from block 0 with ./docker-up.sh" From 333dc97a7f45f127a96a049c9a630a7c4d249d46 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Mon, 21 Sep 2026 23:08:48 +0700 Subject: [PATCH 55/59] revert(deploy): go back to a hardcoded --log=debug Reverts D11 (52a4dfa), which lowered the Nethermind nodes to --log=info, and drops the per-node NMC_LOG_LEVEL mechanism built on top of it. Nethermind exposes the level only through --log, a command-line option with no config property behind it, and Init.LogRules, whose value is Pattern:Level pairs and which throws at startup if given a bare level. Neither makes a good per-node knob in a generated deployment, so the compose command carries the level directly again, as it did originally. The D11 finding still stands -- debug is tens of thousands of lines in twenty minutes on a healthy node -- so change this line to info once the current debugging is done. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/gen_compose.js | 2 +- container-manager/src/gen/gen_xdpos.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/container-manager/src/gen/gen_compose.js b/container-manager/src/gen/gen_compose.js index 7cd3733..26fb451 100644 --- a/container-manager/src/gen/gen_compose.js +++ b/container-manager/src/gen/gen_compose.js @@ -50,7 +50,7 @@ function genSubnetNodes(machine_id, num, start_num = 1) { command: [ "--config=/work/xdc-nmc.json", "--datadir=/work/xdcchain", - "--log=info", + "--log=debug", ], profiles: [compose_profile], ports: port_mappings, diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 4370521..10f093b 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -450,7 +450,7 @@ function genXdposCompose(machine_id, num, start_num = 1) { command: [ "--config=/work/xdc-nmc.json", "--datadir=/work/xdcchain", - "--log=info", + "--log=debug", ], profiles: [compose_profile], ports: port_mappings, From c99f68aeb5880ca22ffdc6094dbe668624fa56ab Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Fri, 25 Sep 2026 16:56:10 +0700 Subject: [PATCH 56/59] fix(deploy): reset-chain.sh works on a subnet deployment Two bugs, both hit on the first real subnet run. It demanded a profile. A subnet has three (machine1, services, subswap), so it refused with "name the one to reset" and did nothing. Naming one would have been wrong anyway: `down` with a single --profile leaves the other profiles' containers running, and a running node whose data directory has just been deleted keeps writing into a path that no longer exists. It now takes no argument and stops every configured profile, passing one --profile flag per entry from `config --profiles` -- verified that plain `down` with no --profile leaves profiled services up, so they have to be named. It did not export HOSTPWD. A subnet docker-compose.yml interpolates it into every volume path, so each compose call printed "The HOSTPWD variable is not set" once per service and resolved the paths to /xdcchain1 rather than the deployment's. It is now exported as the deployment root, which is what it means. The docker-down.sh branch goes with the profile argument: that script only resolves a profile and calls compose, which this now does directly and for every profile, so both deployment shapes take one path. Also corrects two messages that told the operator to restart with ./docker-up.sh, which a subnet deployment does not have -- it starts from the wizard. Verified on a subnet-shaped deployment with four containers across three profiles: all four stopped, both data directories removed, genesis.json, chainspec.json and gen.env kept, and no HOSTPWD warning. The single-profile XDPoS shape still resets with no argument, and the cancel-unless-Y path is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/gen/scripts/reset-chain.sh | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/container-manager/src/gen/scripts/reset-chain.sh b/container-manager/src/gen/scripts/reset-chain.sh index e83da44..163f31f 100755 --- a/container-manager/src/gen/scripts/reset-chain.sh +++ b/container-manager/src/gen/scripts/reset-chain.sh @@ -1,15 +1,16 @@ #!/bin/bash # Reset the chain: stop the containers, then delete every node's data directory -# (xdcchain*) in this deployment. The next docker-up.sh starts again from +# (xdcchain*) in this deployment. Starting the chain again brings it up from # block 0. # # Destructive and not undoable. Keys, genesis.json, chainspec.json and the env # files are kept -- only the chain data goes -- but every block, every account # balance and every deployed contract goes with it. # -# Usage: ./scripts/reset-chain.sh [PROFILE] -# PROFILE is passed to docker-down.sh / docker compose, and is only needed on -# a deployment with more than one. +# Usage: ./scripts/reset-chain.sh +# Takes no arguments: every profile in this deployment is stopped, since the +# chain data is shared and leaving any node running would have it writing into +# a directory that is about to be deleted. # -------------------------------------------------------------------------- # Locate the deployment, strictly. @@ -62,6 +63,12 @@ if ! which docker-compose > /dev/null 2>&1; then compose="docker compose" fi +# A subnet docker-compose.yml interpolates ${HOSTPWD} into every volume path. +# It is normally exported by whatever starts the chain; export it here too, or +# compose warns once per service and resolves the paths to /xdcchain1 etc. +# $root is the deployment directory on the host, which is exactly what it means. +export HOSTPWD="$root" + # -------------------------------------------------------------------------- # Collect the targets. Direct children of $root only: nullglob so an unmatched # pattern expands to nothing rather than the literal string, real directories @@ -196,33 +203,16 @@ fi # -------------------------------------------------------------------------- # Stop first. Deleting a running node's data directory leaves it writing into a -# path that no longer exists. -# -------------------------------------------------------------------------- +# path that no longer exists. Every profile is stopped, not one: `down` with no +# --profile leaves profiled services running, so each configured profile is +# passed explicitly. echo "" echo "Stopping containers..." -if [[ -f docker-down.sh ]]; then - bash docker-down.sh "$@" -else - # a subnet deployment gets no docker-down.sh, so drive compose directly - profile="$1" - if [[ -z $profile ]]; then - profiles=($($compose config --profiles)) - if [[ ${#profiles[@]} == 1 ]]; then - profile="${profiles[0]}" - elif [[ ${#profiles[@]} == 0 ]]; then - echo "Error: no profile found in docker-compose.yml, cannot stop the containers." - echo "NOTHING WAS DELETED." - exit 2 - else - echo "Error: this deployment has more than one profile; name the one to reset." - echo "Usage: ./scripts/reset-chain.sh PROFILE" - echo "Profiles: ${profiles[*]}" - echo "NOTHING WAS DELETED." - exit 2 - fi - fi - $compose --profile "$profile" down -fi +profile_flags=() +for p in $($compose config --profiles 2>/dev/null); do + profile_flags+=(--profile "$p") +done +$compose "${profile_flags[@]}" down down_result=$? if [[ $down_result != 0 ]]; then @@ -244,4 +234,5 @@ done banner \ "Reset complete. ${#datadirs[@]} directory(ies) removed from $root" \ "" \ - "Bring the chain back up from block 0 with ./docker-up.sh" + "Start the chain again to bring it up from block 0:" \ + " ./docker-up.sh, or the Start button in the wizard." From 60c4cf9ec1beb1900d854d569053252f71902bd8 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Fri, 25 Sep 2026 17:35:40 +0700 Subject: [PATCH 57/59] fix(container-manager): ship a bootnode.key so the generated enode matches gen hardcoded the bootnode's enode id on the claim that "the bootnode container always starts from the same bootnode.key". No key was ever shipped. start-bootnode.sh only writes bootnode.key when it is handed a key through PRIVATE_KEY or PRIVATE_KEY_FILE, so it fell through to `bootnode -genkey`, and that key lands in the container's /work, which is not the mounted volume -- a fresh identity on every recreate. Every node was therefore configured with an enode nothing answered to. On a four-node subnet each one logged "Online bootnodes: 0" every 31s and never validated the bootnode, so the mesh was whatever the nodes caught from NEIGHBORS in the first seconds: node1 and node2 each held a single link to node4, and node3 lost its three entries within 30s and sat at block 0 with no peers. Being a validator, its leader slot then timed out every fourth round. Generate the key in config_gen, write it to bootnodes/bootnode.key, and derive the enode id from it -- an enode id is the uncompressed secp256k1 public key minus its 0x04 prefix -- so the two cannot drift. The bootnode reads the key back through PRIVATE_KEY_FILE, set on the service rather than in common.env, which the relayer, stats and frontend containers also read and where PRIVATE_KEY is a generic enough name to be picked up. BOOTNODE_PK reuses an existing bootnode identity. gen_xdpos carried the same hardcoded enode in two more places, for bootnodes.list and for each masternode env file; both now go through gen_env.bootnodeEnode, which is exported for that. Verified against go-ethereum's own `bootnode -writeaddress`, not just ethers: on the subnet path the written key derives to exactly the enode in all four subnet*nmc.env, on the XDPoS path to the one in bootnodes.list and masternode*nmc.env, and BOOTNODE_PK=0x662e81df... reproduces e902b89c... identically across runs. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/config_gen.js | 21 ++++++++++++++++++ container-manager/src/gen/gen.js | 9 ++++++++ container-manager/src/gen/gen_compose.js | 5 +++++ container-manager/src/gen/gen_env.js | 12 +++++++---- container-manager/src/gen/gen_xdpos.js | 27 ++++++++++++++++-------- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/container-manager/src/gen/config_gen.js b/container-manager/src/gen/config_gen.js index 7a3484b..4506a89 100644 --- a/container-manager/src/gen/config_gen.js +++ b/container-manager/src/gen/config_gen.js @@ -11,6 +11,26 @@ dotenv.config({ override: true, }); +// The bootnode's identity has to be fixed: start-bootnode.sh only runs +// `bootnode -genkey` when it is handed no key, and the key it generates lands +// in the container's /work, which is not the mounted volume. So without a key +// of our own the bootnode came up with a fresh enode on every recreate while +// the enode written into every node's env file stayed as it was — no node ever +// matched the bootnode that was actually running. Set BOOTNODE_PK to reuse an +// existing bootnode identity across regenerations. +function bootnodePrivateKey(raw) { + if (!raw) { + return crypto.randomBytes(32).toString("hex"); + } + // go-ethereum's LoadECDSA wants bare lowercase hex, no 0x + const key = raw.trim().replace(/^0x/i, "").toLowerCase(); + if (!/^[0-9a-f]{64}$/.test(key)) { + console.log("Invalid BOOTNODE_PK, expected 64 hex characters"); + process.exit(1); + } + return key; +} + const config = { deployment_path: process.env.CONFIG_PATH || "", num_machines: parseInt(process.env.NUM_MACHINE), @@ -24,6 +44,7 @@ const config = { ), secret_string: process.env.SERVICES_SECRET || crypto.randomBytes(10).toString("hex"), + bootnode_pk: bootnodePrivateKey(process.env.BOOTNODE_PK), relayer_mode: process.env.RELAYER_MODE || "full", docker_image_name: process.env.IMAGE_NAME || "xinfinorg/subnet-generator:generator-v3.1.0", diff --git a/container-manager/src/gen/gen.js b/container-manager/src/gen/gen.js index deb27f7..b83850b 100644 --- a/container-manager/src/gen/gen.js +++ b/container-manager/src/gen/gen.js @@ -132,6 +132,15 @@ function writeGenerated(output_dir) { } ); + // Pins the bootnode's identity so the enode written into every node's env + // file keeps matching it; the bootnode reads this via PRIVATE_KEY_FILE. + fs.mkdirSync(`${output_dir}/bootnodes`, { recursive: true }); + fs.writeFileSync( + `${output_dir}/bootnodes/bootnode.key`, + `${config.bootnode_pk}\n`, + { mode: 0o600 } + ); + fs.writeFileSync(`${output_dir}/common.env`, commonconf, (err) => { if (err) { console.error(err); diff --git a/container-manager/src/gen/gen_compose.js b/container-manager/src/gen/gen_compose.js index 26fb451..13f4a67 100644 --- a/container-manager/src/gen/gen_compose.js +++ b/container-manager/src/gen/gen_compose.js @@ -82,6 +82,11 @@ function genBootNode(machine_id) { volumes: ["${HOSTPWD}/bootnodes:/work/bootnodes"], entrypoint: ["bash", "/work/start-bootnode.sh"], command: ["-verbosity", "6", "-nodekey", "bootnode.key"], + // start-bootnode.sh copies PRIVATE_KEY_FILE into the bootnode.key it runs + // with, so the bootnode keeps the identity gen derived every node's enode + // from. Set here rather than in common.env, which the relayer, stats and + // frontend containers also read. + environment: ["PRIVATE_KEY_FILE=/work/bootnodes/bootnode.key"], ports: ["20301:20301/tcp", "20301:20301/udp"], profiles: [machine], }; diff --git a/container-manager/src/gen/gen_env.js b/container-manager/src/gen/gen_env.js index bf378cb..7c9f508 100644 --- a/container-manager/src/gen/gen_env.js +++ b/container-manager/src/gen/gen_env.js @@ -1,3 +1,4 @@ +const ethers = require("ethers"); const configModule = require("./config_gen"); const config = configModule.config; Object.freeze(config); @@ -7,12 +8,15 @@ module.exports = { genNethermindSubnetConfig, genServicesConfig, genContractDeployEnv, + bootnodeEnode, }; -// The bootnode container always starts from the same bootnode.key, so its enode -// id is fixed; only the host it is reached on changes. -const BOOTNODE_ENODE_ID = - "cc566d1033f21c7eb0eb9f403bb651f3949b5f63b40683917765c343f9c0c596e9cd021e2e8416908cbc3ab7d6f6671a83c85f7b121c1872f8be50a591723a5d"; +// An enode id is the uncompressed secp256k1 public key without its 0x04 prefix. +// Deriving it from the bootnode.key gen writes is what keeps the two in step — +// hardcoding the id let it drift from whatever key the bootnode came up with. +const BOOTNODE_ENODE_ID = new ethers.SigningKey( + `0x${config.bootnode_pk}` +).publicKey.slice(4); function bootnodeEnode(ip_record) { const bootnode_ip = diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index 10f093b..dd6ef24 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -55,7 +55,13 @@ doc["services"]["bootnode"] = { entrypoint: ["bash", "/work/start-bootnode.sh"], command: ["-verbosity", "6", "-nodekey", "bootnode.key"], ports: ["20301:20301/tcp", "20301:20301/udp"], - environment: ["BOOTNODE_PORT=20301"], + // PRIVATE_KEY_FILE is what start-bootnode.sh builds bootnode.key from, so + // the bootnode keeps the identity the enodes below were derived from + // instead of generating a throwaway key on every recreate. + environment: [ + "BOOTNODE_PORT=20301", + "PRIVATE_KEY_FILE=/work/bootnodes/bootnode.key", + ], profiles: ["machine1"], }; @@ -65,9 +71,7 @@ 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`; +bootnode = `${gen_env.bootnodeEnode(ip_record)}\n`; subnetconf = []; for (let i = 1; i <= config.num_subnet; i++) { @@ -119,6 +123,15 @@ function writeGenerated(output_dir) { } }); + // Pins the bootnode's identity so the enode above keeps matching it; the + // bootnode reads this via PRIVATE_KEY_FILE. + fs.mkdirSync(`${output_dir}/bootnodes`, { recursive: true }); + fs.writeFileSync( + `${output_dir}/bootnodes/bootnode.key`, + `${config.bootnode_pk}\n`, + { mode: 0o600 } + ); + fs.writeFileSync( `${output_dir}/docker-compose.yml`, compose_content, @@ -359,11 +372,7 @@ function genXdposNodeConfig(subnet_id, key, ip_record) { const port = 20303 + subnet_id - 1; const rpcport = 8545 + subnet_id - 1; const wsport = 9555 + subnet_id - 1; - const bootnode_ip = - config.num_machines === 1 ? ip_record["bootnode"] : config.ip_1; - bootnode = `enode://cc566d1033f21c7eb0eb9f403bb651f3949b5f63b40683917\ -765c343f9c0c596e9cd021e2e8416908cbc3ab7d6f6671a83c85f7b121c1872f8be\ -50a591723a5d@${bootnode_ip}:20301\n`; + bootnode = `${gen_env.bootnodeEnode(ip_record)}\n`; const stats_ip = config.num_machines === 1 ? ip_record["stats"] : config.ip_1; const config_env = ` INSTANCE_NAME=Masternode${subnet_id} From c350da8e6a4c75027763c2a87c3943b63e56d643 Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Fri, 25 Sep 2026 18:01:00 +0700 Subject: [PATCH 58/59] feat(deploy): add --yes to reset-chain.sh Resetting repeatedly during a peering investigation means answering the same prompt every time, and `./scripts/reset-chain.sh -Y` looked like it did something: the script took no arguments and ignored what it was given, so it printed the summary and sat at the prompt anyway. -y, -Y or --yes skips the prompt. -Y as well as -y because Y is the literal reply the prompt asks for, so it is the spelling a hand reaches for first. An unrecognised argument is now refused with exit 2 rather than ignored, which is what made the original mistake invisible, and the parsing runs ahead of every guard so a typo costs a millisecond instead of a full summary. --yes replaces the prompt and nothing else. The deployment-shape check, the / and $HOME refusal, the docker-compose.yml and genesis.json requirement, and the symlink and resolves-outside-root skipping all stay unconditional, and the summary is still printed -- it is the record of what was destroyed. The one exception is the foreign-chain notice. It fires when a node answering on this deployment's ports reports a different chain id, and it is addressed to whoever is reading. Unattended there is no one, so --yes stops on it instead of rolling past, with no override flag: the recourse is an interactive run, which shows the same notice and lets it be judged. Deleting is still bounded to $root/xdcchain* either way, so this is about not discarding a signal, not about what is at risk. A piped `echo Y` keeps working, since that is the escape hatch people already have. Reaching EOF without an answer now names --yes rather than reporting a bare "Cancelled". Verified on a throwaway deployment: unknown argument refused with exit 2; -y, -Y and --yes each reset and exit 0; piped Y unchanged; piped n and EOF both leave the data in place with exit 1; a live node of another chain id on 8545 makes --yes exit 2 having stopped and deleted nothing; and the script moved out of scripts/ still refuses under --yes. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/gen/scripts/reset-chain.sh | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/container-manager/src/gen/scripts/reset-chain.sh b/container-manager/src/gen/scripts/reset-chain.sh index 163f31f..bc8f6a6 100755 --- a/container-manager/src/gen/scripts/reset-chain.sh +++ b/container-manager/src/gen/scripts/reset-chain.sh @@ -7,10 +7,40 @@ # files are kept -- only the chain data goes -- but every block, every account # balance and every deployed contract goes with it. # -# Usage: ./scripts/reset-chain.sh -# Takes no arguments: every profile in this deployment is stopped, since the -# chain data is shared and leaving any node running would have it writing into -# a directory that is about to be deleted. +# Usage: ./scripts/reset-chain.sh [-y|--yes] +# -y, --yes Skip the confirmation prompt, for scripted resets. Refuses if a +# node of a different chain id answers on this deployment's ports: +# that notice exists for a human to act on, and --yes means there +# is no human. Re-run interactively to decide with it in view. +# +# Takes no other arguments: every profile in this deployment is stopped, since +# the chain data is shared and leaving any node running would have it writing +# into a directory that is about to be deleted. +# +# Exit codes: 0 reset (or nothing to reset), 1 cancelled, 2 refused or bad usage. + +# -------------------------------------------------------------------------- +# Arguments, before any of the work below: a typo should cost a millisecond, +# not a summary. An unrecognised argument is refused rather than ignored -- +# ignoring one is how `-Y` came to look like it had been accepted while the +# script still sat at the prompt. +# -------------------------------------------------------------------------- +assume_yes=0 +while [[ $# -gt 0 ]]; do + case "$1" in + # -Y as well as -y: Y is the literal reply the prompt asks for, so it is + # the spelling a hand reaches for first. + -y | -Y | --yes) + assume_yes=1 + ;; + *) + echo "Error: unknown argument '$1'" + echo "Usage: $(basename "$0") [-y|--yes]" + exit 2 + ;; + esac + shift +done # -------------------------------------------------------------------------- # Locate the deployment, strictly. @@ -195,8 +225,30 @@ banner \ "" \ "This cannot be undone." -read -r -p "Type Y to proceed, anything else to cancel: " reply -if [[ $reply != "Y" ]]; then +if [[ $assume_yes == 1 ]]; then + # Every guard above still applies; --yes replaces the prompt, nothing else. + # The exception is the foreign-chain notice, which is addressed to whoever is + # reading. Unattended, there is no one, so stop instead of rolling past it. + if [[ $foreign -gt 0 ]]; then + banner \ + "Refusing to reset unattended." \ + "" \ + "$foreign node(s) on 8545-$((8544 + ${#datadirs[@]})) answer for a chain id" \ + "other than this deployment's (${chain_id:-unknown})." \ + "" \ + "NOTHING WAS STOPPED AND NOTHING WAS DELETED." \ + "Re-run without --yes to review that and confirm by hand." + exit 2 + fi + echo "Proceeding without confirmation (--yes)." +elif ! read -r -p "Type Y to proceed, anything else to cancel: " reply; then + # EOF rather than an answer: stdin is closed or empty, so this is a scripted + # run that did not say so. Name the flag instead of a bare "cancelled". + echo "" + echo "No answer (stdin reached end of input). Nothing was stopped or deleted." + echo "Pass --yes to reset without the prompt." + exit 1 +elif [[ $reply != "Y" ]]; then echo "Cancelled. Nothing was stopped and nothing was deleted." exit 1 fi From 71483cb3b2bb8919e3d2ea9f46bd01874dba50ba Mon Sep 17 00:00:00 2001 From: wanwiset25 Date: Fri, 25 Sep 2026 18:28:32 +0700 Subject: [PATCH 59/59] fix(deploy): set FilterPeersByRecentIp from the client mix The right value depends on which clients a deployment runs, so a static one in the xdc-nmc*.json that every node mounts alike was bound to be wrong for half the deployments -- and both files were: xdc-nmc-subnet.json true correct mixed, strands an all-Nethermind net xdc-nmc.json false correct all-Nethermind, churns when mixed All Nethermind needs false. The nodes boot together, learn each other at once through the bootnode and dial simultaneously; the collisions leave every retry suppressed for the filter's five-minute window, and the peer manager sits on "EligibleCandidates: 2, Tried: 0" with nodes at zero peers. On a four-node subnet needing three to agree that is a stalled chain, observed twice: blocks [21,21,21,0] peers [1,2,1,0] on one run, and stuck at block 1 with two nodes isolated on another. Upstream Nethermind disables the filter for Hive and the E2E sync tests, which are the same shape -- several nodes, one host, private addresses. Mixed with the Go client needs true, which is the opposite of what the mesh argument above suggests. The Go client has no such filter and redials without pause, so an unfiltered Nethermind tears those sessions down and rebuilds them continuously: 451 failed connections per second, "snappy: corrupt input" on every Go-to-Nethermind handshake, the Go node's own admin.peers holding no Nethermind peer in 400 consecutive samples, and 10 MB of log in fourteen minutes. The chain still ran, on the churn rather than on any session. Leaving the filter on lets an established session stay up instead. So it moves to the per-node env, computed from num_nethermind, which is where it lived before D10 folded it into the shared config; the static value is dropped from both JSON files so there is one source of truth and nothing silently overridden. Underneath this is an interop bug -- devp2p snappy framing between xdcsubnets v0.3.2 and Nethermind on the xdpos2 capability, while Nethermind-to-Nethermind on xdc165 is unaffected. Until that is fixed this setting decides only how hard the two clients retry past it. Verified by generating both paths at three mixes: 4 of 4 gives false, 2 of 4 gives true, 0 of 4 emits no Nethermind env file at all, and no copied config still carries the key. The mixed value is the one tested at 2 of 4, where neither client group reaches quorum alone, so the chain advances only if votes really do cross between them. Co-Authored-By: Claude Opus 5 (1M context) --- container-manager/src/gen/gen_env.js | 21 +++++++++++++++++++ container-manager/src/gen/gen_xdpos.js | 1 + .../src/gen/scripts/xdc-nmc-subnet.json | 1 - .../src/gen/scripts/xdc-nmc.json | 1 - 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/container-manager/src/gen/gen_env.js b/container-manager/src/gen/gen_env.js index 7c9f508..d8db1de 100644 --- a/container-manager/src/gen/gen_env.js +++ b/container-manager/src/gen/gen_env.js @@ -9,6 +9,7 @@ module.exports = { genServicesConfig, genContractDeployEnv, bootnodeEnode, + filterPeersByRecentIp, }; // An enode id is the uncompressed secp256k1 public key without its 0x04 prefix. @@ -24,6 +25,25 @@ function bootnodeEnode(ip_record) { return `enode://${BOOTNODE_ENODE_ID}@${bootnode_ip}:20301`; } +// Whether a Nethermind node rate-limits repeat connection attempts per IP +// (5-minute window). The right value follows the client mix, so it cannot live +// in the xdc-nmc*.json that every node of a deployment mounts alike. +// +// All Nethermind -> false. The nodes boot together, learn each other at once +// and dial simultaneously; the collisions leave every retry suppressed for the +// whole window, which strands nodes at zero peers -- fatal where 3 of 4 have to +// agree. Upstream disables it for Hive and the E2E sync tests, which are this +// same shape: several nodes, one host, private addresses. +// +// Mixed with the Go client -> true. The Go client has no such filter and +// redials without pause, so an unfiltered Nethermind tears those sessions down +// and rebuilds them hundreds of times a second. Leaving the filter on lets an +// established session stay up. Confirmed at 2 of 4, where neither client group +// reaches quorum alone, so the chain advances only if votes do cross. +function filterPeersByRecentIp() { + return config.num_nethermind !== config.num_subnet; +} + function genSubnetConfig(subnet_id, key, ip_record) { const key_name = `key${subnet_id}`; let private_key = key[key_name]["PrivateKey"]; @@ -66,6 +86,7 @@ NETHERMIND_JSONRPCCONFIG_PORT=${rpcport} NETHERMIND_NETWORKCONFIG_P2PPORT=${port} NETHERMIND_NETWORKCONFIG_DISCOVERYPORT=${port} NETHERMIND_NETWORKCONFIG_EXTERNALIP=${ip} +NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP=${filterPeersByRecentIp()} NETHERMIND_NETWORKCONFIG_BOOTNODES=${bootnodeEnode(ip_record)} NETHERMIND_INITCONFIG_DISCOVERYENABLED=true NETHERMIND_MININGCONFIG_ENABLED=true diff --git a/container-manager/src/gen/gen_xdpos.js b/container-manager/src/gen/gen_xdpos.js index dd6ef24..d995a55 100644 --- a/container-manager/src/gen/gen_xdpos.js +++ b/container-manager/src/gen/gen_xdpos.js @@ -411,6 +411,7 @@ NETHERMIND_JSONRPCCONFIG_PORT=${rpcport} NETHERMIND_NETWORKCONFIG_P2PPORT=${port} NETHERMIND_NETWORKCONFIG_DISCOVERYPORT=${port} NETHERMIND_NETWORKCONFIG_EXTERNALIP=${ip} +NETHERMIND_NETWORKCONFIG_FILTERPEERSBYRECENTIP=${gen_env.filterPeersByRecentIp()} NETHERMIND_NETWORKCONFIG_BOOTNODES=${bootnode.trim()} NETHERMIND_INITCONFIG_DISCOVERYENABLED=true NETHERMIND_MININGCONFIG_ENABLED=true diff --git a/container-manager/src/gen/scripts/xdc-nmc-subnet.json b/container-manager/src/gen/scripts/xdc-nmc-subnet.json index 1c4d6c8..aa97120 100644 --- a/container-manager/src/gen/scripts/xdc-nmc-subnet.json +++ b/container-manager/src/gen/scripts/xdc-nmc-subnet.json @@ -8,7 +8,6 @@ "AutoDump": "All" }, "Network": { - "FilterPeersByRecentIp": true, "FilterDiscoveryNodesByRecentIp": false, "EnableEnrDiscovery": false, "DisableDiscV4DnsFeeder": true diff --git a/container-manager/src/gen/scripts/xdc-nmc.json b/container-manager/src/gen/scripts/xdc-nmc.json index 06a6178..4790f18 100644 --- a/container-manager/src/gen/scripts/xdc-nmc.json +++ b/container-manager/src/gen/scripts/xdc-nmc.json @@ -8,7 +8,6 @@ "AutoDump": "All" }, "Network": { - "FilterPeersByRecentIp": false, "FilterDiscoveryNodesByRecentIp": false, "EnableEnrDiscovery": false, "DisableDiscV4DnsFeeder": true