This system is a decentralized marketplace allowing sellers to publish items (digital goods) via IPFS, while deliverers each hold only a partial encrypted piece of those goods. Buyers purchase items on-chain, and deliverers re-encrypt the relevant parts to the buyer upon detecting a valid purchase. If deliverers fail or act maliciously, the seller can step in after a fixed block threshold (100 blocks) to deliver the missing pieces and preserve the sale. All participants stake funds on-chain to ensure honest behavior, and only minimal data—such as store references, item IDs, sale records, and stake balances—is maintained on-chain.
Centralized solutions for digital goods distribution often rely on a single party—like a centralized server or escrow—to manage encryption keys, delivery, and refunds. This single point of failure can lead to security breaches and censorship.
In contrast, this system aims for:
- Decentralized Delivery: Multiple untrusted deliverers collectively ensure the buyer receives the purchased digital item.
- Minimal On-Chain Footprint: Storing large metadata or partial codes on-chain is expensive. Instead, we store only essential references (IPFS links, item IDs, payment receipts, stakes, and disputes).
- Cryptographic Guarantees: By splitting or threshold-encrypting the digital good, no single deliverer has full access.
- Economic Incentives: Staking and slashing ensure that dishonest deliverers or sellers risk losing their deposit if they cheat.
-
Seller
- Creates a “store” on-chain with a reference to an IPFS JSON file containing the store’s items.
- Each item in the JSON has a unique item ID (which we’ll also call
purchaseId
oritemId
). - Splits the digital item (coupon code, license key, or other data) into multiple encrypted parts, each part intended for a distinct deliverer’s public key (but stored only in the IPFS JSON).
- Stakes collateral on-chain to ensure honest behavior.
- May update the IPFS reference (CID) if they need to remove or modify items or replace malicious deliverers.
-
Buyer
- Reads the store’s IPFS JSON (off-chain).
- Checks the item’s price,
itemId
, descriptions, etc. - Initiates a purchase transaction on-chain, referencing the store and the itemId.
- Waits for the deliverers (or fallback seller) to provide the encrypted fragments.
- Decrypts the fragments off-chain using the buyer’s private key, reconstructing the digital good.
- May open disputes if the item isn’t delivered or if the fragments are invalid.
-
Deliverer
- Stakes collateral on-chain, obtains an ephemeral public key or uses an off-chain method to broadcast their public key.
- Scans all IPFS JSON entries from all sellers. Attempts to decrypt every stored fragment to discover which fragments belong to them.
- Monitors on-chain for purchases. When a valid purchase is seen (matching the item’s price), they re-encrypt their fragment with the buyer’s public key, broadcasting the result on-chain via an event.
- Earns a share of the commission from the seller for each successful delivery.
- Risks losing stake if they deliver for an underpaid purchase or maliciously fail to deliver.
-
DAO or Arbitrator (Optional)
- Resolves edge-case disputes on-chain.
- Slashes stakes of dishonest participants.
- Conducts refunds to buyers if necessary.
- IPFS JSON: For each store, the seller uploads a JSON object to IPFS. For example:
{ "storeName": "Alice's Digital Goods", "items": [ { "itemId": "item-001", "title": "Special Discount Code", "price": 100000000000000000, // 0.1 ETH "encryptedParts": [ "0xABC123...", // part for some deliverer "0xDEF456...", ... ], "otherMetadata": "... optional ..." }, { "itemId": "item-002", "title": "Software License Key", "price": 200000000000000000, // 0.2 ETH "encryptedParts": [ ... ] } ] }
- encryptedParts: Each part is encrypted with a different deliverer’s public key. The JSON does not say which part belongs to which deliverer.
- The
price
is in wei. - The buyer uses
itemId
when purchasing on-chain.
A single marketplace contract (or a factory pattern for multiple markets) that stores:
-
Stake Balances:
delivererStakes[delivererAddress]
,sellerStakes[sellerAddress]
. -
Store Registry: For each seller, we store:
sellerStoreCID[sellerAddress] = currentIPFSCID
(the IPFS link to their store’s JSON).
-
Sold Items: A set or mapping indicating
(sellerAddress, itemId) -> soldCount or boolean sold
.- This ensures an item cannot be bought multiple times if the seller wants it to be unique (like a single-use coupon).
- Alternatively, it can track how many times an item has been sold, if the seller wants multiple copies to be available.
-
Purchase Record: A minimal record of each purchase, e.g.
(buyerAddress, sellerAddress, itemId, blockNumberPurchased, delivered, disputed, etc.)
.- Enough to handle refunds or disputes.
-
Fallback: Hardcoded at 100 blocks in the contract.
-
Events:
StoreCreated(seller, storeCID)
orStoreUpdated(seller, oldCID, newCID)
ItemPurchased(buyer, seller, itemId, pricePaid)
FragmentDelivered(delivererOrSeller, buyer, seller, itemId, encryptedFragment)
DisputeOpened(...)
,DisputeResolved(...)
-
Buyer Off-Chain Lookup:
- Buyer fetches the store’s IPFS JSON via
sellerStoreCID[sellerAddress]
. - Finds
itemId
andprice
.
- Buyer fetches the store’s IPFS JSON via
-
On-Chain Purchase:
- Buyer calls
marketplace.buyItem(seller, itemId)
withmsg.value = price
. - The contract checks:
- The item is not already sold out (if it’s unique).
- The seller has an active store.
- Buyer’s payment matches or exceeds the required price.
- Emits
ItemPurchased(buyer, seller, itemId, msg.value)
. - Marks
(seller, itemId)
as sold if it’s a unique item (or increments a sold counter if multiple copies are allowed).
- Buyer calls
-
Delivery:
- All deliverers watch for
ItemPurchased(...)
. - Each deliverer compares
msg.value
to the expected price in IPFS to confirm correctness.- If the payment is correct, the deliverer tries to decrypt the relevant fragment from
encryptedParts
. - If successful, it re-encrypts that part with the buyer’s public key and calls
deliverFragment(...)
, which emits aFragmentDelivered(...)
event.
- If the payment is correct, the deliverer tries to decrypt the relevant fragment from
- The buyer’s front-end listens for these events, retrieves each partial, and decrypts it off-chain.
- All deliverers watch for
-
Fallback after 100 blocks:
- If the buyer has not received all parts, the seller sees that certain fragments are missing.
- After 100 blocks from purchase, the seller calls
deliverFragment(...)
for those missing parts. - The buyer then obtains them from the event logs.
-
Reconstruction:
- The buyer decrypts each part with their private key.
- If it’s a simple split, the buyer concatenates the parts. If it’s a threshold scheme (e.g., Shamir’s Secret Sharing), the buyer reconstructs the secret from the partial shares.
- If the buyer never receives valid fragments, they can call
openDispute(...)
referencing(seller, itemId)
. - If a deliverer delivered at the wrong price (underpaid), the seller can open a dispute to slash them.
- By default, participants settle off-chain. If they fail, the DAO or an arbitrator calls
resolveDispute(...)
, slashing any dishonest party’s stake and/or refunding the buyer if appropriate.
Below is a high-level design. The exact implementation can vary, but this captures the minimal data stored on-chain.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract DecentralizedMarketplace {
// -------------------------------------------------
// 1. Staking
// -------------------------------------------------
mapping(address => uint256) public delivererStakes;
mapping(address => uint256) public sellerStakes;
// Seller -> Current IPFS store reference
mapping(address => string) public sellerStoreCID;
// -------------------------------------------------
// 2. Sale Tracking
// -------------------------------------------------
struct SaleInfo {
address buyer;
uint256 blockPurchased;
bool delivered; // if all parts delivered or final
bool disputed;
// Extend as needed
}
// (seller, itemId) -> SaleInfo
// We assume each itemId can only be sold once if it's unique.
// Alternatively, store an array of SaleInfo for multiple copies.
mapping(address => mapping(string => SaleInfo)) public sales;
// -------------------------------------------------
// 3. Events
// -------------------------------------------------
event DelivererStaked(address indexed deliverer, uint256 amount);
event SellerStaked(address indexed seller, uint256 amount);
event StoreCreated(address indexed seller, string ipfsCid);
event StoreUpdated(address indexed seller, string oldCid, string newCid);
event ItemPurchased(
address indexed buyer,
address indexed seller,
string indexed itemId,
uint256 price
);
event FragmentDelivered(
address indexed sender, // deliverer or fallback seller
address indexed buyer,
address indexed seller,
string itemId,
bytes encryptedFragment
);
event DisputeOpened(
address indexed opener,
address indexed seller,
string indexed itemId
);
// For brevity, skipping DisputeResolved event, etc.
// -------------------------------------------------
// 4. Staking Logic
// -------------------------------------------------
function stakeAsDeliverer() external payable {
require(msg.value > 0, "No stake provided");
delivererStakes[msg.sender] += msg.value;
emit DelivererStaked(msg.sender, msg.value);
}
function stakeAsSeller() external payable {
require(msg.value > 0, "No stake provided");
sellerStakes[msg.sender] += msg.value;
emit SellerStaked(msg.sender, msg.value);
}
// -------------------------------------------------
// 5. Store Creation/Update
// -------------------------------------------------
function createStore(string calldata ipfsCid) external {
require(sellerStakes[msg.sender] > 0, "Seller not staked");
require(bytes(sellerStoreCID[msg.sender]).length == 0, "Store already exists");
sellerStoreCID[msg.sender] = ipfsCid;
emit StoreCreated(msg.sender, ipfsCid);
}
function updateStore(string calldata newCid) external {
require(sellerStakes[msg.sender] > 0, "Seller not staked");
require(bytes(sellerStoreCID[msg.sender]).length != 0, "Store doesn't exist");
string memory oldCid = sellerStoreCID[msg.sender];
sellerStoreCID[msg.sender] = newCid;
emit StoreUpdated(msg.sender, oldCid, newCid);
}
// -------------------------------------------------
// 6. Purchase Logic
// -------------------------------------------------
function buyItem(address seller, string calldata itemId) external payable {
require(sellerStakes[seller] > 0, "Seller not staked");
// Check if it's not already sold (if unique):
require(sales[seller][itemId].buyer == address(0), "Item already sold");
// Minimal check: must pay > 0
// Detailed price check is done off-chain by deliverers
require(msg.value > 0, "Must send some ETH");
// Record sale on-chain
sales[seller][itemId] = SaleInfo({
buyer: msg.sender,
blockPurchased: block.number,
delivered: false,
disputed: false
});
emit ItemPurchased(msg.sender, seller, itemId, msg.value);
}
// -------------------------------------------------
// 7. Delivery of Fragments
// -------------------------------------------------
function deliverFragment(
address buyer,
address seller,
string calldata itemId,
bytes calldata encryptedFragment
) external {
// Could be any deliverer OR the fallback seller
SaleInfo storage sale = sales[seller][itemId];
require(sale.buyer == buyer, "Buyer does not match");
require(!sale.disputed, "Sale is disputed");
require(!sale.delivered, "Already delivered/final");
// Fallback: if msg.sender == seller, check if 100 blocks have passed
if (msg.sender == seller) {
require(block.number > sale.blockPurchased + 100, "Fallback not available yet");
} else {
// If it's a deliverer, verify they are staked
require(delivererStakes[msg.sender] > 0, "Deliverer not staked");
// The deliverer should have verified off-chain that the price was correct
}
// Emit the partial data for the buyer to retrieve off-chain
emit FragmentDelivered(msg.sender, buyer, seller, itemId, encryptedFragment);
}
// -------------------------------------------------
// 8. Disputes
// -------------------------------------------------
function openDispute(address seller, string calldata itemId) external {
SaleInfo storage sale = sales[seller][itemId];
require(msg.sender == sale.buyer || msg.sender == seller, "Only buyer or seller can dispute");
require(!sale.disputed, "Already disputed");
sale.disputed = true;
emit DisputeOpened(msg.sender, seller, itemId);
}
// A real system might have a DAO or arbitrator:
// function resolveDispute(...) external onlyDAO { ... slash stakes ... }
}
- Only critical data is stored:
- Seller’s IPFS link,
- Whether an item is sold,
- Reference to the buyer and a few booleans for dispute/delivery status.
- Everything else (price checks, partial encryption) is off-chain.
- The seller can update the IPFS
CID
anytime (e.g., removing malicious deliverers, changing items). - Once an item is sold, the contract blocks reselling the same
itemId
if it’s unique. If the seller wants multiple “copies,” they can incorporate an internal numeric suffix or repeated itemId logic in the JSON and keep track of sales differently.
-
Seller
- Splits or threshold-encrypts each digital good into
N
parts. - Embeds these encrypted parts in the JSON. No mention of which part belongs to which deliverer.
- Uploads the JSON to IPFS.
- Calls
createStore(ipfsCid)
(orupdateStore(newCid)
).
- Splits or threshold-encrypts each digital good into
-
Deliverers
- Continuously parse all IPFS JSON from all active sellers. Attempt to decrypt each
encryptedPart
. - If it decrypts successfully, that fragment belongs to them.
- Monitor
ItemPurchased
events. If payment matches the item’s price from the JSON, re-encrypt the part to the buyer’s key and calldeliverFragment(...)
.
- Continuously parse all IPFS JSON from all active sellers. Attempt to decrypt each
-
Buyer
- Reads store JSON from
sellerStoreCID[seller]
. - Finds an item with
itemId
, checks the price, callsbuyItem(seller, itemId)
with the correctmsg.value
. - Watches for
FragmentDelivered(sender, buyer, seller, itemId, part)
. Collects all parts, decrypts them off-chain, reconstructs the final digital item.
- Reads store JSON from
-
Fallback
- If deliverers have not delivered parts after 100 blocks, the seller calls
deliverFragment(...)
to ensure the buyer isn’t left without the purchased item.
- If deliverers have not delivered parts after 100 blocks, the seller calls
-
Dispute
- If no valid fragments arrived or a deliverer delivered at the wrong price, participants open a dispute.
- Off-chain negotiation might resolve it. If not, a DAO or governance process calls
resolveDispute()
to slash stakes or refund the buyer.
- The contract keeps
sales[seller][itemId].buyer
to mark an item as sold. - Before the buyer front-end displays items from the store’s JSON, it queries the contract to see if
(seller, itemId)
is already sold. If yes, the UI hides or marks it as “Sold Out.” - This ensures the buyer cannot buy the same itemId multiple times.
- Deliverers stake ETH in
delivererStakes
. If they provide partial items for an underpaid purchase or never deliver at all (leading to a dispute they lose), they risk being slashed. - Sellers stake as well. If they create fake listings, refuse fallback, or cheat in a dispute, they risk losing stake.
- If all deliverers collude, they could reconstruct the entire digital good. Economic incentives should discourage this, and the larger the pool of deliverers, the less likely all would collude.
- Deliverers see only the partial data they can decrypt. The buyer is the only one who can assemble all parts.
- The seller knows the entire digital good by definition.
-
Seller Onboarding
- Seller stakes ETH via
stakeAsSeller()
. - Seller uploads store JSON to IPFS, calls
createStore(cid)
with the resulting IPFS hash.
- Seller stakes ETH via
-
Publishing Items
- The seller has
items[]
in the JSON, each with a uniqueitemId
, price, and array ofencryptedParts
. - If updates are needed, the seller modifies the JSON, re-uploads, and calls
updateStore(...)
.
- The seller has
-
Buyer Browsing
- Buyer’s front-end loads the store data from
sellerStoreCID[seller]
. - Checks contract for each
itemId
to see if it’s sold. If not, shows “Buy” button.
- Buyer’s front-end loads the store data from
-
Buying
- Buyer clicks “Buy.” Their wallet calls
buyItem(seller, itemId)
with the correct ETH. - The contract marks
(seller, itemId)
as sold, emits an event.
- Buyer clicks “Buy.” Their wallet calls
-
Delivery
- Each deliverer sees the
ItemPurchased
event, verifies off-chain that the buyer paid at least theprice
in the JSON. - If correct, each deliverer calls
deliverFragment(buyer, seller, itemId, partEncryptedForBuyer)
. - Buyer’s front-end listens for
FragmentDelivered
events, collectsN
parts, decrypts, and reconstructs.
- Each deliverer sees the
-
Fallback
- If some parts do not arrive after 100 blocks, the seller calls
deliverFragment(...)
.
- If some parts do not arrive after 100 blocks, the seller calls
-
Optional Dispute
- If the buyer doesn’t receive correct parts, they call
openDispute(seller, itemId)
. - A DAO or arbiter eventually resolves it. Possibly slashing stakes or refunding the buyer.
- If the buyer doesn’t receive correct parts, they call
- Threshold Cryptography: Instead of a simple split, use advanced threshold schemes (e.g., Shamir’s Secret Sharing) so only k-out-of-n deliverers are needed to reconstruct.
- Token Payments: Support ERC-20 tokens instead of ETH.
- Ratings & Reputation: Keep minimal integer rating or reputation indexes in the contract, or store them in IPFS for rich text feedback.
- Secondary Market: If items are transferrable or can be resold, one could represent them as NFTs referencing the data.
- Multi-Copy Sales: If multiple copies of the same item can be sold, store a
soldCount
and compare it tomaxSupply
.
This architecture:
- Creates a store for each seller (on-chain pointer to an IPFS JSON).
- Minimizes on-chain storage: We keep only the essential references for items sold, staking balances, and a fallback / dispute mechanism.
- Enforces one-time or limited sales by marking items sold on-chain.
- Ensures decentralized delivery via multiple deliverers scanning for fragments they can decrypt, re-encrypting them to the buyer’s key.
- Provides fallback after 100 blocks, letting the seller rescue the sale.
- Enables dispute resolution and slashing of dishonest parties’ stakes.
By carefully splitting logic between on-chain minimal records and off-chain IPFS + partial encryption, this system achieves both trust-minimized digital goods delivery and low gas usage. Buyers, sellers, and deliverers are economically incentivized to behave honestly through staking deposits and fear of slashing.
This completes the core design of a Decentralized Marketplace for Digital Goods with minimal on-chain data, robust off-chain encryption, seller-owned stores, and secure partial delivery.