Skip to content

Commit

Permalink
feat: Enhance P2P sync testing infrastructure
Browse files Browse the repository at this point in the history
This PR introduces significant improvements to the P2P sync testing framework:

Core Changes:
- Add test reporting functionality with a new dashboard integration
- Implement node type detection (Juno/Pathfinder) with version reporting
- Add `--disable-l1-verification` flag to Juno configuration

Test Infrastructure:
- Separate source and target node configurations in sync tests
- Add proper RPC port management for all sync test scenarios
- Enable Pathfinder from Juno sync test in CI workflow
- Remove unused devnet network test

New Features:
- Add progress reporting with detailed metrics
- Implement test status tracking (In Progress/Passed/Failed)
- Add error handling and reporting capabilities
- Track average block processing time
  • Loading branch information
wojciechos committed Dec 17, 2024
1 parent 1e09ec0 commit 2373758
Show file tree
Hide file tree
Showing 13 changed files with 262 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/kurtosis-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ jobs:
kurtosis run . --main-function-name run_juno_from_juno_sync
kurtosis run . --main-function-name run_juno_from_pathfinder_sync
kurtosis run . --main-function-name run_pathfinder_from_pathfinder_sync
# kurtosis run . --main-function-name run_pathfinder_from_juno_sync
kurtosis run . --main-function-name run_pathfinder_from_juno_sync
kurtosis clean --all
10 changes: 6 additions & 4 deletions e2e/clients/juno/client.star
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
base = import_module("../common/base.star")

def run(plan, name, participant):
image = participant.get("image", "nethermind/juno:latest")
image = participant.get("image", "nethermindeth/juno:fixed-p2p-sync")
is_feeder = participant.get("is_feeder", False)
network = participant.get("network", "") # Changed default to empty string
network = participant.get("network", "")
private_key = participant.get("private_key", "")
http_port = participant.get("http_port", 6060)
p2p_port = participant.get("p2p_port", 7777)
Expand All @@ -15,8 +15,12 @@ def run(plan, name, participant):
"--http-host", "0.0.0.0",
"--log-level", "debug",
"--db-path", "/var/lib/juno",
"--disable-l1-verification"
]

if network:
cmd.extend(["--network", network])

# Add P2P args only if we're in P2P mode
if is_feeder or peer_multiaddrs:
cmd.extend([
Expand All @@ -25,8 +29,6 @@ def run(plan, name, participant):
"--p2p-private-key", private_key
])

if network:
cmd.extend(["--network", network])
if is_feeder:
cmd.append("--p2p-feeder-node")
for peer_multiaddr in peer_multiaddrs:
Expand Down
3 changes: 0 additions & 3 deletions e2e/main.star
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,3 @@ def run_pathfinder_from_juno_sync(plan):

def run_pathfinder_from_pathfinder_sync(plan):
return pathfinder_from_pathfinder.run(plan)

def run_devnet_network(plan):
return devnet_network.run(plan)
18 changes: 18 additions & 0 deletions e2e/tester/config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const SYNC_CONFIG = {
CHECK_INTERVAL: 10_000, // 10 seconds
STALL_TIMEOUT: 5, // 1 minute
NODE_READY_ATTEMPTS: 5,
NODE_READY_INTERVAL: 5_000,
};

export const REPORTING_CONFIG = {
enabled: true,
endpoint: 'https://starknet-p2p-testing-dashboard.onrender.com/update',
testId: Date.now().toString(),
};

export const NODE_TYPES = {
JUNO: 'Juno',
PATHFINDER: 'Pathfinder',
UNKNOWN: 'Unknown'
};
59 changes: 33 additions & 26 deletions e2e/tester/index.mjs
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import { RpcProvider } from "starknet";
import { SYNC_CONFIG } from './config.mjs';
import { TestReporter } from './reporter.mjs';
import { NodeDetector } from './nodeDetector.mjs';

if (!process.argv[2]) {
console.error("Error: Node URL is required");
if (!process.argv[2] || !process.argv[3]) {
console.error("Error: Source and Target Node URLs are required");
process.exit(1);
}

const node = new RpcProvider({ nodeUrl: process.argv[2] });
const timeout = parseInt(process.argv[3], 10);
const targetBlockNumber = parseInt(process.argv[4], 10);
const sourceNode = new RpcProvider({ nodeUrl: process.argv[2] });
const targetNode = new RpcProvider({ nodeUrl: process.argv[3] });
const timeout = parseInt(process.argv[4], 10);
const targetBlockNumber = parseInt(process.argv[5], 10);

// Simple logging with timestamp
const log = (message) => console.log(`[${new Date().toISOString()}] ${message}`);

// Constants for configuration
const CONFIG = {
CHECK_INTERVAL: 10_000, // 10 seconds
STALL_TIMEOUT: 5, // 5 minutes
NODE_READY_ATTEMPTS: 5,
NODE_READY_INTERVAL: 5_000,
};

async function waitForNodeReady() {
for (let attempt = 1; attempt <= CONFIG.NODE_READY_ATTEMPTS; attempt++) {
for (let attempt = 1; attempt <= SYNC_CONFIG.NODE_READY_ATTEMPTS; attempt++) {
try {
const version = await node.getSpecVersion();
log(`✓ Node ready (v${version})`);
return true;
} catch {
log(`⧗ Waiting for node... (${attempt}/${CONFIG.NODE_READY_ATTEMPTS})`);
await new Promise(r => setTimeout(r, CONFIG.NODE_READY_INTERVAL));
const sourceInfo = await NodeDetector.detect(process.argv[2]);
const targetInfo = await NodeDetector.detect(process.argv[3]);

log(`✓ Source node ready (${sourceInfo.type} ${sourceInfo.version})`);
log(`✓ Target node ready (${targetInfo.type} ${targetInfo.version})`);

return { sourceInfo, targetInfo };
} catch (error) {
log(`⧗ Waiting for nodes... (${attempt}/${SYNC_CONFIG.NODE_READY_ATTEMPTS})`);
await new Promise(r => setTimeout(r, SYNC_CONFIG.NODE_READY_INTERVAL));
}
}
throw new Error("Node failed to become ready");
throw new Error("One or both nodes failed to become ready");
}

async function getCurrentBlock() {
try {
return await node.getBlockLatestAccepted();
return await targetNode.getBlockLatestAccepted();
} catch (error) {
if (error.message.includes("There are no blocks")) {
log("➜ Starting from genesis");
Expand All @@ -52,7 +52,9 @@ async function syncNode() {
let lastBlockNumber = (await getCurrentBlock()).block_number;

log(`➜ Starting sync to block ${targetBlockNumber} from ${lastBlockNumber}`);
await waitForNodeReady();
const { sourceInfo, targetInfo } = await waitForNodeReady();

const reporter = new TestReporter(sourceInfo, targetInfo, targetBlockNumber);

return new Promise((resolve, reject) => {
const interval = setInterval(async () => {
Expand All @@ -68,23 +70,28 @@ async function syncNode() {
log(`↑ Block ${currentBlock.block_number} | ${progress}% | ${speed.toFixed(1)} blocks/min`);
lastBlockNumber = currentBlock.block_number;
lastBlockTime = Date.now();

// Report progress
await reporter.reportProgress(currentBlock.block_number, startTime);
}

// Check completion or failure conditions
if (currentBlock.block_number >= targetBlockNumber) {
log(`✓ Sync completed in ${elapsedMinutes.toFixed(1)}m`);
await reporter.reportProgress(currentBlock.block_number, startTime);
clearInterval(interval);
resolve();
} else if ((Date.now() - lastBlockTime) / 1000 / 60 >= CONFIG.STALL_TIMEOUT) {
throw new Error(`Sync stalled for ${CONFIG.STALL_TIMEOUT}m`);
} else if ((Date.now() - lastBlockTime) / 1000 / 60 >= SYNC_CONFIG.STALL_TIMEOUT) {
throw new Error(`Sync stalled for ${SYNC_CONFIG.STALL_TIMEOUT}m`);
} else if (Date.now() - startTime > timeout * 1000) {
throw new Error(`Sync timeout after ${timeout}s`);
}
} catch (error) {
clearInterval(interval);
await reporter.reportProgress(lastBlockNumber, startTime, [error.message]);
reject(error);
}
}, CONFIG.CHECK_INTERVAL);
}, SYNC_CONFIG.CHECK_INTERVAL);
});
}

Expand Down
56 changes: 56 additions & 0 deletions e2e/tester/nodeDetector.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import fetch from 'node-fetch';
import { NODE_TYPES } from './config.mjs';

export class NodeDetector {
static async detect(url) {
const junoVersion = await this._detectJuno(url);
if (junoVersion) {
return { type: NODE_TYPES.JUNO, version: junoVersion };
}

const pathfinderVersion = await this._detectPathfinder(url);
if (pathfinderVersion) {
return { type: NODE_TYPES.PATHFINDER, version: pathfinderVersion };
}

return { type: NODE_TYPES.UNKNOWN, version: 'Unknown' };
}

static async _detectJuno(url) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
method: "juno_version",
jsonrpc: "2.0",
id: 0
})
});

const data = await response.json();
return data.result || null;
} catch {
return null;
}
}

static async _detectPathfinder(url) {
try {
const response = await fetch(`${url}/rpc/pathfinder/v0_1`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
method: "pathfinder_version",
jsonrpc: "2.0",
id: 0
})
});

const data = await response.json();
return data.result || null;
} catch {
return null;
}
}
}
3 changes: 2 additions & 1 deletion e2e/tester/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"license": "ISC",
"type": "module",
"dependencies": {
"starknet": "^6.11.0"
"starknet": "^6.11.0",
"node-fetch": "^3.3.0"
}
}
58 changes: 58 additions & 0 deletions e2e/tester/reporter.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fetch from 'node-fetch';
import { REPORTING_CONFIG } from './config.mjs';

export class TestReporter {
constructor(sourceNode, targetNode, targetBlockNumber) {
this.config = {
...REPORTING_CONFIG,
sourceNode,
targetNode,
targetBlockNumber
};
}

async reportProgress(blockNumber, startTime, errors = []) {
if (!this.config.enabled) return;

try {
const payload = {
type: "updateTest",
data: {
id: this.config.testId,
sourceNode: this.config.sourceNode.type,
sourceVersion: this.config.sourceNode.version,
targetNode: this.config.targetNode.type,
targetVersion: this.config.targetNode.version,
status: this._getStatus(blockNumber, errors),
startTime: new Date(startTime).toISOString(),
blocksProcessed: blockNumber,
totalBlocks: this.config.targetBlockNumber,
avgBlockTime: this._calculateAvgBlockTime(blockNumber, startTime),
errors
}
};

const response = await fetch(this.config.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

return response.status;
} catch (error) {
console.warn(`Warning: Failed to report progress: ${error.message}`);
return null;
}
}

_getStatus(blockNumber, errors) {
if (errors.length > 0) return "Failed";
if (blockNumber >= this.config.targetBlockNumber) return "Passed";
return "In Progress";
}

_calculateAvgBlockTime(blockNumber, startTime) {
if (blockNumber <= 0) return "0.000";
return ((Date.now() - startTime) / blockNumber / 1000).toFixed(3);
}
}
17 changes: 13 additions & 4 deletions e2e/tests/sync/juno_from_juno.star
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ sync_utils = import_module("./sync_test_utils.star")
# Test configuration
SYNC_TIMEOUT_SECONDS = 1800
TARGET_BLOCK_NUMBER = 1000
RPC_PORT = 6061
FEEDER_RPC_PORT = 6060
PEER_RPC_PORT = 6061

def run(plan):
# Run the Juno feeder node
feeder_node = participants.run_participant(plan, "juno-feeder", {
"type": "juno",
"is_feeder": True,
"private_key": "67f8eae550a5265238431d719c2b62163011ab2a3f2ebeee3bc8f3135e2e2500b9e2c2e9e4ebeea82cca787094d74ab6fcae8ec0367e866dc1130de89e37150b",
"http_port": 6060,
"http_port": FEEDER_RPC_PORT,
"network": "sepolia"
})

Expand All @@ -21,10 +22,18 @@ def run(plan):
"type": "juno",
"is_feeder": False,
"private_key": "a5a938ae6f012390fd68a10d3dd91038334fe5f0ed1c96753a3ee7bf0e8f1314e39307aea916c94e2d07e616fa20e315f4625c4f1e598ba2cc589410cc9c5cda",
"http_port": 6061,
"http_port": PEER_RPC_PORT,
"peer_multiaddrs": ["/ip4/" + feeder_node.ip_address + "/tcp/7777/p2p/12D3KooWNKz9BJmyWVFUnod6SQYLG4dYZNhs3GrMpiot63Y1DLYS"],
"network": "sepolia",
})

sync_utils.run_sync_test(plan, feeder_node, peer_node, RPC_PORT, SYNC_TIMEOUT_SECONDS, TARGET_BLOCK_NUMBER)
sync_utils.run_sync_test(
plan,
feeder_node,
peer_node,
FEEDER_RPC_PORT,
PEER_RPC_PORT,
SYNC_TIMEOUT_SECONDS,
TARGET_BLOCK_NUMBER
)
plan.print("Juno to Juno sync test completed")
16 changes: 13 additions & 3 deletions e2e/tests/sync/juno_from_pathfinder.star
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,35 @@ sync_utils = import_module("./sync_test_utils.star")
# Test configuration
SYNC_TIMEOUT_SECONDS = 1800
TARGET_BLOCK_NUMBER = 1000
RPC_PORT = 6061
SOURCE_RPC_PORT = 9545
TARGET_RPC_PORT = 6060

def run(plan):
# Run the Pathfinder as feeder node
feeder_node = participants.run_participant(plan, "pathfinder-feeder", {
"type": "pathfinder",
"is_feeder": True,
"p2p_port": 20002,
"http_port": SOURCE_RPC_PORT,
})

# Run the juno peer node with the feeder node as a peer
peer_node = participants.run_participant(plan, "juno-peer", {
"type": "juno",
"is_feeder": False,
"private_key": "a5a938ae6f012390fd68a10d3dd91038334fe5f0ed1c96753a3ee7bf0e8f1314e39307aea916c94e2d07e616fa20e315f4625c4f1e598ba2cc589410cc9c5cda",
"http_port": 6061,
"http_port": TARGET_RPC_PORT,
"peer_multiaddrs": ["/ip4/" + feeder_node.ip_address + "/tcp/20002/p2p/12D3KooWFY6SaqJkRxJDepwvBi4Rw36iMUGZrejW69qkjYQQ2ydQ"],
"network": "sepolia",
})

sync_utils.run_sync_test(plan, feeder_node, peer_node, RPC_PORT, SYNC_TIMEOUT_SECONDS, TARGET_BLOCK_NUMBER)
sync_utils.run_sync_test(
plan,
feeder_node,
peer_node,
SOURCE_RPC_PORT,
TARGET_RPC_PORT,
SYNC_TIMEOUT_SECONDS,
TARGET_BLOCK_NUMBER
)
plan.print("Juno from Pathfinder sync test completed")
Loading

0 comments on commit 2373758

Please sign in to comment.