Skip to content

[Contract]: Idempotent, replay-safe step payments in release_payment #101

Description

@grantfox-oss

release_payment(orchestrator, task_id, asset, amount) increments task.spent every time it is called. The orchestrator's executor retries on transient RPC/network failures, and a retried release that actually succeeded on-chain the first time will pay the agent twice and double-spend the user's locked funds, up to plan_cost. There is no idempotency key tying a release to a specific plan step, so the contract cannot tell a retry from a legitimate second step. For a payments contract, exactly-once release is a correctness requirement, not a nice-to-have, and it is the kind of hardening that funded projects pay a premium for.

Goal

Make each release idempotent on a caller-supplied step_id so that repeating a release with the same (task_id, step_id) is a safe no-op that returns the original result, while distinct steps still accumulate normally.

Proposed surface (signatures only)

  • release_payment(env, orchestrator, task_id, step_id: u64 /* or BytesN<32> */, asset, amount) -> Result<bool, VaultError>
  • Persist a per-task set/map of processed step_ids (bounded; see constraints)
  • New VaultError::StepAlreadyReleased is not returned on replay; instead the call is an idempotent success. Reserve an error only for the conflicting case (same step_id, different amount).

Requirements and constraints

  • Same (task_id, step_id, amount) seen twice: second call performs no transfer, does not change spent, and returns success (idempotent).
  • Same (task_id, step_id) with a different amount: reject (ReleaseConflict), since it indicates a client bug.
  • Storage growth must be bounded and TTL-managed; store step records under the task's key space and clean them up in finalize_task.
  • This changes the release_payment signature (breaking). Update packages/orchestrator/src/agent-vault-client.ts and the executor to pass a stable step_id derived from the plan step, and document the migration in the PR.
  • Preserve all existing checks (auth, asset match, spent + amount <= plan_cost, pause).

Edge cases

  • Retry after the task is completed (reject cleanly, not a double-pay).
  • Two different steps with the same amount (must both apply).
  • A step that would exceed plan_cost on first sight (reject as today).
  • Replay across a force_complete_stale_task boundary.

Acceptance criteria

  • Repeating a release with identical (task_id, step_id, amount) transfers once and is a safe success on repeats
  • Conflicting amount for an existing step_id is rejected
  • Distinct steps accumulate correctly and still respect plan_cost
  • Step records are TTL-bumped and removed on finalize (no unbounded growth)
  • Orchestrator client + executor updated to supply step_id; integration test simulates a duplicate release and asserts single payment
  • cargo test, cargo clippy --all-targets -- -D warnings, and npm test pass

Pointers

  • contracts/agent-vault/src/lib.rs: release_payment, TaskInfo, finalize_task, DataKey, VaultError.
  • packages/orchestrator/src/agent-vault-client.ts, packages/orchestrator/src/executor.ts.

Notes for contributors

Comment with your storage layout for step records and your step_id derivation before implementing. See CONTRIBUTING.md.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignenhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions