Skip to content

Bugfix/group invite expiry#286

Open
QuickMythril wants to merge 27 commits intoQortal:masterfrom
QuickMythril:bugfix/group-invite-expiry
Open

Bugfix/group invite expiry#286
QuickMythril wants to merge 27 commits intoQortal:masterfrom
QuickMythril:bugfix/group-invite-expiry

Conversation

@QuickMythril
Copy link

Fixes #285 - Bug: GROUP_INVITE expiration not enforced (invites never expire)

Summary

  • Add the groupInviteExpiryHeight feature trigger (enum entry + getter) and wire it through mainnet, testnet, and all test-chain fixtures; startup now validates the trigger presence.
  • Implement invite expiry enforcement in Group.join(...) gated by the trigger: invite-first joins use join tx timestamps with an inclusive boundary and expiry == null sentinel; expired invites are treated as absent and fall back to join requests; pre-trigger behavior is unchanged. Join-first auto-approvals remain TTL-agnostic (any invite approves a pending request).
  • Apply chain-tip-based invite filtering to API endpoints via a shared helper in GroupsResource (inclusive boundary, expiry == null never expires, skip when no tip) and document the behavior in swagger.
  • Update docs (INVITE_EXPIRATION.md, IMPLEMENTATION.md, CONSENSUS_CHANGE.md) with final semantics, activation plan, join-first clarifications, and activation rollout guidance.
  • Add comprehensive tests: invite-first expiry enforcement, join-first TTL-agnostic behavior, backdated/forward-dated windows, pre/post-trigger activation, and API invite filtering (chain-tip filtering and no-tip fallback). Includes a dedicated GroupInviteFilteringApiTests class.

Tests

  • mvn -q -DskipTests=false -Dtest=org.qortal.test.group.MiscTests test
  • mvn -q -DskipTests=false -Dtest=org.qortal.test.api.GroupInviteFilteringApiTests test

Follow-up for activation

  • Choose and set a real groupInviteExpiryHeight for mainnet once network coverage is sufficient; keep the placeholder until then.
  • Communicate the activation height/date to operators and include it in release notes/changelog.
  • Verify testnet/fixture triggers remain at immediate activation for ongoing coverage, adjusting only if the activation scheme changes.

@nbenaglia
Copy link

Docs of this PR should be gathered in a dedicated folder ("group_invite_expiry" for example).
If the docs folder grows up with more documents, it's very difficult to understand to what feature the single document refers to.

@QuickMythril
Copy link
Author

QuickMythril commented Dec 5, 2025

Docs of this PR should be gathered in a dedicated folder ("group_invite_expiry" for example). If the docs folder grows up with more documents, it's very difficult to understand to what feature the single document refers to.

All the documents created were only for the purpose of working on the implementation of this one single feature. They are meant to be temporary can be discarded later if these changes get approved and merged in. They provide a clear explanation of the issue and the causes, as well as other considerations that had to be made, and a record of the which decisions were chosen and why, and also a record of the work that was done. They can be moved to another folder, deleted, or left as is. That doesn't matter because .md files don't affect chain consensus.

@QuickMythril
Copy link
Author

The two responses I have gotten about this so far were both asking about the documents. That's not important at all. The code needs to be checked, reviewed, and merged so we can finally get this fixed. The docs can be deleted later. They were just meant to help the reviewers. Every file, every commit, every desicion - all explained, in case there were any questions about why anything was done.
@kennycud @IceBurst

@QuickMythril
Copy link
Author

Added Tests (details)

testInviteFirstValidBeforeExpiryAddsMember

  • What: Invite-first flow where join timestamp is before invite expiry adds the member and consumes the invite; no join request persists.
  • How: Mint invite with short TTL, join using a timestamp before expiry, assert membership and invite/request cleanup.
  • Why: Verifies the success path of invite-first expiry enforcement.
  • Output:
    • [testInviteFirstValidBeforeExpiryAddsMember] START
    • Join timestamp 1764917672854 before expiry 1764917674354
    • Membership? true
    • Invite should be consumed -> null
    • Join request should be absent -> null
    • PASS

testInviteFirstExpiredCreatesRequest

  • What: Invite-first flow with expired invite results in a stored join request; invite remains stored.
  • How: Mint invite with 1s TTL, join with timestamp past expiry, assert no membership, join request present, invite retained.
  • Why: Confirms expired invites are treated as absent and fall back to request handling.
  • Output:
    • [testInviteFirstExpiredCreatesRequest] START
    • Join timestamp 1764917674379 after expiry 1764917674378
    • Membership? false
    • Join request stored? true
    • Expired invite retained? true
    • PASS

testInviteFirstBackdatedJoinWithinExpiry

  • What: Backdated join inside the expiry window still adds the member even if block time is later.
  • How: Mint invite with TTL, join using a backdated timestamp inside TTL, assert membership.
  • Why: Documents the transaction-timestamp window behavior for invite consumption.
  • Output:
    • [testInviteFirstBackdatedJoinWithinExpiry] START
    • Join timestamp 1764917675587 relative to expiry 1764917676087
    • Membership? true
    • PASS

testJoinFirstInviteLaterAutoAddsIgnoringTtl

  • What: Join-first pending request is auto-approved by a later invite regardless of TTL/age.
  • How: Create request, mint invite with short TTL, assert membership and request/invite consumed.
  • Why: Validates documented TTL-agnostic behavior for join-first path.
  • Output:
    • [testJoinFirstInviteLaterAutoAddsIgnoringTtl] START
    • Stored join request? true
    • Membership after invite? true
    • PASS

testJoinFirstInviteLaterWithBackdatedJoinStillAdds

  • What: Backdated join request is approved when a later invite arrives.
  • How: Create backdated request, mint short-TTL invite later, assert membership and request consumed.
  • Why: Confirms forward/backdating of join requests doesn’t block auto-approval.
  • Output:
    • [testJoinFirstInviteLaterWithBackdatedJoinStillAdds] START
    • Stored join request? true
    • Membership after invite? true
    • PASS

testJoinFirstInviteLaterTtlZero

  • What: Non-expiring invite (TTL=0) still approves a stored join request.
  • How: Create request, mint TTL=0 invite, assert membership and cleanup.
  • Why: Ensures TTL=0 sentinel applies in join-first auto-approval.
  • Output:
    • [testJoinFirstInviteLaterTtlZero] START
    • Stored join request? true
    • Membership after TTL=0 invite? true
    • PASS

testApiFiltersExpiredInvites

  • What: API endpoints omit expired invites and return non-expiring ones using chain-tip time.
  • How: Mint an expired invite and a TTL=0 invite, call both invite endpoints, assert expired invite hidden and TTL=0 visible.
  • Why: Confirms chain-tip-based filtering behavior exposed via API.
  • Output:
    • [testApiFiltersExpiredInvites] START
    • Minting expired invite at 1764917670601 for bob
    • Minting TTL=0 invite for chloe
    • Group invites returned: 1
    • Invites for Chloe: 1
    • Invites for Bob (expired should be filtered): 0
    • PASS

testPrePostTriggerActivation

  • What: Expired invites auto-add pre-trigger but become requests post-trigger.
  • How: Use reflection to raise the trigger (pre) to allow expired invite membership, then restore trigger and assert expired invite becomes a request post-trigger.
  • Why: Validates trigger-gated activation of invite expiry enforcement.
  • Output:
    • [testPrePostTriggerActivation] START
    • Pre-trigger join timestamp 1764917676864 relative to expiry 1764917675864
    • Pre-trigger membership? true
    • Post-trigger join timestamp 1764917676945 relative to expiry 1764917675945
    • Post-trigger membership? false
    • Post-trigger request stored and invite retained
    • PASS

testInviteFilteringByChainTip

  • What: API invite filtering hides expired invites and retains TTL=0/unexpired invites using chain-tip timestamp.
  • How: Mint an expired invite, a non-expiring invite, and an unexpired invite; call invitee and group endpoints; assert expired invite filtered out and others returned.
  • Why: Verifies chain-tip-based filtering behavior exposed via API.
  • Output:
    • TEST START: testInviteFilteringByChainTip - expired invites filtered, TTL=0/unexpired retained.
    • TEST PASS: testInviteFilteringByChainTip - expected bobInvites size=1, actual=1; expected groupInvites size=2, actual=2

testInviteFilteringSkippedWhenNoChainTip

  • What: API invite filtering is skipped when no chain tip is available, so expired invites are returned.
  • How: Mint an expired invite, swap repository factory to return null chain tip, call invitee endpoint, and assert expired invite is present.
  • Why: Confirms documented no-tip fallback for API filtering.
  • Output:
    • TEST START: testInviteFilteringSkippedWhenNoChainTip - filtering is skipped without a chain tip.
    • TEST PASS: testInviteFilteringSkippedWhenNoChainTip - expired invite present=true

@QuickMythril
Copy link
Author

@kennycud @IceBurst

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: GROUP_INVITE expiration not enforced (invites never expire)

2 participants