Skip to content

Commit 9f6bd16

Browse files
forge: config.sparseMarks — re-target the trailing mark instead of stacking
Anchoring "one super-commit per period" clashed with a fast commit cadence: recordTip appends a pending mark per commit, so marking HEAD walked one tx per commit. With config.sparseMarks, when the trailing mark is still unfunded a new tip RE-TARGETS it (re-derives program/address over the same marked prefix) rather than stacking. The chain gains a link only per funded mark, so anchoring HEAD is a single tx however many commits landed — fresh mirrors (frequent commits) now coexist with cheap, sparse anchoring. Default off; explicit opt-in, existing behavior and every other test unchanged. Tests: +1 (127) — two post-genesis pushes re-target ONE pending mark (count holds at 2, address moves to the new tip), genesis stays marked.
1 parent 313d39f commit 9f6bd16

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

forge/AGENT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ Key properties:
134134
and persisted in `pluginDir`).
135135
- `chain` — Blocktrails chain (default a testnet); mainnet requires
136136
`allowMainnet: true`.
137+
- `sparseMarks` — when true, a new tip RE-TARGETS the trailing unfunded mark
138+
instead of stacking one pending mark per commit. The chain then gains a link
139+
only per *funded* mark, so anchoring HEAD is one tx per period regardless of
140+
commit cadence — the "rolling mark" that lets fast commits (fresh mirrors)
141+
coexist with slow, cheap anchoring. Default off (each commit is its own
142+
candidate state).
137143
- `cspConnect: [origin, …]` — extra `connect-src` origins for a login widget
138144
that talks to external identity providers.
139145
- `pushTokenTtl`, `gitHttpBackend` — see `README.md`.

forge/plugin.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,13 @@ export async function activate(api) {
885885
+ '(no owner signature required). DEMO ONLY; never enable on a real forge.');
886886
}
887887

888+
// Sparse marking (opt-in): instead of stacking one pending mark per commit,
889+
// RE-TARGET the trailing UNFUNDED mark at each new tip — so the chain gains a
890+
// link only per actual (funded) mark and anchoring HEAD is ONE tx regardless
891+
// of how many commits landed since. Lets a fast commit cadence (fresh mirrors)
892+
// coexist with a slow mark cadence (one super-commit per period). See recordTip.
893+
const sparseMarks = api.config.sparseMarks === true;
894+
888895
// Tier 3.5: anchoring chain, testnet4 by default. Checked FIRST, before
889896
// any other activation work. Mainnet is REFUSED at
890897
// activate unless the operator opts in explicitly — this plugin derives
@@ -1510,6 +1517,24 @@ export async function activate(api) {
15101517
if (!trail) return;
15111518
const last = trail.marks.at(-1);
15121519
if (last && last.state.commit === tip && last.state.branch === branch) return; // tip unchanged
1520+
if (sparseMarks && last && last.status === 'pending') {
1521+
// sparse: the trailing mark is unfunded, so RE-TARGET it at the new tip
1522+
// (re-derive its program/address chaining the same MARKED prefix) rather
1523+
// than stacking. The chain gains a link only when a mark is actually
1524+
// funded, so anchoring HEAD stays one tx however many commits landed.
1525+
const state = { commit: tip, repo: `${owner}/${name}`, branch };
1526+
const stateHash = markStateHash(state);
1527+
const hashes = [...trail.marks.slice(0, -1).map((m) => m.stateHash), stateHash];
1528+
const program = trailProgram(trail.pubkeyBase, hashes);
1529+
last.state = state;
1530+
last.stateHash = stateHash;
1531+
last.program = program;
1532+
last.address = p2trAddressEncode(chainHrp, Buffer.from(program, 'hex'));
1533+
last.at = Math.floor(Date.now() / 1000);
1534+
saveTrail(owner, name, trail);
1535+
api.log.info(`forge: mark #${last.index} re-targeted (sparse) for ${owner}/${name}@${branch} (${tip.slice(0, 7)}) -> ${last.address}`);
1536+
return;
1537+
}
15131538
if (trail.marks.length >= MARKS_CAP) {
15141539
api.log.warn(`forge: ${owner}/${name} hit the ${MARKS_CAP}-mark cap — not recording ${tip.slice(0, 7)}`);
15151540
return;

forge/test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,57 @@ describe('forge plugin', () => {
24222422
});
24232423
});
24242424

2425+
// ---------------------------------------------------------------------------
2426+
// Sparse marking (config.sparseMarks): the trailing UNFUNDED mark is re-targeted
2427+
// at each new tip instead of stacked, so anchoring HEAD is one tx per period.
2428+
describe('forge sparse marking (config.sparseMarks re-targets, does not stack)', () => {
2429+
let jss; let base; let owner; let wdir;
2430+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-sparse-'));
2431+
const g = (args, opts = {}) => git(args, { ...opts, env: { HOME: tmp, ...(opts.env ?? {}) } });
2432+
const authed = () => ({ 'content-type': 'application/json', authorization: `Bearer ${owner.access_token}` });
2433+
const marksApi = () => `${base}/forge/api/repos/sparsey/r/marks`;
2434+
const marksOf = async () => (await fetch(marksApi())).json();
2435+
const until = async (fn, what, ms = 5000) => {
2436+
const t0 = Date.now();
2437+
for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) throw new Error(`timed out: ${what}`); await new Promise((r) => { setTimeout(r, 50); }); }
2438+
};
2439+
const pushChange = async (content, msg) => {
2440+
fs.writeFileSync(path.join(wdir, 'a.txt'), content);
2441+
await g(['commit', '-aqm', msg], { cwd: wdir });
2442+
await g([...authFlag(owner.access_token), 'push', `${base}/forge/sparsey/r.git`, 'main'], { cwd: wdir });
2443+
return (await g(['rev-parse', 'HEAD'], { cwd: wdir })).stdout.trim();
2444+
};
2445+
2446+
before(async () => {
2447+
fs.writeFileSync(path.join(tmp, '.gitconfig'), '[user]\n\temail = s@e.org\n\tname = S\n[init]\n\tdefaultBranch = main\n');
2448+
jss = await startJss({ idp: true, plugins: [{ id: 'forge', module: module_, prefix: '/forge', config: { sparseMarks: true } }] });
2449+
base = jss.base;
2450+
owner = await registerAndMint(base, 'sparsey');
2451+
wdir = path.join(tmp, 'r'); fs.mkdirSync(wdir, { recursive: true });
2452+
fs.writeFileSync(path.join(wdir, 'a.txt'), '1\n');
2453+
await g(['init', '--quiet'], { cwd: wdir }); await g(['add', '-A'], { cwd: wdir }); await g(['commit', '--quiet', '-m', 'c1'], { cwd: wdir });
2454+
await g([...authFlag(owner.access_token), 'push', `${base}/forge/sparsey/r.git`, 'main'], { cwd: wdir });
2455+
});
2456+
after(async () => { if (jss) await jss.close(); fs.rmSync(tmp, { recursive: true, force: true }); });
2457+
2458+
it('after a funded genesis, repeated pushes re-target ONE pending mark (length stays 2)', async () => {
2459+
// enable + fund genesis so the mark to re-target is #1 (genesis #0 is marked)
2460+
assert.strictEqual((await fetch(`${marksApi()}/enable`, { method: 'POST', headers: authed(), body: '{}' })).status, 201);
2461+
await fetch(`${marksApi()}/0/txo`, { method: 'POST', headers: authed(), body: JSON.stringify({ txid: 'a'.repeat(64), vout: 0, amount: 100000 }) });
2462+
const c2 = await pushChange('2\n', 'c2');
2463+
let m = await until(async () => { const x = await marksOf(); return x.marks.length === 2 && x.marks[1].state.commit === c2 ? x : null; }, 'pending #1 for c2');
2464+
const addr1 = m.marks[1].address;
2465+
assert.strictEqual(m.marks[1].status, 'pending');
2466+
2467+
const c3 = await pushChange('3\n', 'c3');
2468+
m = await until(async () => { const x = await marksOf(); return x.marks[1].state.commit === c3 ? x : null; }, 'pending #1 re-targeted to c3');
2469+
assert.strictEqual(m.marks.length, 2, 'still genesis + ONE pending — re-targeted, not stacked');
2470+
assert.notStrictEqual(m.marks[1].address, addr1, 'the pending mark address moved to match the new tip');
2471+
assert.strictEqual(m.marks[1].status, 'pending');
2472+
assert.strictEqual(m.marks[0].status, 'marked', 'the funded genesis is untouched');
2473+
});
2474+
});
2475+
24252476
// ---------------------------------------------------------------------------
24262477
// NIP-34 emission against a LIVE relay — an in-process ws relay that accepts
24272478
// one EVENT and replies OK. Its own JSS instance (config.announceRelays set)

0 commit comments

Comments
 (0)