fix: use the validStart null flag in TransactionId.compareTo - #2921
Open
the2015 wants to merge 1 commit into
Open
fix: use the validStart null flag in TransactionId.compareTo#2921the2015 wants to merge 1 commit into
the2015 wants to merge 1 commit into
Conversation
The validStart null-handling branch returned based on thisAccountIdIsNull instead of thisStartIsNull. Since that branch is only reachable when both ids agree on accountId nullness, the comparison returned the same sign in both directions, violating the Comparable antisymmetry and transitivity contracts. Fixes hiero-ledger#2740 Signed-off-by: the2015 <35129808+the2015@users.noreply.github.com>
the2015
force-pushed
the
fix/2740-transactionid-compareto-validstart-null
branch
from
August 29, 2026 07:55
78a37fc to
04be7ed
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
TransactionId.compareTohas a copy-paste error in thevalidStartnull-handling branch: itreturns based on
thisAccountIdIsNullinstead ofthisStartIsNull.Once the two ids agree on
accountIdnullness (which is required to reach this branch),thisAccountIdIsNullis the same value for both sides, so the comparison returns the same sign inboth directions. That breaks the
Comparablecontract:sgn(a.compareTo(b)) == -sgn(b.compareTo(a)).Fixes #2740
Fix
Use the flag that the branch is actually about:
Why existing tests did not catch it
TransactionIdTest.compare()has two cases where only one side has avalidStart, but neitherreaches the buggy branch in a way that exposes it:
accountId == nullon both sides, sothisAccountIdIsNullhappens to betrueand-1is coincidentally correct;accountIdnullness, so it returns earlier in theaccountIdblock.Verification
Added
compareToIsAntisymmetricWhenOnlyOneValidStartIsNull, which asserts both directions with andwithout an
accountId.As a negative control I temporarily reverted the one-line fix and re-ran the new test: it fails with
org.opentest4j.AssertionFailedError: expected: -1 but was: 1(13 tests completed, 1 failed). Withthe fix in place
:sdk:test --tests '*TransactionIdTest*'is green (13 tests, 0 failures, 0 errors)and
:sdk:qualityCheckpasses.I also extracted the comparator logic into a standalone harness and exhaustively compared all
ordered pairs of the 18 distinct value combinations
(
accountId in {null, 0.0.1, 0.0.2}xvalidStart in {null, t=100, t=200}xscheduled in {false, true}),i.e. 324 pairs:
Concrete failures on the current
main:(null, null, false)vs(null, t=100, false)returns-1in both directions.(0.0.1, null, false) > (0.0.1, t=100, false) > (0.0.1, null, false)while(0.0.1, null, false).compareTo((0.0.1, null, false)) == 0, i.e. an intransitive cycle.This matters in practice because
TransactionIdisComparableand can end up inTreeSet,TreeMaporList.sort, where an inconsistent comparator can silently drop entries or throwIllegalArgumentException: Comparison method violates its general contract!.Notes (deliberately out of scope)
compareTodoes not considernonce, whileequalsdoes, socompareTo == 0does not implyequals.Comparableonly recommends consistency withequals, andTransactionIdTestcurrently asserts the existing behaviour, so I left it alone.