├── code ├── .npmignore ├── .gitignore ├── examples │ └── dice │ │ ├── images │ │ ├── main_menu.png │ │ ├── Coin Flip Loser.png │ │ └── Coin Flip Winner.png │ │ ├── claims.json │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── dice.ts │ │ └── dice.js ├── src │ ├── chainbet.ts │ ├── utils.spec.ts │ ├── chainfeed.ts │ ├── client.ts │ ├── core.spec.ts │ ├── host.ts │ ├── utils.ts │ ├── messagefeed.ts │ ├── wallet.ts │ ├── coinflipshared.ts │ ├── core.ts │ ├── coinflipclient.ts │ └── coinfliphost.ts ├── test │ ├── fixtures │ │ └── chainbet.json │ └── chainbet.js ├── LICENSE ├── package.json ├── .vscode │ └── launch.json ├── README.md └── tsconfig.json ├── images ├── multilock-small.png └── 68747470733a2f2f692e696d6775722e636f6d2f6d424c59746e432e706e67.png ├── README.md ├── LICENSE ├── AUCTION.md ├── DICE_ROLL.md ├── MULTIPLAYER.md └── PROTOCOL.md /code/.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | src 3 | .vscode 4 | .nyc_output 5 | coverage -------------------------------------------------------------------------------- /code/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | lib 4 | coverage 5 | examples/dice/wallet.json -------------------------------------------------------------------------------- /images/multilock-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyookball/ChainBet/HEAD/images/multilock-small.png -------------------------------------------------------------------------------- /code/examples/dice/images/main_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyookball/ChainBet/HEAD/code/examples/dice/images/main_menu.png -------------------------------------------------------------------------------- /code/examples/dice/images/Coin Flip Loser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyookball/ChainBet/HEAD/code/examples/dice/images/Coin Flip Loser.png -------------------------------------------------------------------------------- /code/examples/dice/images/Coin Flip Winner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyookball/ChainBet/HEAD/code/examples/dice/images/Coin Flip Winner.png -------------------------------------------------------------------------------- /images/68747470733a2f2f692e696d6775722e636f6d2f6d424c59746e432e706e67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyookball/ChainBet/HEAD/images/68747470733a2f2f692e696d6775722e636f6d2f6d424c59746e432e706e67.png -------------------------------------------------------------------------------- /code/examples/dice/claims.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "claimType": "", 4 | "isSpent": false, 5 | "betAmount": 0, 6 | "pubKey": "", 7 | "block": 0, 8 | "txId": "" 9 | } 10 | ] -------------------------------------------------------------------------------- /code/src/chainbet.ts: -------------------------------------------------------------------------------- 1 | export { Core } from './core'; 2 | export { Utils } from './utils'; 3 | export { Wallet } from './wallet'; 4 | 5 | export { Host } from './host'; 6 | export { Client } from './client'; 7 | 8 | export { CoinFlipHost } from './coinfliphost'; 9 | export { CoinFlipClient } from './coinflipclient'; 10 | export { CoinFlipShared } from './coinflipshared'; 11 | 12 | export { MessageFeed} from './messagefeed'; -------------------------------------------------------------------------------- /code/test/fixtures/chainbet.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainbet": { 3 | "encodePhase1": [ 4 | { 5 | } 6 | ], 7 | "encodePhase2": [ 8 | { 9 | } 10 | ], 11 | "encodePhase3": [ 12 | { 13 | } 14 | ], 15 | "encodePhase4": [ 16 | { 17 | } 18 | ], 19 | "encodePhase5": [ 20 | { 21 | } 22 | ], 23 | "encodePhase6": [ 24 | { 25 | } 26 | ], 27 | "decode": [ 28 | { 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChainBet 2 | 3 | ChainBet is a proposed Bitcoin Cash protocol to enable on-chain betting. This initial proposal focuses on a simple coin flip bet, but the principles could be extrapolated to enable more elaborate configurations. The protocol consists of 2 components: a commitment scheme to enable a trustless wager, and an on-chain messaging system to facilitate communication. 4 | 5 | [CLICK HERE TO READ THE PROTOCOL.](https://github.com/fyookball/ChainBet/blob/master/PROTOCOL.md) 6 | 7 | -------------------------------------------------------------------------------- /code/examples/dice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dice", 3 | "version": "0.0.1", 4 | "description": "Example program utilizing ChainBet protocol", 5 | "main": "dice.js", 6 | "author": "", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@types/inquirer": "^0.0.42", 10 | "@types/jsonfile": "^4.0.1", 11 | "bitbox-cli": "^1.4.4", 12 | "chainbet": "^0.0.14", 13 | "commander": "^2.15.1", 14 | "inquirer": "^6.0.0", 15 | "jsonfile": "^4.0.0" 16 | }, 17 | "devDependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /code/src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from './utils'; 2 | import * as assert from 'assert'; 3 | import 'mocha'; 4 | 5 | describe('#amount_2_Hex', () => { 6 | it(`should convert number amount to 8 byte hex big-endian`, () => { 7 | let amount = 10000000000 // 100 BCH 8 | let hex = Utils.amount_2_hex(amount) 9 | assert.equal(hex.toString('hex'), '00000002540be400'); 10 | }); 11 | }); 12 | 13 | describe('#hash160_2_cashAddr', () => { 14 | it(`should convert public key hash160 to bitcoin cash address format`, () => { 15 | let expected_address = 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'; 16 | let actual_pkHash160 = Buffer.from('a0f531f4ff810a415580c12e54a7072946bb927e', 'hex'); 17 | let networkByte = 0x00; 18 | let actual_address = Utils.hash160_2_cashAddr(actual_pkHash160, networkByte); 19 | assert.equal(actual_address, expected_address); 20 | }); 21 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ChainBet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /code/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 James Cramer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainbet", 3 | "version": "0.0.16", 4 | "description": "Methods for the ChainBet protocol", 5 | "main": "./lib/chainbet.js", 6 | "types": "./lib/chainbet.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "nyc mocha --require babel-core/register", 10 | "test-ts": "nyc mocha --require ts-node/register --require source-map-support/register --full-trace --bail src/**/*.spec.ts" 11 | }, 12 | "nyc": { 13 | "include": [ 14 | "src/**/*.ts", 15 | "src/**/*.tsx" 16 | ], 17 | "extension": [ 18 | ".ts", 19 | ".tsx" 20 | ], 21 | "require": [ 22 | "ts-node/register" 23 | ], 24 | "reporter": [ 25 | "text-summary", 26 | "html" 27 | ], 28 | "sourceMap": true, 29 | "instrument": true 30 | }, 31 | "author": "James Cramer ", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/jcramer/chainbet.git" 35 | }, 36 | "license": "MIT", 37 | "dependencies": { 38 | "axios": "^0.18.0", 39 | "bip68": "^1.0.4", 40 | "bitbox-cli": "^1.4.4", 41 | "bs58": "^4.0.1", 42 | "eventsource": "^1.0.5", 43 | "inquirer": "^6.0.0", 44 | "underscore": "^1.9.0" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "^10.5.2", 48 | "@types/chai": "^4.1.4", 49 | "@types/mocha": "^5.2.4", 50 | "@types/assert": "^0.0.31", 51 | "assert": "^1.4.1", 52 | "babel-core": "^6.26.3", 53 | "chai": "^4.1.2", 54 | "mocha": "^5.2.0", 55 | "nconf": "^0.10.0", 56 | "nyc": "^11.6.0", 57 | "sinon": "^4.5.0", 58 | "source-map-support": "^0.5.6", 59 | "ts-node": "^7.0.0", 60 | "typescript": "^2.9.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /code/src/chainfeed.ts: -------------------------------------------------------------------------------- 1 | var eventsource = require('eventsource'); 2 | 3 | export class Chainfeed { 4 | 5 | static listen(onData: (data: any) => void, onConnected: (error: any) => void, onDisconnect: (error: any) => void) { 6 | let source = new eventsource('https://chainfeed.org/stream'); 7 | source.addEventListener('message', function(e: MessageEvent) { 8 | var m = JSON.parse(e.data); 9 | onData(m.data); 10 | }, false) 11 | source.addEventListener('open', function(e: MessageEvent) { 12 | //console.log("Chainfeed Connected"); 13 | if(onConnected != undefined){ 14 | onConnected(e); 15 | } 16 | }, false) 17 | source.addEventListener('error', function(e: MessageEvent) { 18 | if (e) { //.target.readyState == EventSource.CLOSED) { 19 | //console.log("Chainfeed Disconnected", e); 20 | if(onDisconnect != undefined){ 21 | onDisconnect(e); 22 | } 23 | } 24 | // else if (e.target.readyState == EventSource.CONNECTING) { 25 | // //console.log("Chainfeed is connecting...", e); 26 | // } 27 | }, false) 28 | } 29 | 30 | recent(size: number, callback: () => void) { 31 | this._req('https://chainfeed.org/recent/' + size, callback); 32 | } 33 | 34 | range(start: number, end: number, callback: () => void) { 35 | this._req('https://chainfeed.org/range/' + start + ',' + end, callback); 36 | } 37 | 38 | tx(hash: string, callback: () => void) { 39 | this._req('https://chainfeed.org/tx/' + hash); 40 | } 41 | 42 | _req(endpoint: string, callback?: (res: string) => void) { 43 | var xhr = new XMLHttpRequest(); 44 | xhr.open('GET', endpoint); 45 | xhr.responseType = 'json'; 46 | xhr.onload = function(e: any) { 47 | if (this.status == 200 && callback != undefined) { 48 | callback(this.response); 49 | } 50 | }; 51 | xhr.send(); 52 | } 53 | } -------------------------------------------------------------------------------- /code/src/client.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | export class Client { 5 | 6 | // Phase 2: Bet Participant Acceptance 7 | static encodePhase2Message(betId: string, multisigPubKey: Buffer, secretCommitment: Buffer): Buffer { 8 | 9 | let script = [ 10 | BITBOX.Script.opcodes.OP_RETURN, 11 | // 4 byte prefix 12 | Buffer.from('00424554', 'hex'), 13 | // 1 byte version id / 1 betType byte / 1 phase byte 14 | Buffer.from('010102', 'hex'), 15 | // 32 byte betTxId hex 16 | Buffer.from(betId, 'hex'), 17 | // 33 byte participant (Bob) multisig Pub Key hex 18 | multisigPubKey, 19 | // 32 byte participant (Bob) secret commitment 20 | secretCommitment 21 | ]; 22 | 23 | return BITBOX.Script.encode(script) 24 | } 25 | 26 | // Phase 4: Bet Participant Funding 27 | static encodePhase4Message(betId: string, clientEscrowTxId: string, participantSig1: Buffer, participantSig2: Buffer): Buffer { 28 | 29 | let script = [ 30 | BITBOX.Script.opcodes.OP_RETURN, 31 | // 4 byte prefix 32 | Buffer.from('00424554','hex'), 33 | // 1 byte version id / 1 betType byte / 1 phase byte 34 | Buffer.from('010104', 'hex'), 35 | // 32 byte bet tx id 36 | Buffer.from(betId, 'hex'), 37 | // 32 byte bet tx id 38 | Buffer.from(clientEscrowTxId, 'hex'), 39 | // 71-72 bytes for sig 40 | participantSig1, 41 | // 71-72 bytes for sig 42 | participantSig2, 43 | ]; 44 | 45 | return BITBOX.Script.encode(script) 46 | } 47 | 48 | // Phase 6: Bet Participant Resignation 49 | static encodePhase6Message(betId: string, secretValue: Buffer): Buffer { 50 | 51 | let script = [ 52 | BITBOX.Script.opcodes.OP_RETURN, 53 | // 4 byte prefix 54 | Buffer.from('00424554', 'hex'), 55 | // 1 byte version id / 1 betType byte / 1 phase byte 56 | Buffer.from('010106', 'hex'), 57 | // 32 byte bet txn id 58 | Buffer.from(betId, 'hex'), 59 | // 32 byte Secret value 60 | secretValue 61 | ]; 62 | 63 | return BITBOX.Script.encode(script) 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /code/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch dice.js Client with args", 11 | "program": "${workspaceFolder}/examples/dice/dice.js", 12 | "cwd": "${workspaceFolder}/examples/dice", 13 | "args": [ "-m", "client", "-d", "1"] 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Launch dice.ts Client with args", 19 | "cwd": "${workspaceFolder}/examples/dice", 20 | "args": ["${workspaceFolder}/examples/dice/dice.ts", "-m", "client", "-d", "1"], 21 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 22 | "sourceMaps": true, 23 | "protocol": "inspector", 24 | }, 25 | { 26 | "type": "node", 27 | "request": "launch", 28 | "name": "Launch dice.ts Host with args", 29 | "cwd": "${workspaceFolder}/examples/dice", 30 | "args": ["${workspaceFolder}/examples/dice/dice.ts", "-m", "host", "-d", "1"], 31 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 32 | "sourceMaps": true, 33 | "protocol": "inspector", 34 | }, 35 | { 36 | "type": "node", 37 | "request": "launch", 38 | "name": "Mocha Tests", 39 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 40 | "args": [ 41 | "-u", 42 | "tdd", 43 | "--timeout", 44 | "999999", 45 | "--colors", 46 | "${workspaceFolder}/test" 47 | ], 48 | "internalConsoleOptions": "openOnSessionStart" 49 | }, 50 | { 51 | "type": "node", 52 | "request": "launch", 53 | "name": "Launch Program", 54 | "program": "${file}" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /code/src/core.spec.ts: -------------------------------------------------------------------------------- 1 | import { Core } from './core'; 2 | import * as assert from 'assert'; 3 | import 'mocha'; 4 | 5 | describe('#checkScriptNumberHandling', () => { 6 | it(`check proper handling of Script 32-bit signed integers`, () => { 7 | assert.equal(Core.readScriptInt32(Buffer.from('ffffff7f', 'hex')), 2147483647); 8 | assert.equal(Core.readScriptInt32(Buffer.from('ffffffff', 'hex')), -2147483647); 9 | assert.equal(Core.readScriptInt32(Buffer.from('ffffff8f', 'hex')), -268435455); 10 | assert.equal(Core.readScriptInt32(Buffer.from('ffffff3f', 'hex')), 1073741823); 11 | assert.equal(Core.readScriptInt32(Buffer.from('ffffffbf', 'hex')), -1073741823); 12 | assert.equal(Core.readScriptInt32(Buffer.from('00000000', 'hex')), 0); 13 | assert.equal(Core.readScriptInt32(Buffer.from('00000080', 'hex')), 0); 14 | assert.equal(Core.readScriptInt32(Buffer.from('01000000', 'hex')), 1); 15 | assert.equal(Core.readScriptInt32(Buffer.from('01000080', 'hex')), -1); 16 | }); 17 | }); 18 | 19 | describe('#checkSecretNumbers', () => { 20 | it(`check validity of secret numbers`, () => { 21 | var secret = Buffer.from('ffffff7f', 'hex'); 22 | assert.equal(Core.secretIsValid(secret), false); 23 | 24 | var secret = Buffer.from('ffffffff', 'hex'); 25 | assert.equal(Core.secretIsValid(secret), false); 26 | 27 | var secret = Buffer.from('ffffff3f', 'hex'); 28 | assert.equal(Core.secretIsValid(secret), true); 29 | 30 | var secret = Buffer.from('ffffff8f', 'hex'); 31 | assert.equal(Core.secretIsValid(secret), true); 32 | 33 | var secret = Buffer.from('ffffffbf', 'hex'); 34 | assert.equal(Core.secretIsValid(secret), true); 35 | 36 | var secret = Buffer.from('00000000', 'hex'); 37 | assert.equal(Core.secretIsValid(secret), true); 38 | 39 | var secret = Buffer.from('00000080', 'hex'); 40 | assert.equal(Core.secretIsValid(secret), true); 41 | 42 | var secret = Buffer.from('01000000', 'hex'); 43 | assert.equal(Core.secretIsValid(secret), true); 44 | 45 | var secret = Buffer.from('01000080', 'hex'); 46 | assert.equal(Core.secretIsValid(secret), true); 47 | }); 48 | }); -------------------------------------------------------------------------------- /code/src/host.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | let base58 = require('bs58'); 5 | import { Utils } from './utils'; 6 | 7 | export class Host { 8 | constructor(){} 9 | 10 | // Phase 1: Bet Offer Announcement 11 | static encodePhase1Message(amount: number, hostCommitment: Buffer, targetAddress?: string): Buffer { 12 | 13 | let script = [ 14 | BITBOX.Script.opcodes.OP_RETURN, 15 | // 4 byte prefix 16 | Buffer.from('00424554', 'hex'), 17 | // 1 byte version id / 1 betType byte / 1 phase byte 18 | Buffer.from('010101', 'hex'), 19 | // add 8 byte amount 20 | Utils.amount_2_hex(amount), 21 | // add 20 byte host commitment 22 | hostCommitment 23 | ]; 24 | 25 | // add optional 20 byte target address (encode in HASH160 hexidecimal form) 26 | if(targetAddress != undefined) { 27 | 28 | if(BITBOX.Address.isLegacyAddress(targetAddress)) { 29 | // do nothing 30 | } else if(BITBOX.Address.isCashAddress(targetAddress)){ 31 | // convert to legacy address 32 | targetAddress = BITBOX.Address.toLegacyAddress(targetAddress); 33 | } else 34 | throw new Error("Unsupported address format provided"); 35 | 36 | // convert from legacy address to binary 37 | var addrBuf = Buffer.from(base58.decode(targetAddress), 'hex'); 38 | 39 | // chop off network byte and 4 checksum bytes 40 | let hash160 = addrBuf.slice(1,21); 41 | script.push(hash160); 42 | } 43 | 44 | let encoded = BITBOX.Script.encode(script); 45 | //let asm = BITBOX.Script.toASM(encoded); 46 | return encoded; 47 | } 48 | 49 | // Phase 3: Bet Host Funding 50 | static encodePhase3Message(betId: string, participantTxId: string, hostP2SHTxId: string, hostMultisigPubKey: Buffer): Buffer { 51 | 52 | let script = [ 53 | BITBOX.Script.opcodes.OP_RETURN, 54 | // 4 byte prefix 55 | Buffer.from('00424554', 'hex'), 56 | // 1 byte version id / 1 betType byte / 1 phase byte 57 | Buffer.from('010103', 'hex'), 58 | // 32 byte bet tx id 59 | Buffer.from(betId, 'hex'), 60 | // 32 byte participant tx id 61 | Buffer.from(participantTxId, 'hex'), 62 | // 32 byte host P2SH id 63 | Buffer.from(hostP2SHTxId, 'hex'), 64 | // 33 byte host (Alice) Multisig Pub Key 65 | hostMultisigPubKey 66 | ]; 67 | 68 | return BITBOX.Script.encode(script) 69 | } 70 | } -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | ## Node.js implementation of the Bitcoin Cash ChainBet protocol 2 | 3 | This repo contains a node.js implementation of the ChainBet protocol built using TypeScript. The specification of the ChainBet protocol is here: [https://github.com/fyookball/ChainBet](https://github.com/fyookball/ChainBet). An example program (dice.js) is provided to demonstrate how to use the `chainbet` npm package. Instructions for compiling the chainbet npm package from TypeScript source are also provided. 4 | 5 | ### dice.js Main Menu 6 | 7 | ![Main Menu](https://github.com/deskviz/chainbet/blob/master/examples/dice/images/main_menu.png?raw=true) 8 | 9 | ## Running dice.js example 10 | 11 | 1. install node.js (v8.11.3 or later) 12 | 2. `git clone https://github.com/jcramer/chainbet` 13 | 3. `cd chainbet/examples/dice` 14 | 4. `npm install` 15 | 6. `node dice.js` 16 | 17 | ## Compiling TypeScript Source for npm package 18 | 19 | The following dice.js example shows a simple command-line program which facilitates a trustless p2p dice games using the ChainBet npm package. Running this example requires that at least one player is already running the program in "client mode" before another player uses "host mode" to announce a coin flip bet wager. 20 | 21 | 1. install node.js (v8.11.3 or later) 22 | 2. `git clone https://github.com/jcramer/chainbet` 23 | 3. `cd chainbet` 24 | 4. `npm install` 25 | 5. `npm run build` 26 | 6. Verify the libs directory was created with output js and d.ts files. 27 | 28 | ### Dice Winner 29 | 30 | ![Dice Winner](https://github.com/deskviz/chainbet/blob/master/examples/dice/images/Coin%20Flip%20Winner.png?raw=true) 31 | 32 | ### Dice Loser 33 | 34 | ![Dice Loser](https://github.com/deskviz/chainbet/blob/master/examples/dice/images/Coin%20Flip%20Loser.png?raw=true) 35 | 36 | ## Dev Usage 37 | 38 | Install: `npm install chainbet` 39 | 40 | ```js 41 | let chainbet = require('chainbet'); 42 | 43 | // 1) Create Script Buffer object for any phase 44 | chainbet.Host.encodePhase1Message(1000, 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'); 45 | // 46 | 47 | // 2) Decode Script Hex for any ChainBet phase 48 | let scriptHex = Buffer('01010100000000000003e81111111111111111111111111111111111111111a0f531f4ff810a415580c12e54a7072946bb927e'); 49 | chainbet.Core.decodePhaseData(scriptHex); 50 | 51 | // { phase: 1, 52 | // type: 1, 53 | // amount: 1000, 54 | // hostCommitment: 11111111111111111111 55 | // address: 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' } 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /code/src/utils.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | let node_crypto = require('crypto'); 5 | let base58 = require('bs58'); 6 | 7 | export class Utils { 8 | 9 | static getNewPrivKeyWIF() { 10 | 11 | var wif: string; 12 | var n = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 13 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 14 | 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 15 | 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41]); 16 | 17 | var isValid = false; 18 | var pk = new Buffer(0); 19 | 20 | while (!isValid) { 21 | pk = BITBOX.Crypto.randomBytes(32); 22 | 23 | if(Buffer.compare(n, pk)){ 24 | isValid = true; 25 | } 26 | } 27 | 28 | // add 0x01 to priv key for WIF-compressed format 29 | pk = Buffer.concat([pk, Buffer.from('01', 'hex')]) 30 | 31 | // add wif-compressed version prefix (0x80) before calculating checksum 32 | let preHash = Buffer.concat([ Buffer.from('80', 'hex'), pk ]); 33 | 34 | // get hash and append 4 byte checksum 35 | let hash1 = node_crypto.createHash('sha256'); 36 | let hash2 = node_crypto.createHash('sha256'); 37 | hash1.update(preHash); 38 | hash2.update(hash1.digest()); 39 | let checksum = hash2.digest().slice(0,4); 40 | let wifBuf = Buffer.concat([preHash, checksum]); 41 | 42 | // get base58 encoded 43 | wif = base58.encode(wifBuf); 44 | 45 | return wif; 46 | } 47 | 48 | // get big-endian hex from satoshis 49 | static amount_2_hex(amount: number): Buffer { 50 | var hex = amount.toString(16) 51 | const len = hex.length 52 | for (let i = 0; i < 16 - len; i++) { 53 | hex = '0' + hex; 54 | } 55 | let buf = Buffer.from(hex, 'hex'); 56 | return buf 57 | } 58 | 59 | static hash160_2_cashAddr(pkHash160: Buffer, networkByte: number): string { 60 | // handle the network byte prefix 61 | let pkHash160Hex = pkHash160.toString('hex'); 62 | let networkHex = Buffer.from([networkByte]).toString('hex'); 63 | 64 | // calculate checksum and 65 | // add first 4 bytes from double sha256 66 | let hash1 = node_crypto.createHash('sha256'); 67 | let hash2 = node_crypto.createHash('sha256'); 68 | hash1.update(Buffer.from(networkHex + pkHash160Hex, 'hex')); 69 | hash2.update(hash1.digest()); 70 | let checksum = hash2.digest().slice(0,4).toString('hex'); 71 | let addressBuf = Buffer.from(networkHex + pkHash160Hex + checksum, 'hex') 72 | let hex = addressBuf.toString('hex') 73 | let addressBase58 = base58.encode(addressBuf) 74 | 75 | return BITBOX.Address.toCashAddress(addressBase58); 76 | } 77 | 78 | static sleep(ms: number) { 79 | return new Promise(resolve => setTimeout(resolve, ms)); 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /code/src/messagefeed.ts: -------------------------------------------------------------------------------- 1 | import * as core from './core'; 2 | import { Utils } from './utils'; 3 | import { Chainfeed } from './chainfeed'; 4 | 5 | interface FeedState { 6 | connected: boolean; 7 | } 8 | 9 | export class MessageFeed { 10 | messages: (core.Phase1Data|core.Phase2Data|core.Phase3Data|core.Phase4Data|core.Phase6Data)[]; 11 | feedState: FeedState; 12 | //chainfeed: Chainfeed; 13 | 14 | constructor(){ 15 | this.messages = []; 16 | this.feedState = { connected: false }; 17 | //this.chainfeed = new Chainfeed(); 18 | this.listen(); 19 | } 20 | 21 | async listen(){ 22 | Chainfeed.listen(MessageFeed.onData(this.messages), 23 | MessageFeed.onConnect(this.feedState), 24 | MessageFeed.onDisconnect(this.feedState)); 25 | } 26 | 27 | async checkConnection(){ 28 | while(!this.feedState.connected){ 29 | console.log("[MessageFeed] Connecting...") 30 | await Utils.sleep(1000); 31 | } 32 | return 33 | } 34 | 35 | static onData(messages: (core.Phase1Data|core.Phase2Data|core.Phase3Data|core.Phase4Data|core.Phase6Data)[]) { 36 | return function(res: any) { 37 | 38 | //console.log("New transaction found in mempool! = ", res) 39 | 40 | let txs; 41 | if (res.block) 42 | txs = res.reduce((prev: any, cur: any) => [...prev, ...cur], []) 43 | else 44 | txs = res 45 | 46 | for(let tx of txs) { 47 | 48 | if (!tx.data || !tx.data[0].buf || !tx.data[0].buf.data) return 49 | let protocol = Buffer.from(tx.data[0].buf.data).toString('hex').toLowerCase() 50 | if (protocol == '00424554' || protocol == '424554') { 51 | 52 | //let chainbetBuf = Buffer.from(tx.data[1].buf.data); 53 | 54 | var fields:(any)[] = []; 55 | tx.data.forEach((item: any, index: any) => { 56 | if(index > 0) 57 | fields.push(Buffer.from(item.buf.data)); 58 | }); 59 | 60 | let decodedBet = core.Core.decodePhaseData(fields); 61 | 62 | decodedBet.op_return_txnId = tx.tx.hash 63 | //console.log('[MessageFeed] Txn id: ' + tx.tx.hash); 64 | 65 | messages.push(decodedBet); 66 | } 67 | } 68 | } 69 | } 70 | 71 | static onConnect(feedState: FeedState){ 72 | return function(e: MessageEvent){ 73 | feedState.connected = true; 74 | console.log("[MessageFeed] Connected."); 75 | } 76 | } 77 | 78 | static onDisconnect(feedState: FeedState){ 79 | return function(e: MessageEvent){ 80 | feedState.connected = false; 81 | console.log("[MessageFeed] Disconnected."); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /AUCTION.md: -------------------------------------------------------------------------------- 1 | # AUCTION 2 | 3 | A trustless version of an open, ascending price auction can be constructed on the BCH blockchain. In this context we define trustless to mean that A) the seller is guaranteed to receive the funds from the highest bidder, and B) Only the final, highest paying bidder will be paying; all losing, lower bids will be guaranteed to be returned. (Note that in this context trustless does not mean the seller is guaranteed to physically deliver the goods being purchased.) 4 | 5 | This is accomplished using a set of secrets. Each time someone is outbid, a secret is revealed that releases funds back to the losing bidder while the next secret is created for the new high bidder. 6 | 7 | ## OPENING BID 8 | 9 | Alice creates the opening bid by sending funds to a P2SH script that can be unlocked using: 10 | 11 | a) Seller's key AND secret A , where hash(A)= HASH_A, with a timelock encumberance till the end of the auction 12 | 13 | or: 14 | 15 | b) Alice's key AND secret A , where hash(A)= HASH_A. 16 | 17 | After the opening bid, the Seller is guaranteed to receive Alice's money if there are no more bids. In other words, if he does nothing but wait for the timelock to expire. If he reveals secret A prior to that, Alice can take the funds back. 18 | 19 | ## SUBSEQUENT BIDS 20 | 21 | Typically there are more bids. For example, Bob wants to outbid Alice with a higher bid. 22 | 23 | Bob will create a similar P2SH to the opening bid, but first will move his funds to a temporary escrow address. 24 | 25 | **Bob Temporary Escrow** 26 | 27 | can be unlocked with: 28 | 29 | a) Seller key AND secret A , where hash(A)= HASH_A (revealing Alice's secret) 30 | 31 | OR 32 | 33 | b. by Bob back to himself after a short time timelock. 34 | 35 | Normally, once the money is in the escrow address, the seller will send the money to the normal "Bob Main P2SH", revealing Alice's secret that allows her to take her money back while at the same time securing Bob's bid. 36 | 37 | **Bob Main P22H** 38 | 39 | can be unlocked using: 40 | 41 | a) Seller's key AND secret B , where hash(B)= HASH_B with a timelock encumberance till the end of the auction 42 | 43 | or: 44 | 45 | b) Bob's key AND secret B , where hash(B)= HASH_B. 46 | 47 | The process would repeat for Carol, who outbids Bob. She would set up a temporary escrow address that would then reveal Bob's secret while funds are transferred to the main Carol auction address, and so on. 48 | 49 | Finally, when the auction ends, the Seller would claim the funds in the last address used. 50 | 51 | ## Notes 52 | 53 | 1. When sending the money from the "Bob temporary escrow" to the "Bob Main P2SH", Bob's signature is not required, which prevents the case of Alice's secret being leaked (allowing her to cancel the bid) while Bob also cancels his bid. 54 | 55 | 2. In the case when the Seller does not promptly send the funds from the escrow address to the Bob Main P2SH, Bob should claim the funds back before the auction ends to prevent the seller from obtaining both Alice and Bob's funds. 56 | 57 | 3. The extra step of creating an escrow address prevents a double spend attack where the Seller reveals Alice's secret at the same time as he accepts Bob's bid, only to have Bob's bid be doublespent. 58 | 59 | 4. Note that it would be possible to assemble a hash chain where each secret revealed links to the next: Alice's secret A when revealed would be equal to hash(B). B, when revealed would be equal to hash(C), etc. But there is no immediately obvious benefit. Each secret can be unrelated to the others. 60 | 61 | ## Authors: 62 | 63 | Jonald Fyookball 64 | 65 | -------------------------------------------------------------------------------- /code/src/wallet.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | 5 | import { Core } from './core'; 6 | import { AddressDetailsResult } from 'bitbox-cli/lib/Address'; 7 | 8 | export class Wallet { 9 | 10 | static async listAddressDetails(wallet: any): Promise{ 11 | for (let i = 0; i < wallet.length; i++) { 12 | let ecpair = BITBOX.ECPair.fromWIF(wallet[i].wif); 13 | let address = BITBOX.ECPair.toCashAddress(ecpair); 14 | console.log("\nChecking " + address + "..."); 15 | let details = await Core.getAddressDetailsWithRetry(address); 16 | console.log("balance (sat): " + (details.balanceSat + details.unconfirmedBalanceSat)); 17 | console.log("Unconfirmed Txns: " + details.unconfirmedTxApperances); 18 | } 19 | } 20 | 21 | static async selectViableWIF(wallet: any[]): Promise { 22 | 23 | for (let i = 0; i < wallet.length; i++) { 24 | 25 | let ecpair = BITBOX.ECPair.fromWIF(wallet[i].wif); 26 | let address = BITBOX.ECPair.toCashAddress(ecpair); 27 | console.log("\nChecking " + address + "..."); 28 | let details = await Core.getAddressDetailsWithRetry(address); 29 | console.log("balance (sat): " + (details.balanceSat + details.unconfirmedBalanceSat)); 30 | console.log("Unconfirmed Txns: " + details.unconfirmedTxApperances); 31 | 32 | if(details.unconfirmedTxApperances < 15 && (details.balanceSat + details.unconfirmedBalanceSat > 3000)) { 33 | return wallet[i].wif; 34 | } 35 | } 36 | 37 | throw new Error("No viable WIF found in wallet."); 38 | } 39 | 40 | static async sweepToAddress(wallet: any, destinationAddress: string): Promise { 41 | 42 | for (let i = 0; i < wallet.length; i++) { 43 | let ecpair = BITBOX.ECPair.fromWIF(wallet[i].wif); 44 | let address = BITBOX.ECPair.toCashAddress(ecpair); 45 | console.log("Checking address: " + address + "..."); 46 | wallet[i].utxo = await Core.getUtxoWithRetry(address); 47 | 48 | //return new Promise( (resolve, reject) => { 49 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 50 | let hashType = transactionBuilder.hashTypes.SIGHASH_ALL; 51 | 52 | let totalUtxo = 0; 53 | wallet[i].utxo.forEach((item: any, index: any) => { 54 | transactionBuilder.addInput(item.txid, item.vout); 55 | totalUtxo += item.satoshis; 56 | }); 57 | 58 | if(totalUtxo > 0) { 59 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: wallet[i].utxo.length }, { P2SH: 0 }) + 50; 60 | let satoshisAfterFee = totalUtxo - byteCount; 61 | 62 | // let p2sh_hash160 = BITBOX.Crypto.hash160(script); 63 | // let p2sh_hash160_hex = p2sh_hash160.toString('hex'); 64 | // let scriptPubKey = BITBOX.Script.scriptHash.output.encode(p2sh_hash160); 65 | 66 | //let escrowAddress = BITBOX.Address.toLegacyAddress(BITBOX.Address.fromOutputScript(scriptPubKey)); 67 | //let changeAddress = BITBOX.Address.toLegacyAddress(destinationAddress); 68 | // console.log("escrow address: " + address); 69 | // console.log("change satoshi: " + satoshisAfterFee); 70 | // console.log("change bet amount: " + betAmount); 71 | 72 | //transactionBuilder.addOutput(escrowAddress, betAmount); 73 | transactionBuilder.addOutput(destinationAddress, satoshisAfterFee); 74 | //console.log("Added escrow outputs..."); 75 | 76 | //let key = BITBOX.ECPair.fromWIF(wallet.wif); 77 | 78 | let redeemScript: Buffer; 79 | wallet[i].utxo.forEach((item: any, index: any) => { 80 | transactionBuilder.sign(index, ecpair, redeemScript, hashType, item.satoshis); 81 | }); 82 | //console.log("signed escrow inputs..."); 83 | 84 | let hex = transactionBuilder.build().toHex(); 85 | //console.log("built escrow..."); 86 | 87 | let txId = await Core.sendRawTransaction(hex); 88 | console.log("SENT " + totalUtxo + " FROM " + address + " TO " + destinationAddress); 89 | console.log("(txn: " + txId + ")"); 90 | } 91 | else { 92 | console.log("No funds at " + address); 93 | } 94 | } 95 | console.log("Done withdrawing funds from wallet"); 96 | } 97 | 98 | static async checkSufficientBalance(address: string) { 99 | let addrDetails = await Core.getAddressDetailsWithRetry(address); 100 | 101 | if ((addrDetails.unconfirmedBalanceSat <= 0 && addrDetails.balanceSat == 0) || 102 | (addrDetails.unconfirmedBalanceSat + addrDetails.balanceSat == 0)) { 103 | console.log("\nThe address provided has a zero balance... please add funds to this address."); 104 | return false; 105 | } 106 | 107 | console.log("\nconfirmed balance (sat): " + addrDetails.balanceSat); 108 | console.log("unconfirmed balance (sat): " + addrDetails.unconfirmedBalanceSat); 109 | return true; 110 | 111 | } 112 | 113 | static async getConfirmedAndUnconfirmedAddressBalance(address: string){ 114 | let addrDetails = await Core.getAddressDetailsWithRetry(address); 115 | let total = addrDetails.balanceSat + addrDetails.unconfirmedBalanceSat; 116 | return total; 117 | } 118 | } -------------------------------------------------------------------------------- /code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": [ "es2015", "dom" ], /* Specify library files to be included in the compilation. */ 7 | //"allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | //"outFile": "chainbet.js", /* Concatenate and emit output to single file. */ 14 | "outDir": "lib", /* Redirect output structure to the directory. */ 15 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | 17 | // "composite": true, /* Enable project compilation */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "include": [ 61 | "src/**/*" 62 | ], 63 | "exclude": [ 64 | "./examples" 65 | ] 66 | } -------------------------------------------------------------------------------- /code/examples/dice/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": [ "es2015", "dom" ], /* Specify library files to be included in the compilation. */ 7 | //"allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | //"declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | //"outDir": "lib", /* Redirect output structure to the directory. */ 15 | //"rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | 17 | // "composite": true, /* Enable project compilation */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | } 60 | } -------------------------------------------------------------------------------- /DICE_ROLL.md: -------------------------------------------------------------------------------- 1 | # DICE ROLL (spec version 0.1) 2 | 3 | ## Introduction 4 | Extending the ChainBet protocol to go beyond a simple coin flip can begin with dice. Games like craps offer many different kinds of bets. Here we will focus on a simple one-time dice roll. For example, a standard 6-sided die could be rolled and Bob could choose a number at random, say "5". Alice could then offer Bob bet odds at a 6:1 payout. Bob can wager 1 BCH and if a 5 is rolled, he will collect 6 BCH from Alice. If he loses, Alice will collect his 1 BCH. 5 | 6 | ## Protocol Extension 7 | 8 | We can extend the base protocol by introducing a new bet type designating a dice roll. It can configured to allow: 9 | 10 | * different types ('cardinalities') of dice: 6-sided, 12-sided, 20-sided, etc 11 | * different betting odds 12 | * different roles (giving or taking odds) 13 | 14 | The changes are fairly modest: 15 | 16 | 1. Alice needs to advertise the bet differently, specifying the parameters of the bet. 17 | 2. The participants need to fund the bet according to the parameters chosen. 18 | 3. The main betting script should contain changes to handle the bet odds. 19 | 20 | # Phase 1: Bet Offer Announcement 21 | 22 | This phase is modified from the base protocol. Here we use a different bet type (0x02) instead of the coin flip (0x01). The presense of a different bet type will effect the "various_phase_dependent_data" that follows. In other words, the phase dependent data is not only dependent on the phase, but also on the bet type. 23 | 24 | ## Giving or Taking Odds 25 | 26 | Alice could choose to either give odds (pay a bet multiple to Bob if he successfully guesses the outcome of a multi-sided dice roll), or take odds (the roles would reverse: Alice would win the multiple if Bob loses -- if his guess is unluckily rolled). 27 | 28 | Alice can also choose the number of sides of the virtual die. 29 | 30 | Since giving or taking odds is a binary decision, it would be a waste of space to consume an entire byte. Thus, we shall combine the "role" field (giving or taking odds) with the "number of sides of the die" in the same byte, with the most significant bit signaling the role, and the least significant 7 bits designating the number of sides. 31 | 32 | A value of 0 for the "role" indicates Alice is giving odds to Bob (Bob wagers 1 BCH to win 6 BCH) and a value 1 indicates Alice is taking odds. 7 bits for the number of sides allows up to a 128-sided die. 33 | 34 | Bob can also guess the actual outcome (more detail on this in the "Funding Transaction" section below). 35 | 36 | ## Bet Odds, Fairness, and Liquidity 37 | 38 | The next 2 fields in the payload designate payout and payIn amounts. Rather than having a single amount, we need two fields since this is an assymetrical bet. **Note that the amounts do NOT need to correspond to fair probabilities.** If we always wanted a fair bet, only a single amount field would be needed. 39 | 40 | However, by providing the flexibility to give slightly less-than-fair bets, it incentivizes liquidity to enter the system. This is analogous to how a market maker profits from a spread in a speculative marketplace. Therefore, it is essential for implementations to check and handle fairness parameters in a way that makes sense for their users. 41 | 42 | OP_RETURN OUTPUT: 43 | 44 | | Bytes | Name | Hex Value | Description | 45 | | ------------- |-------------| -----|-----------------| 46 | | 1 | Phase | 0x01 | Phase 1 is "Bet Offer Announcement" | 47 | | 1 | Bet Type | 0x02 | Denotes what kind of bet will be contructed. 0x02 for Dice Roll. | 48 | | 1 | Role & Sides | \ | Most significant bit designates who is giving odds; the least significant 7 bits designates the number of sides to the die | 49 | | 1 | Guess | \ | Bob's guess at the outcome of the roll | 50 | | 8 | Payout Amount | \ | Payout amount in Satohis for the bet host (Alice). | 51 | | 8 | PayIn Amount | \ | PayIn amount in Satohis for the bet participant (Bob). | 52 | | 20 | Target Address | \ | Optional. Restricts offer to a specific bet participant. | 53 | 54 | # Funding Transaction 55 | 56 | In the base protocol (coin flip), the bet outcome is deteremined from the sum of 2 random secrets. Here is no different. But instead of merely picking odd or even, the result is based on the remainder of a modulo operation, where the divisor is the number of sides of the die. 57 | 58 | It should be fairly clear why this works: Iterating the modulo operation over a divisor and a set of adjacent dividends produces a simple repeating sequence of integer values, and since the secrets being used are far larger than the set of possible values in the sequence, there is an essentially equal probability of choosing any particular number in the set. (This is actually the same algorithm as the coin flip, with the divisor always being 2). 59 | 60 | Mathematically: 61 | 62 | ![P (k' mod n = m) -> 1/n as k approaches infinity.](https://raw.githubusercontent.com/fyookball/ChainBet/master/images/68747470733a2f2f692e696d6775722e636f6d2f6d424c59746e432e706e67.png) 63 | 64 | 65 | The construction of the Bitcoin script can simply plug in the value (number of sides) desired. 66 | 67 | ## Guessing the Outcome 68 | 69 | Although the "result of a dice roll" is already an abstraction derived from large secret numbers, it is fun for players to guess their lucky numbers. It is therefore valuable to include a way for Bob to choose the number he wants. This is done with the "guess" field in the payload, and needs to be implemented in the script, with an IF-ELSE path. If the guess matches the remainder (plus one), the participant taking odds wins the bet, otherwise he/she loses. 70 | 71 | ## Authors: 72 | 73 | Jonald Fyookball 74 | -------------------------------------------------------------------------------- /code/src/coinflipshared.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | import { Core, WalletKey } from './core'; 5 | 6 | export class CoinFlipShared { 7 | 8 | static buildCoinFlipBetScriptBuffer(hostPubKey: Buffer, 9 | hostCommitment: Buffer, clientPubKey: Buffer, clientCommitment: Buffer): Buffer{ 10 | 11 | let script = [ 12 | BITBOX.Script.opcodes.OP_IF, 13 | BITBOX.Script.opcodes.OP_IF, 14 | BITBOX.Script.opcodes.OP_DUP, 15 | BITBOX.Script.opcodes.OP_HASH160, 16 | clientCommitment.length 17 | ]; 18 | 19 | clientCommitment.forEach(i => script.push(i)); 20 | 21 | script = script.concat([ 22 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 23 | BITBOX.Script.opcodes.OP_OVER, 24 | BITBOX.Script.opcodes.OP_HASH160, 25 | hostCommitment.length 26 | ]); 27 | hostCommitment.forEach(i => script.push(i)); 28 | 29 | script = script.concat([ 30 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 31 | BITBOX.Script.opcodes.OP_4, 32 | BITBOX.Script.opcodes.OP_SPLIT, 33 | BITBOX.Script.opcodes.OP_DROP, 34 | BITBOX.Script.opcodes.OP_BIN2NUM, 35 | BITBOX.Script.opcodes.OP_SWAP, 36 | BITBOX.Script.opcodes.OP_4, 37 | BITBOX.Script.opcodes.OP_SPLIT, 38 | BITBOX.Script.opcodes.OP_DROP, 39 | BITBOX.Script.opcodes.OP_BIN2NUM, 40 | BITBOX.Script.opcodes.OP_ADD, 41 | BITBOX.Script.opcodes.OP_ABS, 42 | BITBOX.Script.opcodes.OP_2, 43 | BITBOX.Script.opcodes.OP_MOD, 44 | BITBOX.Script.opcodes.OP_1, 45 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 46 | BITBOX.Script.opcodes.OP_ELSE, 47 | 0x54, // use 0x54 for 4 blocks 48 | BITBOX.Script.opcodes.OP_CHECKSEQUENCEVERIFY, 49 | BITBOX.Script.opcodes.OP_DROP, 50 | BITBOX.Script.opcodes.OP_ENDIF, 51 | hostPubKey.length 52 | ]); 53 | 54 | hostPubKey.forEach(i => script.push(i)); 55 | 56 | script = script.concat([ 57 | BITBOX.Script.opcodes.OP_CHECKSIG, 58 | BITBOX.Script.opcodes.OP_ELSE, 59 | BITBOX.Script.opcodes.OP_DUP, 60 | BITBOX.Script.opcodes.OP_HASH160, 61 | clientCommitment.length 62 | ]); 63 | clientCommitment.forEach(i => script.push(i)); 64 | 65 | script = script.concat([ 66 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 67 | BITBOX.Script.opcodes.OP_OVER, 68 | BITBOX.Script.opcodes.OP_HASH160, 69 | hostCommitment.length 70 | ]); 71 | hostCommitment.forEach(i => script.push(i)); 72 | 73 | script = script.concat([ 74 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 75 | BITBOX.Script.opcodes.OP_4, 76 | BITBOX.Script.opcodes.OP_SPLIT, 77 | BITBOX.Script.opcodes.OP_DROP, 78 | BITBOX.Script.opcodes.OP_BIN2NUM, 79 | BITBOX.Script.opcodes.OP_SWAP, 80 | BITBOX.Script.opcodes.OP_4, 81 | BITBOX.Script.opcodes.OP_SPLIT, 82 | BITBOX.Script.opcodes.OP_DROP, 83 | BITBOX.Script.opcodes.OP_BIN2NUM, 84 | BITBOX.Script.opcodes.OP_ADD, 85 | BITBOX.Script.opcodes.OP_2, 86 | BITBOX.Script.opcodes.OP_MOD, 87 | BITBOX.Script.opcodes.OP_0, 88 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 89 | clientPubKey.length 90 | ]); 91 | clientPubKey.forEach(i => script.push(i)); 92 | 93 | script = script.concat([ 94 | BITBOX.Script.opcodes.OP_CHECKSIG, 95 | BITBOX.Script.opcodes.OP_ENDIF, 96 | ]); 97 | 98 | return BITBOX.Script.encode(script); 99 | } 100 | 101 | static buildCoinFlipHostEscrowScript(hostPubKey: Buffer, hostCommitment: Buffer, clientPubKey: Buffer): Buffer{ 102 | 103 | let script = [ 104 | BITBOX.Script.opcodes.OP_IF, 105 | BITBOX.Script.opcodes.OP_HASH160, 106 | hostCommitment.length 107 | ]; 108 | 109 | hostCommitment.forEach(i => script.push(i)); 110 | 111 | script = script.concat([ 112 | BITBOX.Script.opcodes.OP_EQUALVERIFY, 113 | BITBOX.Script.opcodes.OP_2, 114 | hostPubKey.length 115 | ]); 116 | 117 | hostPubKey.forEach(i => script.push(i)); 118 | script.push(clientPubKey.length); 119 | clientPubKey.forEach(i => script.push(i)); 120 | 121 | script = script.concat([ 122 | BITBOX.Script.opcodes.OP_2, 123 | BITBOX.Script.opcodes.OP_CHECKMULTISIG, 124 | BITBOX.Script.opcodes.OP_ELSE, 125 | 0x58, // use 0x58 for 8 blocks 126 | BITBOX.Script.opcodes.OP_CHECKSEQUENCEVERIFY, 127 | BITBOX.Script.opcodes.OP_DROP, 128 | hostPubKey.length 129 | ]); 130 | 131 | hostPubKey.forEach(i => script.push(i)); 132 | script = script.concat([ 133 | BITBOX.Script.opcodes.OP_CHECKSIG, 134 | BITBOX.Script.opcodes.OP_ENDIF 135 | ]); 136 | 137 | return BITBOX.Script.encode(script); 138 | } 139 | 140 | static buildCoinFlipClientEscrowScript(hostPubKey: Buffer, clientPubKey: Buffer): Buffer{ 141 | let script = [ 142 | BITBOX.Script.opcodes.OP_IF, 143 | BITBOX.Script.opcodes.OP_2, 144 | hostPubKey.length 145 | ] 146 | 147 | hostPubKey.forEach(i => script.push(i)); 148 | script.push(clientPubKey.length); 149 | clientPubKey.forEach(i => script.push(i)); 150 | 151 | script = script.concat([ 152 | BITBOX.Script.opcodes.OP_2, 153 | BITBOX.Script.opcodes.OP_CHECKMULTISIG, 154 | BITBOX.Script.opcodes.OP_ELSE, 155 | 0x58, // use 0x58 for 8 blocks 156 | BITBOX.Script.opcodes.OP_CHECKSEQUENCEVERIFY, 157 | BITBOX.Script.opcodes.OP_DROP, 158 | clientPubKey.length 159 | ]); 160 | 161 | clientPubKey.forEach(i => script.push(i)); 162 | script = script.concat([ 163 | BITBOX.Script.opcodes.OP_CHECKSIG, 164 | BITBOX.Script.opcodes.OP_ENDIF 165 | ]); 166 | 167 | return BITBOX.Script.encode(script); 168 | } 169 | 170 | static createEscrowSignature(wallet: WalletKey, escrowTxId: string, escrowScript: Buffer, betAmount: number, betScript: Buffer): Buffer{ 171 | let clientKey = BITBOX.ECPair.fromWIF(wallet.wif) 172 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 173 | 174 | let hashType = 0xc1 // transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY | transactionBuilder.hashTypes.SIGHASH_ALL 175 | let satoshisAfterFee = Core.purseAmount(betAmount); 176 | transactionBuilder.addInput(escrowTxId, 0); // No need to worry about sweeping the P2SH address. 177 | 178 | // Determine bet address 179 | let p2sh_hash160 = BITBOX.Crypto.hash160(betScript); 180 | let scriptPubKey = BITBOX.Script.scriptHash.output.encode(p2sh_hash160); 181 | let betAddress = BITBOX.Address.fromOutputScript(scriptPubKey) 182 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(betAddress), satoshisAfterFee); 183 | 184 | let tx = transactionBuilder.transaction.buildIncomplete(); 185 | 186 | // Sign escrow utxo 187 | let sigHash: number = tx.hashForWitnessV0(0, escrowScript, betAmount, hashType); 188 | let sig: Buffer = clientKey.sign(sigHash).toScriptSignature(hashType); 189 | return sig; 190 | } 191 | } -------------------------------------------------------------------------------- /code/examples/dice/dice.ts: -------------------------------------------------------------------------------- 1 | import * as chainbet from 'chainbet'; 2 | 3 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 4 | let BITBOX = new BITBOXCli(); 5 | 6 | import * as fs from 'fs'; 7 | let context = require('commander'); 8 | import * as inquirer from 'inquirer'; 9 | import * as jsonfile from 'jsonfile'; 10 | 11 | context.version('0.0.13') 12 | .option('-m, --mode [mode]', 'set program mode to bypass initial prompt') 13 | .option('-d, --debug [debug]', 'set debugger support (skips user prompts with default values)') 14 | .parse(process.argv); 15 | 16 | context.debug = (context.debug == "1" ? true : false); 17 | 18 | async function main() { 19 | while(true){ 20 | var wallet; 21 | 22 | // present main menu to user 23 | let selection: any = await promptMainMenu(); 24 | 25 | // check if wallet.json file exists 26 | if(fs.existsSync('./wallet.json')) { 27 | if (selection.mode == 'generate') { 28 | console.log("\nGenerating a new bitcoin address...\n"); 29 | var wif = chainbet.Utils.getNewPrivKeyWIF(); 30 | wallet = jsonfile.readFileSync('./wallet.json'); 31 | wallet.push({ 'wif' : wif }); 32 | jsonfile.writeFileSync("./wallet.json", wallet, 'utf8'); 33 | } 34 | } 35 | 36 | wallet = jsonfile.readFileSync('./wallet.json'); 37 | // context.wif = wallet[wallet.length - 1].wif; 38 | // let ecpair = BITBOX.ECPair.fromWIF(context.wif); 39 | // context.pubkey = BITBOX.ECPair.toPublicKey(ecpair); 40 | // context.address = BITBOX.ECPair.toCashAddress(ecpair); 41 | // console.log("\nYour address is: " + context.address); 42 | 43 | // Use chainfeed.org for OP_RETURN messages 44 | let chainfeed = new chainbet.MessageFeed(); 45 | await chainfeed.checkConnection(); 46 | 47 | var wif: string = ""; 48 | 49 | // Startup a single bet workflow 50 | if (selection.mode == 'host'){ 51 | // select appropriate private key for bet 52 | try { 53 | wif = await chainbet.Wallet.selectViableWIF(wallet); 54 | } catch(e) { 55 | console.log("\nNo viable addresses to use, please add funds or wait for 1 confirmation."); 56 | } 57 | 58 | if(wif != "") 59 | { 60 | var betAmount: number = 1500; 61 | if(!context.debug){ 62 | console.log('\n'); 63 | let answer: any = await inquirer.prompt([{ 64 | type: "input", 65 | name: "amount", 66 | message: "Enter bet amount (1500-10000): ", 67 | validate: 68 | function(input: string){ 69 | if(parseInt(input)) 70 | if(parseInt(input) >= 1500 && parseInt(input) <= 10000) return true; 71 | return false; 72 | } 73 | }]); 74 | 75 | betAmount = parseInt(answer.amount); 76 | } 77 | 78 | var bet = new chainbet.CoinFlipHost(wif, betAmount, chainfeed); 79 | bet.run(); 80 | while (!bet.complete) { 81 | await chainbet.Utils.sleep(250); 82 | } 83 | } 84 | } 85 | else if (selection.mode == 'client') { 86 | // select appropriate private key for bet 87 | try { 88 | var wif = await chainbet.Wallet.selectViableWIF(wallet); 89 | 90 | } catch(e) { 91 | console.log('\nNo viable addresses to use, please add funds or wait for 1 confirmation.'); 92 | } 93 | 94 | if(wif != ""){ 95 | var bet = new chainbet.CoinFlipClient(wif, chainfeed, context.debug); 96 | bet.run(); 97 | while (!bet.complete) { 98 | await chainbet.Utils.sleep(250); 99 | } 100 | } 101 | } 102 | else if (selection.mode == 'withdraw') { 103 | console.log("withdrawing funds..."); 104 | let answer: any = await inquirer.prompt([{ 105 | type: "input", 106 | name: "address", 107 | message: "Enter a withdraw address: ", 108 | validate: 109 | function(input: string){ 110 | if(BITBOX.Address.isCashAddress(input) || BITBOX.Address.isLegacyAddress(input)) 111 | return true; 112 | return false; 113 | } 114 | }]) 115 | await chainbet.Wallet.sweepToAddress(wallet, answer.address); 116 | } 117 | else if (selection.mode == 'list') { 118 | await chainbet.Wallet.listAddressDetails(wallet); 119 | } 120 | else if(selection.mode == 'quit') { 121 | process.exit(); 122 | } 123 | console.log('\n'); 124 | let answer: any = await inquirer.prompt([{type:'input', name:'resume', message:"Press ENTER to continue OR type 'q' to Quit..."}]); 125 | if(answer.resume == 'q'){ 126 | console.log("\nThanks for visiting Satoshi's Dice!"); 127 | process.exit(); 128 | } 129 | } 130 | } 131 | 132 | async function promptMainMenu() { 133 | 134 | if(!fs.existsSync('./wallet.json')) { 135 | console.log("\nGenerating a new address and wallet.json file..."); 136 | var wif = chainbet.Utils.getNewPrivKeyWIF(); 137 | fs.writeFileSync('./wallet.json', "", 'utf8'); 138 | jsonfile.writeFileSync("./wallet.json", [{ 'wif': wif }], 'utf8'); 139 | } 140 | 141 | console.log('\n-------------------------------------------------------------------------------'); 142 | console.log("| Welcome to Satoshi's Dice! |"); 143 | console.log('-------------------------------------------------------------------------------'); 144 | console.log(' ===== .-------. ______ ====='); 145 | console.log(' /// / o /| /\\ \\ ///'); 146 | console.log(' /// /_______/o| /o \\ o \\ ///'); 147 | console.log(' /// | o | | / o\\_____\\ ///'); 148 | console.log(' /// | o |o/ \\o /o / ///'); 149 | console.log(' /// | o |/ \\ o/ o / ///'); 150 | console.log(" ===== '-------' \\/____o/ ====="); 151 | console.log('-------------------------------------------------------------------------------'); 152 | console.log("| P2P Gaming with Bitcoin Cash |"); 153 | console.log('-------------------------------------------------------------------------------\n'); 154 | 155 | if(context.mode == undefined){ 156 | return await inquirer.prompt([{ 157 | type: "list", 158 | name: "mode", 159 | message: "What do you want to do?", 160 | choices: [ 161 | new inquirer.Separator("Games"), 162 | { name: ' Roll Dice (ODD wins) - You are Host', value: 'host' }, 163 | { name: ' Roll Dice (EVEN wins) - You wait for a Host', value: 'client' }, 164 | new inquirer.Separator("Wallet Tools"), 165 | { name: ' List balances', value: 'list' }, 166 | { name: ' Generate new BCH address', value: 'generate' }, 167 | { name: ' Withdraw all funds', value: 'withdraw' }, 168 | { name: ' Quit', value: 'quit' } 169 | ], 170 | }]); 171 | } 172 | else { 173 | return { 'mode': context.mode }; 174 | } 175 | } 176 | 177 | main(); -------------------------------------------------------------------------------- /MULTIPLAYER.md: -------------------------------------------------------------------------------- 1 | # MultiPlayer 2 | 3 | ## Introduction 4 | 5 | This type of bet is a winner-takes-all between multiple players with even chances. 6 | 7 | Extending the ChainBet protocol to cover bets involving multiple players adds another level of complexity. The field of study known as [Multi Party Computation](https://en.wikipedia.org/wiki/Secure_multi-party_computation) contains a number of avenues, but not all are suitable for our purpose. 8 | 9 | The ideal solution has the following properties: 10 | 11 | 1. It does not allow cheating via collusion, even against an attacker controlling all of the adverseries. 12 | 2. It protects an honest participant from losing money, even if a dishonest participant acts irrationally. 13 | 14 | This protocol can meet those ideals, although it does have the cost of requiring a security deposit of a multiple (N) of the bet amount, where N is the number of participants. It may be possible to create a different scheme that meets the ideals and has a lower security deposit, but at the cost of added complexity, time, and multiple rounds. We will not explore such a scheme here. 15 | 16 | ## Multilock 17 | 18 | We draw our initial inspiration from a multilock idea originally proposed by Kumaresan and Bentov1, which offers the principle of jointly locking coins for fair exchange. Their proposal calls for a protocol change, using the leaves of the Merkle root to obtain a transaction ID even if transaction is unsigned. 19 | 20 | However, a protocol change is not necessary for our purposes. We can build our solution simply by applying the correct tiering of transactions and time locks. 21 | 22 | ## Scheme 23 | 24 | First, the main betting address (script) is constructed. It can spend outputs in either of two ways. **a)** If all parties sign, or **b)**, if the winner signs and produces all the secrets. The winner is determined by the modulo method described in the the [dice protocol](https://github.com/fyookball/ChainBet/blob/master/DICE_ROLL.md), with Alice winning on a 0 remainder, and the other players winning on a remainder corresponding to their order as described later in phase 3. 25 | 26 | The participants then create the **main funding transaction** which funds this script using a timelocked transaction. Each participant needs to contribute (*N \* Bet Amount* ) where N is the number of players. The total amount of this transaction is therefore ( *N2 \* Bet Amount*). 27 | 28 | Next, each participant creates an escrow address (script) that can spend outputs in either of two ways. **a)** if the participant can sign and produce the secret that solves the commitment hash, or **b)** if all parties BUT the participant sign. For example, if the participants include Alice, Bob, Carol, and Dave (each wagering 1 BCH), then Alice's escrow addrsess can be spent if Alice signs and produces **Secret A** , or if Bob, Carol, and Dave create an **escrow refund transaction** by all signing. This second method also has its own timelock. 29 | 30 | The idea behind the escrow address is to allow each player enough time to reveal their secret, but force them to compensate every other player if they do not, since not revealing the secret would make the bet unwinnable by anyone. This is the reason why the bet multiple is required. 31 | 32 | The refund transactions need to be signed prior to funds being committed. Continuing this example, if Alice defaults then Bob does not want to depend on Carol and Dave to get his money back, so all 3 (Bob, Carol, and Dave) should sign a transaction spending the funds (3 BCH total) from Alice's escrow address back to themselves, so they are each compensated with 1 BCH. The same needs to happen for the other player's escrow addresses: Alice, Carol, and Dave need to sign a transaction spending from Bob's escrow, and so on. 33 | 34 | In the normal case when no one defaults, Alice will spend the 3 BCH back to herself, revealing her secret, and reducing her exposure to the wager amount of 1 BCH. The same is true for all participants. 35 | 36 | After creating the escrow addresses, the players create the **escrow funding transaction** that spends the output of the main betting script and splits it into N outputs that fund the escrow addresses, with the change going back to itself. So for 4 players wagering 1 BCH, we start with 16 BCH, which is spent on 4 outputs of 3 BCH each (12 total), and 4 BCH sent back as change. 37 | 38 | Once everyone is sure that everyone else signed this main escrow transaction, it is safe to allow the timelock on the main funding transaction to expire. If the timelock is about to expire and a participant doesn't see that the main escrow transaction is signed (or that not all the escrow refund trasactions have been signed), they can cancel the bet by trivially double spending the input to the main funding transaction since it is still under timelock. 39 | 40 | Once the main funding transaction timelock expires and the transaction has at least 1 confirmation, it essentially cannot be doublespent. It will then require the winner to produce the secret, but there is assurance that a winner will come forward with all the secrets since the participants will lose money if they do not produce their secret. 41 | 42 | The time lock on the refund script should come AFTER the time lock on the main betting script. Otherwise, the players would be forced to reveal their secrets too early and the bet could be cancelled by double spending. 43 | 44 | ## Diagram of Betting Scheme 45 | 46 | ![Scheme](https://raw.githubusercontent.com/fyookball/ChainBet/master/images/multilock-small.png) 47 | 48 | 49 | # Betting Phases 50 | 51 | ## Phase 1: Bet Offer Announcement 52 | 53 | Alice announces a multiplayer bet and specifies the amount of the bet. 54 | 55 | NOTE: This unique identifier for this bet will be the transaction id of the txn containing this phase 1 message, herein referred to as . 56 | 57 | OP_RETURN OUTPUT: 58 | 59 | | Bytes | Name | Hex Value | Description | 60 | | ------------- |-------------| -----|-----------------| 61 | | 1 | Phase | 0x01 | Phase 1 is "Bet Offer Announcement" | 62 | | 1 | Bet Type | 0x03 | Denotes what kind of bet will be contructed. 0x03 for Multiplayer bet. | 63 | | 8 | Amount | \ | Bet amount in Satohis for each participant. | 64 | 65 | 66 | 67 | ## Phase 2: Bet Participant Acceptance 68 | 69 | Next, other players (up to 11 other players) can join the bet. Hereinafter, we will refer to all other players as "Bob". 70 | Bob(s) need to send their public key and their commitment secret in this phase. 71 | 72 | NOTE: This transaction ID of the transaction containing this phase 2 message, herein referred to as the . 73 | 74 | OP_RETURN OUTPUT: 75 | 76 | | Bytes | Name | Hex Value | Description | 77 | | ------------- |-------------| -----|-----------------| 78 | | 1 | Phase | 0x02 | Phase 2 is " Bet Participant Acceptance" | 79 | | 32 | Bet Txn Id |\ | This lets Alice know Bob wants to bet. | 80 | |33 | Bob Multi-sig Pub Key | \| This is the compressed public key that players should use when creating the funding transaction. | 81 | |32 | Committment | | Hash of the players' secret| 82 | 83 | 84 | ## Phase 3: Player List Announcement 85 | 86 | Alice will publish a list of all players. If there is no list, then things may become confusing based on timing where not everyone is clear on the number of players and who they are. Allowing Alice to pick the list is not a problem since collusion is unprofitable. 87 | 88 | To save space, Alice will only publish the 8 least significant bytes of each participant_opreturn_tx_id, ignoring the edge case where 2 might have a collision. The list of these truncated ids will be sorted with the lowest value coming first, and then concatenated to form a byte string that has a length of between 8 and 88 bytes, depending on how many players are in the list. 89 | 90 | 91 | | Bytes | Name | Hex Value | Description | 92 | | ------------- |-------------| -----|-----------------| 93 | | 1 | Phase | 0x03 | Phase 3 is " Player List Announcement" | 94 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob(s) have multiple bets going.| 95 | | 8-88 | Participant Txn List | \| The 8 byte tail (least significant bits) of the participant_opreturn_txn_id of each participant other than Alice will be put into a list, sorted, and concatenated. | 96 | 97 | 98 | ## Phase 4: Sign Main Funding Transaction 99 | 100 | All participants then deterministically create the betting script and sign the main funding transaction, specifying the input parameters and their signature. 101 | 102 | | Bytes | Name | Hex Value | Description | 103 | | ------------- |-------------| -----|-----------------| 104 | | 1 | Phase | 0x04 | Phase 4 is " Sign Main Funding Transaction" | 105 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob(s) have multiple bets going.| 106 | | 32 | Transaction Input Id | \| The coin Bob will spend to participate in the bet. | 107 | | 1 | vOut | \ | The index of the outpoint | 108 | | 72 | Signature | \ | Signature spending Bob's funds to the main bet script. Sigtype hash ALL \| ANYONECANPAY | 109 | 110 | 111 | ## Phase 5: Sign Escrow Funding Transaction 112 | 113 | 114 | All participants then deterministically create the escrow scripts and sign the escrow funding transaction. 115 | 116 | | Bytes | Name | Hex Value | Description | 117 | | ------------- |-------------| -----|-----------------| 118 | | 1 | Phase | 0x05 | Phase 5 is " Sign Escrow Funding Transaction" | 119 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob(s) have multiple bets going.| 120 | | 72 | Signature | \ | Signature spending funds to all escrow addresses. Sigtype hash ALL \| ANYONECANPAY | 121 | 122 | 123 | 124 | ## Phase 6: Sign Escrow Refund Transaction 125 | 126 | 127 | All participants then sign the escrow refund transactions, assuming the refund address is that which is generated from the public key of each participant, and assuming the bet id ordering described in phase 3. Each player will have to generate multiple signatures (one for each other participant). Because of space, only 2 can fit in each message and thus multiple messages may be required. 128 | 129 | This is the purpose of the signature index. Bob will send the first message with signature index 0x00, indicating his two signatures will be for the Alice escrow refund transaction and the Carol escrow refund transaction respecitively. His second message will have signature index 0x01, indicating Dave's escrow refund transaction. 130 | 131 | Note that the players do not have to wait for anything to happen between messages 4 and 5, or between messages 5 and 6. The only safety requirement is that they double spend their inputs before the first time lock if not all assurances are in place. 132 | 133 | | Bytes | Name | Hex Value | Description | 134 | | ------------- |-------------| -----|-----------------| 135 | | 1 | Phase | 0x06 | Phase 6 is " Sign Escrow Refund Transaction" | 136 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob(s) have multiple bets going.| 137 | | 1 | Signature Index | \ | A special index used for the purposes of organizing escrow refund signatures. 138 | | 72 | Signature 1 | \ | Signature spending escrow refund transaction. Sigtype hash ALL \|ANYONECANPAY | 139 | | 72 | Signature 2 | \ | Signature spending escrow refund transaction. Sigtype hash ALL \|ANYONECANPAY | 140 | 141 | 142 | After the main funding transaction is confirmed, players will begin to reclaim their security depoits and reveal their secrets. Then the winner can claim the prize. They must do so before the escrow timelock expires or they will lose funds while compensating others. 143 | 144 | # Collusion 145 | 146 | Despite the assurances in the scheme, it may appear that a collusion attack is still possible, but upon closer examination, it is unprofitable to do so. 147 | 148 | Imagine that Alice controls 11 adverseries against a single Bob. The Alice group has a member, "late Alice", who always tries to reveal her secret later than Bob so that the Alice group knows the outcome before Bob does. 149 | 150 | If the bet size is 1 BCH, then the Alice group is expected to win 1 BCH on 11 out of 12 bets, but Bob is expected to win 11 BCH on 1 out of 12 bets. Since the Alice group sees the outcome first, their plan is that if Bob loses, they collect his 1 BCH, but if Bob wins, "late Alice" simply doesn't share her secret, and since her compensation transaction mostly pays her own teammates, the Alice group only loses 1 BCH. 151 | 152 | However, they are forgetting that only Bob can claim the 12 BCH still in the main bet account! The Alice group actually loses a total of 12 BCH (11 in the main bet and 1 in the refund transaction), which is 1 more BCH than they would lose if they simply allowed Bob to collect his winnings. 153 | 154 | We still generally consider that the scheme protects Bob from losing money if the participants behave irrationally because secret-withholding is compensated for and wouldn't target a specific bet or person. Technically, the scheme isn't tamper-proof, but a malicious actor would have to go out of their way AND lose money in order to affect the bet. 155 | 156 | ## Final Thoughts 157 | 158 | Due to the large security deposit, the practicality of the bet scheme is in question. The same result could be accomplished with a dice like bet in many applications. For example, 6 people betting each other could be simulated by each of them betting on a dice roll, with more flexibility in that not all 6 people would need to be real. However this scheme may serve as a building block other schemes and give us more ideas for the future. 159 | 160 | 161 | ## Authors 162 | 163 | Jonald Fyookball 164 | 165 | \[1] Kumaresan, Bentov "How to Use Bitcoin to Incentivize Correct Computations" 166 | 167 | -------------------------------------------------------------------------------- /code/examples/dice/dice.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | var __generator = (this && this.__generator) || function (thisArg, body) { 11 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 12 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 13 | function verb(n) { return function (v) { return step([n, v]); }; } 14 | function step(op) { 15 | if (f) throw new TypeError("Generator is already executing."); 16 | while (_) try { 17 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 18 | if (y = 0, t) op = [op[0] & 2, t.value]; 19 | switch (op[0]) { 20 | case 0: case 1: t = op; break; 21 | case 4: _.label++; return { value: op[1], done: false }; 22 | case 5: _.label++; y = op[1]; op = [0]; continue; 23 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 24 | default: 25 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 26 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 27 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 28 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 29 | if (t[2]) _.ops.pop(); 30 | _.trys.pop(); continue; 31 | } 32 | op = body.call(thisArg, _); 33 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 34 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 35 | } 36 | }; 37 | var __importStar = (this && this.__importStar) || function (mod) { 38 | if (mod && mod.__esModule) return mod; 39 | var result = {}; 40 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 41 | result["default"] = mod; 42 | return result; 43 | }; 44 | var __importDefault = (this && this.__importDefault) || function (mod) { 45 | return (mod && mod.__esModule) ? mod : { "default": mod }; 46 | }; 47 | Object.defineProperty(exports, "__esModule", { value: true }); 48 | var chainbet = __importStar(require("chainbet")); 49 | var bitbox_cli_1 = __importDefault(require("bitbox-cli/lib/bitbox-cli")); 50 | var BITBOX = new bitbox_cli_1.default(); 51 | var fs = __importStar(require("fs")); 52 | var context = require('commander'); 53 | var inquirer = __importStar(require("inquirer")); 54 | var jsonfile = __importStar(require("jsonfile")); 55 | context.version('0.0.13') 56 | .option('-m, --mode [mode]', 'set program mode to bypass initial prompt') 57 | .option('-d, --debug [debug]', 'set debugger support (skips user prompts with default values)') 58 | .parse(process.argv); 59 | context.debug = (context.debug == "1" ? true : false); 60 | function main() { 61 | return __awaiter(this, void 0, void 0, function () { 62 | var wallet, selection, wif, chainfeed, wif, e_1, betAmount, answer_1, bet, wif, e_2, bet, answer_2, answer; 63 | return __generator(this, function (_a) { 64 | switch (_a.label) { 65 | case 0: 66 | if (!true) return [3 /*break*/, 28]; 67 | return [4 /*yield*/, promptMainMenu()]; 68 | case 1: 69 | selection = _a.sent(); 70 | // check if wallet.json file exists 71 | if (fs.existsSync('./wallet.json')) { 72 | if (selection.mode == 'generate') { 73 | console.log("\nGenerating a new bitcoin address...\n"); 74 | wif = chainbet.Utils.getNewPrivKeyWIF(); 75 | wallet = jsonfile.readFileSync('./wallet.json'); 76 | wallet.push({ 'wif': wif }); 77 | jsonfile.writeFileSync("./wallet.json", wallet, 'utf8'); 78 | } 79 | } 80 | wallet = jsonfile.readFileSync('./wallet.json'); 81 | chainfeed = new chainbet.MessageFeed(); 82 | return [4 /*yield*/, chainfeed.checkConnection()]; 83 | case 2: 84 | _a.sent(); 85 | wif = ""; 86 | if (!(selection.mode == 'host')) return [3 /*break*/, 12]; 87 | _a.label = 3; 88 | case 3: 89 | _a.trys.push([3, 5, , 6]); 90 | return [4 /*yield*/, chainbet.Wallet.selectViableWIF(wallet)]; 91 | case 4: 92 | wif = _a.sent(); 93 | return [3 /*break*/, 6]; 94 | case 5: 95 | e_1 = _a.sent(); 96 | console.log("\nNo viable addresses to use, please add funds or wait for 1 confirmation."); 97 | return [3 /*break*/, 6]; 98 | case 6: 99 | if (!(wif != "")) return [3 /*break*/, 11]; 100 | betAmount = 1500; 101 | if (!!context.debug) return [3 /*break*/, 8]; 102 | console.log('\n'); 103 | return [4 /*yield*/, inquirer.prompt([{ 104 | type: "input", 105 | name: "amount", 106 | message: "Enter bet amount (1500-10000): ", 107 | validate: function (input) { 108 | if (parseInt(input)) 109 | if (parseInt(input) >= 1500 && parseInt(input) <= 10000) 110 | return true; 111 | return false; 112 | } 113 | }])]; 114 | case 7: 115 | answer_1 = _a.sent(); 116 | betAmount = parseInt(answer_1.amount); 117 | _a.label = 8; 118 | case 8: 119 | bet = new chainbet.CoinFlipHost(wif, betAmount, chainfeed); 120 | bet.run(); 121 | _a.label = 9; 122 | case 9: 123 | if (!!bet.complete) return [3 /*break*/, 11]; 124 | return [4 /*yield*/, chainbet.Utils.sleep(250)]; 125 | case 10: 126 | _a.sent(); 127 | return [3 /*break*/, 9]; 128 | case 11: return [3 /*break*/, 26]; 129 | case 12: 130 | if (!(selection.mode == 'client')) return [3 /*break*/, 20]; 131 | _a.label = 13; 132 | case 13: 133 | _a.trys.push([13, 15, , 16]); 134 | return [4 /*yield*/, chainbet.Wallet.selectViableWIF(wallet)]; 135 | case 14: 136 | wif = _a.sent(); 137 | return [3 /*break*/, 16]; 138 | case 15: 139 | e_2 = _a.sent(); 140 | console.log('\nNo viable addresses to use, please add funds or wait for 1 confirmation.'); 141 | return [3 /*break*/, 16]; 142 | case 16: 143 | if (!(wif != "")) return [3 /*break*/, 19]; 144 | bet = new chainbet.CoinFlipClient(wif, chainfeed, context.debug); 145 | bet.run(); 146 | _a.label = 17; 147 | case 17: 148 | if (!!bet.complete) return [3 /*break*/, 19]; 149 | return [4 /*yield*/, chainbet.Utils.sleep(250)]; 150 | case 18: 151 | _a.sent(); 152 | return [3 /*break*/, 17]; 153 | case 19: return [3 /*break*/, 26]; 154 | case 20: 155 | if (!(selection.mode == 'withdraw')) return [3 /*break*/, 23]; 156 | console.log("withdrawing funds..."); 157 | return [4 /*yield*/, inquirer.prompt([{ 158 | type: "input", 159 | name: "address", 160 | message: "Enter a withdraw address: ", 161 | validate: function (input) { 162 | if (BITBOX.Address.isCashAddress(input) || BITBOX.Address.isLegacyAddress(input)) 163 | return true; 164 | return false; 165 | } 166 | }])]; 167 | case 21: 168 | answer_2 = _a.sent(); 169 | return [4 /*yield*/, chainbet.Wallet.sweepToAddress(wallet, answer_2.address)]; 170 | case 22: 171 | _a.sent(); 172 | return [3 /*break*/, 26]; 173 | case 23: 174 | if (!(selection.mode == 'list')) return [3 /*break*/, 25]; 175 | return [4 /*yield*/, chainbet.Wallet.listAddressDetails(wallet)]; 176 | case 24: 177 | _a.sent(); 178 | return [3 /*break*/, 26]; 179 | case 25: 180 | if (selection.mode == 'quit') { 181 | process.exit(); 182 | } 183 | _a.label = 26; 184 | case 26: 185 | console.log('\n'); 186 | return [4 /*yield*/, inquirer.prompt([{ type: 'input', name: 'resume', message: "Press ENTER to continue OR type 'q' to Quit..." }])]; 187 | case 27: 188 | answer = _a.sent(); 189 | if (answer.resume == 'q') { 190 | console.log("\nThanks for visiting Satoshi's Dice!"); 191 | process.exit(); 192 | } 193 | return [3 /*break*/, 0]; 194 | case 28: return [2 /*return*/]; 195 | } 196 | }); 197 | }); 198 | } 199 | function promptMainMenu() { 200 | return __awaiter(this, void 0, void 0, function () { 201 | var wif; 202 | return __generator(this, function (_a) { 203 | switch (_a.label) { 204 | case 0: 205 | if (!fs.existsSync('./wallet.json')) { 206 | console.log("\nGenerating a new address and wallet.json file..."); 207 | wif = chainbet.Utils.getNewPrivKeyWIF(); 208 | fs.writeFileSync('./wallet.json', "", 'utf8'); 209 | jsonfile.writeFileSync("./wallet.json", [{ 'wif': wif }], 'utf8'); 210 | } 211 | console.log('\n-------------------------------------------------------------------------------'); 212 | console.log("| Welcome to Satoshi's Dice! |"); 213 | console.log('-------------------------------------------------------------------------------'); 214 | console.log(' ===== .-------. ______ ====='); 215 | console.log(' /// / o /| /\\ \\ ///'); 216 | console.log(' /// /_______/o| /o \\ o \\ ///'); 217 | console.log(' /// | o | | / o\\_____\\ ///'); 218 | console.log(' /// | o |o/ \\o /o / ///'); 219 | console.log(' /// | o |/ \\ o/ o / ///'); 220 | console.log(" ===== '-------' \\/____o/ ====="); 221 | console.log('-------------------------------------------------------------------------------'); 222 | console.log("| P2P Gaming with Bitcoin Cash |"); 223 | console.log('-------------------------------------------------------------------------------\n'); 224 | if (!(context.mode == undefined)) return [3 /*break*/, 2]; 225 | return [4 /*yield*/, inquirer.prompt([{ 226 | type: "list", 227 | name: "mode", 228 | message: "What do you want to do?", 229 | choices: [ 230 | new inquirer.Separator("Games"), 231 | { name: ' Roll Dice (ODD wins) - You are Host', value: 'host' }, 232 | { name: ' Roll Dice (EVEN wins) - You wait for a Host', value: 'client' }, 233 | new inquirer.Separator("Wallet Tools"), 234 | { name: ' List balances', value: 'list' }, 235 | { name: ' Generate new BCH address', value: 'generate' }, 236 | { name: ' Withdraw all funds', value: 'withdraw' }, 237 | { name: ' Quit', value: 'quit' } 238 | ], 239 | }])]; 240 | case 1: return [2 /*return*/, _a.sent()]; 241 | case 2: return [2 /*return*/, { 'mode': context.mode }]; 242 | } 243 | }); 244 | }); 245 | } 246 | main(); 247 | -------------------------------------------------------------------------------- /code/src/core.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | // let TransactionBuilder = require('bitbox-cli/lib/TransactionBuilder').default; 5 | // import { ITransactionBuilder } from 'bitbox-cli/lib/TransactionBuilder'; 6 | 7 | let bip68 = require('bip68'); 8 | 9 | import { Utils } from './utils'; 10 | 11 | export interface WalletKey { 12 | wif: string, 13 | pubkey: Buffer, 14 | address: string, 15 | utxo?: any 16 | } 17 | 18 | export interface BetState { 19 | phase: number, 20 | amount?: number, 21 | hostCommitment?: Buffer, 22 | betId?: string, 23 | secret?: Buffer, 24 | secretCommitment?: Buffer, 25 | clientTxId?: string, 26 | hostMultisigPubKey?: Buffer, 27 | hostP2SHTxId?: string, 28 | 29 | // host only side 30 | clientmultisigPubKey?: Buffer, 31 | clientCommitment?: Buffer, 32 | clientP2SHTxId?: string, 33 | participantSig1?: Buffer, 34 | participantSig2?: Buffer, 35 | clientSecret?: Buffer, 36 | 37 | } 38 | 39 | export interface PhaseData { 40 | betType: number, 41 | version: number, 42 | phase: number, 43 | op_return_txnId?: string 44 | } 45 | 46 | // Phase 1 - Host Advertisement 47 | export interface Phase1Data extends PhaseData { 48 | // amount to bet 49 | amount: number, 50 | // 20 byte host's secret hash 51 | hostCommitment: Buffer, 52 | // optional target client address 53 | address?: string 54 | } 55 | 56 | // Phase 2 - Client Accepts Host's offer 57 | export interface Phase2Data extends PhaseData { 58 | // 32 byte Bet Txn Id 59 | betTxId: Buffer, 60 | // 33 byte public key 61 | multisigPubKey: Buffer, 62 | // 20 byte secret hash 63 | secretCommitment: Buffer, 64 | } 65 | 66 | // Phase 3 - Host Acknowledges client and funds escrow 67 | export interface Phase3Data extends PhaseData { 68 | // 32 byte Bet Txn Id 69 | betTxId: Buffer, 70 | // 32 byte Participant Txn Id 71 | participantOpReturnTxId: Buffer, 72 | // 32 byte Host P2SH txid 73 | hostP2SHTxId: Buffer, 74 | // 33 byte Host (Alice) multsig pubkey 75 | hostMultisigPubKey: Buffer 76 | } 77 | 78 | // Phase 4 - Client funds escrow 79 | export interface Phase4Data extends PhaseData { 80 | // 32 byte Bet Txn Id 81 | betTxId: Buffer, 82 | // 32 byte Participant Txn Id 83 | participantP2SHTxId: Buffer, 84 | // 71-72 byte Participant Signature 1 85 | participantSig1: Buffer, 86 | // 71-72 byte Participant Signature 2 87 | participantSig2: Buffer 88 | } 89 | 90 | // Phase 6 - Client sends resignation if loses. 91 | export interface Phase6Data extends PhaseData { 92 | // 32 byte Bet Txn Id 93 | betTxId: Buffer, 94 | // 32 byte Secret Value 95 | secretValue: Buffer 96 | } 97 | 98 | export class Core { 99 | 100 | // Method to get Script 32-bit integer (little-endian signed magnitude representation) 101 | static readScriptInt32(buffer: Buffer): number { 102 | var number: number; 103 | if(buffer.readUInt32LE(0) > 2147483647) 104 | number = -1 * (buffer.readUInt32LE(0) - 2147483648); 105 | else 106 | number = buffer.readUInt32LE(0); 107 | return number; 108 | } 109 | 110 | // Method to check whether or not a secret value is valid 111 | static secretIsValid(buffer: Buffer): boolean { 112 | var number = this.readScriptInt32(buffer); 113 | if(number > 1073741823 || number < -1073741823) 114 | return false; 115 | return true; 116 | } 117 | 118 | static generateSecretNumber(): Buffer { 119 | var secret: Buffer = BITBOX.Crypto.randomBytes(32); 120 | while(!this.secretIsValid(secret)){ 121 | secret = BITBOX.Crypto.randomBytes(32); 122 | } 123 | return secret; 124 | } 125 | 126 | static async sendRawTransaction(hex: string, retries=20): Promise { 127 | var result: string = ""; 128 | 129 | var i = 0; 130 | 131 | while(result == ""){ 132 | result = await BITBOX.RawTransactions.sendRawTransaction(hex); 133 | i++; 134 | if (i > retries) 135 | throw new Error("BITBOX.RawTransactions.sendRawTransaction endpoint experienced a problem.") 136 | await Utils.sleep(250); 137 | } 138 | 139 | if(result.length != 64) 140 | console.log("An error occured while sending the transaction:\n" + result); 141 | 142 | return result; 143 | } 144 | 145 | static async getAddressDetailsWithRetry(address: string, retries: number = 20){ 146 | var result; 147 | var count = 0; 148 | 149 | while(result == undefined){ 150 | result = await BITBOX.Address.details(address); 151 | count++; 152 | if(count > retries) 153 | throw new Error("BITBOX.Address.details endpoint experienced a problem"); 154 | 155 | await Utils.sleep(250); 156 | } 157 | 158 | return result; 159 | } 160 | 161 | static async getUtxoWithRetry(address: string, retries: number = 20){ 162 | var result; 163 | var count = 0; 164 | 165 | while(result == undefined){ 166 | result = await Core.getUtxo(address) 167 | count++; 168 | if(count > retries) 169 | throw new Error("BITBOX.Address.utxo endpoint experienced a problem"); 170 | await Utils.sleep(250); 171 | } 172 | 173 | return result; 174 | } 175 | 176 | static async getUtxo(address: string) { 177 | return new Promise( (resolve, reject) => { 178 | BITBOX.Address.utxo(address).then((result: any) => { 179 | resolve(result) 180 | }, (err: any) => { 181 | console.log(err) 182 | reject(err) 183 | }) 184 | }) 185 | } 186 | 187 | static purseAmount(betAmount: number): number { 188 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2SH: 1 }); 189 | return (betAmount * 2 ) - byteCount - 750; 190 | } 191 | 192 | 193 | static decodePhaseData(bufArray: (Buffer)[], networkByte: number = 0x00): Phase1Data | Phase2Data | Phase3Data | Phase4Data| Phase6Data { 194 | 195 | let result: Phase1Data | Phase2Data | Phase3Data | Phase3Data | Phase4Data | Phase6Data; 196 | 197 | // convert op_return buffer to hex string 198 | //op_return = op_return.toString('hex'); 199 | 200 | // split the op_return payload and get relavant data 201 | //let data = op_return.split("04004245544c"); // pushdata (0x04) + Terab ID + pushdata (0x4c) 202 | //let buf = Buffer.from(data[0].trim(), 'hex'); // NOTE: the index of data was changed to 0 due to MessageFeed listen method. 203 | 204 | // grab the common fields 205 | //var results: PhaseData; 206 | try 207 | { 208 | let version = bufArray[0].slice(0,1).readUInt8(0); 209 | let betType = bufArray[0].slice(1,2).readUInt8(0); 210 | let phase = bufArray[0].slice(2,3).readUInt8(0); 211 | //results = { betType: betType, version: version, phase: phase }; 212 | 213 | // Phase 1 specific fields 214 | if(phase === 1) { 215 | let phase1Result: Phase1Data = { 216 | betType: betType, 217 | version: version, 218 | phase: phase, 219 | amount: parseInt(bufArray[1].toString('hex'), 16), 220 | hostCommitment: bufArray[2] 221 | }; 222 | 223 | // Target address (as hash160 without network or sha256) 224 | if (bufArray.length > 3){ 225 | var pkHash160 = bufArray[3]; 226 | phase1Result.address = Utils.hash160_2_cashAddr(pkHash160, networkByte); 227 | } 228 | 229 | result = phase1Result; 230 | 231 | // Phase 2 specific fields 232 | } else if(phase === 2) { 233 | let phase2Result: Phase2Data = { 234 | betType: betType, 235 | version: version, 236 | phase: phase, 237 | betTxId: bufArray[1], 238 | multisigPubKey: bufArray[2], 239 | secretCommitment: bufArray[3] 240 | }; 241 | 242 | result = phase2Result; 243 | 244 | // Phase 3 specific fields 245 | } else if(phase === 3) { 246 | 247 | let phase3Result: Phase3Data = { 248 | betType: betType, 249 | version: version, 250 | phase: phase, 251 | betTxId: bufArray[1], 252 | participantOpReturnTxId: bufArray[2], 253 | hostP2SHTxId: bufArray[3], 254 | hostMultisigPubKey: bufArray[4] 255 | }; 256 | 257 | result = phase3Result; 258 | 259 | //Phase 4 specific fields 260 | } else if(phase === 4) { 261 | 262 | let phase4Result: Phase4Data = { 263 | betType: betType, 264 | version: version, 265 | phase: phase, 266 | betTxId: bufArray[1], 267 | participantP2SHTxId: bufArray[2], 268 | participantSig1: bufArray[3], 269 | participantSig2: bufArray[4] 270 | } 271 | 272 | result = phase4Result; 273 | 274 | // Phase 6 specific fields 275 | } else if(phase === 6) { 276 | let phase6Result: Phase6Data = { 277 | betType: betType, 278 | version: version, 279 | phase: phase, 280 | betTxId: bufArray[1], 281 | secretValue: bufArray[2] 282 | } 283 | 284 | result = phase6Result; 285 | } 286 | else { 287 | throw new Error("Phase not detected during decoding bet message data.") 288 | } 289 | } 290 | catch(e){ 291 | throw new Error("Unable to decode OP_RETURN message with same protocol identifier.") 292 | } 293 | 294 | return result; 295 | } 296 | 297 | static async createOP_RETURN(wallet: any, op_return_buf: Buffer) { 298 | 299 | // THIS MAY BE BUGGY TO HAVE THIS HERE 300 | //wallet.utxo = await this.getUtxo(wallet.address); 301 | 302 | //return new Promise((resolve, reject) => { 303 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 304 | let hashType = transactionBuilder.hashTypes.SIGHASH_ALL; 305 | 306 | let totalUtxo = 0; 307 | wallet.utxo.forEach((item: any, index: any) => { 308 | transactionBuilder.addInput(item.txid, item.vout); 309 | totalUtxo += item.satoshis; 310 | }); 311 | 312 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: wallet.utxo.length }, { P2SH: 0 }) + op_return_buf.length + 100; 313 | let satoshisAfterFee = totalUtxo - byteCount 314 | 315 | transactionBuilder.addOutput(op_return_buf, 0); // OP_RETURN Message 316 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(wallet.utxo[0].cashAddress), satoshisAfterFee); // Change 317 | //console.log("txn fee: " + byteCount); 318 | //console.log("satoshis left: " + satoshisAfterFee); 319 | let key = BITBOX.ECPair.fromWIF(wallet.wif); 320 | 321 | let redeemScript: Buffer; 322 | wallet.utxo.forEach((item: any, index: number) => { 323 | transactionBuilder.sign(index, key, redeemScript, hashType, item.satoshis); 324 | }); 325 | 326 | let hex = transactionBuilder.build().toHex(); 327 | 328 | //console.log("Create op_return message hex:", hex); 329 | 330 | let txId = await Core.sendRawTransaction(hex); 331 | return txId; 332 | } 333 | 334 | static async createEscrow(wallet: any, script: Buffer, betAmount: number){ 335 | 336 | //return new Promise( (resolve, reject) => { 337 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 338 | let hashType = transactionBuilder.hashTypes.SIGHASH_ALL | transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY; 339 | 340 | let totalUtxo = 0; 341 | wallet.utxo.forEach((item: any, index: any) => { 342 | transactionBuilder.addInput(item.txid, item.vout); 343 | totalUtxo += item.satoshis; 344 | }); 345 | 346 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: wallet.utxo.length }, { P2SH: 1 }) + 50; 347 | let satoshisAfterFee = totalUtxo - byteCount - betAmount 348 | 349 | let p2sh_hash160 = BITBOX.Crypto.hash160(script); 350 | let p2sh_hash160_hex = p2sh_hash160.toString('hex'); 351 | let scriptPubKey = BITBOX.Script.scriptHash.output.encode(p2sh_hash160); 352 | 353 | let escrowAddress = BITBOX.Address.toLegacyAddress(BITBOX.Address.fromOutputScript(scriptPubKey)); 354 | let changeAddress = BITBOX.Address.toLegacyAddress(wallet.utxo[0].cashAddress); 355 | // console.log("escrow address: " + address); 356 | // console.log("change satoshi: " + satoshisAfterFee); 357 | // console.log("change bet amount: " + betAmount); 358 | 359 | transactionBuilder.addOutput(escrowAddress, betAmount); 360 | transactionBuilder.addOutput(changeAddress, satoshisAfterFee); 361 | //console.log("Added escrow outputs..."); 362 | 363 | let key = BITBOX.ECPair.fromWIF(wallet.wif); 364 | 365 | let redeemScript: Buffer; 366 | wallet.utxo.forEach((item: any, index: any) => { 367 | transactionBuilder.sign(index, key, redeemScript, hashType, item.satoshis); 368 | }); 369 | //console.log("signed escrow inputs..."); 370 | 371 | let hex = transactionBuilder.build().toHex(); 372 | //console.log("built escrow..."); 373 | 374 | let txId = await Core.sendRawTransaction(hex); 375 | return txId; 376 | } 377 | 378 | static async redeemEscrowToEscape(wallet: any, redeemScript: Buffer, txid: Buffer, betAmount: number){ 379 | 380 | //return new Promise( (resolve, reject) => { 381 | 382 | let hostKey = BITBOX.ECPair.fromWIF(wallet.wif) 383 | //let participantKey = BITBOX.ECPair.fromWIF(client.wif) 384 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 385 | 386 | let hashType = 0xc1 // transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY | transactionBuilder.hashTypes.SIGHASH_ALL 387 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2SH: 1 }); 388 | let satoshisAfterFee = betAmount - byteCount - 350; 389 | 390 | // NOTE: must set the Sequence number below 391 | transactionBuilder.addInput(txid, 0, bip68.encode({ blocks: 1 })); // No need to worry about sweeping the P2SH address. 392 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(wallet.utxo[0].cashAddress), satoshisAfterFee); 393 | 394 | let tx = transactionBuilder.transaction.buildIncomplete(); 395 | 396 | let signatureHash = tx.hashForWitnessV0(0, redeemScript, betAmount, hashType); 397 | let hostSignature = hostKey.sign(signatureHash).toScriptSignature(hashType); 398 | //let participantSignature = participantKey.sign(signatureHash).toScriptSignature(hashType); 399 | 400 | let redeemScriptSig: any = []; // start by pushing with true for makeBet mode 401 | 402 | // host signature 403 | redeemScriptSig.push(hostSignature.length); 404 | hostSignature.forEach((item: number, index: number) => { redeemScriptSig.push(item); }); 405 | 406 | // push mode onto stack for MakeBet mode 407 | redeemScriptSig.push(0x00); //use 0 for escape mode 408 | 409 | if (redeemScript.length > 75) redeemScriptSig.push(0x4c); 410 | redeemScriptSig.push(redeemScript.length); 411 | redeemScript.forEach((item, index) => { redeemScriptSig.push(item); }); 412 | 413 | redeemScriptSig = Buffer.from(redeemScriptSig); 414 | 415 | let redeemScriptSigHex = redeemScriptSig.toString('hex'); 416 | let redeemScriptHex = redeemScript.toString('hex'); 417 | 418 | tx.setInputScript(0, redeemScriptSig); 419 | let hex = tx.toHex(); 420 | 421 | let txId = await Core.sendRawTransaction(hex); 422 | 423 | return txId; 424 | } 425 | } -------------------------------------------------------------------------------- /code/src/coinflipclient.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | var inquirer = require('inquirer'); 5 | 6 | import { Core, Phase1Data, Phase3Data, WalletKey, BetState } from './core'; 7 | import { Utils } from './utils'; 8 | import { Client } from './client'; 9 | import { MessageFeed } from './messagefeed'; 10 | import { CoinFlipShared } from './coinflipshared'; 11 | import { AddressDetailsResult } from 'bitbox-cli/lib/Address'; 12 | 13 | export class CoinFlipClient extends Client { 14 | wallet: WalletKey; 15 | isDebugging: boolean; 16 | betState: BetState; 17 | complete: boolean; 18 | feed: MessageFeed; 19 | 20 | constructor(wif: string, feed: MessageFeed, debug: boolean = false){ 21 | super(); 22 | 23 | let ecpair = BITBOX.ECPair.fromWIF(wif); 24 | 25 | this.wallet = { 26 | wif: wif, 27 | pubkey: BITBOX.ECPair.toPublicKey(ecpair), 28 | address: BITBOX.ECPair.toCashAddress(ecpair) 29 | }; 30 | 31 | this.isDebugging = debug; 32 | 33 | this.betState = { phase: 1 }; 34 | this.complete = false; 35 | 36 | this.feed = feed; 37 | } 38 | 39 | async run() { 40 | 41 | // Phase 1 -- keep checking for a host to bet with 42 | console.log('\n-------------------------------------------------------------------------------'); 43 | console.log('| PHASE 1: Waiting for a host bet announcement... |'); 44 | console.log('-------------------------------------------------------------------------------'); 45 | 46 | while(this.betState.phase == 1) { 47 | 48 | var hostPhase1Messages = this.feed.messages.filter(function(item){ 49 | return item.phase == 1 && item.betType == 1; 50 | }); 51 | 52 | if(hostPhase1Messages.length > 0){ 53 | // ignore target address field from host for now... 54 | let newBet = hostPhase1Messages[hostPhase1Messages.length-1]; 55 | this.betState.hostCommitment = newBet.hostCommitment; 56 | this.betState.amount = newBet.amount; 57 | this.betState.betId = newBet.op_return_txnId; 58 | 59 | // NOTE: to simplify we will automatically accept the first bet host we see 60 | console.log("\nCoin flip bet discovered! \n(msg txn: " + this.betState.betId + ")"); 61 | console.log("\nHost's Bet Amount: " + newBet.amount); 62 | console.log('\n'); 63 | if(!this.isDebugging) { 64 | let answer = await inquirer.prompt([{ 65 | type: "input", 66 | name: "response", 67 | message: "Do you want to accept this bet (y/n)?", 68 | validate: 69 | function(input: string): boolean{ 70 | if(input == "y" || input == "n") return true; 71 | return false; 72 | } 73 | }]); 74 | if(answer.response == "y") 75 | this.betState.phase = 2; 76 | else{ 77 | console.log("Bet ignored..."); 78 | this.complete = true; 79 | return; 80 | } 81 | } 82 | else{ 83 | console.log("Automatically accepting bet (in debug mode)..."); 84 | this.betState.phase = 2; 85 | } 86 | } 87 | 88 | await Utils.sleep(250); 89 | } 90 | 91 | // Phase 2 -- allow user to choose a secret number then send host our acceptance message 92 | this.betState.secret = Core.generateSecretNumber(); //Buffer("c667c14e218cf530c405e2d50def250b0c031f69593e95171048c772ad0e1bce",'hex'); 93 | this.betState.secretCommitment = BITBOX.Crypto.hash160(this.betState.secret); 94 | console.log("Your secret number is: " + Core.readScriptInt32(this.betState.secret)); 95 | 96 | console.log('\n-------------------------------------------------------------------------------'); 97 | console.log('| PHASE 2: Accepting bet & sending our secret commitment... |'); 98 | console.log('-------------------------------------------------------------------------------'); 99 | 100 | this.betState.clientTxId = await CoinFlipClient.sendPhase2Message(this.wallet, this.betState.betId, this.wallet.pubkey, this.betState.secretCommitment); 101 | console.log("\nMessage to accept the bet sent. \n(msg txn: " + this.betState.clientTxId + ")") 102 | this.betState.phase = 3; 103 | 104 | // Phase 3 -- keep checking for host to fund his side of bet... 105 | console.log('\n-------------------------------------------------------------------------------') 106 | console.log('| PHASE 3: Waiting for host confirmation & to fund his bet... |') 107 | console.log('-------------------------------------------------------------------------------') 108 | 109 | 110 | while(this.betState.phase == 3) { 111 | 112 | let betId = this.betState.betId; 113 | let clientId = this.betState.clientTxId; 114 | 115 | var clientPhase3Messages = this.feed.messages.filter(function(item){ 116 | if(item.phase == 3 && betId != undefined) { 117 | let p3item = item; 118 | if(p3item.betTxId.toString('hex') == betId && p3item.participantOpReturnTxId.toString('hex') == clientId){ 119 | return true; } 120 | } 121 | return false; 122 | }); 123 | 124 | if(clientPhase3Messages.length > 0){ 125 | // ignore target address field from host for now... 126 | // assume its the host who is sending the message (TODO this will need to be fixed to prevent fake hosts) 127 | let bet = clientPhase3Messages[clientPhase3Messages.length-1]; 128 | this.betState.hostMultisigPubKey = bet.hostMultisigPubKey; 129 | this.betState.hostP2SHTxId = bet.hostP2SHTxId.toString('hex'); 130 | 131 | // NOTE: to simplify we will automatically accept the first bet host we see 132 | console.log("\nThe bet host has decided to bet with you! :-) \n(msg txn: " + bet.op_return_txnId + ")"); 133 | console.log("(escrow txn: " + this.betState.hostP2SHTxId); 134 | 135 | this.betState.phase = 4; 136 | } 137 | await Utils.sleep(250); 138 | } 139 | 140 | // Phase 4 -- Send Host your Escrow Details 141 | console.log('\n-------------------------------------------------------------------------------'); 142 | console.log('| PHASE 4: Funding our side of the bet... |'); 143 | console.log('-------------------------------------------------------------------------------'); 144 | 145 | let escrowBuf = CoinFlipShared.buildCoinFlipClientEscrowScript( this.betState.hostMultisigPubKey, this.wallet.pubkey); 146 | //console.log(BITBOX.Script.toASM(escrowBuf)); 147 | this.wallet.utxo = await Core.getUtxoWithRetry( this.wallet.address); 148 | let escrowTxid = await Core.createEscrow(this.wallet, escrowBuf, this.betState.amount); 149 | console.log('\nOur escrow address has been funded! \n(txn: ' + escrowTxid); 150 | await Utils.sleep(500); // need to wait for BITBOX mempool to sync 151 | 152 | console.log('Sending our escrow details and signatures to host...'); 153 | let betScriptBuf = CoinFlipShared.buildCoinFlipBetScriptBuffer( this.betState.hostMultisigPubKey, this.betState.hostCommitment, this.wallet.pubkey, this.betState.secretCommitment); 154 | 155 | // build bob's signatures 156 | let bobEscrowSig = CoinFlipShared.createEscrowSignature(this.wallet, escrowTxid, escrowBuf, this.betState.amount, betScriptBuf); 157 | 158 | let hostEscrowBuf = CoinFlipShared.buildCoinFlipHostEscrowScript( this.betState.hostMultisigPubKey, this.betState.hostCommitment, this.wallet.pubkey); 159 | let aliceEscrowSig = CoinFlipShared.createEscrowSignature(this.wallet, this.betState.hostP2SHTxId, hostEscrowBuf, this.betState.amount, betScriptBuf); 160 | 161 | // prepare to send Phase 4 OP_RETURN 162 | let phase4MsgTxId = await CoinFlipClient.sendPhase4Message(this.wallet, this.betState.betId, escrowTxid, bobEscrowSig, aliceEscrowSig); 163 | console.log('Message sent. \n(msg txn: ' + phase4MsgTxId +')'); 164 | this.betState.phase = 5; 165 | 166 | console.log('\n-------------------------------------------------------------------------------'); 167 | console.log('| PHASE 5: Waiting for host to broadcast coin flip... |'); 168 | console.log('-------------------------------------------------------------------------------'); 169 | 170 | // 5) keep check to see if the host's P2SH escrow has been spent (indicates bet is created). 171 | 172 | // Determine host's P2SH address 173 | let host_p2sh_hash160 = BITBOX.Crypto.hash160(hostEscrowBuf); 174 | let host_p2sh_scriptPubKey = BITBOX.Script.scriptHash.output.encode(host_p2sh_hash160); 175 | let host_p2sh_Address = BITBOX.Address.fromOutputScript(host_p2sh_scriptPubKey); 176 | 177 | while(this.betState.phase == 5){ 178 | 179 | try{ 180 | var host_p2sh_details = await Core.getAddressDetailsWithRetry(host_p2sh_Address, 30); 181 | } catch(e){ 182 | console.log("\nHost failed to broadcast message in a timely manner."); 183 | this.complete = true; 184 | return; 185 | } 186 | 187 | if((host_p2sh_details.unconfirmedBalanceSat + host_p2sh_details.balanceSat) == 0 && host_p2sh_details.transactions.length == 2) 188 | { 189 | var bet_txnId = host_p2sh_details.transactions[0]; 190 | // TODO: Use a wrapped version of these 191 | var raw_txn = await BITBOX.RawTransactions.getRawTransaction(bet_txnId); 192 | var decoded_bet_txn = await BITBOX.RawTransactions.decodeRawTransaction(raw_txn); //decoded_bet_txn.vin[0].scriptSig.asm 193 | let host_secret; 194 | if(decoded_bet_txn != undefined) 195 | host_secret = CoinFlipClient.parseHostSecretFromASM(decoded_bet_txn.vin[0].scriptSig.asm); 196 | else 197 | continue; 198 | //throw new Error("Raw bet transaction could not be decoded."); 199 | 200 | // must remove right hand zeros so that the numbers aren't always even.. 201 | let client_int_le = Core.readScriptInt32(this.betState.secret); 202 | let host_int_le = Core.readScriptInt32(host_secret); 203 | 204 | console.log("\n " + client_int_le + " <- your secret (shortened)"); 205 | console.log("+ " + host_int_le + " <- host's secret (shortened)"); 206 | console.log("=============================================="); 207 | console.log(" " + (client_int_le + host_int_le + " <- result")); 208 | 209 | let isEven = (client_int_le + host_int_le) % 2 == 0; 210 | 211 | if(isEven) { 212 | console.log("\nYou win! (because the result is EVEN)"); 213 | let winTxnId = await CoinFlipClient.clientClaimWin(this.wallet, host_secret, this.betState.secret, betScriptBuf, bet_txnId, this.betState.amount); 214 | if(winTxnId.length != 64) 215 | { 216 | console.log("We're sorry. Something terrible went wrong when trying to claim your winnings... " + winTxnId); 217 | } else { 218 | console.log("\nYou've been paid! \n(txn: " + winTxnId + ")"); 219 | } 220 | this.complete = true; 221 | return 222 | } 223 | console.log("\nYou Lose... (becuase the result is ODD)"); 224 | this.betState.phase = 6; 225 | } 226 | 227 | await Utils.sleep(250); 228 | } 229 | 230 | if(this.betState.phase == 6) { 231 | 232 | console.log('\n-------------------------------------------------------------------------------'); 233 | console.log('| PHASE 6: Sending resignation to Host... |'); 234 | console.log('-------------------------------------------------------------------------------'); 235 | 236 | // 6) Send resignation to the client 237 | let phase6TxnId = await CoinFlipClient.sendPhase6Message(this.wallet, this.betState.betId, this.betState.secret); 238 | if(phase6TxnId.length == 64) 239 | console.log("\nResignation message sent. \n(msg txn: " + phase6TxnId + ")"); 240 | else { 241 | 242 | } 243 | } 244 | 245 | this.complete = true; 246 | } 247 | 248 | static parseHostSecretFromASM(asm: string): Buffer{ 249 | let secretHex = asm.split(" ")[3]; 250 | let secretBuf = Buffer.from(secretHex, 'hex'); 251 | return secretBuf; 252 | } 253 | 254 | static async sendPhase2Message(wallet: WalletKey, betId: string, multisigPubKey: Buffer, secretCommitment: Buffer): Promise{ 255 | let phase2Buf = this.encodePhase2Message(betId, multisigPubKey, secretCommitment); 256 | wallet.utxo = await Core.getUtxoWithRetry( wallet.address); 257 | let txnId = await Core.createOP_RETURN(wallet, phase2Buf); 258 | return txnId; 259 | } 260 | 261 | static async sendPhase4Message(wallet: WalletKey, betId: string, escrowTxid: string, bobEscrowSig: Buffer, aliceEscrowSig: Buffer): Promise{ 262 | let phase4Buf = this.encodePhase4Message(betId, escrowTxid, bobEscrowSig, aliceEscrowSig); 263 | wallet.utxo = await Core.getUtxoWithRetry(wallet.address); 264 | let txnId = await Core.createOP_RETURN(wallet, phase4Buf); 265 | return txnId; 266 | } 267 | 268 | static async sendPhase6Message(wallet: WalletKey, betId: string, clientSecret: Buffer): Promise{ 269 | let phase6Buf = this.encodePhase6Message(betId, clientSecret); 270 | wallet.utxo = await Core.getUtxoWithRetry( wallet.address); 271 | let txnId = await Core.createOP_RETURN(wallet, phase6Buf); 272 | return txnId; 273 | } 274 | 275 | static async clientClaimWin(wallet: WalletKey, hostSecret: Buffer, clientSecret: Buffer, betScript: Buffer, betTxId: string, betAmount: number): Promise { 276 | 277 | let purseAmount = Core.purseAmount(betAmount); 278 | 279 | let clientKey = BITBOX.ECPair.fromWIF(wallet.wif) 280 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 281 | 282 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2SH: 1 }); 283 | 284 | let hashType = 0xc1 // transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY | transactionBuilder.hashTypes.SIGHASH_ALL 285 | let satoshisAfterFee = purseAmount - byteCount - betScript.length - 142; 286 | transactionBuilder.addInput(betTxId, 0); 287 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(wallet.utxo[0].cashAddress), satoshisAfterFee); 288 | 289 | let tx = transactionBuilder.transaction.buildIncomplete(); 290 | 291 | // Sign bet tx 292 | let sigHash = tx.hashForWitnessV0(0, betScript, purseAmount, hashType); 293 | let clientSig = clientKey.sign(sigHash).toScriptSignature(hashType); 294 | 295 | let redeemScriptSig: number[] = [] 296 | 297 | // client signature 298 | redeemScriptSig.push(clientSig.length) 299 | clientSig.forEach((item, index) => { redeemScriptSig.push(item); }); 300 | 301 | // host secret 302 | redeemScriptSig.push(hostSecret.length); 303 | hostSecret.forEach((item, index) => { redeemScriptSig.push(item); }); 304 | 305 | // client secret 306 | redeemScriptSig.push(clientSecret.length); 307 | clientSecret.forEach((item, index) => { redeemScriptSig.push(item); }); 308 | 309 | redeemScriptSig.push(0x00); // zero is client wins mode 310 | 311 | if (betScript.length > 75) redeemScriptSig.push(0x4c); 312 | redeemScriptSig.push(betScript.length); 313 | betScript.forEach((item, index) => { redeemScriptSig.push(item); }); 314 | 315 | let redeemScriptSigBuf = Buffer.from(redeemScriptSig); 316 | tx.setInputScript(0, redeemScriptSigBuf); 317 | 318 | // uncomment for viewing script hex 319 | // console.log("Bet redeem script hex: " + redeemScriptSig.toString('hex')); 320 | // console.log("Bet Script Hex: " + betScript.toString('hex')); 321 | console.log("Winning amount after fees: " + satoshisAfterFee); 322 | 323 | let hex: string = tx.toHex(); 324 | 325 | let txId = await Core.sendRawTransaction(hex); 326 | return txId; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # ChainBet - protocol spec version 0.3 2 | 3 | ## Abstract 4 | 5 | ChainBet is a proposed Bitcoin Cash protocol to enable on-chain betting. This initial proposal focuses on a simple coin flip bet, but the principles could be extrapolated to enable more elaborate configurations. The protocol consists of 2 components: a commitment scheme to enable a trustless wager, and an on-chain messaging system to facilitate communication. 6 | 7 | ## Motivation 8 | 9 | Since paleolithic times, humans have engaged in games of chance, and probably always will. Blockchain technology can increase the fairness, transparency, and safety of these activities. The Bitcoin Cash ecosystem can gain more users and more transaction volume by providing a trustless gaming mechanism. 10 | 11 | ## Summary 12 | 13 | To perform a trustless coinflip wager, Alice and Bob should each create secret values. If the sum of the values is odd, Alice wins the bet. If the sum of the values is even, Bob wins the bet. Alice and Bob will use a cryptographic scheme where both parties can lock in the bet and reveal their secrets fairly. 14 | 15 | In addition, there is a messaging component of the protocol so that the parties do not have to send any extraneous communication through the internet. They can use the blockchain for everything. Moreover, multiple parties can participate and match themselves into coinflip betting pairs, thus creating a virtual casino that functions nearly entirely on the blockchain. 16 | 17 | An app will still be required on the user end to use the protocol, to send transactions from the users’ addresses. 18 | 19 | # Commitment Scheme 20 | 21 | ## Overview 22 | 23 | Alice and Bob begin by each creating secret values, and then creating a hash of those values which serve as cryptographic commitments. 24 | 25 | The scheme is based on the parties funding a multisignature address and the principle that Bob has the responsibility to claim his winnings after learning Alice’s secret. If he does not, Alice would be able to claim a win by default based on a time lock. The default win would be necessary since Bob would be motivated to do nothing (keep his secret a secret) once he discovers he has lost the bet. 26 | 27 | But there is a flaw: What compels Alice to reveal the secret, knowing she would be guaranteed to win by “time out” if she doesn’t reveal it? Note that Alice’s secret can’t be part of the multisignature script because Bob would then know it before he has committed funds. And there is no apparent way to allow Bob to cancel the script if Alice doesn’t share her secret since he could always claim it wasn’t shared if he sees a loss. 28 | 29 | To solve this problem, we add some extra steps prior to the **funding transaction** which moves funds to the primary multiignature address. Essentially, Alice and Bob jointly create the funding transaction using inputs from both Alice and Bob, with Alice's input coming from a script that requires revealing her secret. Bob's signatures should be collected first, ensuring that Alice's secret is not revealed prior to Bob committing his funds. 30 | 31 | In addition, we will provide a means of preventing double spend attacks with the use of escrow addresses. 32 | 33 | ## Escrow Preparation 34 | 35 | To prepare, Alice and Bob will each set up a temporary "escrow" address to be used as a holding place before creating the funding transaction. Both escrow addreses will require both Alice's and Bob's signatures. Each escrow address will also have its own emergency timelock option to retrieve the funds if one of the parties stops cooperating. 36 | 37 | ## Alice Escrow Address 38 | 39 | The main purpose of Alice's escrow address is to reveal Alice's **Secret A** when spent. It will require both Alice and Bob's signature plus the secret. By requiring the secret, it reveals it to Bob, thus fulfilling that part of the commitment scheme. 40 | 41 | Alternatively, Alice can retrieve the funds unilaterally after 8 confirmations in the situation when Bob abandonds the betting process. 42 | 43 | **Script:** 44 | 45 | ``` 46 | OP_IF "8 blocks" 47 | OP_CHECKSEQUENCEVERIFY 48 | OP_ELSE 49 | OP_HASH160 OP_EQUALVERIFY 50 | OP_2 51 | OP_2 OP_CHECKMULTISIG 52 | OP_ENDIF 53 | ``` 54 | ## Bob Escrow Address 55 | 56 | The main purpose of Bob's escrow address is to prevent Bob from double spending. Once the funding transaction is created, Alice's secret will be revealed. If Bob sees that he has a loss, he could theoretically attempt to double spend his input to the funding transaction, thereby invalidating it. 57 | 58 | By first moving the funds into escrow and requiring Alice's signature in addition to Bob's to spend, Bob cannot on his own attempt a doublespend. 59 | 60 | Of course, it is necessary for the transaction that funds the escrow account to have at least 1 confirmation before the funding transaction is attempted, because otherwise Bob could doublespend that, invalidating both itself and the child transaction (the funding transaction). 61 | 62 | Alternatively, Bob can also retrieve his own funds unilaterally after 8 confirmations in the situation when Alice abandonds the betting process. 63 | 64 | **Script:** 65 | 66 | ``` 67 | OP_IF "8 blocks" 68 | OP_CHECKSEQUENCEVERIFY 69 | OP_ELSE 70 | OP_2 71 | OP_2 OP_CHECKMULTISIG 72 | OP_ENDIF 73 | ``` 74 | 75 | ## Why Alice's Escrow Needs Bob's Signature 76 | 77 | Bob's escrow requires Alice's signature for the simple reason that it prevents Bob from double spending his input to the funding transaction. However, it is not immediately obvious why Alice should also have Bob's signature, since she is not the one with the easy double spend opportunity: Once the funding transaction happens, Alice's secret is revealed and if Bob won, he could simply avoid any double spend attacks by waiting for the funding transaction to get 1 confirmation before claiming the win. 78 | 79 | However, this is inefficient because it requires more confirmations. Since funding Bob's escrow account already requires waiting for a confirmation, it makes sense to use that time to prevent Alice's funds from being double spent as well. This renders it unnecessary for Bob to wait for an additional confirmation after the funding transaction is sent before claiming a win. 80 | 81 | ## OP_RETURN Communication Messages 82 | 83 | The ChainBet protocol operates through 6 phases. Most phases require a communication message to be sent across the network. This is accomplished by broadcasting a BCH trasnsaction that contains an OP_RETURN output. 84 | 85 | Each OP_RETURN transaction will be assumed to have outputs going back to the sender's originating address. The output amount back to the sender will simply be the entire balance (for standard UTXOs) of the sender minus the txn fee. This approach is simple and it minimizes the impacts to the UTXO set over time, making each OP_RETURN message like a sweep transaction. 86 | 87 | The OP_RETURN command is followed by a series of pushdata commands (each pushdata is represented using tags `<...>`) in the following order: 88 | 89 | ` ... ` 90 | 91 | The protocol_id is a [standard Terab 4-byte prefix](https://github.com/Lokad/Terab/blob/master/spec/opreturn-prefix-guideline.md) with a value of **0x00424554** (ASCII equivalent of "BET"). Following the Terab guidelines, the operation will utilize a pair of OP_PUSHDATA codes: one for the protocol_id and another for everything else in the payload. 92 | 93 | The version_id is a one-byte value that can be used to upgrade the protocol in the future. Currently, it is **0x01**. protocol_id and version_id should be present in all OP_RETURN payloads but will be ommitted in the subsequent message detail descriptions. 94 | 95 | Note that only Phase 5 does not include an OP_RETURN message but consists of the main funding transaction itself. 96 | 97 | # Protocol Phases 98 | 99 | ## Phase 1: Bet Offer Announcement 100 | 101 | Alice advertises to the network that she is hosting a bet for anyone to accept. She will wait until someone responds to accept her bet. 102 | 103 | NOTE: This unique identifier for this Bet will be the transaction id of the txn containing this phase 1 message, herein referred to as . 104 | 105 | 106 | OP_RETURN OUTPUT: 107 | 108 | | Bytes | Name | Hex Value | Description | 109 | | ------ |--------------------------| ----------|-----------------| 110 | | 3 | Version, Bet Type, Phase | 0x010101 | Protocol Version 1, Bet Type 1, Phase 1 - "Bet Announcement" | 111 | | 8 | Amount | \ | Bet amount in Satohis for each client. | 112 | | 20 | Alice Commitment | \ | Alice's commitment so Bob can build Alice's P2SH escrow address | 113 | | 20 | Target Address | \ | Optional. Restricts offer to a specific bet client's address in Hash160 form. | 114 | 115 | 116 | ## Phase 2: Bet Client Acceptance 117 | 118 | After Bob detects Alice’s Phase 1 message, Bob responds to Alice’s bet announcement letting Alice know that he accepts her bet. (there may have been others on the network whom also accept the bet, but Bob happens to be the first to accept her bet) 119 | 120 | NOTE: This transaction ID of the transaction containing this phase 2 message, herein referred to as the . 121 | 122 | OP_RETURN OUTPUT: 123 | 124 | | Bytes | Name | Hex Value | Description | 125 | | ------ |--------------------------| ----------|-----------------| 126 | | 3 | Version, Bet Type, Phase | 0x010102 | Phase 2 is " Bet Client Acceptance" | 127 | | 32 | Bet Txn Id |\ | This lets Alice know Bob wants to bet. | 128 | | 33 | Bob Multi-sig Pub Key | \| This is the compressed public key that Alice should use when creating her p2sh input for the bet. | 129 | | 20 | Bob Commitment | \ | Bob's commitment so Alice can build Bob's P2SH escrow address | 130 | 131 | 132 | ## Phase 3: Bet Host Funding 133 | 134 | Alice detects Bob's Phase 2 message. She then prepares her escrow address and funds it. Then she sends this announcement to Bob, indicating acceptance of the bet with Bob (thus others whom also accepted Alice's original announcement will know that they are being ignored and should look for other bets). 135 | 136 | OP_RETURN OUTPUT: 137 | 138 | | Bytes | Name | Hex Value | Description | 139 | | ----- |--------------------------| ----------|-----------------| 140 | | 3 | Version, Bet Type, Phase | 0x010103 | Phase 3 is " Bet Host Funding" | 141 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob have multiple bets going.| 142 | | 32 | Client Txn Id | \| This is Alice's acknowledgement that Bob is the client. | 143 | | 32 | Host P2SH txid | \ | (Alice Escrow Address). Bob needs this, so he can verify the Bet Host has committed her funds to the bet, and the bet is real. Bob will also need this so he can also see what Alice’s committed value is once she tries to spend the final bet. | 144 | | 33 | Host Multi-sig Pub Key | \ | Bob needs this so he can construct his own P2SH (multisig) input. Bob also needs this so he can deterministically compute the Host's P2SH (multisig with value commitment). | 145 | 146 | ## Phase 4: Bet Client Funding 147 | 148 | After Bob detects the Phase 3 message from Alice, he will know that he is in fact the bet client. He can first check that Alice created her P2SH address with the correct amount so he knows the bet is real. 149 | 150 | Bob can then create his own (escrow) P2SH address which will be used as an input to the main funding transaction. Bob funds this address, and announces that he has submitted his P2SH to cover his side of the bet, and passes the signatures for both P2SH addresses to Alice. 151 | 152 | Bob should now monitor the network, looking first for the spending of Alice's escrow address in order to see Alice's secret value. Then he can calculate if we won the bet. 153 | 154 | OP_RETURN OUTPUT: 155 | 156 | | Bytes | Name | Hex Value | Description | 157 | | ------ |--------------------------| ----------|-----------------| 158 | | 3 | Version, Bet Type, Phase | 0x010104 | Phase 4 is " Bet Client Funding" | 159 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob have multiple bets going.| 160 | | 32 | Client Txn Id | \| Alice will need this so she can try to spend Bob’s side of the bet. | 161 | | 71-72 | Client Signature 1 | \| Bob's signature. Alice will need this to sign **Bob's** P2SH funds so she can submit the bet transaction to the network. Sigtype hash ALL \| ANYONECANPAY | 162 | | 71-72 | Client Signature 2 | \| Bob's signature. Alice will need this to sign **Alice's** P2SH funds so she can submit the bet transaction to the network Sigtype hash ALL \| ANYONECANPAY | 163 | 164 | ## Phase 5: Funding Transaction 165 | 166 | Alice should now have both of Bob's signatures, so she can spend from both escrow addresses to create the (main) funding transaction. Alice should wait until both escrow transactions have at least one confirmation before broadcasting the funding transaction. Otherwise, she risks a double spend attack where Bob learns her secret, discovers he has lost the bet, and then tries to double spend the input to the Bob escrow account. 167 | 168 | Using a shorthand notation where Alice's Secret is "A" and the hash is "HASH_A", and Bob's Secret is "B" and its hash is "HASH_B", 169 | then we can say that the main P2SH address is a script that allows the funds to be spent if: 170 | 171 | Alice can sign for her public key AND Hash(A)= HASH_A AND Hash(B)=HASH_B AND A+B is an odd number. 172 | 173 | ...or if Bob can sign for his public key AND Hash(A)= HASH_A AND Hash(B)=HASH_B AND A+B is an even number. 174 | 175 | ...or if Alice can sign for her public key and the transaction is more than 4 blocks old. 176 | 177 | **Bet Script:** 178 | 179 | ``` 180 | OP_IF 181 | OP_IF 182 | OP_HASH160 OP_EQUALVERIFY 183 | OP_OVER OP_HASH160 OP_EQUALVERIFY 184 | OP_4 OP_SPLIT OP_DROP OP_BIN2NUM 185 | OP_SWAP OP_4 OP_SPLIT OP_DROP OP_BIN2NUM 186 | OP_ADD OP_ABS 187 | OP_2 OP_MOD OP_1 OP_EQUALVERIFY 188 | OP_ELSE 189 | "4 blocks" OP_CHECKSEQUENCEVERIFY OP_DROP 190 | OP_ENDIF 191 | OP_CHECKSIG 192 | OP_ELSE 193 | OP_DUP OP_HASH160 OP_EQUALVERIFY 194 | OP_OVER OP_HASH160 OP_EQUALVERIFY 195 | OP_4 OP_SPLIT OP_DROP OP_BIN2NUM 196 | OP_SWAP OP_4 OP_SPLIT OP_DROP OP_BIN2NUM 197 | OP_ADD 198 | OP_2 OP_MOD OP_0 OP_EQUALVERIFY 199 | OP_CHECKSIG 200 | OP_ENDIF 201 | ``` 202 | The 256 bit secret numbers are converted to signed 32 bit integers using the new OP_SPLIT and OP_BIN2NUM. OP_MOD is also used to determine if the result is even or odd. 203 | 204 | By monitoring the blockchain, Bob can determine if he or Alice can claim the funds. 205 | 206 | 207 | ## Phase 6: Bet Client Resignation 208 | 209 | After Bob detects Alice’s P2SH has been spent Bob will know the final bet transaction has been submitted to the network. This message reveals Bob’s secret value so that Alice can immediately claim the funds if she won. 210 | 211 | This final message is not required for the Smart Contract to function properly. It is purely to enhance the user experience for Alice if she wins. If Bob did not send this message after he loses then Alice could still spend the bet after the bet's time lock expires to claim the funds. 212 | 213 | 214 | OP_RETURN OUTPUT: 215 | 216 | | Bytes | Name | Hex Value | Description | 217 | | ----- |--------------------------| ----------|-----------------| 218 | | 3 | Version, Bet Type, Phase | 0x010106 | Phase 6 is " Bet Client Resignation" | 219 | | 32 | Bet Txn Id |\ |This is the bet id that is needed in case Alice or Bob have multiple bets going.| 220 | | 32 | Secret Value | \| Bob's secret value revelaed to Alice so she can see the math behind the bet's outcome. | 221 | 222 | ## Phase 7: Bet Claim 223 | 224 | When the bet winnings are claimed by either participant a final message should be included as the second txn output in order to simplify analysis of protocol performance. The message should simply have the following form: 225 | 226 | ` ` 227 | 228 | # Considerations 229 | 230 | 1. Implementations can optionally choose not to require that escrow transactions get confirmations. There is a trade-off of speed vs security which may be acceptable if the double-spend incentives for mining pools are negligible. 231 | 2. We considered having messaging transactions send a minimal output between Alice and Bob to allow SPV wallets to more easily implement the protocol but decided that since they must monitor the blockchain for at least one of the components, then it is not adding much cost to continue to do that for all messages. This is why messaging transactions send back to themselves. Clients need to monitor OP_RETURN for all phases. 232 | 3. We recommend that implementations spend the winning bet to the originating P2PKH address that created the original OP_RETURN advertisement or acceptance. 233 | 234 | 235 | ## Authors 236 | 237 | Jonald Fyookball 238 | 239 | James Cramer 240 | 241 | Chris Pacia 242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /code/test/chainbet.js: -------------------------------------------------------------------------------- 1 | let fixtures = require('./fixtures/chainbet.json') 2 | //let chai = require('chai'); 3 | let assert = require('assert'); 4 | let chainbet = require('../lib/chainbet'); 5 | 6 | let BITBOXCli = require('bitbox-cli/lib/bitbox-cli').default; 7 | let BITBOX = new BITBOXCli(); 8 | 9 | var script_phase1; 10 | var script_phase1_noAddr; 11 | var script_phase2; 12 | var script_phase3; 13 | var script_phase4; 14 | var script_phase6; 15 | 16 | describe('#chainbet', () => { 17 | 18 | describe('#checkScriptNumberHandling', () => { 19 | fixtures.chainbet.encodePhase2.forEach((fixture) => { 20 | it(`check proper handling of Script 32-bit signed integers`, () => { 21 | 22 | assert.equal(chainbet.Core.readScriptInt32(Buffer('ffffff7f', 'hex')), 2147483647); 23 | assert.equal(chainbet.Core.readScriptInt32(Buffer('ffffffff', 'hex')), -2147483647); 24 | assert.equal(chainbet.Core.readScriptInt32(Buffer('ffffff8f', 'hex')), -268435455); 25 | assert.equal(chainbet.Core.readScriptInt32(Buffer('ffffff3f', 'hex')), 1073741823); 26 | assert.equal(chainbet.Core.readScriptInt32(Buffer('ffffffbf', 'hex')), -1073741823); 27 | assert.equal(chainbet.Core.readScriptInt32(Buffer('00000000', 'hex')), 0); 28 | assert.equal(chainbet.Core.readScriptInt32(Buffer('00000080', 'hex')), 0); 29 | assert.equal(chainbet.Core.readScriptInt32(Buffer('01000000', 'hex')), 1); 30 | assert.equal(chainbet.Core.readScriptInt32(Buffer('01000080', 'hex')), -1); 31 | 32 | }); 33 | }); 34 | }); 35 | 36 | describe('#checkSecretNumbers', () => { 37 | fixtures.chainbet.encodePhase2.forEach((fixture) => { 38 | it(`check validity of secret numbers`, () => { 39 | 40 | var secret = Buffer('ffffff7f', 'hex'); 41 | assert.equal(chainbet.Core.secretIsValid(secret), false); 42 | 43 | var secret = Buffer('ffffffff', 'hex'); 44 | assert.equal(chainbet.Core.secretIsValid(secret), false); 45 | 46 | var secret = Buffer('ffffff3f', 'hex'); 47 | assert.equal(chainbet.Core.secretIsValid(secret), true); 48 | 49 | var secret = Buffer('ffffff8f', 'hex'); 50 | assert.equal(chainbet.Core.secretIsValid(secret), true); 51 | 52 | var secret = Buffer('ffffffbf', 'hex'); 53 | assert.equal(chainbet.Core.secretIsValid(secret), true); 54 | 55 | var secret = Buffer('00000000', 'hex'); 56 | assert.equal(chainbet.Core.secretIsValid(secret), true); 57 | 58 | var secret = Buffer('00000080', 'hex'); 59 | assert.equal(chainbet.Core.secretIsValid(secret), true); 60 | 61 | var secret = Buffer('01000000', 'hex'); 62 | assert.equal(chainbet.Core.secretIsValid(secret), true); 63 | 64 | var secret = Buffer('01000080', 'hex'); 65 | assert.equal(chainbet.Core.secretIsValid(secret), true); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('#encodePhase1', () => { 71 | fixtures.chainbet.encodePhase1.forEach((fixture) => { 72 | it(`should encodePhase1`, () => { 73 | 74 | var hostCommitment = Buffer('1111111111111111111111111111111111111111', 'hex'); 75 | 76 | // Phase 1 with optional target address 77 | let script_buf = chainbet.Host.encodePhase1Message(1000, hostCommitment, 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'); 78 | assert.equal(script_buf.length <= 223, true); 79 | 80 | script_phase1 = script_buf.toString('hex'); 81 | 82 | let asm_phase1 = BITBOX.Script.toASM(script_buf); 83 | assert.equal(asm_phase1, 'OP_RETURN 00424554 010101 00000000000003e8 1111111111111111111111111111111111111111 a0f531f4ff810a415580c12e54a7072946bb927e'); 84 | 85 | // Phase 1 with no target address 86 | let script_buf_noAddr = chainbet.Host.encodePhase1Message(1000, hostCommitment); 87 | 88 | script_phase1_noAddr = script_buf.toString('hex'); 89 | 90 | let asm_phase1_noAddr = BITBOX.Script.toASM(script_buf_noAddr); 91 | assert.equal(asm_phase1_noAddr, 'OP_RETURN 00424554 010101 00000000000003e8 1111111111111111111111111111111111111111'); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('#encodePhase2', () => { 97 | fixtures.chainbet.encodePhase2.forEach((fixture) => { 98 | it(`should encodePhase2`, () => { 99 | let betTxId = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b' 100 | let multiSigPubKey = Buffer('000000000000000000000000000000000000000000000000000000000000000000', 'hex'); 101 | let secretCommitment = Buffer('1111111111111111111111111111111111111111','hex'); 102 | 103 | let script_buf = chainbet.Client.encodePhase2Message(betTxId, multiSigPubKey, secretCommitment) 104 | assert.equal(script_buf.length <= 223, true); 105 | 106 | script_phase2 = script_buf.toString('hex'); 107 | 108 | let asm_phase2 = BITBOX.Script.toASM(script_buf) 109 | assert.equal(asm_phase2, 'OP_RETURN 00424554 010102 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b 000000000000000000000000000000000000000000000000000000000000000000 1111111111111111111111111111111111111111'); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('#encodePhase3', () => { 115 | fixtures.chainbet.encodePhase3.forEach((fixture) => { 116 | it(`should encodePhase3`, () => { 117 | let betTxId = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b' 118 | let participantTxId = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098' 119 | let hostP2SHTxId = '999e1c837c76a1b7fbb7e57baf87b309960f5ffefbf2a9b95dd890602272f644' 120 | let hostmultiSigPubKey = Buffer.from('111111111111111111111111111111111111111111111111111111111111111111', 'hex'); 121 | let script_buf = chainbet.Host.encodePhase3(betTxId, participantTxId, hostP2SHTxId, hostmultiSigPubKey) 122 | assert.equal(script_buf.length <= 223, true); 123 | 124 | script_phase3 = script_buf.toString('hex'); 125 | 126 | asm_phase3 = BITBOX.Script.toASM(script_buf) 127 | assert.equal(asm_phase3, 'OP_RETURN 00424554 010103 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098 999e1c837c76a1b7fbb7e57baf87b309960f5ffefbf2a9b95dd890602272f644 111111111111111111111111111111111111111111111111111111111111111111'); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('#encodePhase4', () => { 133 | fixtures.chainbet.encodePhase4.forEach((fixture) => { 134 | it(`should encodePhase4`, () => { 135 | let betTxId = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'; 136 | let participantTxId = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098'; 137 | 138 | // added 1 byte padding for 72 byte signatures 139 | let participantSig1 = Buffer('3045022100c12a7d54972f26d14cb311339b5122f8c187417dde1e8efb6841f55c34220ae0022066632c5cd4161efa3a2837764eee9eb84975dd54c2de2865e9752585c53e7cc1', 'hex'); 140 | let participantSig2 = Buffer('3045022100c12a7d54972f26d14cb311339b5122f8c187417dde1e8efb6841f55c34220ae0022066632c5cd4161efa3a2837764eee9eb84975dd54c2de2865e9752585c53e7cc1', 'hex'); 141 | let script_buf = chainbet.Client.encodePhase4(betTxId, participantTxId, participantSig1, participantSig2); 142 | assert.equal(script_buf.length <= 223, true); 143 | 144 | script_phase4 = script_buf.toString('hex'); 145 | 146 | asm_phase4 = BITBOX.Script.toASM(script_buf); 147 | assert.equal(asm_phase4, 'OP_RETURN 00424554 010104 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098 3045022100c12a7d54972f26d14cb311339b5122f8c187417dde1e8efb6841f55c34220ae0022066632c5cd4161efa3a2837764eee9eb84975dd54c2de2865e9752585c53e7cc1 3045022100c12a7d54972f26d14cb311339b5122f8c187417dde1e8efb6841f55c34220ae0022066632c5cd4161efa3a2837764eee9eb84975dd54c2de2865e9752585c53e7cc1'); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('#encodePhase6', () => { 153 | fixtures.chainbet.encodePhase6.forEach((fixture) => { 154 | it(`should encodePhase6`, () => { 155 | let betTxId = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'; 156 | let secrVal = Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); 157 | let script_buf = chainbet.Client.encodePhase6(betTxId, secrVal); 158 | assert.equal(script_buf.length <= 223, true); 159 | 160 | script_phase6 = script_buf.toString('hex'); 161 | 162 | asm_phase6 = BITBOX.Script.toASM(script_buf) 163 | assert.equal(asm_phase6, 'OP_RETURN 00424554 010106 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b 0000000000000000000000000000000000000000000000000000000000000000'); 164 | }); 165 | }); 166 | }); 167 | 168 | describe('#decodePhase1', () => { 169 | fixtures.chainbet.decode.forEach((fixture) => { 170 | it(`should decodePhase1`, () => { 171 | 172 | // Decode Phase 1 (with optional address) 173 | //var phaseData = Buffer(script_phase1.split('004245544c')[1], 'hex').slice(1,script_phase1.split('00424554')[1].length-1); 174 | var phaseData = []; 175 | BITBOX.Script.toASM(Buffer(script_phase1, 'hex')).split(" ") 176 | .forEach((item, index) => { 177 | if(index > 1) 178 | phaseData.push(Buffer(item, 'hex')); 179 | }); 180 | let actual_phase1 = chainbet.Core.decodePhaseData(phaseData); 181 | let expected_phase1 = { 182 | betType: 0x01, 183 | version: 0x01, 184 | phase: 0x01, 185 | amount: 1000, 186 | hostCommitment: Buffer('1111111111111111111111111111111111111111', 'hex'), 187 | address: 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c' 188 | }; 189 | assert.equal(actual_phase1.hostCommitment.length, 20); 190 | assert.equal(actual_phase1.address.length, 54); 191 | assert.equal(actual_phase1.betType, actual_phase1.betType); 192 | assert.equal(actual_phase1.version, expected_phase1.version); 193 | assert.equal(actual_phase1.phase, expected_phase1.phase); 194 | assert.equal(actual_phase1.betType, expected_phase1.betType); 195 | assert.equal(actual_phase1.amount, expected_phase1.amount); 196 | assert.equal(actual_phase1.hostCommitment.toString('hex'), expected_phase1.hostCommitment.toString('hex')) 197 | assert.equal(actual_phase1.address, expected_phase1.address); 198 | 199 | // Decode Phase 1 (without optional address) 200 | phaseData = []; 201 | BITBOX.Script.toASM(Buffer(script_phase1_noAddr, 'hex')).split(" ") 202 | .forEach((item, index) => { 203 | if(index > 1) 204 | phaseData.push(Buffer(item, 'hex')); 205 | }); 206 | let actual_phase1_noAddr = chainbet.Core.decodePhaseData(phaseData); 207 | let expected_phase1_noAddr = { 208 | betType: 0x01, 209 | version: 0x01, 210 | phase: 0x01, 211 | amount: 1000, 212 | hostCommitment: Buffer('1111111111111111111111111111111111111111', 'hex') 213 | }; 214 | assert.equal(actual_phase1_noAddr.hostCommitment.length, 20); 215 | assert.equal(actual_phase1_noAddr.betType, actual_phase1_noAddr.betType); 216 | assert.equal(actual_phase1_noAddr.version, expected_phase1_noAddr.version); 217 | assert.equal(actual_phase1_noAddr.phase, expected_phase1_noAddr.phase); 218 | assert.equal(actual_phase1_noAddr.betType, expected_phase1_noAddr.betType); 219 | assert.equal(actual_phase1_noAddr.amount, actual_phase1_noAddr.amount); 220 | assert.equal(actual_phase1_noAddr.hostCommitment.toString('hex'), expected_phase1.hostCommitment.toString('hex')) 221 | }); 222 | }); 223 | }); 224 | 225 | describe('#decodePhase2', () => { 226 | fixtures.chainbet.decode.forEach((fixture) => { 227 | it(`should decodePhase2`, () => { 228 | 229 | // Decode Phase 2 230 | var phaseData = []; 231 | BITBOX.Script.toASM(Buffer(script_phase2, 'hex')).split(" ") 232 | .forEach((item, index) => { 233 | if(index > 1) 234 | phaseData.push(Buffer(item, 'hex')); 235 | }); 236 | let actual_phase2 = chainbet.Core.decodePhaseData(phaseData); 237 | var expected_phase2 = { 238 | betType: 0x01, 239 | version: 0x01, 240 | phase: 0x02, 241 | betTxId: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', 242 | multisigPubKey: '000000000000000000000000000000000000000000000000000000000000000000', 243 | secretCommitment: '1111111111111111111111111111111111111111' 244 | }; 245 | assert.equal(actual_phase2.betTxId.length, 32); 246 | assert.equal(actual_phase2.multisigPubKey.length, 33); 247 | assert.equal(actual_phase2.secretCommitment.length, 20); 248 | assert.equal(actual_phase2.betType, actual_phase2.betType); 249 | assert.equal(actual_phase2.version, expected_phase2.version); 250 | assert.equal(actual_phase2.phase, expected_phase2.phase); 251 | assert.equal(actual_phase2.betTxId.toString('hex'), expected_phase2.betTxId); 252 | assert.equal(actual_phase2.multisigPubKey.toString('hex'), expected_phase2.multisigPubKey); 253 | 254 | }); 255 | }); 256 | }); 257 | 258 | describe('#decodePhase3', () => { 259 | fixtures.chainbet.decode.forEach((fixture) => { 260 | it(`should decodePhase3`, () => { 261 | 262 | // Decode Phase 3 263 | var phaseData = []; 264 | BITBOX.Script.toASM(Buffer(script_phase3, 'hex')).split(" ") 265 | .forEach((item, index) => { 266 | if(index > 1) 267 | phaseData.push(Buffer(item, 'hex')); 268 | }); 269 | let actual_phase3 = chainbet.Core.decodePhaseData(phaseData); 270 | let expected_phase3 = { 271 | betType: 0x01, 272 | version: 0x01, 273 | phase: 0x03, 274 | betTxId: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', 275 | participantOpReturnTxId: '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098', 276 | hostP2SHTxId: '999e1c837c76a1b7fbb7e57baf87b309960f5ffefbf2a9b95dd890602272f644', 277 | hostMultisigPubKey: '111111111111111111111111111111111111111111111111111111111111111111' 278 | } 279 | assert.equal(actual_phase3.betTxId.length, 32); 280 | assert.equal(actual_phase3.participantOpReturnTxId.length, 32); 281 | assert.equal(actual_phase3.hostP2SHTxId.length, 32); 282 | assert.equal(actual_phase3.hostMultisigPubKey.length, 33); 283 | assert.equal(actual_phase3.betType, actual_phase3.betType); 284 | assert.equal(actual_phase3.version, expected_phase3.version); 285 | assert.equal(actual_phase3.phase, expected_phase3.phase); 286 | assert.equal(actual_phase3.betTxId.toString('hex'), expected_phase3.betTxId); 287 | assert.equal(actual_phase3.participantOpReturnTxId.toString('hex'), expected_phase3.participantOpReturnTxId); 288 | assert.equal(actual_phase3.hostP2SHTxId.toString('hex'), expected_phase3.hostP2SHTxId); 289 | assert.equal(actual_phase3.hostMultisigPubKey.toString('hex'), expected_phase3.hostMultisigPubKey); 290 | 291 | }); 292 | }); 293 | }); 294 | 295 | describe('#decodePhase4', () => { 296 | fixtures.chainbet.decode.forEach((fixture) => { 297 | it(`should decodePhase4`, () => { 298 | // Decode Phase 4 299 | var phaseData = []; 300 | BITBOX.Script.toASM(Buffer(script_phase4, 'hex')).split(" ") 301 | .forEach((item, index) => { 302 | if(index > 1) 303 | phaseData.push(Buffer(item, 'hex')); 304 | }); 305 | let actual_phase4 = chainbet.Core.decodePhaseData(phaseData); 306 | let expected_phase4 = { 307 | betType: 0x01, 308 | version: 0x01, 309 | phase: 0x04, 310 | betTxId: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', 311 | participantP2SHTxId: '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098', 312 | participantSig1: Buffer('3045022100c12a7d54972f26d14cb311339b5122f8c187417dde1e8efb6841f55c34220ae0022066632c5cd4161efa3a2837764eee9eb84975dd54c2de2865e9752585c53e7cc1', 'hex'), 313 | participantSig2: Buffer('3045022100c12a7d54972f26d14cb311339b5122f8c187417dde1e8efb6841f55c34220ae0022066632c5cd4161efa3a2837764eee9eb84975dd54c2de2865e9752585c53e7cc1', 'hex') 314 | } 315 | assert.equal(actual_phase4.participantP2SHTxId.length, 32); 316 | assert.equal(actual_phase4.participantSig1.length, expected_phase4.participantSig1.length); 317 | assert.equal(actual_phase4.participantSig2.length, expected_phase4.participantSig2.length); 318 | assert.equal(actual_phase4.betType, expected_phase4.betType); 319 | assert.equal(actual_phase4.version, expected_phase4.version); 320 | assert.equal(actual_phase4.phase, expected_phase4.phase); 321 | assert.equal(actual_phase4.betTxId.toString('hex'), expected_phase4.betTxId); 322 | assert.equal(actual_phase4.participantP2SHTxId.toString('hex'), expected_phase4.participantP2SHTxId); 323 | assert.equal(actual_phase4.participantSig1.toString('hex'), expected_phase4.participantSig1.toString('hex')); 324 | assert.equal(actual_phase4.participantSig2.toString('hex'), expected_phase4.participantSig2.toString('hex')); 325 | }); 326 | }); 327 | }); 328 | 329 | describe('#decodePhase6', () => { 330 | fixtures.chainbet.decode.forEach((fixture) => { 331 | it(`should decodePhase6`, () => { 332 | // Decode Phase 6 333 | var phaseData = []; 334 | BITBOX.Script.toASM(Buffer(script_phase6, 'hex')).split(" ") 335 | .forEach((item, index) => { 336 | if(index > 1) 337 | phaseData.push(Buffer(item, 'hex')); 338 | }); let actual_phase6 = chainbet.Core.decodePhaseData(phaseData); 339 | let expected_phase6 = { 340 | betType: 0x01, 341 | version: 0x01, 342 | phase: 0x06, 343 | betTxId: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', 344 | secretValue: '0000000000000000000000000000000000000000000000000000000000000000' 345 | } 346 | assert.equal(actual_phase6.betTxId.length, 32); 347 | assert.equal(actual_phase6.secretValue.length, 32); 348 | assert.equal(actual_phase6.betType, actual_phase6.betType); 349 | assert.equal(actual_phase6.version, expected_phase6.version); 350 | assert.equal(actual_phase6.phase, expected_phase6.phase); 351 | assert.equal(actual_phase6.betTxId.toString('hex'), expected_phase6.betTxId); 352 | assert.equal(actual_phase6.secretValue.toString('hex'), expected_phase6.secretValue); 353 | }); 354 | }); 355 | }); 356 | 357 | describe('#amount_2_Hex', () => { 358 | fixtures.chainbet.decode.forEach((fixture) => { 359 | it(`should convert number amount to 8 byte hex big-endian`, () => { 360 | let amount = 10000000000 // 100 BCH 361 | let hex = chainbet.Utils.amount_2_hex(amount) 362 | assert.equal(hex.toString('hex'), '00000002540be400'); 363 | }); 364 | }); 365 | }); 366 | 367 | describe('#hash160_2_cashAddr', () => { 368 | fixtures.chainbet.decode.forEach((fixture) => { 369 | it(`should convert public key hash160 to bitcoin cash address format`, () => { 370 | let expected_address = 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'; 371 | let actual_pkHash160 = 'a0f531f4ff810a415580c12e54a7072946bb927e'; 372 | let networkByte = 0x00; 373 | let actual_address = chainbet.Utils.hash160_2_cashAddr(actual_pkHash160, networkByte); 374 | assert.equal(actual_address, expected_address); 375 | }); 376 | }); 377 | }); 378 | }); 379 | -------------------------------------------------------------------------------- /code/src/coinfliphost.ts: -------------------------------------------------------------------------------- 1 | import BITBOXCli from 'bitbox-cli/lib/bitbox-cli'; 2 | let BITBOX = new BITBOXCli(); 3 | 4 | var inquirer = require('inquirer'); 5 | 6 | import { Core, WalletKey, BetState, Phase2Data, Phase4Data, Phase6Data } from './core'; 7 | import { Utils } from './utils'; 8 | import { Host } from './host'; 9 | import { CoinFlipShared } from './coinflipshared'; 10 | import { MessageFeed } from './messagefeed'; 11 | import { AddressDetailsResult } from 'bitbox-cli/lib/Address'; 12 | 13 | let bip68 = require('bip68'); 14 | 15 | // CoinFlipClient class represents 1 bet's state management 16 | export class CoinFlipHost extends Host { 17 | 18 | wallet: WalletKey; 19 | isDebugging: boolean; 20 | betState: BetState; 21 | complete: boolean; 22 | feed: MessageFeed; 23 | 24 | constructor(wif: string, betAmount: number, feed: MessageFeed, debug: boolean = false) { 25 | super(); 26 | let ecpair = BITBOX.ECPair.fromWIF(wif); 27 | 28 | this.wallet = { 29 | wif: wif, 30 | pubkey: BITBOX.ECPair.toPublicKey(ecpair), 31 | address: BITBOX.ECPair.toCashAddress(ecpair) 32 | }; 33 | 34 | this.isDebugging = debug; 35 | 36 | this.betState = { 37 | amount: betAmount, 38 | phase: 1, 39 | }; 40 | this.betState.phase = 1; 41 | this.complete = false; 42 | 43 | this.feed = feed; 44 | } 45 | 46 | async run(){ 47 | 48 | this.betState.secret = Core.generateSecretNumber(); 49 | this.betState.secretCommitment = BITBOX.Crypto.hash160(this.betState.secret); 50 | console.log("Your secret number (shortened) is: " + Core.readScriptInt32(this.betState.secret)); 51 | 52 | // Phase 1 -- Send out a bet announcement 53 | console.log('\n-------------------------------------------------------------------------------'); 54 | console.log('| PHASE 1: Sending coin flip bet announcement... |'); 55 | console.log('-------------------------------------------------------------------------------') 56 | 57 | this.betState.betId = await CoinFlipHost.sendPhase1Message(this.wallet, this.betState.amount, this.betState.secretCommitment); 58 | if(this.betState.betId.length == 64){ 59 | console.log('\nCoinflip announcement sent. \n(msg txn: ' + this.betState.betId + ')'); 60 | this.betState.phase = 2; 61 | } else { 62 | console.log("An error occured: " + this.betState.betId); 63 | this.complete = true; 64 | return; 65 | } 66 | 67 | // Phase 2 -- Wait for a bet client to accept... 68 | console.log('\n-------------------------------------------------------------------------------'); 69 | console.log('| PHASE 2: Waiting for someone to accept your bet... |'); 70 | console.log('-------------------------------------------------------------------------------'); 71 | 72 | while(this.betState.phase == 2){ 73 | 74 | let betId = this.betState.betId; 75 | 76 | var clientPhase2Messages = this.feed.messages.filter(function(item){ 77 | if(item.phase == 2){ 78 | let p2item = item; 79 | if(p2item.betTxId.toString('hex') == betId){ return true; } 80 | } 81 | return false; 82 | }); 83 | 84 | if(clientPhase2Messages.length > 0){ 85 | // ignore target address field from host for now... 86 | // accept first bet detected 87 | let bet = clientPhase2Messages[clientPhase2Messages.length-1]; 88 | this.betState.clientTxId = bet.op_return_txnId; 89 | this.betState.clientmultisigPubKey = bet.multisigPubKey; 90 | this.betState.clientCommitment = bet.secretCommitment; 91 | 92 | // NOTE: to simplify we will automatically accept the first bet host we see 93 | console.log("\nSomeone has accepted your bet! \n(msg txn: " + bet.op_return_txnId + ")"); 94 | //console.log("Client txn Id: " + this.betState.clientTxId); 95 | 96 | this.betState.phase = 3; 97 | } 98 | await Utils.sleep(250); 99 | } 100 | 101 | // Phase 3 -- Send Client your Escrow Details and multisig pub key so he can create escrow 102 | console.log('\n-------------------------------------------------------------------------------'); 103 | console.log("| PHASE 3: Funding the host's side of the bet... |"); 104 | console.log('-------------------------------------------------------------------------------'); 105 | 106 | let escrowBuf = CoinFlipShared.buildCoinFlipHostEscrowScript( 107 | this.wallet.pubkey, 108 | this.betState.secretCommitment, 109 | this.betState.clientmultisigPubKey 110 | ); 111 | this.wallet.utxo = await Core.getUtxoWithRetry(this.wallet.address); 112 | let escrowTxid = await Core.createEscrow(this.wallet, escrowBuf, this.betState.amount); 113 | console.log('\nOur escrow address has been funded! \n(txn: ' + escrowTxid); 114 | await Utils.sleep(500); // short wait for BITBOX mempool to sync 115 | 116 | let phase3MsgTxId = await CoinFlipHost.sendPhase3Message(this.wallet, this.betState.betId, this.betState.clientTxId, escrowTxid); 117 | console.log('Message sent to client with our escrow details. \n(msg txn: ' + phase3MsgTxId + ')'); 118 | this.betState.phase = 4; 119 | 120 | // Phase 4 -- Wait for Client's Escrow Details 121 | console.log('\n-------------------------------------------------------------------------------'); 122 | console.log('| PHASE 4: Waiting for client to fund their side of bet... |') 123 | console.log('-------------------------------------------------------------------------------'); 124 | 125 | while(this.betState.phase == 4){ 126 | 127 | let betId = this.betState.betId; 128 | 129 | // TODO filter message for selected bet participant 130 | 131 | var clientPhase4Messages = this.feed.messages.filter(function(item){ 132 | if(item.phase == 4) { 133 | let p4item = item; 134 | if(p4item.betTxId.toString('hex') == betId){ // && item.participantP2SHTxId.toString('hex') == clientId){ 135 | return true; } 136 | } 137 | return false; 138 | }); 139 | 140 | if(clientPhase4Messages.length > 0) { 141 | // ignore target address field from host for now... 142 | let bet = clientPhase4Messages[clientPhase4Messages.length-1]; 143 | this.betState.clientP2SHTxId = bet.participantP2SHTxId.toString('hex'); 144 | this.betState.participantSig1 = bet.participantSig1; 145 | this.betState.participantSig2 = bet.participantSig2; 146 | 147 | console.log("\nThe client has funded the bet! \n(msg txn: " + bet.op_return_txnId + ")"); 148 | console.log("(escrow txn: " + this.betState.clientP2SHTxId + ")"); 149 | 150 | this.betState.phase = 5; 151 | } 152 | await Utils.sleep(250); 153 | } 154 | 155 | // Phase 5 -- Submit Bet Transaction & try to claim it. 156 | console.log('\n-------------------------------------------------------------------------------'); 157 | console.log('| PHASE 5: Submitting Coin flip bet... |'); 158 | console.log('-------------------------------------------------------------------------------'); 159 | 160 | let betScriptBuf = CoinFlipShared.buildCoinFlipBetScriptBuffer( 161 | this.wallet.pubkey, 162 | this.betState.secretCommitment, 163 | this.betState.clientmultisigPubKey, 164 | this.betState.clientCommitment 165 | ); 166 | 167 | let clientEscrowBuf = CoinFlipShared.buildCoinFlipClientEscrowScript( 168 | this.wallet.pubkey, 169 | this.betState.clientmultisigPubKey 170 | ); 171 | 172 | let betTxId = await CoinFlipHost.redeemEscrowToMakeBet( 173 | this.wallet, 174 | this.betState.secret, 175 | this.betState.participantSig1, 176 | this.betState.participantSig2, 177 | escrowBuf, clientEscrowBuf, betScriptBuf, escrowTxid, 178 | this.betState.clientP2SHTxId, 179 | this.betState.amount 180 | ); 181 | 182 | if(betTxId.length == 64){ 183 | console.log("\nBet Submitted! \n(txn: " + betTxId + ")"); 184 | this.betState.phase = 6; 185 | } 186 | else { 187 | console.log("\nSomething went wrong when submitting the bet: " + betTxId); 188 | this.complete = true; 189 | return; 190 | } 191 | 192 | // Phase 6 -- Wait for Client's Resignation if we lost. 193 | console.log('\n-------------------------------------------------------------------------------'); 194 | console.log('| PHASE 6: Wait for Client WIN or LOSS |'); 195 | console.log('-------------------------------------------------------------------------------'); 196 | 197 | let p2sh_hash160 = BITBOX.Crypto.hash160(betScriptBuf); 198 | let scriptPubKey = BITBOX.Script.scriptHash.output.encode(p2sh_hash160); 199 | let betAddress = BITBOX.Address.fromOutputScript(scriptPubKey); 200 | 201 | while(this.betState.phase == 6){ 202 | 203 | // 1) we need check for 1 of 2 events, bet txn spending by the client or client resignation. 204 | 205 | let betId = this.betState.betId; 206 | 207 | // TODO filter message for selected bet participant 208 | 209 | var clientPhase6Messages = this.feed.messages.filter(function(item){ 210 | if(item.phase == 6) { 211 | let p6Item = item; 212 | if(p6Item.betTxId.toString('hex') == betId){ // && item.participantP2SHTxId.toString('hex') == clientId){ 213 | return true; 214 | } 215 | } 216 | return false; 217 | }); 218 | 219 | if(clientPhase6Messages.length > 0) { 220 | // ignore target address field from host for now... 221 | let bet = clientPhase6Messages[clientPhase6Messages.length-1]; 222 | this.betState.clientSecret = bet.secretValue; 223 | 224 | let host_int_le = Core.readScriptInt32(this.betState.secret); 225 | let client_int_le = Core.readScriptInt32(this.betState.clientSecret); 226 | console.log("\n " + client_int_le + " <- client secrect (shortened)"); 227 | console.log("+ " + host_int_le + " <- your secret (shortened)"); 228 | console.log("=========================================================") 229 | console.log(" " + (client_int_le + host_int_le) + " <- result"); 230 | console.log("\nYou WIN! (because the result is an ODD number)"); 231 | 232 | this.betState.phase = 7; 233 | } 234 | 235 | try { 236 | var betDetails = await Core.getAddressDetailsWithRetry(betAddress); 237 | } catch(e) { 238 | console.log("\nClient failed to claim his win or report loss..."); 239 | this.complete = true; 240 | return; 241 | } 242 | 243 | if((betDetails.balanceSat + betDetails.unconfirmedBalanceSat) <= 0 && betDetails.transactions.length == 2){ 244 | console.log("\nYou Lose... (because the result is EVEN)"); 245 | console.log("The client has claimed their winnings..."); 246 | this.complete = true; 247 | return; 248 | } 249 | 250 | await Utils.sleep(250); 251 | } 252 | 253 | console.log('\n-------------------------------------------------------------------------------'); 254 | console.log('| PHASE 7: Claiming Our Winnings... |'); 255 | console.log('-------------------------------------------------------------------------------'); 256 | 257 | 258 | let winTxnId = await CoinFlipHost.hostClaimWinSecret(this.wallet, 259 | this.betState.secret, 260 | this.betState.clientSecret, 261 | betScriptBuf, 262 | betTxId, 263 | this.betState.amount); 264 | if(winTxnId.length != 64) 265 | { 266 | console.log("\nWe're sorry. Something terrible went wrong when trying to claim your winnings... " + winTxnId); 267 | } else { 268 | console.log("\nYou've been paid! \n(txn: " + winTxnId + ")"); 269 | } 270 | 271 | this.complete = true; 272 | } 273 | 274 | static async sendPhase1Message(wallet: WalletKey, betAmount: number, hostCommitment: Buffer, clientTargetAddress?: string): Promise{ 275 | let phase1Buf = this.encodePhase1Message(betAmount, hostCommitment, clientTargetAddress); 276 | // console.log("Phase 1 OP_RETURN (hex): " + phase1Buf.toString('hex')); 277 | // console.log("Phase 1 OP_RETURN (ASM): " + BITBOX.Script.toASM(phase1Buf)); 278 | wallet.utxo = await Core.getUtxoWithRetry(wallet.address); 279 | let txnId = await Core.createOP_RETURN(wallet, phase1Buf); 280 | return txnId; 281 | } 282 | 283 | static async sendPhase3Message(wallet: WalletKey, betId: string, clientTxId: string, escrowTxId: string): Promise{ 284 | let phase3Buf = this.encodePhase3Message(betId, clientTxId, escrowTxId, wallet.pubkey); 285 | // console.log("Phase 3 OP_RETURN (hex): " + phase3Buf.toString('hex')); 286 | // console.log("Phase 3 OP_RETURN (ASM): " + BITBOX.Script.toASM(phase3Buf)); 287 | wallet.utxo = await Core.getUtxoWithRetry(wallet.address); 288 | let txnId = await Core.createOP_RETURN(wallet, phase3Buf); 289 | return txnId; 290 | } 291 | 292 | static async redeemEscrowToMakeBet(wallet: WalletKey, hostSecret: Buffer, clientSig1: Buffer, clientSig2: Buffer, 293 | hostRedeemScript: Buffer, clientRedeemScript: Buffer, betScript: Buffer, 294 | hostTxId: string, clientTxId: string, betAmount: number): Promise { 295 | 296 | let hostKey = BITBOX.ECPair.fromWIF(wallet.wif) 297 | //let clientKey = BITBOX.ECPair.fromWIF(client.wif) 298 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 299 | 300 | let hashType = 0xc1 // transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY | transactionBuilder.hashTypes.SIGHASH_ALL 301 | let satoshisAfterFee = Core.purseAmount(betAmount); 302 | transactionBuilder.addInput(hostTxId, 0); 303 | transactionBuilder.addInput(clientTxId, 0); 304 | 305 | // Determine bet address 306 | let p2sh_hash160 = BITBOX.Crypto.hash160(betScript); 307 | let scriptPubKey = BITBOX.Script.scriptHash.output.encode(p2sh_hash160); 308 | let betAddress = BITBOX.Address.fromOutputScript(scriptPubKey); 309 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(betAddress), satoshisAfterFee); 310 | 311 | let tx = transactionBuilder.transaction.buildIncomplete(); 312 | 313 | // Sign alices escrow 314 | let sigHash: number = tx.hashForWitnessV0(0, hostRedeemScript, betAmount, hashType); 315 | let hostSig: Buffer = hostKey.sign(sigHash).toScriptSignature(hashType); 316 | //let clientSig = //clientKey.sign(sigHash).toScriptSignature(hashType); 317 | 318 | let redeemScriptSig: number[] = []; // start by pushing with true for makeBet mode 319 | 320 | // multisig off by one fix 321 | redeemScriptSig.push(BITBOX.Script.opcodes.OP_0); 322 | 323 | // host signature 324 | redeemScriptSig.push(hostSig.length); 325 | hostSig.forEach((item, index) => { redeemScriptSig.push(item); }); 326 | 327 | // participant signature 328 | redeemScriptSig.push(clientSig2.length) 329 | clientSig2.forEach((item, index) => { redeemScriptSig.push(item); }); 330 | 331 | // alice secret 332 | redeemScriptSig.push(hostSecret.length); 333 | hostSecret.forEach((item, index) => { redeemScriptSig.push(item); }); 334 | 335 | // push mode onto stack for MakeBet mode 336 | redeemScriptSig.push(0x51); // non-zero is makeBet mode 337 | 338 | if (hostRedeemScript.length > 75) redeemScriptSig.push(0x4c); 339 | redeemScriptSig.push(hostRedeemScript.length); 340 | hostRedeemScript.forEach((item, index) => { redeemScriptSig.push(item); }); 341 | 342 | let redeemScriptSigBuf: Buffer = Buffer.from(redeemScriptSig); 343 | tx.setInputScript(0, redeemScriptSigBuf); 344 | 345 | // Sign bob's escrow 346 | let sigHash2: number = tx.hashForWitnessV0(1, clientRedeemScript, betAmount, hashType); 347 | let hostSig2: Buffer = hostKey.sign(sigHash2).toScriptSignature(hashType); 348 | //let clientSig2 = clientKey.sign(sigHash2).toScriptSignature(hashType); 349 | 350 | let redeemScriptSig2: number[] = [] 351 | 352 | // multisig off by one fix 353 | redeemScriptSig2.push(BITBOX.Script.opcodes.OP_0) 354 | 355 | // host signature 356 | redeemScriptSig2.push(hostSig2.length) 357 | hostSig2.forEach((item, index) => { redeemScriptSig2.push(item); }) 358 | 359 | // participant signature 360 | redeemScriptSig2.push(clientSig1.length) 361 | clientSig1.forEach((item, index) => { redeemScriptSig2.push(item); }); 362 | 363 | // push mode onto stack for MakeBet mode 364 | redeemScriptSig2.push(0x51); // non-zero is makeBet mode 365 | 366 | if (clientRedeemScript.length > 75) redeemScriptSig2.push(0x4c) 367 | redeemScriptSig2.push(clientRedeemScript.length) 368 | clientRedeemScript.forEach((item, index) => { redeemScriptSig2.push(item); }); 369 | 370 | let redeemScriptSig2Buf = Buffer.from(redeemScriptSig2); 371 | tx.setInputScript(1, redeemScriptSig2Buf); 372 | 373 | // uncomment for viewing script hex 374 | // let redeemScriptSigHex = redeemScriptSig.toString('hex'); 375 | // let redeemScriptHex = redeemScript.toString('hex'); 376 | 377 | let hex: string = tx.toHex(); 378 | 379 | let txId = await Core.sendRawTransaction(hex); 380 | return txId; 381 | } 382 | 383 | static async hostClaimWinSecret(wallet: WalletKey, hostSecret: Buffer, clientSecret: Buffer, betScript: Buffer, betTxId: string, betAmount: number): Promise{ 384 | 385 | let purseAmount = Core.purseAmount(betAmount); 386 | 387 | let hostKey = BITBOX.ECPair.fromWIF(wallet.wif) 388 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 389 | 390 | let hashType = 0xc1 // transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY | transactionBuilder.hashTypes.SIGHASH_ALL 391 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2SH: 1 }); 392 | let satoshisAfterFee = purseAmount - byteCount - betScript.length - 109; 393 | transactionBuilder.addInput(betTxId, 0) 394 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(wallet.utxo[0].cashAddress), satoshisAfterFee); 395 | 396 | let tx = transactionBuilder.transaction.buildIncomplete(); 397 | 398 | // Sign bet tx 399 | let sigHash: number = tx.hashForWitnessV0(0, betScript, purseAmount, hashType); 400 | let hostSig: Buffer = hostKey.sign(sigHash).toScriptSignature(hashType); 401 | 402 | let redeemScriptSig: number[] = []; // start by pushing with true for makeBet mode 403 | 404 | // host signature 405 | redeemScriptSig.push(hostSig.length) 406 | hostSig.forEach((item, index) => { redeemScriptSig.push(item); }); 407 | 408 | // host secret 409 | redeemScriptSig.push(hostSecret.length); 410 | hostSecret.forEach((item, index) => { redeemScriptSig.push(item); }); 411 | 412 | // client secret 413 | redeemScriptSig.push(clientSecret.length); 414 | clientSecret.forEach((item, index) => { redeemScriptSig.push(item); }); 415 | 416 | // Host wins with client secret mode 417 | redeemScriptSig.push(0x51); 418 | redeemScriptSig.push(0x51); 419 | 420 | if (betScript.length > 75) redeemScriptSig.push(0x4c); 421 | redeemScriptSig.push(betScript.length); 422 | betScript.forEach((item, index) => { redeemScriptSig.push(item); }); 423 | 424 | tx.setInputScript(0, Buffer.from(redeemScriptSig)); 425 | 426 | // uncomment for viewing script hex 427 | // let redeemScriptSigHex = redeemScriptSig.toString('hex'); 428 | // let redeemScriptHex = redeemScript.toString('hex'); 429 | // uncomment for viewing script hex 430 | // console.log("Bet redeem script hex: " + redeemScriptSig.toString('hex')); 431 | // console.log("Bet Script Hex: " + betScript.toString('hex')); 432 | console.log("Winning amount after fees: " + satoshisAfterFee); 433 | 434 | let hex = tx.toHex(); 435 | 436 | let txId = await Core.sendRawTransaction(hex); 437 | return txId; 438 | } 439 | 440 | static async hostClaimWinTimeout(wallet: WalletKey, betScript: Buffer, betTxId: string, betAmount: number): Promise{ 441 | 442 | let hostKey = BITBOX.ECPair.fromWIF(wallet.wif) 443 | let transactionBuilder = new BITBOX.TransactionBuilder('bitcoincash'); 444 | 445 | let hashType = 0xc1 // transactionBuilder.hashTypes.SIGHASH_ANYONECANPAY | transactionBuilder.hashTypes.SIGHASH_ALL 446 | let byteCount = BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2SH: 1 }); 447 | let satoshisAfterFee = betAmount - byteCount - 800; 448 | transactionBuilder.addInput(betTxId, 0, bip68.encode({ blocks: 1 })) 449 | transactionBuilder.addOutput(BITBOX.Address.toLegacyAddress(wallet.utxo[0].cashAddress), satoshisAfterFee); 450 | 451 | let tx = transactionBuilder.transaction.buildIncomplete(); 452 | 453 | // Sign bet tx 454 | let sigHash: number = tx.hashForWitnessV0(0, betScript, betAmount, hashType); 455 | let hostSig: Buffer = hostKey.sign(sigHash).toScriptSignature(hashType); 456 | 457 | let redeemScriptSig: number[] = []; // start by pushing with true for makeBet mode 458 | 459 | // host signature 460 | redeemScriptSig.push(hostSig.length) 461 | hostSig.forEach((item, index) => { redeemScriptSig.push(item); }); 462 | 463 | // Host wins with timeout mode 464 | redeemScriptSig.push(0x00); 465 | redeemScriptSig.push(0x51); 466 | 467 | if (betScript.length > 75) redeemScriptSig.push(0x4c); 468 | redeemScriptSig.push(betScript.length); 469 | betScript.forEach((item, index) => { redeemScriptSig.push(item); }); 470 | 471 | tx.setInputScript(0, Buffer.from(redeemScriptSig)); 472 | 473 | // uncomment for viewing script hex 474 | // let redeemScriptSigHex = redeemScriptSig.toString('hex'); 475 | // let redeemScriptHex = redeemScript.toString('hex'); 476 | 477 | let hex: string = tx.toHex(); 478 | 479 | let txId = await Core.sendRawTransaction(hex); 480 | return txId; 481 | } 482 | } --------------------------------------------------------------------------------