Shopping Without Servers

So far we don't have a convenient shopping cart so merchants can take payment in ether. We could build one the old way: put it on a server, use a database to hold cart data, create a new address to receive each payment, etc. But on Ethereum we don't need all that. We don't even need a server.

One way would be to store all the items and quantities in the contract, but that's a bit heavyweight. Worse, we also need shipping data, so we need better privacy for that if nothing else.

So let's hold all that off-chain, in the browser's local storage, using some kind of well-defined data format. When it's time to check out, we take a hash of that data, and post it to the contract along with the total ether cost.

Then, we use some kind of communication channel (like Whisper) to send the cart data to the merchant, who can hash it and check the contract to see that payment was made for that order.

We can also make the merchant prove he received the order, before he can withdraw funds. All we have to do is hash our order like this:

outerhash = sha1(sha1(order))

Post the outerhash to the contract upon making payment. To get the money, the merchant submits sha1(order) to the contract, which calculates the outerhash, checks how much ether was sent with it, and forwards to the merchant. If the merchant doesn't submit the innerhash within, say, one week, then the purchaser can get a refund.

We can have an online store hosted nothing more than a static web page, plus something that takes posted data and forwards it to the merchant (maybe even by email). Once we have Swarm and Whisper fully up and running, we don't need a server at all, and all the merchant needs is a standard client on a laptop.

Here's the (untested) contract:

contract Cart is owned {
    uint refundDelay = 7 days;
    struct Payment {
        address purchaser;
        uint amount;
        uint creationTime;
    }
    mapping (bytes32 => Payment) purchases;

    event logPurchase(bytes32 indexed outerhash, 
                      address indexed purchaser, 
                      uint amount);
    event logClaim(bytes32 indexed outerhash);
    event logRefund(bytes32 indexed outerhash);

    function purchase(bytes32 outerhash) payable {
        if (purchases[outerhash].creationTime == 0) throw;
        if (msg.value == 0) throw;

        purchases[outerhash].creationTime = now;
        purchases[outerhash].purchaser = msg.sender;
        purchases[outerhash].amount = msg.value;
        logPurchase(outerhash, msg.sender, msg.value);
    }

    function payout(bytes32 outerhash, bool isRefund) private {
        uint amount = purchases[outerhash].amount;
        address purchaser = purchases[outerhash].purchaser;

        purchases[outerhash].creationTime = 0;

        if (isRefund) {
            if (!purchaser.send(amount)) throw;
        } else {
            if (!owner.call.value(amount)()) throw;
        }
    }

    function claim(bytes32 innerhash) {
        bytes32 outerhash = sha3(innerhash);
        if (purchases[outerhash].creationTime == 0) throw;
        logClaim(outerhash);
        payout(outerhash, false);
    }

    function refund(bytes32 outerhash) {
        Payment p = purchases[outerhash];
        if (p.creationTime == 0 || 
            p.creationTime + refundDelay > now) throw;
        logRefund(outerhash);
        payout(outerhash, true);
    }
}

To save storage, in the payout function we could zero out the payment data. On the other hand, it might be nice if we had some sort of reputation system, and for that we might want to keep the data around.

Efficiency

I saw a post by a large merchant accepting Bitcoin, complaining about a $75 fee on a single transaction. It got so high because he made a single withdraw transaction, with inputs from over a hundred purchases.

I posted that Ethereum eliminates the problem, by using simple account balances instead of UTXOs. Then I realized my store contract destroys that advantage by requiring a separate withdrawal for every purchase.

We don't necessarily have to do it that way. The purchaser will want a receipt, but it doesn't have to be on chain. It can just be messaged directly to the purchaser. That makes the contract really simple:

contract Cart is owned {
    mapping (bytes32 => address) public purchases;

    event logPurchase(bytes32 indexed hash, 
                      address indexed purchaser, 
                      uint amount);

    function purchase(bytes32 hash) payable {
        if (msg.value == 0) throw;
        purchases[hash] = msg.sender;
        logPurchase(hash, msg.sender, msg.value);
    }

    function withdraw() onlyOwner {
        owner.send(this.balance);
    }
}

There's no refund function this way, but if the merchant wants to cheat someone, the original contract doesn't keep him from claiming the money anyway. The original does, however, prove on-chain that the merchant had to have received the order. We don't want a situation where the merchant can claim he didn't get the information he needed to fill the order.

One way to handle it would be to give the buyer a short time to change his mind, if the merchant doesn't send a receipt. A more rigorous approach, which is also simpler, is to send the order to the merchant first. The merchant makes a digital signature on the hash of your order, and sends that back to you. Then you post to the contract the order hash, the merchant's signature, and your ether payment. Here's our new purchase function:

function purchase(bytes32 hash,
                  uint8 v, bytes32 r, bytes32 s) 
payable {
    if (msg.value == 0) throw;
    if (owner != ecrecover(hash, v, r, s)) throw;
    purchases[hash] = msg.sender;
    logPurchase(hash, msg.sender, msg.value);
}

Now the purchaser can't deposit money at all until he gets a receipt back from the merchant. Just by looking at the purchases in the contract we know for sure that the merchant saw them all. But the merchant can still consolidate his payouts.

This does mean the merchant has to be more on top of things; he needs a machine online that can receive messages, sign them, and immediately send the back. But that's the only infrastructure he needs.

Ratings

Good online commerce needs reputations. That's hard on Ethereum without some kind of Sybil-resistant identities; positive ratings can be faked at will by the merchant, who can pay ether to himself from as many addresses as he likes.

Negative ratings are another matter. In social psych experiments, it's turned out that people who've been wronged are willing to pay money to punish the offender, even if they gain no personal advantage in doing so. So we can add a simple complaint function, which lets people burn ether to complain about a merchant. The ether just gets deposited in the contract with no way for anyone to retrieve it.

It's still not perfect; a shady merchant could make a store contract, avoid telling anyone about it, make a bunch of "sales" to himself over time, and eventually appear to be a solid merchant free of complaints. A competitor willing to spend money could pay for lots of complaints. But it's not like reputation systems on the Web are perfect either.

We can make the rating system separate from the cart contract; so we can have several competing reputation schemes. Here's a simple contract that lets you complain about any address on Ethereum:

contract Complaint {
    mapping (address => uint) complaintValue;
    mapping (address => uint) complaintCount;

    event logComplaint(address cart, bytes32 hash, uint amount);

    function complain(address cart, bytes32 hash) payable {
        complaintValue[cart] += msg.value;
        complaintCount[cart] += 1;
        logComplaint(cart, hash, msg.value);
    }
}

We might prefer to allow only verified purchasers to make complaints. That makes the complaint system a little less general; it can only complain about addresses which implement the Cart interface. It also means we can let registered purchasers complain without necessarily sending ether (though their complaint won't show as much commitment).

function complain(address cart, bytes32 hash) payable {
    if (Cart(cart).purchases(hash) != msg.sender) throw;
    complaintValue[cart] += msg.value;
    complaintCount[cart] += 1;
    logComplaint(cart, hash, msg.value);
}

Do we really have to burn the ether? It can't go to charity, because maybe the user wants to donate to the charity anyway, so their complaint shows more emphasis than it really deserves. It has to be a little annoying to spend complaint money, so people only do it when they really mean it.

I haven't noticed that people especially want to donate money to me, so how about I get all the complaint money? Tempting, but then I could make unlimited complaints at zero cost (besides gas). So I think there's no alternative, the complaint funds have to be destroyed, or stranded in the contract forever. (Or maybe I could take just a small percentage. That'd make the complaint process extra annoying, which is perfect! But maybe I shouldn't have an incentive to make contracts that people complain about :)