Skip to content

Commit a1d7906

Browse files
authored
[ACNHOT-1186] Fix MEMO_ID validation to support full Stellar uint64 range (#1918)
### Description - Fix MEMO_ID validation to support the full Stellar uint64 range (0 to 18,446,744,073,709,551,615) - Replace Long.parseLong and Long.longValue() with BigInteger for MEMO_ID parsing and conversion - Consolidate memo ID creation into a shared MemoHelper.makeMemoId() method ### Context Partner reported that SEP-24 requests with refund_memo 11872666534918305457 were rejected with "Invalid Memo" due to this refund_memo value above Java's `Long.MAX_VALUE `(9,223,372,036,854,775,807) Stellar protocol defines `MEMO_ID` as uint64, but the platform was using Java's signed long for parsing, which only supports half the range. The same issue existed in SEP-10 memo validation, `xdrMemoToString` (used by the payment observer for memo matching), and muxed account memo handling in `DefaultPaymentListener` ### Testing - ./gradlew test - Added new unit tests to cover all cases ### Documentation N/A ### Known limitations N/A
1 parent 73edd3f commit a1d7906

5 files changed

Lines changed: 63 additions & 11 deletions

File tree

core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.lang.String.format;
44
import static org.stellar.anchor.util.Log.*;
5+
import static org.stellar.anchor.util.MemoHelper.makeMemoId;
56
import static org.stellar.anchor.util.MetricConstants.SEP10_CHALLENGE_CREATED;
67
import static org.stellar.anchor.util.MetricConstants.SEP10_CHALLENGE_VALIDATED;
78
import static org.stellar.anchor.util.StringHelper.isEmpty;
@@ -199,15 +200,10 @@ Transaction newChallenge(ChallengeRequest request, String clientSigningKey, Memo
199200

200201
@Override
201202
public Memo validateChallengeRequestMemo(ChallengeRequest request) throws SepException {
202-
// Validate memo. It should be 64-bit positive integer if not null.
203+
// Validate memo. It should be a positive uint64 integer if not null.
203204
try {
204205
if (request.getMemo() != null) {
205-
long memoLong = Long.parseUnsignedLong(request.getMemo());
206-
if (memoLong <= 0) {
207-
infoF("Invalid memo value: {}", request.getMemo());
208-
throw new SepValidationException(format("Invalid memo value: %s", request.getMemo()));
209-
}
210-
return new MemoId(memoLong);
206+
return makeMemoId(request.getMemo());
211207
} else {
212208
return null;
213209
}

core/src/main/java/org/stellar/anchor/util/MemoHelper.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.stellar.anchor.util.StringHelper.isEmpty;
44
import static org.stellar.sdk.xdr.MemoType.*;
55

6+
import java.math.BigInteger;
67
import java.nio.charset.StandardCharsets;
78
import java.util.Base64;
89
import org.apache.commons.codec.DecoderException;
@@ -78,7 +79,7 @@ public static Memo makeMemo(String memo, MemoType memoType) throws SepException
7879
try {
7980
switch (memoType) {
8081
case MEMO_ID:
81-
return new MemoId(Long.parseLong(memo));
82+
return makeMemoId(memo);
8283
case MEMO_TEXT:
8384
return new MemoText(memo);
8485
case MEMO_HASH:
@@ -99,6 +100,20 @@ public static Memo makeMemo(String memo, MemoType memoType) throws SepException
99100
}
100101
}
101102

103+
/**
104+
* Creates a MemoId from a string, supporting the full uint64 range (1 to
105+
* 18,446,744,073,709,551,615) as defined by the Stellar protocol.
106+
*/
107+
public static MemoId makeMemoId(String memo) {
108+
try {
109+
return new MemoId(new BigInteger(memo));
110+
} catch (IllegalArgumentException e) {
111+
NumberFormatException nfe = new NumberFormatException(e.getMessage());
112+
nfe.initCause(e);
113+
throw nfe;
114+
}
115+
}
116+
102117
public static String convertBase64ToHex(String memo) {
103118
return Hex.encodeHexString(Base64.getDecoder().decode(memo.getBytes()));
104119
}
@@ -160,7 +175,7 @@ public static String xdrMemoToString(org.stellar.sdk.xdr.Memo memoXdr) {
160175
return switch (memoXdr.getDiscriminant()) {
161176
case MEMO_NONE -> null; // No memo
162177
case MEMO_TEXT -> new String(memoXdr.getText().getBytes(), StandardCharsets.UTF_8);
163-
case MEMO_ID -> String.valueOf(memoXdr.getId().getUint64().getNumber().longValue());
178+
case MEMO_ID -> memoXdr.getId().getUint64().getNumber().toString();
164179
case MEMO_HASH, MEMO_RETURN ->
165180
Base64.getEncoder().encodeToString(memoXdr.getHash().getHash());
166181
};

core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ internal class Sep10ServiceTest {
401401
}
402402

403403
@ParameterizedTest
404-
@ValueSource(strings = ["ABC", "12AB", "-1", "0", Integer.MIN_VALUE.toString()])
404+
@ValueSource(strings = ["ABC", "12AB", "-1", Integer.MIN_VALUE.toString()])
405405
fun `test createChallenge() with bad memo`(badMemo: String) {
406406
every { sep10Config.isClientAttributionRequired } returns false
407407
val cr =

core/src/test/kotlin/org/stellar/anchor/util/MemoHelperTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,47 @@ internal class MemoHelperTest {
6363
)
6464
}
6565

66+
@Test
67+
fun `test makeMemoId with valid uint64 values`() {
68+
// Small value - should not throw
69+
makeMemoId("1234")
70+
71+
// Max signed long - should not throw
72+
makeMemoId("9223372036854775807")
73+
74+
// Above signed long max (valid uint64)
75+
makeMemoId("11872666534918305457")
76+
77+
// Max uint64 - should not throw
78+
makeMemoId("18446744073709551615")
79+
}
80+
81+
@Test
82+
fun `test makeMemo rejects invalid memo id values`() {
83+
assertThrows<SepValidationException> { makeMemo("-1", "id") }
84+
assertThrows<SepValidationException> { makeMemo("18446744073709551616", "id") }
85+
assertThrows<SepValidationException> { makeMemo("abc", "id") }
86+
}
87+
88+
@Test
89+
fun `test xdrMemoToString round-trip with uint64 memo ids`() {
90+
// Round-trip: makeMemoId -> toXdr -> xdrMemoToString should preserve the original value
91+
val testValues =
92+
listOf("1234", "9223372036854775807", "11872666534918305457", "18446744073709551615")
93+
for (value in testValues) {
94+
val memo = makeMemoId(value)
95+
val xdr = toXdr(memo)
96+
assertEquals(value, xdrMemoToString(xdr))
97+
}
98+
}
99+
100+
@Test
101+
fun `test xdrMemoToString with null and none`() {
102+
assertEquals(null, xdrMemoToString(null))
103+
val noneMemo = toXdr(null)
104+
assertEquals(null, xdrMemoToString(noneMemo))
105+
}
106+
66107
@Test
67108
fun `test toXdr()`() {
68109
val memoId: MemoId = makeMemo("123", MEMO_ID) as MemoId

platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ void processAndDispatchLedgerPayment(
149149
&& accountType(ledgerPayment.getTo()) == Muxed) {
150150
MuxedAccount muxedAccount = new MuxedAccount(ledgerPayment.getTo());
151151
toAccount = muxedAccount.getAccountId();
152-
memo = Memo.id(Objects.requireNonNull(muxedAccount.getMuxedId()).longValue());
152+
memo = Memo.id(Objects.requireNonNull(muxedAccount.getMuxedId()));
153153
} else {
154154
toAccount = ledgerPayment.getTo();
155155
memo = Memo.fromXdr(ledgerTransaction.getMemo());

0 commit comments

Comments
 (0)