instantout/reservation: avoid uint32 underflow in expiry-bounds check

InitFromClientRequestAction validates that the server-returned
absolute expiry is within +/- expiryDelta of expectedExpiry =
relativeExpiry + heightHint. Both sides were uint32, so when
expectedExpiry < expiryDelta (low regtest heights, fresh
deployments, anything with heightHint = 0 like the existing test
fixtures) expectedExpiry - expiryDelta wrapped to ~2^32. The lower
bound check then trivially admitted any reasonable response, and the
client would accept e.g. Expiry = 0 from the server, immediately past
the reservation's own deadline -- meaning the server can sweep via
the expiry script path while the client still believes it owns the
reservation slot.

Promote the comparison to int64 so the arithmetic is sign-honest.
This is the smallest patch that closes the underflow; a follow-up
should also add an absolute floor (e.g. Expiry >= heightHint +
minSafeExpiry) so the server cannot return a near-deadline reservation
even within the delta.
This commit is contained in:
Slyghtning 2026-05-11 15:31:36 +02:00
parent 508b9dfdc1
commit eb58fcaa03
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF

View file

@ -76,9 +76,12 @@ func (f *FSM) InitFromClientRequestAction(ctx context.Context,
expectedExpiry := reservationRequest.relativeExpiry +
reservationRequest.heightHint
// Check that the expiry is in the delta.
if requestResponse.Expiry < expectedExpiry-expiryDelta ||
requestResponse.Expiry > expectedExpiry+expiryDelta {
// Check that the expiry is in the delta. Compare as int64 so the
// lower bound stays meaningful when expectedExpiry < expiryDelta
// (which would otherwise underflow the uint32 and accept any
// response below the upper bound).
if int64(requestResponse.Expiry) < int64(expectedExpiry)-int64(expiryDelta) ||
int64(requestResponse.Expiry) > int64(expectedExpiry)+int64(expiryDelta) {
return f.HandleError(
fmt.Errorf("unexpected expiry height: %v, expected %v",