Skip to content

Instantly share code, notes, and snippets.

@TPXP
Created March 10, 2025 10:20
Show Gist options
  • Save TPXP/6efd77026263b9bf523fb3f2534f05c5 to your computer and use it in GitHub Desktop.
Save TPXP/6efd77026263b9bf523fb3f2534f05c5 to your computer and use it in GitHub Desktop.
const axios = require('axios');
// Epoch to data to send to the finalize endpoint
const knownCheckpoints = {
"117136": {
"epoch": "117136",
"state_root": "0x6d92a3a81ce8b90ad71c11d7bcd80c551866c0dcf334f843e0234df216d90770",
"block_root": "0xe48389dee2da56c7101f0dacc4c5020ec91eb425ff40d16e3b924271eef53edd",
},
"117729": {
"epoch": "117729",
"state_root": "0x1505b771a01bd5991b0ad3d412f26aaefe1bc03ee40369467d5ced9d85c6e243",
"block_root": "0x5b1a381ca774dd62ba3a7061d356c2e66d6c19a405d082236ef87c6b522f8111",
}
};
const hosts = process.env.LIGHTHOUSE_HOSTS.split(";").map(host => {
if (!host.startsWith('http')) {
host = `http://${host}`;
}
return host;
})
const SLOTS_PER_EPOCH = 32;
async function singleHostLogic(host) {
try {
// Ask the beacon about its known head
const lastCheckpoint = await axios.get(`${host}/eth/v1/beacon/headers/head`)
.then(r => r.data.data)
.then(r => r.header.message.slot)
.then(r => Math.floor(r / SLOTS_PER_EPOCH) - 3);
console.log(`${host} is at epoch ${lastCheckpoint}`)
let lastCheckpointData, tryCheckpoint = lastCheckpoint;
while (!lastCheckpointData) {
lastCheckpointData = knownCheckpoints[tryCheckpoint];
if (lastCheckpointData) {
break;
}
lastCheckpointData = await axios.get(`${host}/eth/v1/beacon/headers/${tryCheckpoint * SLOTS_PER_EPOCH}`)
.then(r => r.data.data)
.then(r => r.header.message.slot && r) // Ensure the data is present
.then(r => knownCheckpoints[tryCheckpoint] = {
epoch: tryCheckpoint,
block_root: r.root,
state_root: r.header.message.state_root,
})
.catch(e => {
console.warn(`Could not fetch data for epoch ${tryCheckpoint} from ${host}, trying older: ${e}`);
});
// Some epochs don't have a valid checkpoint. In that case try the epoch before
tryCheckpoint--;
if (lastCheckpoint > tryCheckpoint + 10) {
throw new Error("Could not find a working checkpoint in the last 10 epochs. Can't force finalize for now");
}
}
console.log(`Asking Lighthouse to finalize with data ${JSON.stringify(lastCheckpointData)}`);
// Now, send a finalize request
await axios.post(`${host}/lighthouse/finalize`, lastCheckpointData)
.then(r => r.data)
.then(r => console.log(`${host} finalization request result: ${JSON.stringify(r)}`))
await axios.post(`${host}/lighthouse/compaction`)
.then(r => r.data)
.then(r => console.log(`${host} compaction request result: ${JSON.stringify(r)}`));
} catch (e) {
console.error(`Host error for ${host}`, e);
}
}
async function lightHouseLoop() {
console.log("Loop start");
await Promise.all(hosts.map(singleHostLogic));
console.log("Loop end");
}
const loopInterval = setInterval(lightHouseLoop, 600e3);
lightHouseLoop();
process.on('SIGINT', () => {
clearInterval(loopInterval);
});
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: tmp-lighthouse-sync-helper
spec:
selector:
matchLabels:
app: tmp-lighthouse-sync-helper
serviceName: tmp-lighthouse-sync-helper
replicas: 1
template:
metadata:
labels:
app: tmp-lighthouse-sync-helper
author: kiln-fi
details: lighthouse_holesky_sync_helper
spec:
nodeSelector:
k8s.scaleway.com/role: metal
containers:
- name: tmp-lighthouse-sync-helper
image: node:23
command:
- /bin/bash
- -c
- npm i --no-package-lock && node index.js
workingDir: /app
env:
- name: LIGHTHOUSE_HOSTS
value: holesky-lighthouse-beacon-rbx-1-0.holesky-lighthouse-beacon-rbx-1:5052;holesky-lighthouse-beacon-rbx-4-0.holesky-lighthouse-beacon-rbx-4:5052;holesky-lighthouse-beacon-rbx-5-0.holesky-lighthouse-beacon-rbx-5:5052;holesky-lighthouse-beacon-sbg-1-0.holesky-lighthouse-beacon-sbg-1:5052
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 250m
memory: 128Mi
volumeMounts:
- name: app
mountPath: /app
- name: node-modules
mountPath: /app/node_modules
volumes:
- name: app
configMap:
name: tmp-lighthouse-sync-helper
volumeClaimTemplates:
- metadata:
name: node-modules
spec:
# storageClassName: k8s-storage
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tmp-lighthouse-sync-helper
data:
package.json: |
{
"name": "tmp-lighthouse-sync-helper",
"version": "1.0.0",
"description": "A script forcing Lighthouse to prune its storage, useful to handle holesky pectra forks.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Kiln.fi",
"license": "ISC",
"dependencies": {
"axios": "^1.8.2"
}
}
index.js: |
const axios = require('axios');
// Epoch to data to send to the finalize endpoint
const knownCheckpoints = {
"117136": {
"epoch": "117136",
"state_root": "0x6d92a3a81ce8b90ad71c11d7bcd80c551866c0dcf334f843e0234df216d90770",
"block_root": "0xe48389dee2da56c7101f0dacc4c5020ec91eb425ff40d16e3b924271eef53edd",
},
"117729": {
"epoch": "117729",
"state_root": "0x1505b771a01bd5991b0ad3d412f26aaefe1bc03ee40369467d5ced9d85c6e243",
"block_root": "0x5b1a381ca774dd62ba3a7061d356c2e66d6c19a405d082236ef87c6b522f8111",
}
};
const hosts = process.env.LIGHTHOUSE_HOSTS.split(";").map(host => {
if (!host.startsWith('http')) {
host = `http://${host}`;
}
return host;
})
const SLOTS_PER_EPOCH = 32;
async function singleHostLogic(host) {
try {
// Ask the beacon about its known head
const lastCheckpoint = await axios.get(`${host}/eth/v1/beacon/headers/head`)
.then(r => r.data.data)
.then(r => r.header.message.slot)
.then(r => Math.floor(r / SLOTS_PER_EPOCH) - 3);
console.log(`${host} is at epoch ${lastCheckpoint + 3} - working with epoch ${lastCheckpoint}`)
let lastCheckpointData, tryCheckpoint = lastCheckpoint;
while (!lastCheckpointData) {
lastCheckpointData = knownCheckpoints[tryCheckpoint];
if (lastCheckpointData) {
break;
}
lastCheckpointData = await axios.get(`${host}/eth/v1/beacon/headers/${tryCheckpoint * SLOTS_PER_EPOCH}`)
.then(r => r.data.data)
.then(r => r.header.message.slot && r) // Ensure the data is present
.then(r => knownCheckpoints[tryCheckpoint] = {
epoch: tryCheckpoint,
block_root: r.root,
state_root: r.header.message.state_root,
})
.catch(e => {
console.warn(`Could not fetch data for epoch ${tryCheckpoint} from ${host}, trying older: ${e}`);
});
// Some epochs don't have a valid checkpoint. In that case try the epoch before
tryCheckpoint--;
if (lastCheckpoint > tryCheckpoint + 10) {
throw new Error("Could not find a working checkpoint in the last 10 epochs. Can't force finalize for now");
}
}
console.log(`Asking Lighthouse to finalize with data ${JSON.stringify(lastCheckpointData)}`);
// Now, send a finalize request
await axios.post(`${host}/lighthouse/finalize`, lastCheckpointData)
.then(r => r.data)
.then(r => console.log(`${host} finalization request result: ${JSON.stringify(r)}`))
await axios.post(`${host}/lighthouse/compaction`)
.then(r => r.data)
.then(r => console.log(`${host} compaction request result: ${JSON.stringify(r)}`));
} catch (e) {
console.error(`Host error for ${host}`, e);
}
}
async function lightHouseLoop() {
console.log("Loop start");
await Promise.all(hosts.map(singleHostLogic));
console.log("Loop end");
}
const loopInterval = setInterval(lightHouseLoop, 600e3);
lightHouseLoop();
process.on('SIGINT', () => {
clearInterval(loopInterval);
});
{
"name": "tmp-lighthouse-sync-helper",
"version": "1.0.0",
"description": "A script forcing Lighthouse to prune its storage, useful to handle holesky pectra forks.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Kiln.fi",
"license": "ISC",
"dependencies": {
"axios": "^1.8.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment