Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,4 @@ vite.config.ts.timestamp-*

# IA
.CLAUDE.md
.agents
140 changes: 140 additions & 0 deletions apps/backend/src/contributions/contribution-transitions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ParticipantStatus, ParticipantContributionStep } from 'src/types/enums';
import {
canCreateContribution,
canSetContributionValidity,
TransitionParticipant,
CREATE_TRANSITION_ERROR,
SET_VALID_TRANSITION_ERROR,
} from './contribution-transitions';

describe('Contribution transition helpers', () => {
describe('canCreateContribution', () => {
const allowedCombinations: [ParticipantStatus, ParticipantContributionStep][] = [
[ParticipantStatus.CONTRIBUTING, ParticipantContributionStep.UPLOADING],
[ParticipantStatus.CONTRIBUTING, ParticipantContributionStep.VERIFYING],
];

it.each(allowedCombinations)(
'should return true for status=%s step=%s',
(status, contributionStep) => {
const participant: TransitionParticipant = { status, contributionStep };
expect(canCreateContribution(participant)).toBe(true);
},
);

const disallowedStatuses = [
ParticipantStatus.CREATED,
ParticipantStatus.WAITING,
ParticipantStatus.READY,
ParticipantStatus.CONTRIBUTED,
ParticipantStatus.DONE,
ParticipantStatus.FINALIZING,
ParticipantStatus.FINALIZED,
ParticipantStatus.TIMEDOUT,
ParticipantStatus.EXHUMED,
];

it.each(disallowedStatuses)(
'should return false for status=%s regardless of step',
(status) => {
for (const step of Object.values(ParticipantContributionStep)) {
const participant: TransitionParticipant = {
status,
contributionStep: step,
};
expect(canCreateContribution(participant)).toBe(false);
}
},
);

const disallowedStepsForContributing = [
ParticipantContributionStep.DOWNLOADING,
ParticipantContributionStep.COMPUTING,
ParticipantContributionStep.COMPLETED,
];

it.each(disallowedStepsForContributing)(
'should return false for CONTRIBUTING + step=%s',
(step) => {
const participant: TransitionParticipant = {
status: ParticipantStatus.CONTRIBUTING,
contributionStep: step,
};
expect(canCreateContribution(participant)).toBe(false);
},
);
});

describe('canSetContributionValidity', () => {
const allowedCombinations: [ParticipantStatus, ParticipantContributionStep][] = [
[ParticipantStatus.CONTRIBUTED, ParticipantContributionStep.VERIFYING],
[ParticipantStatus.CONTRIBUTED, ParticipantContributionStep.COMPLETED],
[ParticipantStatus.FINALIZED, ParticipantContributionStep.VERIFYING],
[ParticipantStatus.FINALIZED, ParticipantContributionStep.COMPLETED],
];

it.each(allowedCombinations)(
'should return true for status=%s step=%s',
(status, contributionStep) => {
const participant: TransitionParticipant = { status, contributionStep };
expect(canSetContributionValidity(participant)).toBe(true);
},
);

const disallowedStatuses = [
ParticipantStatus.CREATED,
ParticipantStatus.WAITING,
ParticipantStatus.READY,
ParticipantStatus.CONTRIBUTING,
ParticipantStatus.DONE,
ParticipantStatus.FINALIZING,
ParticipantStatus.TIMEDOUT,
ParticipantStatus.EXHUMED,
];

it.each(disallowedStatuses)(
'should return false for status=%s regardless of step',
(status) => {
for (const step of Object.values(ParticipantContributionStep)) {
const participant: TransitionParticipant = {
status,
contributionStep: step,
};
expect(canSetContributionValidity(participant)).toBe(false);
}
},
);

const disallowedStepsForContributed = [
ParticipantContributionStep.DOWNLOADING,
ParticipantContributionStep.COMPUTING,
ParticipantContributionStep.UPLOADING,
];

it.each(disallowedStepsForContributed)(
'should return false for CONTRIBUTED + step=%s',
(step) => {
const participant: TransitionParticipant = {
status: ParticipantStatus.CONTRIBUTED,
contributionStep: step,
};
expect(canSetContributionValidity(participant)).toBe(false);
},
);
});

describe('error message constants', () => {
it('should export CREATE_TRANSITION_ERROR', () => {
expect(CREATE_TRANSITION_ERROR).toContain('CONTRIBUTING');
expect(CREATE_TRANSITION_ERROR).toContain('UPLOADING');
expect(CREATE_TRANSITION_ERROR).toContain('VERIFYING');
});

it('should export SET_VALID_TRANSITION_ERROR', () => {
expect(SET_VALID_TRANSITION_ERROR).toContain('CONTRIBUTED');
expect(SET_VALID_TRANSITION_ERROR).toContain('FINALIZED');
expect(SET_VALID_TRANSITION_ERROR).toContain('VERIFYING');
expect(SET_VALID_TRANSITION_ERROR).toContain('COMPLETED');
});
});
});
94 changes: 94 additions & 0 deletions apps/backend/src/contributions/contribution-transitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { ParticipantStatus, ParticipantContributionStep } from 'src/types/enums';

/**
* Allowed participant states for creating a contribution record.
*
* In the p0tion ceremony flow, a contribution document is created when the
* participant is the current contributor (`CONTRIBUTING`) and has reached the
* upload or verification phase (`UPLOADING` or `VERIFYING`).
*
* Earlier steps (`DOWNLOADING`, `COMPUTING`) mean the participant has not yet
* produced a zKey to record; later/other statuses mean the contribution window
* has passed.
*/
const ALLOWED_CREATE_STATUSES = new Set<ParticipantStatus>([ParticipantStatus.CONTRIBUTING]);

const ALLOWED_CREATE_STEPS = new Set<ParticipantContributionStep>([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a technical comment rather a design doubt, if one can create a contribution before uploading or during the artifact upload, why don't we rather create the contribution when the artifact was actually uploaded on the backend.

Also, the 'ing' semanthics of the states seem a little bit ambigous, why don't we just say 'verified' or 'uploaded', but for sure there is a good reason for this second point, since the files are big, but still don't fully grasp it.

The states should be something to revisit in a further meeting.

ParticipantContributionStep.UPLOADING,
ParticipantContributionStep.VERIFYING,
]);

/**
* Allowed participant states for setting the `valid` field on a contribution.
*
* Verification results are recorded after the participant has contributed
* (`CONTRIBUTED` or `FINALIZED`) and the contribution step is in
* `VERIFYING` (verification running) or `COMPLETED` (verification finished).
*
* This ensures that `valid` can only be set once the actual verification
* process has been reached, aligning with p0tion's `verifycontribution` flow.
*/
const ALLOWED_SET_VALID_STATUSES = new Set<ParticipantStatus>([
ParticipantStatus.CONTRIBUTED,
ParticipantStatus.FINALIZED,
]);

const ALLOWED_SET_VALID_STEPS = new Set<ParticipantContributionStep>([
ParticipantContributionStep.VERIFYING,
ParticipantContributionStep.COMPLETED,
]);

/**
* Participant-like shape required by the transition helpers.
* Only the fields needed for lifecycle checks are required.
*/
export interface TransitionParticipant {
status: ParticipantStatus;
contributionStep: ParticipantContributionStep;
}

/**
* Determines whether a contribution record can be created for the given
* participant based on their current status and contribution step.
*
* @param participant - The participant whose state is being checked
* @returns `true` if the participant is in a valid state to create a contribution
*
* @example
* ```ts
* canCreateContribution({ status: 'CONTRIBUTING', contributionStep: 'UPLOADING' }); // true
* canCreateContribution({ status: 'WAITING', contributionStep: 'DOWNLOADING' }); // false
* ```
*/
export function canCreateContribution(participant: TransitionParticipant): boolean {
return (
ALLOWED_CREATE_STATUSES.has(participant.status) &&
ALLOWED_CREATE_STEPS.has(participant.contributionStep)
);
}

/**
* Determines whether the `valid` field can be set on a contribution
* based on the owning participant's current status and contribution step.
*
* @param participant - The participant whose state is being checked
* @returns `true` if the participant is in a valid state to have contribution validity set
*
* @example
* ```ts
* canSetContributionValidity({ status: 'CONTRIBUTED', contributionStep: 'VERIFYING' }); // true
* canSetContributionValidity({ status: 'CONTRIBUTING', contributionStep: 'COMPUTING' }); // false
* ```
*/
export function canSetContributionValidity(participant: TransitionParticipant): boolean {
return (
ALLOWED_SET_VALID_STATUSES.has(participant.status) &&
ALLOWED_SET_VALID_STEPS.has(participant.contributionStep)
);
}

export const CREATE_TRANSITION_ERROR =
'Contribution can only be created when participant is CONTRIBUTING in UPLOADING or VERIFYING step';

export const SET_VALID_TRANSITION_ERROR =
'Contribution validity can only be set when participant is CONTRIBUTED or FINALIZED in VERIFYING or COMPLETED step';
Loading