Problem
GenericEvent.update() restamps created_at with the clock before it computes the id:
public void update() {
update(Instant.now().getEpochSecond());
}
This has been the behaviour in every release (before 2.2.0 the body set this.createdAt = Instant.now()... inline). Since 2.2.0 it is documented, and update(long createdAt) keeps a chosen timestamp. But the name gives no hint that it changes a field the caller may have just set, and the natural pattern is broken:
event.setCreatedAt(chosen);
event.update(); // created_at is now "now", not chosen
identity.sign(event);
Impact seen downstream
- Mismatched ids. Callers that read
created_at from their own copy of the event rather than back from the GenericEvent publish an id computed for a different second. In 398ja/imani-wallet-lib#98, NIP-60 events were rejected by relay.primal.net as invalid: bad event id, 59 of them in two hours on staging, whenever the second ticked between building and signing. The id matched exactly at created_at + 1.
- NIP-59 privacy defeated. Seal and gift-wrap builders that randomise
created_at and then call update() get the send time instead, which is exactly what NIP-59's randomisation exists to hide.
- Still present elsewhere. The same pattern exists in other downstream code, for example an identity signing adapter that sets the caller's
created_at, calls update(), and returns only the id and signature.
Suggested fix
Either of these, ideally with the first:
- Deprecate
update() in favour of explicit forms, e.g. update(long createdAt) plus updateWithCurrentTime() (or stampNowAndUpdate()), so the restamp is visible at the call site.
- Make
update() keep an already-set created_at and only stamp the clock when it is unset (null/0). This changes behaviour for callers relying on the refresh, so it would need a release note.
A regression test: set created_at to a fixed past value, call update(), and assert that getCreatedAt() is unchanged and that getId() equals the NIP-01 hash for that value.
Environment
nostr-java 2.3.1 (nostr-java-event, nostr.event.impl.GenericEvent).
Problem
GenericEvent.update()restampscreated_atwith the clock before it computes the id:This has been the behaviour in every release (before 2.2.0 the body set
this.createdAt = Instant.now()...inline). Since 2.2.0 it is documented, andupdate(long createdAt)keeps a chosen timestamp. But the name gives no hint that it changes a field the caller may have just set, and the natural pattern is broken:Impact seen downstream
created_atfrom their own copy of the event rather than back from theGenericEventpublish an id computed for a different second. In 398ja/imani-wallet-lib#98, NIP-60 events were rejected by relay.primal.net asinvalid: bad event id, 59 of them in two hours on staging, whenever the second ticked between building and signing. The id matched exactly atcreated_at + 1.created_atand then callupdate()get the send time instead, which is exactly what NIP-59's randomisation exists to hide.created_at, callsupdate(), and returns only the id and signature.Suggested fix
Either of these, ideally with the first:
update()in favour of explicit forms, e.g.update(long createdAt)plusupdateWithCurrentTime()(orstampNowAndUpdate()), so the restamp is visible at the call site.update()keep an already-setcreated_atand only stamp the clock when it is unset (null/0). This changes behaviour for callers relying on the refresh, so it would need a release note.A regression test: set
created_atto a fixed past value, callupdate(), and assert thatgetCreatedAt()is unchanged and thatgetId()equals the NIP-01 hash for that value.Environment
nostr-java 2.3.1 (
nostr-java-event,nostr.event.impl.GenericEvent).