Skip to content

views: zero-copy XDR view extractors — events, tx-hashes, tx-details, tx-pages (#764) - #778

Closed
chowbao wants to merge 4 commits into
fh-ingest-basefrom
fh-764-views
Closed

views: zero-copy XDR view extractors — events, tx-hashes, tx-details, tx-pages (#764)#778
chowbao wants to merge 4 commits into
fh-ingest-basefrom
fh-764-views

Conversation

@chowbao

@chowbao chowbao commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Implements #764 — the four zero-copy XDR view extractors, promoted from the rpc-hack bench harness into a production package cmd/stellar-rpc/internal/fullhistory/views/. Each turns a raw LedgerCloseMeta (wrapped as xdr.LedgerCloseMetaView) into the shape RPC v2 needs, without UnmarshalBinary — outputs alias the view buffer (callers copy what they retain).

Stacked. Based on a throwaway integration base (fh-ingest-base = feature/full-history + #756 merged) so it compiles against the events index. Will retarget to feature/full-history once #756 lands. The ingestion PR (#765) stacks on this branch.

The four extractors (all of #764)

#764 requirement Entrypoint File
events — events-index payloads ExtractEvents(lcm) ([]events.Payload, error) events.go
tx-hashes — txHash per tx ExtractTxHashes(lcm) ([]txhash.Entry, error) txhash.go
tx-details by hash — envelope/result/meta for a target hash (getTransaction) ExtractTxDetailsByHash(lcm, hash, passphrase) (Transaction, bool, error) txdetails.go
tx-pages — transactions in order with cursor (getTransactions) ExtractTransactions(lcm, startIdx, limit, passphrase) ([]Transaction, error) txdetails.go
  • events / tx-hashes read only TxProcessing (which carries the hash + meta), so they take no passphrase; ExtractEvents returns ErrV0Unsupported for V0 (V0 has no contract events).
  • tx-details / tx-pages also need each tx's envelope (in the TxSet, which is in agreed-set/hash-sorted order, not apply order), so they pair envelope→tx by hash (network.HashTransactionInEnvelope) and take the network passphrase. startIdx/limit are the getTransactions cursor. They return a package-local views.Transaction.
  • Supporting: envelopes.go (version/phase-aware envelope enumeration + hashing, used by the two read-path extractors).

Tests

  • Differential vs the parsed path: ExtractEvents/ExtractTxHashes vs the struct LCMToPayloads/lcm.TransactionHash; ExtractTxDetailsByHash/ExtractTransactions vs db.ParseTransaction (driven by the SDK LedgerTransactionReader) — field-by-field, wire-identical bytes, across LCM V0/V1/V2 and meta V1–V4 (incl. V0Components + ParallelTxs phases, staged top-level + per-op events, diagnostic events).
  • pairing_test.go — reversed-order regression (TxSet order ≠ apply order) guarding the by-hash pairing.
  • Negative paths (unsupported meta version, missing-hash, startIdx<0/limit<0), a zero-copy aliasing assertion, and per-extractor go test -bench benchmarks.

SDK vs stellar-rpc split (issue #764 acceptance item)

Rule: low-level XDR navigation → go-stellar-sdk; RPC-specific data shapes → stellar-rpc.

Determination: nothing in this task belongs in the SDK — the boundary already sat where the rule wants it. The SDK ships all the low-level navigation, so this package is a pure consumer that composes those primitives into RPC shapes.

SDK-side (consumed, nothing added) — production imports:

  • xdr.LedgerCloseMetaView + the generated *View accessors (.V()/.V0..V2(), .Iter(), .At(), .Count(), .Raw(), .Value()) — zero-copy, schema-generated XDR navigation.
  • network.HashTransactionInEnvelope — canonical protocol-level tx-hash (envelope↔tx pairing).
  • toid — protocol-level transaction/operation-ID (apply order / cursor).

stellar-rpc-side (this package) — the four extractors plus the RPC-specific shapes (events.Payload, txhash.Entry, the package-local views.Transaction) and semantics (event-ID/term ordering, startIdx/limit paging, envelope-by-hash pairing). Documented in views/doc.go.

The differential tests pull in the SDK's ingest.LedgerTransactionReader only as the reference oracle to validate the view path against — it is deliberately not a production dependency.

Related layering choice (within stellar-rpc): the read-path extractors return a package-local views.Transaction rather than internal/db.Transaction (even though db doesn't import views, so there'd be no cycle) — keeping views a clean leaf and avoiding an inversion of the db → views layering.

Differences from rpc-hack

A faithful promotion of rpc-hack's view-extraction logic (the V1–V4 event walk and XDR-view navigation are essentially line-for-line), with the bench-only scaffolding dropped (roundtrip-comparison harness, CLI/CSV/cache) and a few deliberate changes:

  • One correctness fix (the notable divergence): rpc-hack paired each transaction's envelope to its TxProcessing entry by array position, which is only correct when TxSet order == apply order — it isn't in general. That's a latent bug still present on rpc-hack; its differential tests missed it because their fixtures built the TxSet in apply order. Here, envelopes are paired by hash (envelopesByHash, mirroring the SDK's LedgerTransactionReader), with pairing_test.go as a reversed-order regression.
  • Packaging: moved from package events + the bench package main into one leaf package views; read-path extractors return views.Transaction (flat LedgerSequence/LedgerCloseTime, raw [32]byte hash) rather than internal/db.Transaction.
  • Events: term keys are no longer precomputed inline onto the payload; they're derived downstream via events.TermsForBytes. The (previously ignored) passphrase param was dropped from ExtractEvents; it's now a real, used input on the read-path extractors.
  • Hardening not in rpc-hack: V3 contract-event gating on IsSorobanTx (matches db.ParseTransaction; rpc-hack over-emitted for a classic-envelope V3 tx), startIdx<0/limit<0 rejection, an explicit missing-hash error, and differential tests vs db.ParseTransaction across LCM V0/V1/V2.

Follow-up (per #764 acceptance, not in this PR)

  • Full-history sweep harness that walks every ledger and asserts view-extraction == full-decode (separate tracking issue).

Test plan

  • go test ./cmd/stellar-rpc/internal/fullhistory/views/ (+ -bench), go vet, gofmt — green.

Comment thread cmd/stellar-rpc/internal/fullhistory/views/txdetails.go
@chowbao
chowbao marked this pull request as ready for review June 9, 2026 20:22
@chowbao chowbao changed the title [DRAFT] views: zero-copy XDR view extractors — events + tx-hashes (#764) Views: zero-copy XDR view extractors — events + tx-hashes (#764) Jun 9, 2026
@chowbao chowbao changed the title Views: zero-copy XDR view extractors — events + tx-hashes (#764) [DRAFT] views: zero-copy XDR view extractors — events, tx-hashes, tx-details, tx-pages (#764) Jun 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c842d0525

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread cmd/stellar-rpc/internal/fullhistory/views/txdetails.go
@chowbao chowbao changed the title [DRAFT] views: zero-copy XDR view extractors — events, tx-hashes, tx-details, tx-pages (#764) views: zero-copy XDR view extractors — events, tx-hashes, tx-details, tx-pages (#764) Jun 9, 2026
Four zero-copy XDR view extractors over xdr.LedgerCloseMetaView (events,
tx-hashes, tx-details-by-hash, tx-pages); outputs alias the view buffer.
Transactions paired to TxSet envelopes by hash (mirroring
ingest.LedgerTransactionReader); V3 contract events gated on IsSorobanTx.
Differential-tested vs the parsed / db.ParseTransaction path across LCM
V0/V1/V2, meta V1-V4, V0Components + ParallelTxs, order mismatch,
diagnostic events, empty, sponsorship, large-tx, and protocol-transition
fixtures; aliasing + negative-path coverage; per-extractor benches.
Leaf package (no internal/db dep).

Closes #764.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7250ad5e7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread cmd/stellar-rpc/internal/fullhistory/views/events.go

@Shaptic Shaptic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass

Comment on lines +44 to +50
// Transient decode purely to compute the hash; the returned raw slice is
// the original zero-copy .Raw() view buffer, not this decoded value.
var decoded xdr.TransactionEnvelope
if uerr := decoded.UnmarshalBinary(raw); uerr != nil {
return envPart{}, fmt.Errorf("views: envelope decode for hashing: %w", uerr)
}
hash, err := network.HashTransactionInEnvelope(decoded, passphrase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this more-or-less just re-marshal it anyway? It's a little more involved than that and obviously the code would be really gross but the round trip here does feelsbadman a bit.

Alternatively, since you're already unmarshaling the envelope, you can remove the code above that fetches type via views and just do it the "normal" way for brevity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing the loop: the decode is gone entirely now — the hash preimage is built from the wire bytes directly (da9ee15), so neither the decode nor the round-trip exists anymore.

Comment thread cmd/stellar-rpc/internal/fullhistory/views/envelopes.go Outdated
// Supported shapes:
//
// - LCM V1, V2 (apply order = TxProcessing array order)
// - TransactionMeta V1, V2 (no events, skipped)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also TransactionMeta"V0" which is just the Operations *[]OperationMeta field being filled out which doesn't appear to be considered here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had that question too but it seems like current RPC doesn't support V0

- db.ParseTransaction → ingestTx.GetTransactionEvents() → SDK ledger_transaction.go:308 has a default:
  that returns "unsupported TransactionMeta version: 0".

Hmm but I guess so with this implementation of rpc v2 we should?

@chowbao chowbao Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. I remember one other thing. Full history rpc v2 would realistically never have V0 txmeta because it would ingest with the unified events and backfill flags set to true cause otherwise there wouldn't be any events for classic

Anyhoo I'm forcing claude to add it in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah current RPC doesn't need to support it because it'd never come up, but with full history it's possible. You're 100% right about the flags, though - but better to be defensive!

if err != nil {
return 0, 0, fmt.Errorf("views: CloseTime value: %w", err)
}
return seqVal, int64(ctVal), nil //nolint:gosec // TimePoint is uint64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's gooooootta be a better way to do this than per-property error inspection.... It'd be nice if you could do:

ctVal := header.HeaderW().LedgerSeqW().ValueW().ScpValueW().CloseTimeW().ValueW()
if ctVal.hasError {
  // ... has the first error that occurred during unwrapping
}

where the *W() variant returns a "wrapped" version that tracks whether or not there's an error, e.g.

struct LedgerHeaderW {
  LedgerHeader
  hasError error
}

func (*hdr LedgerHeaderHistoryEntryView) HeaderW() LedgerHeaderW {
	inner, err := header.Header()
	return LedgerHeaderW{LedgerHeader: inner, hasError: err}
}

// and so on

where each child method just propagates the error until the end if it's present. I could be making up Go semantics here and maybe this isn't possible, but it'd be a way nicer api ux if we could chain it this way. @tamirms is this even tenable in Go or am I making up syntax?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems possible but that wrapped types would have to go into the go-stellar-sdk and can't be done here.

I'm not sure that's worth it at this time just to make the error handling a little nicer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah 100% this is just a side comment from the actual PR here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out the SDK already generates this: Must* accessors panic with *ViewError and xdr.Try recovers them. da9ee15 switches to that style and deletes the local helper.

@Shaptic Shaptic Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bruh I keep forgetting that that exists

Comment thread cmd/stellar-rpc/internal/fullhistory/views/events.go Outdated
Comment on lines +352 to +361
// payloadsFromV4Meta extracts V4 events:
//
// 1. Top-level TransactionEvents first, dispatched on Stage:
// BeforeAllTxs -> uses ledger-wide state.beforeIndex
// AfterAllTxs -> uses ledger-wide state.afterIndex
// AfterTx -> uses per-tx counter (reset here)
// 2. Then per-operation contract events in (op, event) order.
//
// Order matches LCMToPayloads / db.InsertEvents and the (TxIdx, OpIdx,
// EventIdx) sentinels match the struct path's encoding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm misunderstanding here: this order from current RPC operates on a per-ledger basis. Are you suggesting that this creates a list of orders that are then correctly interleaved later?

@chowbao chowbao Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't interleave later. I forgot about the event ordering

Edit: It passes off the interleaving to the ingest system downstream. This is just the extraction of all events with the correct IDs to be used to order later

It follows the same part of getting the payload out as the code you linked where ops happen after all tx events.

And to get the correctly ordered events you'd just iterate through the event indexes

Comment thread cmd/stellar-rpc/internal/fullhistory/views/pairing_test.go
Comment thread cmd/stellar-rpc/internal/fullhistory/views/txdetails.go
chowbao added 2 commits June 9, 2026 19:31
…, error-chain helper

- ExtractEvents/ExtractTransactions/ExtractTxDetailsByHash now treat legacy
  TransactionMeta V0 (pre-Soroban, Operations only) as event-free instead of
  erroring, so full-history backfill from genesis can read those ledgers. The
  SDK reference path (GetTransactionEvents / LCMToPayloads / db.ParseTransaction)
  rejects V0, so this is deliberately more permissive and documented as such;
  the two tests that asserted the V0 error now assert V0 success.
- envPartFromView reads the envelope type from the decode it already performs
  for hashing, dropping a redundant view .Type() traversal.
- Add a generic short-circuiting step()/viewChain helper and use it in
  readLedgerHeader and readTxHash to collapse the per-accessor error ladder.
The decode is not 'purely for the hash' — it also feeds the envelope type
and the soroban flag (which must inspect Tx.Ext), so hashing piggybacks on a
decode that is required regardless. Fix the now-stale comments that still
described it as transient/hash-only.

@tamirms tamirms left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two main suggestions; the details are in the inline comments.

1. Error handling. The package handles errors with an explicit check after every accessor call, plus a hand-rolled viewChain/step helper. The Go SDK already provides a better option: every view has generated Must* accessors that chain fluently, and xdr.Try/TryVoid convert their failures back into errors (see the "Try / TryVoid" section of xdrgen/views_api.md). The viewChain doc comment says that a fluent API would need SDK-generated wrapper types, but those already exist. If each exported extractor wraps its body in a single recover boundary, the package shrinks to roughly half its size and the //nolint:cyclop,funlen suppressions become unnecessary. Error quality is preserved because ViewError already carries the kind, byte offset, and detail. The views documentation says Must methods are intended for trusted input, which is what we have here.

2. Envelope pairing should not need to decode. envPartFromView calls UnmarshalBinary and network.HashTransactionInEnvelope on every envelope in the TxSet, and this dominates the cost of the read path. The decode is avoidable, because all three of its outputs can be derived from the view directly: the envelope type comes from the discriminant, isSoroban comes from the Ext discriminant, and the hash can be computed from the wire bytes. Details in the inline comment.

On the SDK-split question from #764, I would revisit the conclusion that nothing belongs in the SDK. View-side envelope hashing, IsSorobanTx, and the version-agnostic LCM helpers that this package hand-rolls all mirror existing struct-side SDK APIs: readLedgerHeader mirrors lcm.LedgerSequence() and lcm.LedgerCloseTime(), and the TxSet walkers mirror lcm.TransactionEnvelopes(). Keeping copies of that logic here means we have to track SDK behavior changes by hand. The envelopeIsSoroban comment already describes itself as "a known drift point to update in lockstep".

// which hashes every TxSet envelope (via network.HashTransactionInEnvelope,
// which handles the fee-bump case) because the TxSet is in agreed-set /
// hash-sorted order, NOT TxProcessing apply order.
func envPartFromView(env xdr.TransactionEnvelopeView, passphrase string) (envPart, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix: The decode can be removed entirely. The transaction hash can be computed directly from the wire bytes as SHA256(networkID ‖ envType-tag ‖ innerTx.Raw()). If the network ID is computed once per call and the SHA256 state is reused, hashing allocates nothing per envelope. The TX_V0 case hashes as its V1 conversion, which on the wire only adds the 4-byte muxed key-type prefix before the source key; the V0 optional-TimeBounds encoding and the V1 Preconditions encoding are identical, as are all the fields after them. The isSoroban flag can be read from V1().Tx().Ext(), or the fee-bump inner equivalent. Regarding aad6776: once the envelope type and the soroban flag are derived from the view, nothing needs the decode anymore. Separately, it would be worth adding TX_V0-envelope fixtures — no current fixture exercises one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in da9ee15. envPartFromView no longer decodes the envelope: the hash is computed straight from the wire as SHA256(networkID ‖ envType tag ‖ innerTx.Raw()), with the network ID derived once per extraction and a reused SHA-256 state, so per-envelope hashing allocates nothing. TX_V0 hashes as its V1 conversion (4-byte ed25519 key-type prefix + the unchanged V0 bytes — the optional-TimeBounds/Preconditions equivalence is documented on the function), and isSoroban now reads the inner Tx.Ext view discriminant. Added TX_V0 fixtures with TimeBounds present and absent, paired against the reference path.

// opening is delegated to openHeaderAndTxProc; only the version-specific
// TxSet open (TransactionSetView vs GeneralizedTransactionSetView) and its
// envelope enumerator are handled here.
func dispatchLCM(lcm xdr.LedgerCloseMetaView) (lcmDispatch, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix: This switches on the same discriminant twice: dispatchLCM calls openHeaderAndTxProc and then re-opens lcm.V0()/V1()/V2() to get the TxSet. The comment saying the TxSet open "cannot share this path" doesn't quite hold, because the enumerateEnvs closure already absorbs the type differences. Could this be a single dispatch that returns the header, the TxProcessing iterable, and the envelope enumerator, with the events path simply ignoring the enumerator?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — dispatchLCM (now in dispatch.go) is the single switch, returning the header, the TxProcessing sequence, and a lazily-opened envelope enumerator. openHeaderAndTxProc is gone; ExtractEvents and ExtractTxHashes use the same dispatch and just never touch the enumerator.

// apply order), so a TxProcessing entry's TransactionHash can locate its OWN
// envelope rather than the one that happens to sit at the same array index.
// The returned envPart raw bytes remain zero-copy aliases of the view buffer.
func envelopesByHash(d lcmDispatch, passphrase string) (map[[32]byte]envPart, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This hashes every TxSet envelope even when limit is 1. Since the page's hashes are collected before the envelopes are enumerated, the enumeration could stop as soon as every page hash has been resolved. For small pages that is most of the cost of the call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — ExtractTransactions collects the page's hashes first and envelopesForHashes stops the TxSet enumeration as soon as every wanted hash resolves.

// check. This is the package-local stand-in for the fluent `.HeaderW().…W()`
// chaining wished for in review; a true fluent API would need SDK-generated
// wrapper types.
type viewChain struct{ err error }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix: Two things here. First, the doc block attached to viewChain opens with readLedgerHeader's documentation, so the wrong declaration is documented and readLedgerHeader has none. Second, the comment says a fluent API "would need SDK-generated wrapper types", but the Go SDK already generates them: the Must* accessors chain fluently and Try recovers their failures as errors. With that style this helper disappears entirely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — viewChain/step are deleted; readLedgerHeader and readTxHash use the generated Must* chains under xdr.Try, and the doc comment is back on the right declaration.

// IsSorobanTx the way the struct path does. This is why the differential
// test's V3 fixtures, which always pair a present SorobanMeta with a
// soroban envelope, agree on both paths.
func payloadsFromV3SorobanMeta(metaView xdr.TransactionMetaView, state *txWalkState, dst []events.Payload) ([]events.Payload, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: ExtractEvents emits V3 events whenever SorobanMeta is present, relying on the core invariant, while the read path additionally gates them on the envelope (gateV3ContractEvents). On an inconsistent LCM, the events index and getTransaction would therefore disagree about the same transaction. That is fine under the trusted-input assumption, but since this is a package-level contract, would you move the invariant statement into doc.go so that both halves reference one place?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved into doc.go as the package's trusted-input invariant; payloadsFromV3SorobanMeta and gateV3ContractEvents both reference it now.

if herr != nil {
return Transaction{}, false, herr
}
if bytes.Equal(h[:], hash[:]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Both sides are [32]byte, so h == xdr.Hash(hash) works instead of bytes.Equal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return xdr.Hash{}, c.err
}
if len(hb) != 32 {
return xdr.Hash{}, fmt.Errorf("views: tx hash length %d != 32", len(hb))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This check can't fire: HashView is a fixed opaque[32], so Value() always returns exactly 32 bytes on success.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed — readTxHash converts with xdr.Hash(hb) directly.

// struct path purely on the nil-vs-empty axis.
//
//nolint:cyclop,funlen // linear dispatch on meta version
func extractEventRawsFromMeta(mv xdr.TransactionMetaView) (metaEvents, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: gofmt has reflowed the V0/V3/V4 list in this doc comment into stray indented blocks, which will render as code in godoc. It needs reformatting as a proper list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reformatted as a proper bullet list.

// that buf[off:off+len(sub)] shares backing storage with sub, or -1. Since
// the view fields are sub-slices of buf, a content match at a unique offset
// confirms aliasing for our fixtures.
func indexOfSubslice(buf, sub []byte) int {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: A content match would also pass for a copy that happens to match at a unique offset. events_test.go proves aliasing correctly with unsafe.SliceData pointer containment; could we reuse that helper here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — assertAliasesRaw now proves aliasing by unsafe.SliceData pointer containment; the content scanner is gone.

// pairing returns each tx's OWN envelope. We assert each returned Envelope
// equals the envelope wire-bytes for that tx's OWN hash.
//
// This test FAILS on the old positional code and passes after the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Several comments narrate review and benchmarking history: "wished for in review", "previously-0%-coverage", "FAILS on the old positional code", and the rpc-hack provenance notes. The PR description already preserves that history. Could the code comments keep just the forward-looking contracts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swept — all of them are gone (the "wished for in review" one went with viewChain itself).

…ispatch

- envelopes.go: drop the per-envelope TransactionEnvelope decode. The tx
  hash is computed straight from wire bytes as
  SHA256(networkID | envType tag | innerTx.Raw()) with the network ID
  derived once per extraction and a reused SHA-256 state (r3386543211).
  TX_V0 hashes as its V1 conversion (4-byte ed25519 key-type prefix);
  isSoroban now reads the inner Tx.Ext view discriminant.
- dispatch.go: single V0/V1/V2 dispatch returning header, TxProcessing
  sequence, and a lazy TxSet envelope enumerator; openHeaderAndTxProc and
  the second discriminant switch in dispatchLCM are gone (r3386543217).
- ExtractTransactions resolves only the page's envelope hashes and stops
  TxSet enumeration once all are found (r3386543224).
- readLedgerHeader/readTxHash use the SDK's Must*/xdr.Try chains; the
  viewChain/step helper is deleted (r3386543231).
- doc.go now owns the SorobanMeta-presence trusted-input invariant,
  referenced from ExtractEvents and gateV3ContractEvents (r3386543237).
- nits: generic widen() replaces the txProcIter adapter; xdr.Hash ==
  comparison instead of bytes.Equal; dead 32-byte length check removed;
  extractEventRawsFromMeta doc list reformatted; assertAliasesRaw proves
  aliasing via unsafe.SliceData pointer containment; review-history
  narration comments dropped (r3386543242/50/52/56/64/71).
- pairing_test.go: TX_V0 envelope fixtures (TimeBounds present + absent)
  paired against the reference path.
@chowbao

chowbao commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of stellar/go-stellar-sdk#5949 + #779.

The conclusion of this PR ("nothing in this task belongs in the SDK") was reversed: the zero-copy XDR view extractors are general-purpose XDR navigation — the view twins of the parsed ingest.LedgerTransaction / LedgerTransactionReader, network.HashTransactionInEnvelope, and TransactionResult.Successful — so they were promoted into the go-stellar-sdk (stellar/go-stellar-sdk#5949):

  • ingest: DispatchLedgerCloseMetaView, TransactionEventsView + TransactionEventsFromMeta / DiagnosticEventsFromMeta, TransactionView + TransactionViewByHash / TransactionViewRange
  • network: TransactionViewHasher
  • xdr: LedgerCloseMetaView.LedgerCloseTime, TransactionResultView.Successful

validated wire-identical against the parsed path across LCM V0/V1/V2 and TransactionMeta V0–V4.

The remaining RPC-specific glue (the thin views adapters — ExtractEvents / ExtractTxHashes and the events.Payload / txhash.Entry shapes, plus the EventIdx removal) is folded into #779, which has been retargeted onto fh-ingest-base and now consumes the SDK directly. Nothing here is lost — it lives in #5949 and #779.

@chowbao chowbao closed this Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants