What changed, and why it matters
This patch tightens validation of Monero 'key images'—the special values that prevent the same coin from being spent twice. Before the change, a malformed key image that was either the identity point or had a small-order/torsion component could potentially slip through ring-signature verification. The fix rejects the identity key image outright and multiplies the key image by the curve order to confirm it lies on the proper subgroup, returning false if it does not. This closes a path that could let an attacker craft a signature that looks valid but does not actually mark a real coin as spent, which in Monero can translate to a double-spend or balance-inflation risk.
Treat this as a security-hardening fix with potential consensus implications. Nodes and wallets should upgrade promptly, and the change should be reviewed against network consensus rules to ensure the stricter validation does not fork the chain. Operators should monitor for any related double-spend or key-image exploit reports.
Security signals we found
Adds subgroup-order check for key images in ring signature verification
Rejects identity element as a valid key image
Exposes curve order constant sc_l for scalar multiplication checks
Targets Monero's anti-double-spend key-image mechanism
Defensive hardening of cryptographic verification path
Evidence from the diff
The commit adds the curve order constant sc_l (l = 2^252 + 27742317777372353535851937790883648493) to crypto-ops and exposes it via crypto-ops.h. In crypto.cpp it defines a single ec_point infinity constant and, inside check_ring_signature(), rejects key images equal to the identity point and then verifies that l * image_unp is the point at infinity. This ensures the key image is a valid, non-identity point on the prime-order subgroup of ed25519. The change is small and defensive, but it addresses a real cryptographic invariant that ring signature verification previously did not enforce.
Changed components
src/crypto/crypto.cppsrc/crypto/crypto-ops-data.csrc/crypto/crypto-ops.hMonero ring signature verificationkey image validationInspect captured patch +11 / −1
### src/crypto/crypto-ops-data.c
@@ -880,3 +880,6 @@ const ge_p3 ge_p3_H = {
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{23443568, -5110398, -8776029, -4345135, 6889568, -14710814, 7474843, 3279062, 14550766, -7453428}
};
+
+/* curve order l = 2^252 + 27742317777372353535851937790883648493 */
+const unsigned char sc_l[32] = {0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10};
### src/crypto/crypto-ops.h
@@ -153,6 +153,7 @@ extern const fe fe_a_inv_3;
extern const fe fe_c;
extern const ge_p3 ge_p3_identity;
extern const ge_p3 ge_p3_H;
+extern const unsigned char sc_l[32];
void ge_fromfe_frombytes_vartime(ge_p2 *, const unsigned char *);
void sc_0(unsigned char *);
void sc_1(unsigned char *);
### src/crypto/crypto.cpp
@@ -77,6 +77,8 @@ namespace crypto {
const crypto::public_key null_pkey = crypto::public_key{};
const crypto::secret_key null_skey = crypto::secret_key{};
+ static constexpr ec_point infinity = {{1}};
+
static inline unsigned char *operator &(ec_point &point) {
return &reinterpret_cast<unsigned char &>(point);
}
@@ -377,7 +379,6 @@ namespace crypto {
}
ge_double_scalarmult_base_vartime(&tmp2, &sig.c, &tmp3, &sig.r);
ge_tobytes(&buf.comm, &tmp2);
- static const ec_point infinity = {{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
if (memcmp(&buf.comm, &infinity, 32) == 0)
return false;
hash_to_scalar(&buf, sizeof(s_comm), c);
@@ -815,10 +816,15 @@ POP_WARNINGS
assert(check_key(*pubs[i]));
}
#endif
+ if (0 == memcmp(image.data, infinity.data, sizeof(image)))
+ return false; // false if key image is identity
if (ge_frombytes_vartime(&image_unp, &image) != 0) {
return false;
}
ge_dsm_precomp(image_pre, &image_unp);
+ ge_scalarmult_p3(&image_unp, sc_l, &image_unp);
+ if (!ge_p3_is_point_at_infinity_vartime(&image_unp))
+ return false; // false if key image is torsioned
sc_0(&sum);
buf->h = prefix_hash;
for (i = 0; i < pubs_count; i++) {Why this scored 77/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.