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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@
"@types/node-localstorage": "^1.3.3",
"@types/semver": "^7.7.1",
"@types/shelljs": "^0.8.17",
"@types/sinon": "^21.0.1",
"chai": "^4",
"eslint": "^9",
"eslint-config-oclif": "^6",
"eslint-config-prettier": "^10",
"mocha": "^10",
"oclif": "^4",
"shx": "^0.3.3",
"sinon": "^22.0.0",
"ts-node": "^10.9.2",
"typescript": "^5"
},
Expand Down
79 changes: 45 additions & 34 deletions src/checks/check-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,18 @@ export const checkNetwork = {
},

async checkSeedList() {
if(configStore.hasProjectFlag('seedListChecked')) {
return;
}

// Seed list membership is dynamic — the upstream lists (especially testnet /
// integrationnet) get rotated and nodes can be dropped. Re-validate on every run
// instead of trusting a cached "passed" flag, otherwise a node that was removed
// upstream keeps launching and only gets rejected later by the protocol.
const { type } = configStore.getNetworkInfo();

clm.preStep(`Checking inclusion into seed list for ${type.toUpperCase()} network...`);
const { nodeId, projectDir } = configStore.getProjectInfo();
const seedListFile = path.resolve(projectDir, 'seedlist');
if (fs.existsSync(seedListFile)) {
const found = fs.readFileSync(seedListFile, 'utf8').includes(nodeId);
if (found) {
clm.postStep(`✅ Node ID found in ${type.toUpperCase()} seed list.`);
configStore.setProjectFlag('seedListChecked', true);
return;
}
}

clm.preStep(`Checking inclusion into seed list for ${type.toUpperCase()} network...`);

const isInLocalSeedList = () =>
fs.existsSync(seedListFile) && fs.readFileSync(seedListFile, 'utf8').includes(nodeId);

const printNotFoundError = () => {
clm.warn(`Node ID not found in ${type.toUpperCase()} seed list. You may try again later.`);
Expand All @@ -69,29 +64,45 @@ export const checkNetwork = {
}

if (type === 'mainnet') {
// the mainnet seed list comed from a network release
printNotFoundError();
} else {

const url = `https://constellationlabs-dag.s3.us-west-1.amazonaws.com/${type}-seedlist`
const seedList = await fetch(url)
.then(res => {
if (res.ok) return res.text();
throw new Error(`Failed`);
})
.catch(() => {
clm.error(`Failed to fetch seed list from ${url}. Try again later.`);
return '';
});
if (seedList.includes(nodeId)) {
clm.postStep(`Node ID found in ${type.toUpperCase()} seed list.`);
fs.writeFileSync(seedListFile, seedList);
configStore.setProjectFlag('seedListChecked', true);
// the mainnet seed list comes from a network release (downloaded by install.sh);
// validate against the local file the node actually mounts.
if (isInLocalSeedList()) {
clm.postStep(`✅ Node ID found in ${type.toUpperCase()} seed list.`);
return;
}
else {
printNotFoundError();

printNotFoundError();
return;
}

const url = `https://constellationlabs-dag.s3.us-west-1.amazonaws.com/${type}-seedlist`
const remoteSeedList = await fetch(url)
.then(res => {
if (res.ok) return res.text();
throw new Error(`Failed`);
})
.catch(() => '');

if (remoteSeedList) {
if (remoteSeedList.includes(nodeId)) {
// refresh the seed list the node mounts so it stays current
fs.writeFileSync(seedListFile, remoteSeedList);
clm.postStep(`✅ Node ID found in ${type.toUpperCase()} seed list.`);
return;
}

printNotFoundError();
return;
}

// Remote unreachable — fall back to the local seed list the node uses.
clm.warn(`Could not fetch the ${type.toUpperCase()} seed list from ${url}. Falling back to local copy.`);
if (isInLocalSeedList()) {
clm.postStep(`✅ Node ID found in local ${type.toUpperCase()} seed list.`);
return;
}

printNotFoundError();
},

async configureIpAddress() {
Expand Down
1 change: 0 additions & 1 deletion src/helpers/key-file-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ export const keyFileHelper = {
}

configStore.setProjectFlag('duplicateNodeIdChecked', false);
configStore.setProjectFlag('seedListChecked', false);
},

async promptIfNoKeyFile() {
Expand Down
25 changes: 10 additions & 15 deletions src/helpers/prompt-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,19 @@ export const promptHelper = {
throw new Error('No supported networks found');
}

if (supportedTypes.length === 1) {
configStore.setNetworkInfo({type: supportedTypes[0], version: "latest"});
// configStore.setEnvNetworkInfo(configStore.getNetworkEnvInfo(supportedTypes[0]));
return;
}

const networkType = await select({
choices: [
{disabled: !supportedTypes.includes('mainnet'), name: 'Mainnet', value: 'mainnet'},
{disabled: !supportedTypes.includes('testnet'), name: 'Testnet', value: 'testnet'},
{disabled: !supportedTypes.includes('integrationnet'), name: 'Integrationnet', value: 'integrationnet'}
],
message: 'Select network type:'
}) as NetworkType;
const networkType = supportedTypes.length === 1
? supportedTypes[0]
: await select({
choices: [
{disabled: !supportedTypes.includes('mainnet'), name: 'Mainnet', value: 'mainnet'},
{disabled: !supportedTypes.includes('testnet'), name: 'Testnet', value: 'testnet'},
{disabled: !supportedTypes.includes('integrationnet'), name: 'Integrationnet', value: 'integrationnet'}
],
message: 'Select network type:'
}) as NetworkType;

configStore.setNetworkInfo({type: networkType, version: "latest"});
configStore.setProjectFlag('duplicateNodeIdChecked', false);
configStore.setProjectFlag('seedListChecked', false);
configStore.setProjectFlag('javaMemoryChecked', false);
},

Expand Down
127 changes: 127 additions & 0 deletions test/checks/check-network.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {expect} from 'chai'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import sinon from 'sinon'

import {checkNetwork} from '../../src/checks/check-network.js'
import {clm} from '../../src/clm.js'
import {configStore} from '../../src/config-store.js'
import {NetworkType} from '../../src/config-store.js'

// real node id from the integrationnet seed-list bug report
const NODE_ID = 'de51af87c1ac6bd06e428009b7f34dd3b76c5799ff44b8924b8630fc6ed28eb7c148e1c538c29a136a5bc946d528cf51fc48b6256eecdb3bfdf71ed3f6c0f8d7'
const OTHER_ID = 'aaaa1111bbbb2222cccc3333dddd4444eeee5555ffff6666aaaa7777bbbb8888'

// sentinel so we can assert the "not found" path without calling process.exit
class SeedListError extends Error {}

describe('checkNetwork.checkSeedList', () => {
let projectDir: string
let seedListFile: string
let fetchStub: sinon.SinonStub
let errorStub: sinon.SinonStub
let postStepStub: sinon.SinonStub
let warnStub: sinon.SinonStub

function stubConfig(type: NetworkType) {
sinon.stub(configStore, 'getNetworkInfo').returns({supportedTypes: [type], type, version: 'latest'})
sinon.stub(configStore, 'getProjectInfo').returns({nodeId: NODE_ID, projectDir} as ReturnType<typeof configStore.getProjectInfo>)
}

function mockFetch(body: string) {
fetchStub.resolves({ok: true, text: async () => body} as Response)
}

beforeEach(() => {
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpilot-seedlist-'))
seedListFile = path.join(projectDir, 'seedlist')

fetchStub = sinon.stub(globalThis, 'fetch')
sinon.stub(clm, 'preStep')
postStepStub = sinon.stub(clm, 'postStep')
warnStub = sinon.stub(clm, 'warn')
errorStub = sinon.stub(clm, 'error').throws(new SeedListError())
})

afterEach(() => {
sinon.restore()
fs.rmSync(projectDir, {force: true, recursive: true})
})

it('passes and refreshes the local seedlist when the remote list contains the node id', async () => {
stubConfig('integrationnet')
const remote = `${OTHER_ID}\n${NODE_ID}\n`
mockFetch(remote)

await checkNetwork.checkSeedList()

expect(errorStub.called, 'should not error').to.equal(false)
expect(postStepStub.calledWithMatch(/found/i)).to.equal(true)
expect(fs.readFileSync(seedListFile, 'utf8')).to.equal(remote)
})

it('errors when the node id is not in the remote list (the reported bug)', async () => {
stubConfig('integrationnet')
mockFetch(`${OTHER_ID}\n`)

try {
await checkNetwork.checkSeedList()
expect.fail('expected checkSeedList to error out')
} catch (error) {
expect(error).to.be.instanceOf(SeedListError)
}

expect(errorStub.called, 'should report not-found').to.equal(true)
expect(fs.existsSync(seedListFile), 'must not write seedlist on miss').to.equal(false)
})

it('re-validates every run instead of trusting a cached flag', async () => {
// even if a prior "checked" flag were set, the remote must still be consulted
sinon.stub(configStore, 'hasProjectFlag').returns(true)
stubConfig('integrationnet')
mockFetch(`${NODE_ID}\n`)

await checkNetwork.checkSeedList()

expect(fetchStub.calledOnce, 'remote list must be fetched').to.equal(true)
})

it('falls back to the local seedlist when the remote is unreachable', async () => {
stubConfig('integrationnet')
fs.writeFileSync(seedListFile, `${NODE_ID}\n`)
fetchStub.rejects(new Error('network down'))

await checkNetwork.checkSeedList()

expect(errorStub.called, 'should not error on fallback hit').to.equal(false)
expect(warnStub.calledWithMatch(/fall(ing)? back/i)).to.equal(true)
expect(postStepStub.calledWithMatch(/found/i)).to.equal(true)
})

it('errors when the remote is unreachable and the local list misses', async () => {
stubConfig('integrationnet')
fs.writeFileSync(seedListFile, `${OTHER_ID}\n`)
fetchStub.rejects(new Error('network down'))

try {
await checkNetwork.checkSeedList()
expect.fail('expected checkSeedList to error out')
} catch (error) {
expect(error).to.be.instanceOf(SeedListError)
}

expect(errorStub.called).to.equal(true)
})

it('validates mainnet against the local release seedlist without fetching', async () => {
stubConfig('mainnet')
fs.writeFileSync(seedListFile, `${NODE_ID}\n`)

await checkNetwork.checkSeedList()

expect(fetchStub.called, 'mainnet must not hit S3').to.equal(false)
expect(errorStub.called).to.equal(false)
expect(postStepStub.calledWithMatch(/found/i)).to.equal(true)
})
})
54 changes: 54 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,28 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668"
integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==

"@sinonjs/commons@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd"
integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==
dependencies:
type-detect "4.0.8"

"@sinonjs/fake-timers@^15.4.0":
version "15.4.0"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz#5d40c151a9e66075fe4520bec40bccfe54931962"
integrity sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==
dependencies:
"@sinonjs/commons" "^3.0.1"

"@sinonjs/samsam@^10.0.2":
version "10.0.2"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-10.0.2.tgz#d2cb34f0bcddb955b6971585c2f0334e68a9e66d"
integrity sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==
dependencies:
"@sinonjs/commons" "^3.0.1"
type-detect "^4.1.0"

"@smithy/abort-controller@^4.0.5":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-4.0.5.tgz#2872a12d0f11dfdcc4254b39566d5f24ab26a4ab"
Expand Down Expand Up @@ -2141,6 +2163,18 @@
"@types/node" "*"
glob "^11.0.3"

"@types/sinon@^21.0.1":
version "21.0.1"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-21.0.1.tgz#f995e2afdf15be832d5f1645803d82a8eb95a1bc"
integrity sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==
dependencies:
"@types/sinonjs__fake-timers" "*"

"@types/sinonjs__fake-timers@*":
version "15.0.1"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz#49f731d9453f52d64dd79f5a5626c1cf1b81bea4"
integrity sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==

"@types/uuid@^9.0.1":
version "9.0.8"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
Expand Down Expand Up @@ -3436,6 +3470,11 @@ diff@^5.1.0, diff@^5.2.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==

diff@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-9.0.0.tgz#297c31cd7c280f13dfe335791ec2063bd4a73a6f"
integrity sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==

doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
Expand Down Expand Up @@ -7370,6 +7409,16 @@ simple-get@^2.7.0:
once "^1.3.1"
simple-concat "^1.0.0"

sinon@^22.0.0:
version "22.0.0"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-22.0.0.tgz#01cea95c919468f6ef01e21d406397e5c02aad9b"
integrity sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==
dependencies:
"@sinonjs/commons" "^3.0.1"
"@sinonjs/fake-timers" "^15.4.0"
"@sinonjs/samsam" "^10.0.2"
diff "^9.0.0"

smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
Expand Down Expand Up @@ -7865,6 +7914,11 @@ type-check@^0.4.0, type-check@~0.4.0:
dependencies:
prelude-ls "^1.2.1"

type-detect@4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==

type-detect@^4.0.0, type-detect@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
Expand Down