What
assemble() (cmd/soroban-cli/src/assembled.rs) was written for the case of assembling a freshly built transaction with empty auth and a classic-only fee. When tx simulate is run on an envelope that is already assembled or partially authorized, two things go wrong:
- Recorded auth is dropped.
assemble() only copies simulation.results[*].auth onto the operation when body.auth.is_empty(). With --auth-mode=root|non-root the RPC records fresh auth even when entries already exist, but the assembled output keeps the original entries and silently discards the recorded set.
- The resource fee is double-counted.
assemble() computes the fee as raw.fee + min_resource_fee. If the incoming tx already carries SorobanTransactionData (ext V1) whose resource_fee is folded into raw.fee, re-assembling adds the simulated resource fee on top of one that is already there.
Both bugs surface on the same path — re-simulating an already-assembled/partially-authorized envelope, e.g. re-recording a partially authorized transaction to capture non-root sub-invocation auth.
Proposed Fix
- Auth replacement. Thread the requested auth mode into
assemble(). When an explicit record mode (root / record_allow_nonroot) was used, replace body.auth with the simulation-recorded entries even if it is non-empty. For enforce / unset, keep the current is_empty()-only behavior so caller-provided auth is preserved.
- Fee protection. Before adding
min_resource_fee, subtract any resource fee already present in the incoming transaction. Mirror the JS SDK guard: if raw.ext is TransactionExt::V1(data), and raw.fee - data.resource_fee > 0, subtract data.resource_fee from the classic fee first. See js-stellar-sdk assembleTransaction (src/rpc/transaction.ts).
Acceptance criteria
tx simulate --auth-mode=root|non-root on an envelope that already contains auth entries emits XDR containing the newly recorded authorization entries.
tx simulate on an already-assembled Soroban envelope (ext V1 with a resource fee) does not inflate the resource fee.
enforce / unset still preserve caller-provided auth.
invoke / deploy / upload behavior is unchanged.
- Tests: re-record a pre-populated envelope through
tx simulate (assert recorded auth present); re-simulate an assembled envelope (assert the fee is not double-counted).
References
What
assemble()(cmd/soroban-cli/src/assembled.rs) was written for the case of assembling a freshly built transaction with empty auth and a classic-only fee. Whentx simulateis run on an envelope that is already assembled or partially authorized, two things go wrong:assemble()only copiessimulation.results[*].authonto the operation whenbody.auth.is_empty(). With--auth-mode=root|non-rootthe RPC records fresh auth even when entries already exist, but the assembled output keeps the original entries and silently discards the recorded set.assemble()computes the fee asraw.fee + min_resource_fee. If the incoming tx already carriesSorobanTransactionData(extV1) whoseresource_feeis folded intoraw.fee, re-assembling adds the simulated resource fee on top of one that is already there.Both bugs surface on the same path — re-simulating an already-assembled/partially-authorized envelope, e.g. re-recording a partially authorized transaction to capture non-root sub-invocation auth.
Proposed Fix
assemble(). When an explicit record mode (root/record_allow_nonroot) was used, replacebody.authwith the simulation-recorded entries even if it is non-empty. Forenforce/ unset, keep the currentis_empty()-only behavior so caller-provided auth is preserved.min_resource_fee, subtract any resource fee already present in the incoming transaction. Mirror the JS SDK guard: ifraw.extisTransactionExt::V1(data), andraw.fee - data.resource_fee > 0, subtractdata.resource_feefrom the classic fee first. Seejs-stellar-sdkassembleTransaction(src/rpc/transaction.ts).Acceptance criteria
tx simulate --auth-mode=root|non-rooton an envelope that already contains auth entries emits XDR containing the newly recorded authorization entries.tx simulateon an already-assembled Soroban envelope (extV1with a resource fee) does not inflate the resource fee.enforce/ unset still preserve caller-provided auth.invoke/deploy/uploadbehavior is unchanged.tx simulate(assert recorded auth present); re-simulate an assembled envelope (assert the fee is not double-counted).References