Summary
The auction's buyer-side code (node/crates/c0mpute-core/src/buyer.rs, run_auction()) accepts JobBid and JobReceipt gossip messages based only on the self-reported bidder_peer_id / worker_peer_id string fields inside the JSON payload. It never checks these against GossipMessage.source (Option<PeerId>), which is the actual libp2p-authenticated sender identity already attached to every received message.
Since PeerId is just a string a bidder or worker puts in their own JSON payload, any peer on the network can claim to be any other peer_id in a JobBid or JobReceipt — there is currently nothing stopping it.
Where
- Bid loop:
buyer.rs lines ~122-136 — matches on bid.job_id == job_id && bid.bidder_peer_id != offer.buyer_peer_id, no source check.
- Receipt loop:
buyer.rs lines ~191-208 — matches on receipt.job_id == job_id && receipt.worker_peer_id == winner.bidder_peer_id, no source check.
Compare with capabilities.rs (node/crates/c0mpute-core/src/capabilities.rs, around line 172), which already does this correctly for capability ads: it discards any ad with msg.source == None and keys its registry by the verified source, not by the self-reported peer_id field inside the ad payload.
Impact
JobAccept (naming the winning bidder + agreed_price_usd) is broadcast publicly on the job's gossip topic — every subscribed peer, not just participants, can see who won a given job and for how much.
- Any peer can then publish a
JobReceipt with worker_peer_id set to the winner's peer_id and a fabricated output_hash/status: Completed, before the real winner finishes the work.
run_auction() returns on the first message matching job_id + the claimed peer_id string, so whichever receipt arrives first — forged or genuine — wins. This means either: (a) a forger can get an AuctionOutcome credited to the real worker's identity for work never actually performed, or (b) at minimum it griefs the real worker, since their later genuine receipt is simply never observed (the function already returned).
- The same spoofing applies to bids: an attacker can publish a lowball bid under a different peer's
bidder_peer_id, get it selected as the winner, and then never deliver — while the real bidder with that identity never placed that bid.
JobReceipt.signature is documented as "Signed by the worker's CoinPay DID. Verified by buyer + validator" but is never actually checked anywhere in the codebase (confirmed via grep -rn "\.signature" node/crates — no hits outside the unrelated secure-chat module). That's presumably tracked as future work once CoinPay's DID signing ships. This report is about something available today, independent of that: GossipMessage.source is already a strongly-authenticated value (derived from the peer's actual keypair at the libp2p transport layer) and costs nothing to check — it's just not being checked.
Suggested fix (PR opened)
Before accepting a bid or receipt, verify msg.source.map(|p| p.to_base58()) == claimed_peer_id. Discard (log + skip) on mismatch, exactly like capabilities.rs already discards ads with no verifiable source.
Summary
The auction's buyer-side code (
node/crates/c0mpute-core/src/buyer.rs,run_auction()) acceptsJobBidandJobReceiptgossip messages based only on the self-reportedbidder_peer_id/worker_peer_idstring fields inside the JSON payload. It never checks these againstGossipMessage.source(Option<PeerId>), which is the actual libp2p-authenticated sender identity already attached to every received message.Since
PeerIdis just a string a bidder or worker puts in their own JSON payload, any peer on the network can claim to be any other peer_id in aJobBidorJobReceipt— there is currently nothing stopping it.Where
buyer.rslines ~122-136 — matches onbid.job_id == job_id && bid.bidder_peer_id != offer.buyer_peer_id, no source check.buyer.rslines ~191-208 — matches onreceipt.job_id == job_id && receipt.worker_peer_id == winner.bidder_peer_id, no source check.Compare with
capabilities.rs(node/crates/c0mpute-core/src/capabilities.rs, around line 172), which already does this correctly for capability ads: it discards any ad withmsg.source == Noneand keys its registry by the verifiedsource, not by the self-reportedpeer_idfield inside the ad payload.Impact
JobAccept(naming the winning bidder +agreed_price_usd) is broadcast publicly on the job's gossip topic — every subscribed peer, not just participants, can see who won a given job and for how much.JobReceiptwithworker_peer_idset to the winner's peer_id and a fabricatedoutput_hash/status: Completed, before the real winner finishes the work.run_auction()returns on the first message matchingjob_id+ the claimed peer_id string, so whichever receipt arrives first — forged or genuine — wins. This means either: (a) a forger can get anAuctionOutcomecredited to the real worker's identity for work never actually performed, or (b) at minimum it griefs the real worker, since their later genuine receipt is simply never observed (the function already returned).bidder_peer_id, get it selected as the winner, and then never deliver — while the real bidder with that identity never placed that bid.JobReceipt.signatureis documented as "Signed by the worker's CoinPay DID. Verified by buyer + validator" but is never actually checked anywhere in the codebase (confirmed viagrep -rn "\.signature" node/crates— no hits outside the unrelatedsecure-chatmodule). That's presumably tracked as future work once CoinPay's DID signing ships. This report is about something available today, independent of that:GossipMessage.sourceis already a strongly-authenticated value (derived from the peer's actual keypair at the libp2p transport layer) and costs nothing to check — it's just not being checked.Suggested fix (PR opened)
Before accepting a bid or receipt, verify
msg.source.map(|p| p.to_base58()) == claimed_peer_id. Discard (log + skip) on mismatch, exactly likecapabilities.rsalready discards ads with no verifiable source.