Firebets: Onchain P2P Bitcoin Cash betting
There are some really interesting opportunities that arises with the upcoming May 2022 upgrade for smart contracts. I have an idea I would like to share with the world and hopefully someone somewhere will take this and create a working betting protocol.
The basic idea is that somewhere there is an oracle that produces messages. Let's say an oracle somewhere produces a file, for instance olympics.json
, of the form:
{
"OraclePublicKey": "0x123451",
"BetSignature": "<signature of the bets using the above public key>",
"bets": {
"d41d8cd98f00b204e9800998ecf8427e": {
"Description": "USA wins Figure Skating - Ice Dance",
"Date of completion": "2022-02-15T09:15:00 CET",
},
"d41d8cd98f00b204e9800998ecf8427e": {
"Description": "Canada wins Figure Skating - Ice Dance",
"Date of completion": "2022-02-15T09:15:00 CET",
},
[...]
}
}
And this file can be distributed in whatever way: IPFS, BitTorrent, a OP_RETURN protocol or whatever.
My idea is this (100% untested) script.
pragma cashscript ^0.7.0;
contract Firebet(pubkey oraclePk, bytes oraclebetid, int oraclebetresult, int betTimeout, int errorTimeout, bytes20 bettorLockingScript, int amountbet) {
function payout(datasig oracleSig, bytes oracleMessage, bytes parenttx) {
// One party gets all the money
require(tx.outputs.length == 1);
// Verify the oracles message
int betid = bytes(oracleMessage.split(128)[0]);
int result = int(oracleMessage.split(128)[1]);
require(betid == oraclebetid);
require(checkDataSig(oracleSig, oracleMessage, oraclePk));
if (result == oraclebetresult) {
// Bettor won. Claim all money!
bytes25 bettorLockingBytecode = new LockingBytecodeP2PKH(bettorLockingScript);
require(tx.outputs[0].lockingBytecode == bettorLockingBytecode);
} else {
// Punter won. Claim all money to the address used by the punter to fund the bet.
require(tx.inputs[0].outpointTransactionHash == hash256(parenttx));
// Get the punters pubKey from the parent tx
// I don't know if this actually works... you get the idea.
bytes punterPk = parenttx.split(109)[1].split(33)[0];
bytes25 punterLockingBytecode = new LockingBytecodeP2PKH(hash160(punterPk));
require(tx.outputs[0].lockingBytecode == punterLockingBytecode);
}
}
function timeout(bytes parenttx) {
require(tx.time >= betTimeout);
require(tx.outputs.length == 2);
// The bettor can receive his money
bytes25 bettorLockingBytecode = new LockingBytecodeP2PKH(bettorLockingScript);
require(tx.outputs[0].lockingBytecode == bettorLockingBytecode);
require(tx.outputs[0].value == amountbet);
// Verify that the tx parameter *is* the parent tx
require(tx.inputs[0].outpointTransactionHash == hash256(parenttx));
// Get the punters pubKey from the parent tx
bytes punterPk = parenttx.split(109)[1].split(33)[0];
// Send back the rest of the money to the punter
bytes25 punterLockingBytecode = new LockingBytecodeP2PKH(hash160(punterPk));
require(tx.outputs[1].lockingBytecode == punterLockingBytecode);
}
function timeout2() {
// The punter messed up the transaction. Give back *all* the money to the bettor.
require(tx.time >= errorTimeout);
bytes25 recipientLockingBytecode = new LockingBytecodeP2PKH(bettorLockingScript);
require(tx.outputs[0].lockingBytecode == recipientLockingBytecode);
}
}
Now consider two parties. One "Bettor" that wants to make a 1 BCH bet that USA wins the Ice Dance event in Beijing OS and wants a 3:1 odds. He creates a transaction where he spends a UTXO of 1 BCH and creates 1 P2SH output of 4 BCH with the above script and the parameters corresponding to the oraclepublickey (0x123451
) and betid (d41d8cd98f00b204e9800998ecf8427e
) from olympics.json and also two different timeouts and a way for the bettor to get payed. The bettor signs this transaction SIGHASH_ALL|ANYONECANPAY
. Please note that at this point this transaction can not be broadcasted on the network since the amount in the input (1 BCH) is less then the amount in the output (4 BCH).
The bettor can now distribute (more on this later) the parameters
and signature
to this transaction which eventually gets picked up by another party, let's call him "punter". The punter can now verify the bet and create a transaction with the bettors input and output and then places his own 3 BCH P2PKH spend (SIGHASH_ALL
) as the first input to the transaction and broadcast to the BCH network.
Once the oracle publishes the message (betid+result, i.e. d41d8cd98f00b204e9800998ecf8427e01
and a signature) anyone that has knowledge about the parameters can settle the bet. If there isn't any message (for instance if the Ice Skating event was cancelled) the money is returned to each participant after the set timeout, typically set to days after the event was supposed to be held. If the Punter messed up his transaction, for instance not using a P2PKH spend, the bettor can claim all money after the longer timeout, typically set to weeks after the event.
How can the bettor distribute the parameters? Someone could create a service for indexing and serving these types of bets where wallets pushes the parameters and pulls other bets based on certain parameters ("give me all bets from oracle x with bet y valued between 1-3 BCH with a return of 2-2.5"). It should also be possible to compress and distribute these parameters in an OP_RETURN protocol which would make this completely on-chain.
The novel functionality with this contract is the asynchronous fire-and-forget nature of the bettor. Just create a bet and distribute it. No handshaking needed!
As most people will notice the most critical issue in this system is by far the trust in the oracle. What is stopping the oracle for taking a lot of bets on one side and then produce a faulty message that gives the oracle all the money? I don't know really. Hopefully the script can be extended to use several oracles and bet condition, like 2-of-3 oracles to claim the money. I hope that there will be a good business in keeping an oracle honest.
Entrepreneurs, go nuts!