From 7bf1d5149f8b0417dd01f598d274814816537ea5 Mon Sep 17 00:00:00 2001 From: Jason-Gregoire Date: Mon, 28 Sep 2026 21:50:30 -0400 Subject: [PATCH] Carry the BVN a discovered transaction was written on, and its block there, from the Directory Network search into the intent and prove it on that BVN, instead of stamping every intent with the Directory Network partition so that no intent could be proved. --- pkg/accumulate/dn_search_test.go | 17 ++++++++++++ pkg/accumulate/liteclient_adapter.go | 35 +++++++++++++++++++++--- pkg/consensus/async_attestation.go | 14 +++++----- pkg/consensus/intent.go | 14 ++++++---- pkg/execution/proof_partition_test.go | 5 ++-- pkg/intent/discovery.go | 20 +++++++------- pkg/intent/proof_partition_test.go | 39 +++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 pkg/intent/proof_partition_test.go diff --git a/pkg/accumulate/dn_search_test.go b/pkg/accumulate/dn_search_test.go index 0ab47f66..ca58a6a4 100644 --- a/pkg/accumulate/dn_search_test.go +++ b/pkg/accumulate/dn_search_test.go @@ -201,6 +201,11 @@ func TestADNBlockIsSearchedThroughEveryAnchoredBlock(t *testing.T) { if len(txs) != 1 || !strings.EqualFold(txs[0].Hash, intentTx) || txs[0].BlockHeight != dnH || txs[0].AccountURL != "acc://user.acme/data" { t.Fatalf("found %+v; want the one intent, at DN height %d", txs, dnH) } + // RB4-F46: the BVN the transaction was written on, and its block there - the L1-L3 proof is built on it. Both + // were dropped, so every intent was proved on "acc://dn.acme" and no proof could be built. + if txs[0].ProofPartition != "bvn1" || txs[0].ProofBlockIndex != bvn1Block { + t.Fatalf("the intent was written on bvn1 block %d; it carries proof partition %q block %d", bvn1Block, txs[0].ProofPartition, txs[0].ProofBlockIndex) + } // The BVNs were read at their own block, never at the DN's height. for _, q := range f.blockQueries { if strings.HasPrefix(q, "acc://bvn") && strings.Contains(q, fmt.Sprintf("|%d|", dnH)) { @@ -270,3 +275,15 @@ func TestAnAnchorEntryThatIsNotAnAnchorIsAnError(t *testing.T) { t.Fatal("an anchor-pool entry that is not an anchor was skipped") } } + +// RB4-F46: a BVN partition name is read from its partition URL exactly; anything else is no BVN, never a default. +func TestTheBVNNameIsReadFromItsPartitionURL(t *testing.T) { + for in, want := range map[string]string{ + "acc://bvn-BVN1.acme": "bvn1", "acc://bvn-bvn0.acme": "bvn0", "acc://BVN-Apollo.acme": "apollo", + "acc://dn.acme": "", "acc://bvn-.acme": "", "acc://bvn-BVN1.acme/ledger": "", "": "", "bvn1": "", + } { + if got := BVNNameOf(in); got != want { + t.Errorf("BVNNameOf(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/pkg/accumulate/liteclient_adapter.go b/pkg/accumulate/liteclient_adapter.go index 7fab7046..903c1228 100644 --- a/pkg/accumulate/liteclient_adapter.go +++ b/pkg/accumulate/liteclient_adapter.go @@ -352,9 +352,28 @@ type CertenTransaction struct { Timestamp time.Time `json:"timestamp"` IntentData map[string]interface{} `json:"intent_data"` TransactionType string `json:"transaction_type"` - // Legacy fields for backward compatibility + // Partition is the partition whose block BlockHeight counts: the Directory Network block the intent was + // discovered in (it anchored the BVN block that carried it). Batch settlement keeps the two as one pair. Partition string `json:"partition,omitempty"` RawTx map[string]interface{} `json:"raw_tx,omitempty"` + // ProofPartition is the BVN the transaction was written on ("bvn1") and ProofBlockIndex its block there: an + // L1-L3 proof is built on that BVN (RB4-F46). Empty when the entry was not read from a BVN. + ProofPartition string `json:"proof_partition,omitempty"` + ProofBlockIndex int64 `json:"proof_block_index,omitempty"` +} + +// BVNNameOf reads a BVN's partition name from its partition URL, acc://bvn-.acme -> lower(), exactly. +// Anything else - the Directory Network, a ledger URL, a malformed name - is no BVN: "". +func BVNNameOf(partitionURL string) string { + u := strings.ToLower(strings.TrimSpace(partitionURL)) + if !strings.HasPrefix(u, "acc://bvn-") || !strings.HasSuffix(u, ".acme") { + return "" + } + id := strings.TrimSuffix(strings.TrimPrefix(u, "acc://bvn-"), ".acme") + if id == "" || strings.ContainsAny(id, "/.@") { + return "" + } + return id } // isCertenTransaction checks if a block entry is a CERTEN intent transaction @@ -635,6 +654,10 @@ func (l *LiteClientAdapter) parseCertenTransaction(entry BlockEntry, block *Mino RawTx: entry.Data, IntentData: make(map[string]interface{}), } + if bvn := BVNNameOf(entry.Partition); bvn != "" { + certenTx.ProofPartition = bvn + certenTx.ProofBlockIndex = entry.PartitionBlock + } // Try to extract intent data from the correct transaction structure intentData := l.extractIntentDataFromEntry(entry) @@ -1232,6 +1255,10 @@ type BlockEntry struct { Index int `json:"index"` Type string `json:"type"` Data map[string]interface{} `json:"data"` + // Partition and PartitionBlock are the partition URL the entry was read from and its block there + // (RB4-F46: a DN block's transactions are read from the BVN blocks it anchored, and which BVN was lost). + Partition string `json:"partition,omitempty"` + PartitionBlock int64 `json:"partition_block,omitempty"` } // parseMinorBlockRecord parses a single MinorBlockRecord from the v3 API response @@ -1308,8 +1335,10 @@ func (l *LiteClientAdapter) getBlockEntries(blockData map[string]interface{}, bl for entryIdx, entry := range allEntries { if entryMap, ok := entry.(map[string]interface{}); ok { blockEntry := BlockEntry{ - Index: entryIdx, - Data: entryMap, + Index: entryIdx, + Data: entryMap, + Partition: partition, + PartitionBlock: blockHeight, } // Extract entry type if available diff --git a/pkg/consensus/async_attestation.go b/pkg/consensus/async_attestation.go index 739ec2d7..7b8279d2 100644 --- a/pkg/consensus/async_attestation.go +++ b/pkg/consensus/async_attestation.go @@ -454,9 +454,10 @@ func (bv *BFTValidator) RunProofCycle( commitment, att.CertenIntent.AccountURL, att.CertenIntent.TransactionHash, - // The partition the transaction was discovered on - the chain's answer, and the one consensus - // built its proof on. It used to be "", leaving the cycle to recompute it (RB3-F89). - att.CertenIntent.Partition, + // The BVN the transaction was written on - the chain's answer, and the one consensus built its + // proof on (RB4-F46: this was the Directory Network partition discovery found it through). It used + // to be "", leaving the cycle to recompute it (RB3-F89). + att.CertenIntent.ProofPartition, ); err != nil { // The orchestrator records the refusal as the member's outcome where the member can be placed // (RB3-F103); this line is the validator's own record of it. @@ -636,9 +637,10 @@ func (bv *BFTValidator) recordFailedProofCycle( commitment, att.CertenIntent.AccountURL, att.CertenIntent.TransactionHash, - // The partition the transaction was discovered on - the chain's answer, and the one consensus - // built its proof on. It used to be "", leaving the cycle to recompute it (RB3-F89). - att.CertenIntent.Partition, + // The BVN the transaction was written on - the chain's answer, and the one consensus built its + // proof on (RB4-F46: this was the Directory Network partition discovery found it through). It used + // to be "", leaving the cycle to recompute it (RB3-F89). + att.CertenIntent.ProofPartition, ); err != nil { bv.logger.Printf("⚠️ [PROOF-CYCLE] could not record the failure of intent %s: %v — the "+ "intent is settled nowhere AND recorded nowhere, which needs operator attention", diff --git a/pkg/consensus/intent.go b/pkg/consensus/intent.go index 005db323..b5283de5 100644 --- a/pkg/consensus/intent.go +++ b/pkg/consensus/intent.go @@ -71,11 +71,15 @@ type CertenIntent struct { TransactionHash string `json:"transactionHash"` AccountURL string `json:"accountUrl"` // Principal account URL (where TX lives): .../data OrganizationADI string `json:"organizationAdi"` // Organization ADI (for policy/routing): org ADI only - Partition string `json:"partition"` // BVN partition name (e.g., "bvn1") for L1-L3 proof generation - IntentData []byte `json:"intentData"` // Raw JSON blob - canonicalized later in commitment pipeline - CrossChainData []byte `json:"crossChainData"` // Raw JSON blob - canonicalized later in commitment pipeline - GovernanceData []byte `json:"governanceData"` // Raw JSON blob - canonicalized later in commitment pipeline - ReplayData []byte `json:"replayData"` // Raw JSON blob - canonicalized later in commitment pipeline + // Partition is the partition whose block the intent was discovered in (the Directory Network block that + // anchored it), paired with the discovery height for batch settlement's commit block. + Partition string `json:"partition"` + // ProofPartition is the BVN the intent was written on ("bvn1"): the L1-L3 proof is built on it (RB4-F46). + ProofPartition string `json:"proof_partition,omitempty"` + IntentData []byte `json:"intentData"` // Raw JSON blob - canonicalized later in commitment pipeline + CrossChainData []byte `json:"crossChainData"` // Raw JSON blob - canonicalized later in commitment pipeline + GovernanceData []byte `json:"governanceData"` // Raw JSON blob - canonicalized later in commitment pipeline + ReplayData []byte `json:"replayData"` // Raw JSON blob - canonicalized later in commitment pipeline // CRITICAL: Proof class determines execution routing per FIRST_PRINCIPLES 2.5 // On-demand vs on-cadence proofs are NEVER interchangeable diff --git a/pkg/execution/proof_partition_test.go b/pkg/execution/proof_partition_test.go index 32234ffb..d754237a 100644 --- a/pkg/execution/proof_partition_test.go +++ b/pkg/execution/proof_partition_test.go @@ -27,14 +27,15 @@ func TestAProofIsNeverBuiltOnAGuessedPartition(t *testing.T) { } } -// The proof cycle is started with the partition consensus used, not "". +// The proof cycle is started with the partition consensus used, not "": the BVN the transaction was written on +// (RB4-F46 - the intent Partition holds the Directory Network block discovery found it through). func TestTheProofCycleIsGivenTheDiscoveredPartition(t *testing.T) { raw, err := os.ReadFile("../consensus/async_attestation.go") if err != nil { t.Fatal(err) } src := string(raw) - if n := strings.Count(src, "att.CertenIntent.Partition,\n\t); err != nil {"); n != 2 { + if n := strings.Count(src, "att.CertenIntent.ProofPartition,\n\t); err != nil {"); n != 2 { t.Fatalf("%d of the 2 proof-cycle starts pass the discovered partition", n) } } diff --git a/pkg/intent/discovery.go b/pkg/intent/discovery.go index 62c5f613..7d39aefa 100644 --- a/pkg/intent/discovery.go +++ b/pkg/intent/discovery.go @@ -1245,6 +1245,8 @@ func (id *IntentDiscovery) convertCertenTransactionToIntent(certenTx *accumulate if certenTx.Partition != "" { intent.Partition = strings.ToLower(certenTx.Partition) } + // The BVN it was written on, for its L1-L3 proof (RB4-F46). + intent.ProofPartition = certenTx.ProofPartition // The minor block's own time, read with the block that carried the transaction: consensus data. intent.BlockTime = certenTx.Timestamp @@ -1570,7 +1572,7 @@ func (id *IntentDiscovery) processIntent(intent *CertenIntent, blockHeight uint6 defer cancel() // REAL L1-L3 consensus-bound chained proof (requires txHash, partition, CometBFT binding). - realProofApplicable := id.proofGenerator.HasRealProofBuilder() && intent.TransactionHash != "" && intent.Partition != "" + realProofApplicable := id.proofGenerator.HasRealProofBuilder() && intent.TransactionHash != "" && intent.ProofPartition != "" if realProofApplicable { // on_demand (financial) gets in-line retry to absorb DN-anchoring latency / transient // DN-BVN RPC blips; on_cadence makes a single attempt and may fall back to a basic proof. @@ -1579,9 +1581,9 @@ func (id *IntentDiscovery) processIntent(intent *CertenIntent, blockHeight uint6 inlineAttempts = id.config.ChainedProofInlineRetries } id.logger.Printf("🔗 [REAL-PROOF] Generating L1-L4 chained proof for %s (txHash=%s, partition=%s, attempts=%d)", - intent.IntentID, intent.TransactionHash[:16]+"...", intent.Partition, inlineAttempts) + intent.IntentID, intent.TransactionHash[:16]+"...", intent.ProofPartition, inlineAttempts) - cp, perr := id.buildChainedCertenProof(ctx, accountURL, intent.TransactionHash, intent.Partition, intent.IntentID, inlineAttempts) + cp, perr := id.buildChainedCertenProof(ctx, accountURL, intent.TransactionHash, intent.ProofPartition, intent.IntentID, inlineAttempts) if perr != nil { id.logger.Printf("⚠️ [REAL-PROOF] L1-L4 chained proof unavailable for %s: %v", intent.IntentID, perr) } else { @@ -1610,9 +1612,9 @@ func (id *IntentDiscovery) processIntent(intent *CertenIntent, blockHeight uint6 // each must be backed by the same chained proof. if certenProof == nil { if !realProofApplicable { - return consensus.TargetChainFailed, fmt.Errorf("intent %s (proofClass=%s): %w (realBuilder=%v txHash=%q partition=%q)", + return consensus.TargetChainFailed, fmt.Errorf("intent %s (proofClass=%s): %w (realBuilder=%v txHash=%q proof partition=%q)", intent.IntentID, proofClass, errChainedProofTerminal, - id.proofGenerator.HasRealProofBuilder(), intent.TransactionHash, intent.Partition) + id.proofGenerator.HasRealProofBuilder(), intent.TransactionHash, intent.ProofPartition) } return consensus.TargetChainFailed, fmt.Errorf("intent %s (proofClass=%s): %w", intent.IntentID, proofClass, errChainedProofUnavailable) } @@ -1732,7 +1734,7 @@ func (id *IntentDiscovery) processMultiLegIntent(intent *CertenIntent, blockHeig defer cancel() // REAL L1-L3 consensus-bound chained proof (same fail-closed policy as single-leg). - realProofApplicable := id.proofGenerator.HasRealProofBuilder() && intent.TransactionHash != "" && intent.Partition != "" + realProofApplicable := id.proofGenerator.HasRealProofBuilder() && intent.TransactionHash != "" && intent.ProofPartition != "" if realProofApplicable { inlineAttempts := 1 if proofClass == "on_demand" { @@ -1740,7 +1742,7 @@ func (id *IntentDiscovery) processMultiLegIntent(intent *CertenIntent, blockHeig } id.logger.Printf("🔗 [MULTI-LEG] Generating L1-L3 chained proof for %s (attempts=%d)", intent.IntentID, inlineAttempts) - cp, perr := id.buildChainedCertenProof(ctx, accountURL, intent.TransactionHash, intent.Partition, intent.IntentID, inlineAttempts) + cp, perr := id.buildChainedCertenProof(ctx, accountURL, intent.TransactionHash, intent.ProofPartition, intent.IntentID, inlineAttempts) if perr != nil { id.logger.Printf("⚠️ [MULTI-LEG] L1-L3 chained proof unavailable for %s: %v", intent.IntentID, perr) } else { @@ -1754,9 +1756,9 @@ func (id *IntentDiscovery) processMultiLegIntent(intent *CertenIntent, blockHeig // idempotent so the requeue is replay-safe. if certenProof == nil { if !realProofApplicable { - return consensus.TargetChainFailed, fmt.Errorf("multi-leg intent %s (proofClass=%s): %w (realBuilder=%v txHash=%q partition=%q)", + return consensus.TargetChainFailed, fmt.Errorf("multi-leg intent %s (proofClass=%s): %w (realBuilder=%v txHash=%q proof partition=%q)", intent.IntentID, proofClass, errChainedProofTerminal, - id.proofGenerator.HasRealProofBuilder(), intent.TransactionHash, intent.Partition) + id.proofGenerator.HasRealProofBuilder(), intent.TransactionHash, intent.ProofPartition) } return consensus.TargetChainFailed, fmt.Errorf("multi-leg intent %s (proofClass=%s): %w", intent.IntentID, proofClass, errChainedProofUnavailable) } diff --git a/pkg/intent/proof_partition_test.go b/pkg/intent/proof_partition_test.go new file mode 100644 index 00000000..b9638696 --- /dev/null +++ b/pkg/intent/proof_partition_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 Certen Protocol + +package intent + +import ( + "log" + "os" + "strings" + "testing" + + "github.com/certen/independant-validator/pkg/accumulate" +) + +// RB4-F46: an intent is proved on the BVN it was written on. Discovery reads a Directory Network block through +// the BVN blocks it anchored and stamped every intent with "acc://dn.acme"; the L1-L3 proof refuses a non-BVN +// partition, so no intent could be proved (Phase C, 2026-09-29: "partition acc://dn.acme ... is not a BVN"). +// The discovery pair (the DN block and its height) stays the batch commit pair; the BVN rides beside it. +func TestAnIntentCarriesTheBVNItWasWrittenOn(t *testing.T) { + id := &IntentDiscovery{logger: log.New(os.Stderr, "", 0)} + ci, err := id.convertCertenTransactionToIntent(&accumulate.CertenTransaction{ + Hash: strings.Repeat("b2", 32), AccountURL: "acc://harbor.acme/data", BlockHeight: 9987981, + Partition: "acc://dn.acme", ProofPartition: "bvn1", ProofBlockIndex: 8270001, + IntentData: map[string]interface{}{ + "intentData": map[string]interface{}{"intent_id": "f46", "proof_class": "on_demand"}, + "crossChainData": map[string]interface{}{}, + "governanceData": map[string]interface{}{"organizationAdi": "acc://harbor.acme"}, + "replayData": map[string]interface{}{"expires_at": 1790600000}, + }, + }) + if err != nil { + t.Fatal(err) + } + if ci.ProofPartition != "bvn1" { + t.Fatalf("the intent is proved on %q; it was written on bvn1", ci.ProofPartition) + } + if ci.Partition != "acc://dn.acme" { + t.Fatalf("the discovery partition (the batch commit pair) became %q", ci.Partition) + } +}