feat(gasless): Ed25519 signature-based gasless task assignment (#077) - #103
Conversation
…rFixxx#77) Contributors can claim an open task without holding XLM: they sign a structured GaslessAssignment message off-chain with their Ed25519 key and a gas relayer submits it via assign_task_gasless. The contract re-derives the canonical XDR payload, verifies the signature against the contributor's registered key, and enforces a per-contributor nonce to block replay. - src/gasless.rs: GaslessAssignment type, GaslessKey storage, key registration (self-service + admin), nonce tracking, relayer endpoint with domain separation and expiration checks - src/lib.rs: extract shared apply_task_assignment helper used by both assign_task and the gasless path; expose six new contract methods - src/events.rs: signing-key + gasless-assignment events - src/gasless_test.rs: 11 unit tests - Cargo.toml: ed25519-dalek dev-dependency for test signing
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (8)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughAdds Ed25519 signature-based gasless task assignment. Contributors or admins register signing keys. Relayers submit validated assignments with contract-bound payloads, expirations, and contributor nonces. Shared assignment logic updates task state and emits events. Tests cover success, rejection, onboarding, and replay protection. ChangesGasless assignment flow
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR adds gasless task assignment with signature and nonce validation while preserving the existing assignment path. No actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant Contributor
participant Relayer
participant SmartContract
participant ContractStorage
Contributor->>SmartContract: Request canonical assignment payload
SmartContract->>ContractStorage: Read signing key and nonce
SmartContract-->>Contributor: Return payload bytes
Contributor->>Relayer: Send signed assignment
Relayer->>SmartContract: Submit assign_task_gasless
SmartContract->>ContractStorage: Validate and consume nonce
SmartContract->>SmartContract: Apply task assignment
SmartContract-->>Relayer: Emit assignment event
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The PR satisfies the linked issue objectives. It adds structured authorization payload handling, Ed25519 signature parsing and verification, per-contributor nonce replay protection, relayer execution endpoints, and comprehensive unit tests. Full details: Docstring CoverageExplanation Docstring coverage is 70.27% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 37 functions across 4 files. (4 skipped: 4 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
feat(gasless): Ed25519 signature-based gasless task assignment (#77)
Closes #77
Summary
Adds a gasless path for claiming an open task. A contributor signs a
structured authorization message off-chain with their Ed25519 key; a gas
relayer submits it on-chain and pays the fees. The contract re-derives the
exact signed bytes from the call arguments, verifies the signature against the
contributor's registered key, and enforces a per-contributor nonce so a
captured message can never be replayed.
Today
assign_task(src/lib.rs) requiresassignee.require_auth()— thecontributor must hold XLM and submit the transaction themselves. This PR keeps
that path untouched and adds
assign_task_gaslessalongside it.How it works
Key registration (once). The contributor registers an Ed25519 public key,
either themselves via
register_signing_keyor through an admin viaadmin_set_signing_key(so onboarding can itself be gasless).Off-chain signing. The contributor's client builds a
GaslessAssignment:{ "contract": "C...", // this contract (domain separation) "task_id": 42, "contributor": "G...", // address to be assigned "nonce": 0, // == current on-chain nonce for contributor "expiration_ledger": 1234567 // request void once ledger passes this }It fetches the canonical bytes from
gasless_assignment_payload(the SDK XDRencoding of the struct) and signs them, producing a 64-byte signature.
Relayer submission. The relayer calls
assign_task_gasless(relayer, request, signature). Onlyrelayerauthorizesthe transaction.
On-chain validation (every failure reverts):
request.contractmust bethis deployment; the ledger must not have passed
expiration_ledger;request.noncemust equal the contributor's current nonce (then incremented);the contributor must have a registered key;
env.crypto().ed25519_verifymustaccept the signature over the re-encoded request; the task must exist and be
Open.Acceptance criteria
GaslessAssignmentcontract type + full field validation inassign_task_gaslessenv.crypto().ed25519_verifyagainst the contributor's registered keyGaslessKey::Nonce(Address), checked and consumed per assignmentgasless_assignment_payloadreturns the exact bytes to sign; canonical XDR round-tripassign_task_gasless(relayer, request, signature)— relayer is sole authorizer/fee payersrc/gasless_test.rs— 11 testsSECP256k1 is intentionally out of scope — Ed25519 is the Stellar-native key type.
It is a mechanical follow-up via
env.crypto().secp256k1_recoverbehind akey-type discriminator.
Changes
src/gasless.rs— new module.GaslessAssignmenttype,GaslessKeystorage enum,
register_signing_key/admin_set_signing_key/get_signing_key/
get_assignment_nonce/assignment_payload/assign_task_gasless. Moduleheader documents the full off-chain flow and JSON shape.
src/lib.rs— registers the module; extracts the assignment statetransition from
assign_taskinto a sharedapply_task_assignmenthelper usedby both the interactive and gasless paths; exposes the six new contract methods.
src/events.rs—emit_signing_key_registered,emit_gasless_assignment.src/gasless_test.rs— 11 unit tests (below).Cargo.toml— dev-dependencyed25519-dalek = "2"(already in the lockgraph via
soroban-sdktestutils) to produce real signatures in tests.README.md— module table row.Test plan
cargo test --lib gasless— 11 passed, 0 failed:test_gasless_assignment_happy_path— task →InProgress,assignee == contributor,nonce 0→1, and
env.auths()shows the relayer authorized while the contributor did not.test_replay_rejected— resubmitting the same signed message panics.test_wrong_signer_rejected— signature from an unregistered key panics.test_tampered_field_rejected— relayer swapscontributorafter signing → panics.test_expired_request_rejected—expiration_ledgerin the past panics.test_unregistered_contributor_rejected— no key on file panics.test_wrong_contract_rejected— request bound to another deployment panics.test_non_open_task_rejected— second gasless claim on an assigned task panics.test_admin_set_signing_key_enables_full_gasless_onboarding— admin registers key, gasless assign works.test_admin_set_signing_key_rejects_non_admin— non-admin registration panics.test_nonce_advances_across_sequential_assignments— nonce 0→1→2 over two tasks; stale nonce rejected.cargo test --lib— 127 passed / 12 failed. The 12 failures arepre-existing on
main(confirmed viagit stash): 9benchmark::*,swap_router_test::test_refund_on_missing_oracle_price,swap_router_test::test_refund_on_unresolved_route,test::test_create_and_complete_task_flow. This PR adds 11 passing tests andintroduces no regressions.
cargo build --target wasm32-unknown-unknown --release— clean.cargo clippy --lib --tests— no new warnings in the added files.Summary by CodeRabbit
New Features
Documentation