@@ -8,6 +8,7 @@ import { serializeMine } from "../cogop/index.js";
88import { openWalletReadContext , type WalletReadContext } from "../read/index.js" ;
99import type { WalletRuntimePaths } from "../runtime.js" ;
1010import type { WalletSecretProvider } from "../state/provider.js" ;
11+ import { loadWalletState } from "../state/storage.js" ;
1112import {
1213 assertFixedInputPrefixMatches ,
1314 buildWalletMutationTransaction ,
@@ -53,7 +54,9 @@ import {
5354 miningPublishMayStillExist ,
5455} from "./state.js" ;
5556import {
57+ ensureIndexerTruthIsCurrent ,
5658 refreshMiningCandidateFromCurrentStateDetailed ,
59+ getIndexerTruthKey ,
5760 type MiningCandidateRefreshFailureReason ,
5861 type MiningEligibleAnchoredRoot ,
5962} from "./candidate.js" ;
@@ -469,6 +472,150 @@ function candidateTargetsCurrentNodeTip(
469472 && candidate . referencedBlockHashDisplay === nodeBestHash ;
470473}
471474
475+ type PublishFastRevalidationResult =
476+ | { ok : true }
477+ | {
478+ ok : false ;
479+ reason : string ;
480+ errorName ?: string | null ;
481+ } ;
482+
483+ function managedCoreWalletIdentityMatches (
484+ current : WalletStateV1 [ "managedCoreWallet" ] ,
485+ loaded : WalletStateV1 [ "managedCoreWallet" ] ,
486+ ) : boolean {
487+ return loaded . walletName === current . walletName
488+ && loaded . internalPassphrase === current . internalPassphrase
489+ && loaded . descriptorChecksum === current . descriptorChecksum
490+ && ( loaded . walletAddress ?? null ) === ( current . walletAddress ?? null )
491+ && ( loaded . walletScriptPubKeyHex ?? null ) === ( current . walletScriptPubKeyHex ?? null )
492+ && loaded . proofStatus === current . proofStatus ;
493+ }
494+
495+ function walletPublishIdentityMatches (
496+ context : ReadyMiningReadContext ,
497+ loaded : WalletStateV1 ,
498+ ) : boolean {
499+ const current = context . localState . state ;
500+ return loaded . stateRevision === current . stateRevision
501+ && loaded . walletRootId === current . walletRootId
502+ && loaded . walletRootId === context . localState . walletRootId
503+ && loaded . network === current . network
504+ && loaded . descriptor . publicExternal === current . descriptor . publicExternal
505+ && loaded . descriptor . checksum === current . descriptor . checksum
506+ && loaded . funding . address === current . funding . address
507+ && loaded . funding . scriptPubKeyHex === current . funding . scriptPubKeyHex
508+ && managedCoreWalletIdentityMatches ( current . managedCoreWallet , loaded . managedCoreWallet ) ;
509+ }
510+
511+ async function validateFastPublishReadContext ( options : {
512+ readContext : ReadyMiningReadContext ;
513+ candidate : MiningCandidate ;
514+ dataDir : string ;
515+ provider : WalletSecretProvider ;
516+ paths : WalletRuntimePaths ;
517+ throwIfStopping ?: ( ) => void ;
518+ } ) : Promise < PublishFastRevalidationResult > {
519+ options . throwIfStopping ?.( ) ;
520+
521+ if ( options . candidate . provenance === undefined ) {
522+ return {
523+ ok : false ,
524+ reason : "missing-candidate-provenance" ,
525+ } ;
526+ }
527+
528+ if ( candidateProvenanceSnapshotChanged ( options . readContext , options . candidate ) ) {
529+ return {
530+ ok : false ,
531+ reason : "candidate-provenance-changed" ,
532+ } ;
533+ }
534+
535+ const truthKey = getIndexerTruthKey ( options . readContext ) ;
536+ if ( truthKey === null ) {
537+ return {
538+ ok : false ,
539+ reason : "missing-snapshot-lease" ,
540+ } ;
541+ }
542+
543+ try {
544+ await ensureIndexerTruthIsCurrent ( {
545+ dataDir : options . dataDir ,
546+ truthKey,
547+ } ) ;
548+ } catch ( error ) {
549+ return {
550+ ok : false ,
551+ reason : "indexer-truth-changed" ,
552+ errorName : error instanceof Error ? error . name : "unknown" ,
553+ } ;
554+ }
555+
556+ let loadedState : WalletStateV1 ;
557+ try {
558+ loadedState = ( await loadWalletState ( {
559+ primaryPath : options . paths . walletStatePath ,
560+ backupPath : options . paths . walletStateBackupPath ,
561+ } , {
562+ provider : options . provider ,
563+ } ) ) . state ;
564+ } catch ( error ) {
565+ return {
566+ ok : false ,
567+ reason : "wallet-state-unavailable" ,
568+ errorName : error instanceof Error ? error . name : "unknown" ,
569+ } ;
570+ }
571+
572+ if ( ! walletPublishIdentityMatches ( options . readContext , loadedState ) ) {
573+ return {
574+ ok : false ,
575+ reason : "wallet-state-changed" ,
576+ } ;
577+ }
578+
579+ return { ok : true } ;
580+ }
581+
582+ async function appendFastPublishRevalidationEvent ( options : {
583+ appendEventFn : AppendMiningEventFn ;
584+ paths : WalletRuntimePaths ;
585+ candidate : MiningCandidate ;
586+ runId : string | null ;
587+ readContext : ReadyMiningReadContext ;
588+ durationMs : number ;
589+ result : PublishFastRevalidationResult ;
590+ } ) : Promise < void > {
591+ await appendPublishTimingEvent (
592+ options . appendEventFn ,
593+ options . paths ,
594+ "timing-publish-fast-revalidation" ,
595+ options . result . ok
596+ ? "Validated current mining read context before publish."
597+ : "Current mining read context needs a publish-time refresh." ,
598+ createPublishTimingContext ( {
599+ candidate : options . candidate ,
600+ runId : options . runId ,
601+ level : options . result . ok ? "info" : "warn" ,
602+ durationMs : options . durationMs ,
603+ metrics : {
604+ outcome : options . result . ok ? "success" : "fallback" ,
605+ reason : options . result . ok ? null : options . result . reason ,
606+ errorName : options . result . ok ? null : options . result . errorName ?? null ,
607+ snapshotSeq : options . readContext . snapshot . snapshotSeq ?? options . readContext . indexer . snapshotSeq ?? null ,
608+ daemonInstanceId : options . readContext . snapshot . daemonInstanceId ?? options . readContext . indexer . daemonInstanceId ?? null ,
609+ stateRevision : options . readContext . localState . state . stateRevision ,
610+ coreBestHeight : options . readContext . nodeStatus ?. nodeBestHeight ?? null ,
611+ coreBestHash : options . readContext . nodeStatus ?. nodeBestHashHex ?? null ,
612+ indexerTipHeight : options . readContext . snapshot . tip ?. height ?? options . readContext . indexer . snapshotTip ?. height ?? null ,
613+ indexerTipHash : options . readContext . snapshot . tip ?. blockHashHex ?? options . readContext . indexer . snapshotTip ?. blockHashHex ?? null ,
614+ } ,
615+ } ) ,
616+ ) ;
617+ }
618+
472619interface MiningCoreTip {
473620 height : number | null ;
474621 hash : string | null ;
@@ -1178,6 +1325,7 @@ export async function publishCandidate(options: {
11781325 provider : WalletSecretProvider ;
11791326 paths : WalletRuntimePaths ;
11801327 fallbackState : WalletStateV1 ;
1328+ currentReadContext ?: ReadyMiningReadContext ;
11811329 openReadContext : typeof openWalletReadContext ;
11821330 attachService : typeof attachOrStartManagedBitcoindService ;
11831331 rpcFactory : ( config : Parameters < typeof createRpcClient > [ 0 ] ) => MiningRpcClient ;
@@ -1290,66 +1438,9 @@ export async function publishCandidate(options: {
12901438 } ;
12911439 } ;
12921440
1293- options . throwIfStopping ?.( ) ;
1294- const readContextRefreshStartedAt = performance . now ( ) ;
1295- let lockedReadContext : WalletReadContext ;
1296- try {
1297- lockedReadContext = await options . openReadContext ( {
1298- dataDir : options . dataDir ,
1299- databasePath : options . databasePath ,
1300- secretProvider : options . provider ,
1301- walletControlLockHeld : true ,
1302- paths : options . paths ,
1303- } ) ;
1304- } catch ( error ) {
1305- await appendPublishTimingEvent (
1306- options . appendEventFn ,
1307- options . paths ,
1308- "timing-read-context-refresh" ,
1309- "Refreshed mining read context before publish." ,
1310- createPublishTimingContext ( {
1311- candidate : options . candidate ,
1312- runId : options . runId ,
1313- level : "warn" ,
1314- durationMs : performance . now ( ) - readContextRefreshStartedAt ,
1315- metrics : {
1316- outcome : "error" ,
1317- errorName : error instanceof Error ? error . name : "unknown" ,
1318- } ,
1319- } ) ,
1320- ) ;
1321- throw error ;
1322- }
1323-
1324- try {
1325- options . throwIfStopping ?.( ) ;
1326- const readyReadContext = resolveReadyMiningReadContext ( lockedReadContext ) ;
1327- await appendPublishTimingEvent (
1328- options . appendEventFn ,
1329- options . paths ,
1330- "timing-read-context-refresh" ,
1331- "Refreshed mining read context before publish." ,
1332- createPublishTimingContext ( {
1333- candidate : options . candidate ,
1334- runId : options . runId ,
1335- durationMs : performance . now ( ) - readContextRefreshStartedAt ,
1336- metrics : {
1337- outcome : readyReadContext === null ? "snapshot-unavailable" : "success" ,
1338- indexerTruthSource : lockedReadContext . indexer . source ?? null ,
1339- snapshotSeq : lockedReadContext . snapshot ?. snapshotSeq ?? lockedReadContext . indexer . snapshotSeq ?? null ,
1340- daemonInstanceId : lockedReadContext . snapshot ?. daemonInstanceId ?? lockedReadContext . indexer . daemonInstanceId ?? null ,
1341- hasSnapshot : lockedReadContext . snapshot !== null ,
1342- hasModel : lockedReadContext . model !== null ,
1343- coreBestHeight : lockedReadContext . nodeStatus ?. nodeBestHeight ?? null ,
1344- coreBestHash : lockedReadContext . nodeStatus ?. nodeBestHashHex ?? null ,
1345- indexerTipHeight : lockedReadContext . snapshot ?. tip ?. height ?? lockedReadContext . indexer . snapshotTip ?. height ?? null ,
1346- indexerTipHash : lockedReadContext . snapshot ?. tip ?. blockHashHex ?? lockedReadContext . indexer . snapshotTip ?. blockHashHex ?? null ,
1347- } ,
1348- } ) ,
1349- ) ;
1350- if ( readyReadContext === null ) {
1351- return await createSnapshotUnavailableRetryResult ( ) ;
1352- }
1441+ const publishWithReadyReadContext = async (
1442+ readyReadContext : ReadyMiningReadContext ,
1443+ ) : Promise < MiningPublishOutcome > => {
13531444 if ( ! candidateTargetsCurrentNodeTip ( readyReadContext , options . candidate ) ) {
13541445 return await createRestartResult (
13551446 readyReadContext . localState . state ,
@@ -1495,6 +1586,94 @@ export async function publishCandidate(options: {
14951586
14961587 throw error ;
14971588 }
1589+ } ;
1590+
1591+ if ( options . currentReadContext !== undefined ) {
1592+ const fastRevalidationStartedAt = performance . now ( ) ;
1593+ const fastRevalidationResult = await validateFastPublishReadContext ( {
1594+ readContext : options . currentReadContext ,
1595+ candidate : options . candidate ,
1596+ dataDir : options . dataDir ,
1597+ provider : options . provider ,
1598+ paths : options . paths ,
1599+ throwIfStopping : options . throwIfStopping ,
1600+ } ) ;
1601+ await appendFastPublishRevalidationEvent ( {
1602+ appendEventFn : options . appendEventFn ,
1603+ paths : options . paths ,
1604+ candidate : options . candidate ,
1605+ runId : options . runId ,
1606+ readContext : options . currentReadContext ,
1607+ durationMs : performance . now ( ) - fastRevalidationStartedAt ,
1608+ result : fastRevalidationResult ,
1609+ } ) ;
1610+
1611+ if ( fastRevalidationResult . ok ) {
1612+ return await publishWithReadyReadContext ( options . currentReadContext ) ;
1613+ }
1614+ }
1615+
1616+ options . throwIfStopping ?.( ) ;
1617+ const readContextRefreshStartedAt = performance . now ( ) ;
1618+ let lockedReadContext : WalletReadContext ;
1619+ try {
1620+ lockedReadContext = await options . openReadContext ( {
1621+ dataDir : options . dataDir ,
1622+ databasePath : options . databasePath ,
1623+ secretProvider : options . provider ,
1624+ walletControlLockHeld : true ,
1625+ paths : options . paths ,
1626+ } ) ;
1627+ } catch ( error ) {
1628+ await appendPublishTimingEvent (
1629+ options . appendEventFn ,
1630+ options . paths ,
1631+ "timing-read-context-refresh" ,
1632+ "Refreshed mining read context before publish." ,
1633+ createPublishTimingContext ( {
1634+ candidate : options . candidate ,
1635+ runId : options . runId ,
1636+ level : "warn" ,
1637+ durationMs : performance . now ( ) - readContextRefreshStartedAt ,
1638+ metrics : {
1639+ outcome : "error" ,
1640+ errorName : error instanceof Error ? error . name : "unknown" ,
1641+ } ,
1642+ } ) ,
1643+ ) ;
1644+ throw error ;
1645+ }
1646+
1647+ try {
1648+ options . throwIfStopping ?.( ) ;
1649+ const readyReadContext = resolveReadyMiningReadContext ( lockedReadContext ) ;
1650+ await appendPublishTimingEvent (
1651+ options . appendEventFn ,
1652+ options . paths ,
1653+ "timing-read-context-refresh" ,
1654+ "Refreshed mining read context before publish." ,
1655+ createPublishTimingContext ( {
1656+ candidate : options . candidate ,
1657+ runId : options . runId ,
1658+ durationMs : performance . now ( ) - readContextRefreshStartedAt ,
1659+ metrics : {
1660+ outcome : readyReadContext === null ? "snapshot-unavailable" : "success" ,
1661+ indexerTruthSource : lockedReadContext . indexer . source ?? null ,
1662+ snapshotSeq : lockedReadContext . snapshot ?. snapshotSeq ?? lockedReadContext . indexer . snapshotSeq ?? null ,
1663+ daemonInstanceId : lockedReadContext . snapshot ?. daemonInstanceId ?? lockedReadContext . indexer . daemonInstanceId ?? null ,
1664+ hasSnapshot : lockedReadContext . snapshot !== null ,
1665+ hasModel : lockedReadContext . model !== null ,
1666+ coreBestHeight : lockedReadContext . nodeStatus ?. nodeBestHeight ?? null ,
1667+ coreBestHash : lockedReadContext . nodeStatus ?. nodeBestHashHex ?? null ,
1668+ indexerTipHeight : lockedReadContext . snapshot ?. tip ?. height ?? lockedReadContext . indexer . snapshotTip ?. height ?? null ,
1669+ indexerTipHash : lockedReadContext . snapshot ?. tip ?. blockHashHex ?? lockedReadContext . indexer . snapshotTip ?. blockHashHex ?? null ,
1670+ } ,
1671+ } ) ,
1672+ ) ;
1673+ if ( readyReadContext === null ) {
1674+ return await createSnapshotUnavailableRetryResult ( ) ;
1675+ }
1676+ return await publishWithReadyReadContext ( readyReadContext ) ;
14981677 } finally {
14991678 await lockedReadContext . close ( ) ;
15001679 }
0 commit comments