From 32e8131500c5087bbce293e33d09d2db433557c3 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 14 Oct 2026 23:21:33 -0700 Subject: [PATCH] Orgchain v1.6.1: under Last act, a timed beat goes to whoever acted last A timed or manual beat used to pay the holding being worked in. Now it pays the holding behind the newest cued act or send; with nothing cued, Beat now still pays the one being worked in. At a timed tempo, holdings race for it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh --- README.md | 5 +++-- src/main.js | 10 +++++----- src/orgchain/actions.js | 15 ++++++++++++++- tests/money.test.js | 10 +++++++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e6a0f2f..5c6bdab 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ chain itself records who did what, and when. stood at any beat. - **Money.** Test BTC lives at each node's SegWit address. **Mining** is a lab setting: under *Split* (the default) every active holding mines, and each beat's 50 test BTC splits pro rata by hashrate, a dial from 0 to 9 on each - holding (1 unless set), one coinbase output per holding; under *Last act* the holding that acts takes the whole - beat, and *Beat now* pays the one being worked in. **Send** moves test BTC from any active node to any other, in + holding (1 unless set), one coinbase output per holding; under *Last act* the holding behind the latest act or send + takes the whole beat (with nothing cued, *Beat now* pays the one being worked in), so at a timed tempo acts race + for the next beat. **Send** moves test BTC from any active node to any other, in its holding or another, signed by the sender's BIP-84 key: at most two coins in, at most 50 test BTC out, and the toy chain's fixed 500-sat fee. Sends land like acts, at once or cued, and *Beat now* mines a beat even when nothing is cued. A graduation moves each re-keyed node's money to its new address along with its inscription. Balances diff --git a/src/main.js b/src/main.js index 974b1e9..4affa50 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,7 @@ import './orgchain.css'; import { MetadataIndex } from './adapters/metadata-index.js'; import { census, lineageEvents, reduce, rows, slug, summary, trail } from './orgchain/lineage.js'; import { LAB_KEY, beat, decodeLab, emptyLab, encodeLab, mutate, readLab, setRigs, setRule, setTempo, tick } from './orgchain/store.js'; -import { SAMPLE, act, gasCheck, gasCost, minerOf, send, seedSample } from './orgchain/actions.js'; +import { SAMPLE, act, gasCheck, gasCost, lastHolding, minerOf, send, seedSample } from './orgchain/actions.js'; import { addressesOf, derivedSubtree, holderOf, keyOf, leavesOf, secretsOf } from './orgchain/keys.js'; import { DUST, FEE, balances, brief, btc, coins, ledger, miners, most, owners, payerOf, rigsOf, satsOf, tankOf, tanks } from './orgchain/money.js'; import { readEnvelope } from './adapters/inscription.js'; @@ -143,7 +143,7 @@ function renderHoldings() { })); for (const b of document.querySelectorAll('[data-rule]')) b.setAttribute('aria-pressed', String((lab.mining?.rule ?? 'split') === b.dataset.rule)); $('mining-note').textContent = split ? "Every active holding mines: each beat's 50 tBTC splits by hashrate, set on each holding (0 sits out)." - : "The holding that acts takes the whole beat, and Beat now pays the one you're working in. Every act costs gas, so it's a race."; + : "The holding behind the latest act or send takes the whole beat; with nothing cued, Beat now pays the one you're working in. At a timed tempo it's a race for the next beat."; $('holdings-empty').hidden = live.roots.length > 0; $('holdings-sub').textContent = live.roots.length ? String(live.roots.length) : ''; $('sample').disabled = live.roots.some(r => r.name === SAMPLE && r.status === 'active'); @@ -522,16 +522,16 @@ for (const b of document.querySelectorAll('[data-rule]')) b.addEventListener('cl // Time: the tempo, Beat now, and the clock that lands cued acts when their beat is due for (const b of document.querySelectorAll('[data-tempo]')) b.addEventListener('click', async () => { const value = b.dataset.tempo, tempo = /^\d+$/.test(value) ? Number(value) : value; - if (!(await run(l => setTempo(l, tempo, Date.now(), minerOf(l, ui.holding))))) return; + if (!(await run(l => setTempo(l, tempo, Date.now(), minerOf(l, lastHolding(l) ?? ui.holding))))) return; say(value === 'each' ? 'Each act is its own beat.' : value === 'manual' ? 'Acts are cued until you press Beat now.' : `Acts are cued and land together on the next beat, at most ${value} seconds after the first.`); }); -$('beat-now').addEventListener('click', () => landCue(l => { beat(l, minerOf(l, ui.holding)); })); +$('beat-now').addEventListener('click', () => landCue(l => { beat(l, minerOf(l, lastHolding(l) ?? ui.holding)); })); setInterval(() => { renderClock(); const { tempo, nextAt } = lab.clock; if (ui.busy || document.hidden || typeof tempo !== 'number' || !lab.chain.mempool.length || (nextAt && Date.now() < nextAt)) return; - landCue(l => tick(l, Date.now(), minerOf(l, ui.holding))); + landCue(l => tick(l, Date.now(), minerOf(l, lastHolding(l) ?? ui.holding))); }, 1000); addEventListener('storage', event => { if (event.key === LAB_KEY || event.key === null) load(); }); diff --git a/src/orgchain/actions.js b/src/orgchain/actions.js index 46fe4ad..9f6d0a8 100644 --- a/src/orgchain/actions.js +++ b/src/orgchain/actions.js @@ -11,7 +11,7 @@ import { newMnemonic } from '../adapters/lineage-keys.js'; import { holderCoin, makeClaim, countersign } from '../adapters/handoff.js'; import { PROTOCOL, lineageEvents, reduce } from './lineage.js'; import { armClock, beat } from './store.js'; -import { derivedSubtree, emptyKeys, holderOf, keyOf, rootWallet } from './keys.js'; +import { derivedSubtree, emptyKeys, holderOf, keyOf, rootWallet, scriptHex } from './keys.js'; import { DUST, SAT, canSpend, covers, pay, payouts, payerOf, spend, sweep } from './money.js'; const FIELDS = { found: ['name'], birth: ['name', 'kind'], award: ['to', 'skill', 'belt'], decide: ['title', 'why'], graduate: ['of'], close: [] }; @@ -102,6 +102,19 @@ function inscribe(lab, wallet, op, by, metadata) { // What a beat pays (money.js payouts): every mining holding's share, or, under 'last', the holding that acted. const minerFor = (lab, id) => { const state = lineageState(lab); return payouts(lab, state, state.nodes.get(id)?.root ?? null); }; export const minerOf = (lab, holdingId) => payouts(lab, lineageState(lab), holdingId); +// The holding behind the newest cued act or send, or null: under 'last', a timed beat goes to whoever acted last. +export function lastHolding(lab) { + const state = lineageState(lab), byScript = new Map(); + for (const n of state.nodes.values()) { const w = payerOf(lab.keys, state, n); if (w) byScript.set(scriptHex(w.script), n.root); } + for (const raw of [...lab.chain.mempool].reverse()) { + const record = lab.chain.transactions.get(Tx.util.getTxid(raw)); + if (record?.inscription) { const root = state.log.find(e => e.id === record.inscription.id)?.root; if (root) return root; continue; } + const input = record?.tx.vin[0], prev = input && lab.chain.transactions.get(input.txid)?.tx.vout[input.vout]; + const root = prev && byScript.get(scriptHex(prev.scriptPubKey)); + if (root) return root; + } + return null; +} // Who signs for a node: whichever key holds its inscription. function signerFor(lab, state, id) { const holder = holderOf(lab, state, id); diff --git a/tests/money.test.js b/tests/money.test.js index 6692899..16be3be 100644 --- a/tests/money.test.js +++ b/tests/money.test.js @@ -8,7 +8,7 @@ import { getVin, getVout } from '../src/upstream/primitives.js'; import { MetadataIndex } from '../src/adapters/metadata-index.js'; import { lineageEvents, reduce } from '../src/orgchain/lineage.js'; import { decodeLab, emptyLab, encodeLab, miningPreference, setRigs, setRule, setTempo, tick } from '../src/orgchain/store.js'; -import { act, gasCheck, lineageState, minerOf, seedSample, send } from '../src/orgchain/actions.js'; +import { act, gasCheck, lastHolding, lineageState, minerOf, seedSample, send } from '../src/orgchain/actions.js'; import { FEE, PayWallet, REWARD, SAT, balances, brief, btc, ledger, most, owners, payerOf, satsOf, tankOf, tanks } from '../src/orgchain/money.js'; import { keyOf } from '../src/orgchain/keys.js'; @@ -88,6 +88,14 @@ test('beats split across holdings by hashrate, or go whole to the last act', () act(lab, 'decide', beta, { title: 'Clicked' }); assert.deepEqual(mined(6), [['beta', REWARD]]); assert.equal(minerOf(lab, acme).length, 1); + setTempo(lab, 30, 1000); + act(lab, 'decide', acme, { title: 'First' }, 2000); + act(lab, 'decide', beta, { title: 'Last in' }, 3000); + assert.equal(lastHolding(lab), beta); + assert.equal(tick(lab, 32000, minerOf(lab, lastHolding(lab))), true); + assert.deepEqual(mined(7), [['beta', REWARD]]); // the race for a timed beat: whoever acted last takes it + assert.equal(lastHolding(lab), null); + setTempo(lab, 'each'); assert.deepEqual(decodeLab(encodeLab(lab)).mining, { rule: 'last', rigs: { [acme]: 3 } }); assert.throws(() => miningPreference({ rule: 'luck' }), /Invalid mining rule/); assert.throws(() => miningPreference({ rigs: { [acme]: 10 } }), /Invalid hashrate/);