Skip to content

txnbuild: build SetOptions XDR from the current fields on each call - #6021

Merged
marwen-abid merged 6 commits into
mainfrom
txnbuild-setoptions-stale-xdr
Oct 9, 2026
Merged

marwen-abid merged 6 commits into
mainfrom
txnbuild-setoptions-stale-xdr

Conversation

@marwen-abid

@marwen-abid marwen-abid commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What

SetOptions.BuildXDR now builds a new xdr.SetOptionsOp on each call. Before, it wrote into an unexported struct field (xdrOp) that wasn't being properly reset. SetOptions.FromXDR now resets the receiver before it reads the operation.

Why

A reused SetOptions kept fields that the caller had cleared:

so := &txnbuild.SetOptions{
	Signer:       &txnbuild.Signer{Address: signerAddr, Weight: 1},
	MasterWeight: txnbuild.NewThreshold(0),
}
so.BuildXDR() // first transaction

so.Signer, so.MasterWeight = nil, nil
so.HomeDomain = txnbuild.NewHomeDomain("example.com")
op, _ := so.BuildXDR() // second transaction
// before fix:    adds signerAddr, sets master weight 0 (disables the master key), sets home domain
// after fix: sets home domain only

FromXDR into a populated value had the same defect:

so := &txnbuild.SetOptions{
	SetFlags: []txnbuild.AccountFlag{txnbuild.AuthRequired},
	Signer:   &txnbuild.Signer{Address: signerAddr, Weight: 1},
}
so.FromXDR(flagsOp) // flagsOp sets only SetFlags = AuthRevocable
// before fix:    SetFlags = [AuthRequired, AuthRevocable], Signer kept
// after fix: SetFlags = [AuthRevocable], Signer = nil

A nice side effect to this change is that BuildXDR no longer writes to the receiver, so concurrent builds of one SetOptions do not race.

Compatibility

xdrOp was unexported, so the public API does not change. A caller that builds a new SetOptions for each operation gets the same XDR as before.

SetOptions kept its XDR operation in an unexported struct field.
BuildXDR wrote each optional field into it only when the public field
was non-nil, and nothing reset it. When a caller reused a SetOptions
and cleared a field such as Signer or MasterWeight, the next build
still contained the old value. The emitted operation could add a
signer or change the master weight that the public fields no longer
showed.

BuildXDR now builds a new xdr.SetOptionsOp on each call, as the other
operations do. This also stops BuildXDR from writing to the receiver,
so concurrent builds of one SetOptions no longer race.

FromXDR had the same problem in the other direction: it set only the
fields present in the XDR and kept the others. It now resets the
receiver first.
Copilot AI lite review requested due to automatic review settings October 6, 2026 17:57

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

The changes are covered by regression tests and no unresolved issues remain.

Review effort: Lite
Findings: None

What changed in this PR

Updates SetOptions XDR handling to prevent stale optional fields when instances are reused.

Changes:

  • Builds fresh XDR state on each call.
  • Resets receivers before parsing XDR.
  • Adds regression tests and changelog documentation.
File Description
txnbuild/​set_options.go Uses local XDR state and resets decoded values.
txnbuild/​set_options_test.go Adds reuse regression tests and updates helpers.
txnbuild/​CHANGELOG.md Documents the bug fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@marwen-abid marwen-abid self-assigned this Oct 6, 2026
FromXDR into a populated SetOptions used to append the XDR flags to the
existing SetFlags and ClearFlags. The reuse test now puts flags in the
source operation, so a fix that clears only the pointer fields fails.

SetOptions had no marshaling round-trip test, unlike the other
operations, because the removed xdrOp field made the struct comparison
fail after BuildXDR. Add one for plain and muxed source accounts.

The changelog entry now names the flag append.
Copilot AI lite review requested due to automatic review settings October 8, 2026 18:32

Copilot AI 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.

🟢 Approval recommended

No unresolved blocking issues were identified.

0 open findings

🧠 Review effort: Lite

…account's destination unchanged

The old comment read as if the struct could not unset the field. The
rule belongs to the network.
Copilot AI lite review requested due to automatic review settings October 8, 2026 19:04

Copilot AI 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.

🟢 Approval recommended

No unresolved blocking issues were identified.

0 open findings

🧠 Review effort: Lite

Copilot AI 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.

🟢 Approval recommended

The implementation fully addresses stale state while preserving API compatibility and adding targeted regression coverage.

0 open findings

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 8, 2026 19:25

Copilot AI 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.

🟢 Approval recommended

The implementation fully addresses stale state while preserving existing public behavior and includes focused regression coverage.

0 open findings

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 8, 2026 19:33

Copilot AI 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.

🟢 Approval recommended

The implementation addresses stale state cleanly and includes comprehensive regression coverage.

0 open findings

🧠 Review effort: Balanced

@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.

Not for this PR: CreateClaimableBalance.FromXDR has the same merge problem. It appends to cb.Destinations (create_claimable_balance.go:153) without resetting, so decoding into a populated value keeps the old claimants, and rebuilding lets them claim. A *cb = CreateClaimableBalance{} after the type check, as done here, would fix it. RevokeSponsorship.FromXDR also leaves the previous type's field set, though BuildXDR ignores it.

@marwen-abid
marwen-abid merged commit 3cac101 into main Oct 9, 2026
12 checks passed
@marwen-abid
marwen-abid deleted the txnbuild-setoptions-stale-xdr branch October 9, 2026 17:28
tamirms added a commit that referenced this pull request Oct 20, 2026
Pending becomes 0.8.0 for the Protocol 30 release (#6028). Four of its
entries already shipped in v0.7.3 and move to a new 0.7.3 section:
LedgerCloseMetaView.LedgerHeader (#5982), rpcclient Client.URL() and
both processors/token_transfer to_muxed_id fixes. Adds the txnbuild
SetOptions fix (#6021), which reached protocol-next with #6027.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MBWddBqppX2FsWGbuaJ82g
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