| … | … | @@ -27,6 +27,8 @@ import process from 'bare-process' |
| 27 | 27 | |
| 28 | 28 | import hyperdbDefinition from '../spec/state-base/contract/hyperdb/index.js' |
| 29 | 29 | import dispatch from '../spec/state-base/contract/hyperdispatch/index.js' |
| 30 | import { SignerPresence } from './signer-presence.js' |
| 31 | import { attachPresence } from './signer-presence-channel.js' |
| 30 | 32 | |
| 31 | 33 | const { Router, encode } = dispatch |
| 32 | 34 | |
| … | … | @@ -136,6 +138,11 @@ export class StateBaseV3 { |
| 136 | 138 | this._selfKey = opts.selfKey |
| 137 | 139 | ? toBuf(opts.selfKey) |
| 138 | 140 | : (this._selfKeyPair ? toBuf(this._selfKeyPair.publicKey) : null) |
| 141 | // Leak-free single-signer election from live CONNECTION PRESENCE (replaces the __lease__ ts-heartbeat, |
| 142 | // which appended to the log every ~8s forever). self = our member key; graceMs = failover blip-tolerance |
| 143 | // (default matches the old lease TTL). Fed by attachSignerPresence() on each swarm connection; the sign |
| 144 | // loop reads holdsSigner(). Null when we have no member identity (read-only opens). |
| 145 | this._presence = this._selfKey ? new SignerPresence({ self: this._selfKey.toString('hex'), graceMs: Number(opts.signerGraceMs) || 15000 }) : null |
| 139 | 146 | this._getMembers = typeof opts.getMembers === 'function' ? opts.getMembers : null |
| 140 | 147 | // Injectable crypto for membership-gated admission (keet attestation primitives). |
| 141 | 148 | // attest: sign localKey‖base.key as our keet identity; verifyAttest: IdentityKey.verify. |
| … | … | @@ -370,6 +377,7 @@ export class StateBaseV3 { |
| 370 | 377 | // (the testy1/AI-Testing-Town "can't write" deadlock). cf. pear-git-v3 |
| 371 | 378 | // pear-git.js:447 ("corestore.replicate unreliable for writer cores"). |
| 372 | 379 | try { this.base.replicate(socket) } catch (err) { console.error('[state-base-v3] replicate threw:', err.message) } |
| 380 | try { this.attachSignerPresence(socket) } catch {} // ephemeral signer-presence handshake — feeds the election, appends nothing |
| 373 | 381 | }) |
| 374 | 382 | } |
| 375 | 383 | this.swarm.join(this.base.discoveryKey) |
| … | … | @@ -378,6 +386,19 @@ export class StateBaseV3 { |
| 378 | 386 | this.base.key.toString('hex').slice(0, 12), this.base.writable) |
| 379 | 387 | } |
| 380 | 388 | |
| 389 | // Attach the ephemeral signer-presence handshake to a connection (announce our member key, learn the |
| 390 | // peer's → feed the election). Public so the multi-DID daemon can also attach it on the shared swarm; |
| 391 | // the single-DID path calls it from the internal connection handler. Returns a detach fn; no-op without a |
| 392 | // member identity. Appends NOTHING — presence lives and dies with the connection. |
| 393 | attachSignerPresence (conn) { return this._presence ? attachPresence(conn, { selfKey: this._selfKey.toString('hex'), presence: this._presence }) : (() => {}) } |
| 394 | |
| 395 | // Do WE hold the active-signer role right now? Drop-in for the old lease.holds() in the sign loop: |
| 396 | // election is a deterministic function of who is CONNECTED (lowest-key effective co-owner), computed in |
| 397 | // RAM. A device with no co-owners connected is the sole signer (holds=true). No writes, ever. |
| 398 | holdsSigner (now) { return this._presence ? this._presence.holds(now) : true } |
| 399 | currentSigner (now) { return this._presence ? this._presence.currentSigner(now) : (this._selfKey ? this._selfKey.toString('hex') : null) } |
| 400 | signerState (now) { return this._presence ? this._presence.state(now) : { self: null, signer: null, effective: [], holds: true } } |
| 401 | |
| 381 | 402 | // keet's _autobaseApply commit shape: a per-batch transaction; handlers write |
| 382 | 403 | // into it; flush on success / close on error (never insert on the db directly). |
| 383 | 404 | async _apply (nodes, view, base) { |