Summary
The HTLC claim-vs-reclaim decision is evaluated during transaction validation using the validating node's local wall clock (time.Now()), instead of the deterministic ledger/block timestamp. The same transaction can therefore be judged a Claim on one validation and a Reclaim on another, depending on when and where validation runs.
Where
Both validators compute now from the wall clock and feed it into the HTLC deadline check:
token/core/fabtoken/v1/validator/validator_transfer.go:143 — now := time.Now()
token/core/zkatdlog/nogh/v1/validator/validator_transfer.go:151 — now := time.Now()
That now flows into htlc.VerifyOwner(...) (token/services/identity/interop/htlc/validator.go), which decides the operation from now.Before(script.Deadline):
- before the deadline → must be a Claim (output owner must be the recipient),
- at/after the deadline → must be a Reclaim (output owner must be the sender).
It also reaches script.Validate(now) in the same files.
Why this matters
Token-request validation runs on multiple peers and at more than one point in a transaction's life (endorsement, then commit-time re-validation). Using time.Now() makes the Claim/Reclaim verdict depend on which node and what moment the check runs:
- Cross-peer clock skew: for a spend submitted near the deadline, one endorser (clock just before) validates it as a Claim and accepts, while another (clock just after) expects a Reclaim and rejects. Endorsement becomes inconsistent.
- Endorse vs commit on the same peer: a spend valid as a Claim at endorsement time can flip to "should have been a Reclaim" by commit-time re-validation, or vice versa.
For HTLC-based atomic swaps this is a safety problem, not just a robustness one: the deadline is the hinge of the swap, and a fuzzy/non-deterministic deadline can let a claim through past the intended cutoff or let a claim and a reclaim race.
Note: the fix hook already exists but is unused
htlc.Verifier has a ClockFunc func() time.Time field (token/services/interop/htlc/signer.go:96-100) whose own doc says callers with the Fabric block timestamp should inject it "to ensure deterministic, clock-skew-safe deadline enforcement." ClockFunc is currently never set anywhere, and the validators bypass it by calling time.Now() directly.
Summary
The HTLC claim-vs-reclaim decision is evaluated during transaction validation using the validating node's local wall clock (
time.Now()), instead of the deterministic ledger/block timestamp. The same transaction can therefore be judged a Claim on one validation and a Reclaim on another, depending on when and where validation runs.Where
Both validators compute
nowfrom the wall clock and feed it into the HTLC deadline check:token/core/fabtoken/v1/validator/validator_transfer.go:143—now := time.Now()token/core/zkatdlog/nogh/v1/validator/validator_transfer.go:151—now := time.Now()That
nowflows intohtlc.VerifyOwner(...)(token/services/identity/interop/htlc/validator.go), which decides the operation fromnow.Before(script.Deadline):It also reaches
script.Validate(now)in the same files.Why this matters
Token-request validation runs on multiple peers and at more than one point in a transaction's life (endorsement, then commit-time re-validation). Using
time.Now()makes the Claim/Reclaim verdict depend on which node and what moment the check runs:For HTLC-based atomic swaps this is a safety problem, not just a robustness one: the deadline is the hinge of the swap, and a fuzzy/non-deterministic deadline can let a claim through past the intended cutoff or let a claim and a reclaim race.
Note: the fix hook already exists but is unused
htlc.Verifierhas aClockFunc func() time.Timefield (token/services/interop/htlc/signer.go:96-100) whose own doc says callers with the Fabric block timestamp should inject it "to ensure deterministic, clock-skew-safe deadline enforcement."ClockFuncis currently never set anywhere, and the validators bypass it by callingtime.Now()directly.