Skip to content

GenericEvent.update() silently overwrites created_at, so callers that set it get a mismatched id #559

Description

@tcheeric

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:

  1. 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.
  2. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions