├── .gitignore ├── .gitmodules ├── README.txt ├── cleandown.sh ├── docker-compose.yml ├── red-noc ├── .dockerignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock └── find_them_and_destroy_them.rb ├── run_simulation.sh ├── runner ├── .dockerignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── bitcoin_user.rb └── load_the_jump_program.rb ├── seed-network.sh ├── snitch ├── .dockerignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── blorkchain.rb ├── there_is_no_spoon.rb └── views │ └── rubber_dinghy_rapids_bro.erb └── start_from_scratch.sh /.gitignore: -------------------------------------------------------------------------------- 1 | */debug.log 2 | *-node-data 3 | docker-compose.yml-e 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bitcoin"] 2 | path = bitcoin 3 | url = git@github.com:mikekelly/bitcoin.git 4 | [submodule "cpuminer"] 5 | path = cpuminer 6 | url = git@github.com:pooler/cpuminer.git 7 | [submodule "evil-bitcoin"] 8 | path = evil-bitcoin 9 | url = git@github.com:mikekelly/bitcoin.git 10 | branch = evilnode 11 | [submodule "btc-rpc-explorer"] 12 | path = btc-rpc-explorer 13 | url = git@github.com:mikekelly/btc-rpc-explorer.git 14 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | /$$$$$$$ /$$$$$$$$ /$$$$$$ /$$ 2 | | $$__ $$|__ $$__//$$__ $$ | $$ 3 | | $$ \ $$ | $$ | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ 4 | | $$$$$$$ | $$ | $$ /$$__ $$| $$__ $$ /$$__ $$ /$$__ $$ |____ $$| $$_ $$_ $$ /$$__ $$ 5 | | $$__ $$ | $$ | $$ | $$$$$$$$| $$ \ $$| $$ | $$| $$ \ $$ /$$$$$$$| $$ \ $$ \ $$| $$$$$$$$ 6 | | $$ \ $$ | $$ | $$ $$ | $$_____/| $$ | $$| $$ | $$| $$ | $$ /$$__ $$| $$ | $$ | $$| $$_____/ 7 | | $$$$$$$/ | $$ | $$$$$$/ | $$$$$$$| $$ | $$| $$$$$$$| $$$$$$$| $$$$$$$| $$ | $$ | $$| $$$$$$$ 8 | |_______/ |__/ \______/ \_______/|__/ |__/ \_______/ \____ $$ \_______/|__/ |__/ |__/ \_______/ 9 | /$$ \ $$ 10 | | $$$$$$/ 11 | \______/ 12 | 13 | 14 | BTC endgame is a research project aiming to test defensive strategies of 15 | the Bitcoin network under various attack scenarios, via a wargame. 16 | 17 | v1 simulates a nation state sabotaging Bitcoin through a denial of service 18 | attack on the network; preventing users' payments from going through by 19 | stopping any transactions from being confirmed, trapping them in the mempool 20 | forever. 21 | 22 | The attack strategy works by using majority mining power to continually mine 23 | empty blocks, which effectively "jams up" the network. 24 | 25 | A detailed summary of the attack is here: 26 | https://joekelly100.medium.com/how-to-kill-bitcoin-part-1-is-bitcoin-unstoppable-code-7a1b366f65ee 27 | https://joekelly100.medium.com/how-to-kill-bitcoin-part-2-no-can-spend-66e59385a4a5 28 | 29 | There is currently no defense for this attack in the Bitcoin protocol itself, as the 30 | simulation demonstrates. 31 | 32 | The idea of this wargame is to challenge others to develop a defense strategy 33 | against the attack, in response to which further attack strategies can be 34 | proposed and tested, then a further defense strategy, and so on and so forth. 35 | 36 | Bitcoin users can choose to violate the heaviest chain rule and ignore specific 37 | chains they beleive are nefarious. Bitcoin Core has an RPC endpoint called 38 | `invalidateblock` for this purpose. An attacker can shape blocks (with their own transactions) 39 | in order to make identifying nefarious chains harder for users, creating the risk of 40 | chainsplits. For this reason, it is likely that mechanisms for coordinating 41 | invalidateblock between nodes would be necessary. At time of writing, the 42 | simulated attacker does not bother shaping blocks, mostly to save my time :) 43 | 44 | The intent of this work is to research potential ways to make Bitcoin more 45 | robust under attack, and to identify/discuss where there may be limitations or 46 | trade-offs with various defensive strategies. 47 | 48 | If you'd like to sponsor development of this project, you can do so here: 49 | 50 | BTC: bc1q9nyrtnwfkh2yu5dejjzzpmlpe40g6mzyf6jave 51 | XMR: 8BxwbtkaXmHLpG1F2E18QD8TLPU7yKp5gCM4towfR7xRMUFeJDaBHw8gck9BXYKoGJ9xpuvgBGtNc49BbtgzuRobJjmk7Ue 52 | 53 | 54 | ---------------------------------------------------------------------------------------------------- 55 | 56 | 57 | More tech details... 58 | 59 | It is a simulation of mainnet Bitcoin, composed of various docker containers 60 | orchestrated via docker-compose: 61 | 62 | * full nodes 63 | * blue team mining operation (ie. honest miners) 64 | * red team mining operation (ie. the attacker) 65 | * red team "noc" that orchestrates the attack strategy 66 | * simulated users transacting on the network 67 | 68 | Pulling submodule deps: 69 | 70 | $ git submodule init && git submodule update 71 | 72 | 73 | Running the simulation is a two step process: 74 | 75 | $ ./seed-network.sh 76 | $ docker-compose up -d 77 | 78 | 79 | There's a block explorer included for observing the network, it's a web 80 | server exposed on port 3002: 81 | 82 | http://localhost:3002/blocks 83 | 84 | 85 | If you want to inspect a node just exec into it and you can use bitcoin-cli 86 | from there (the cookie auth is all setup for you): 87 | $ docker-compose exec blue-node bash 88 | 89 | Notes: 90 | - At time of writing, it based on a fork of Bitcoin Core 0.21.0 91 | -------------------------------------------------------------------------------- /cleandown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | git checkout docker-compose.yml 5 | docker-compose down 6 | rm -rf *-node-data 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | 5 | runner: 6 | build: runner 7 | volumes: 8 | - "./runner:/usr/src/app" 9 | environment: 10 | RED_NODE_URL: "http://red-node:8332" 11 | BLUE_NODE_URL: "http://blue-node:8332" 12 | JACK_NODE_URL: "http://jack-node:8332" 13 | JILL_NODE_URL: "http://jill-node:8332" 14 | networks: 15 | - default 16 | command: bundle exec ruby load_the_jump_program.rb 17 | 18 | red-noc: 19 | build: red-noc 20 | volumes: 21 | - "./red-noc:/usr/src/app" 22 | environment: 23 | MIN_BLOCK_HEIGHT: 51 # this should be enough depth for blue-miner to confirm txn that funds jack and jill 24 | RED_NODE_URL: "http://red-node:8332" 25 | networks: 26 | - default 27 | command: bundle exec ruby find_them_and_destroy_them.rb 28 | 29 | red-node: 30 | build: evil-bitcoin 31 | volumes: 32 | - "./red-node-data:/root/.bitcoin" 33 | command: bitcoind 34 | -txindex 35 | -printtoconsole 36 | -loadblock=/root/.bitcoin/blocks/blk00000.dat 37 | -wallet=redwallet 38 | -addnode=blue-node:8333 39 | -rpcallowip=0.0.0.0/0 40 | -rpcbind=0.0.0.0 41 | -rpcauth=satoshi:c0394c5b5db0bd5d1ee2ce7590125899$$7d2e0b6f839d5af1f4cd04a9e61fb6b0bb57cdc6dfe13d92b88ae008f1658e4a 42 | 43 | red-miner: 44 | build: cpuminer 45 | deploy: 46 | resources: 47 | limits: 48 | cpus: '0.50' 49 | command: 50 | --url=http://red-node:8332 51 | -a sha256d 52 | --coinbase-addr=REPLACE_WITH_RED_COINBASE_ADDR 53 | --no-getwork 54 | --user=satoshi 55 | --pass=waswrong 56 | --coinbase-sig=red-team 57 | 58 | blue-node: 59 | build: bitcoin 60 | volumes: 61 | - "./blue-node-data:/root/.bitcoin" 62 | command: bitcoind 63 | -txindex 64 | -printtoconsole 65 | -loadblock=/root/.bitcoin/blocks/blk00000.dat 66 | -wallet=bluewallet 67 | -addnode=red-node:8333 68 | -rpcallowip=0.0.0.0/0 69 | -rpcbind=0.0.0.0 70 | -rpcauth=satoshi:c0394c5b5db0bd5d1ee2ce7590125899$$7d2e0b6f839d5af1f4cd04a9e61fb6b0bb57cdc6dfe13d92b88ae008f1658e4a 71 | 72 | blue-miner: 73 | build: cpuminer 74 | deploy: 75 | resources: 76 | limits: 77 | cpus: '0.25' 78 | command: 79 | --url=http://blue-node:8332 80 | -a sha256d 81 | --coinbase-addr=REPLACE_WITH_BLUE_COINBASE_ADDR 82 | --no-getwork 83 | --user=satoshi 84 | --pass=waswrong 85 | --coinbase-sig=blue-team 86 | 87 | jack-node: 88 | build: bitcoin 89 | volumes: 90 | - "./jack-node-data:/root/.bitcoin" 91 | command: bitcoind 92 | -txindex 93 | -printtoconsole 94 | -loadblock=/root/.bitcoin/blocks/blk00000.dat 95 | -wallet=bluewallet 96 | -addnode=blue-node:8333 97 | -rpcallowip=0.0.0.0/0 98 | -rpcbind=0.0.0.0 99 | -rpcauth=satoshi:c0394c5b5db0bd5d1ee2ce7590125899$$7d2e0b6f839d5af1f4cd04a9e61fb6b0bb57cdc6dfe13d92b88ae008f1658e4a 100 | 101 | jill-node: 102 | build: bitcoin 103 | volumes: 104 | - "./jill-node-data:/root/.bitcoin" 105 | command: bitcoind 106 | -txindex 107 | -printtoconsole 108 | -loadblock=/root/.bitcoin/blocks/blk00000.dat 109 | -wallet=bluewallet 110 | -addnode=blue-node:8333 111 | -rpcallowip=0.0.0.0/0 112 | -rpcbind=0.0.0.0 113 | -rpcauth=satoshi:c0394c5b5db0bd5d1ee2ce7590125899$$7d2e0b6f839d5af1f4cd04a9e61fb6b0bb57cdc6dfe13d92b88ae008f1658e4a 114 | 115 | explorer: 116 | build: btc-rpc-explorer 117 | ports: 118 | - "3002:3002" 119 | environment: 120 | BTCEXP_HOST: explorer 121 | BTCEXP_BITCOIND_HOST: blue-node 122 | BTCEXP_BITCOIND_PORT: 8332 123 | BTCEXP_BITCOIND_USER: satoshi 124 | BTCEXP_BITCOIND_PASS: waswrong 125 | BTCEXP_NO_INMEMORY_RPC_CACHE: 'true' 126 | networks: 127 | - default 128 | - dmz 129 | 130 | snitch: 131 | build: snitch 132 | ports: 133 | - "4566:4567" 134 | volumes: 135 | - "./snitch:/usr/src/app" 136 | command: bundle exec ruby there_is_no_spoon.rb 137 | networks: 138 | - default 139 | - dmz 140 | 141 | networks: 142 | default: 143 | internal: true 144 | dmz: 145 | driver: bridge 146 | -------------------------------------------------------------------------------- /red-noc/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /red-noc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.0-alpine3.13 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add --update build-base libffi-dev libcurl git 6 | 7 | COPY Gemfile Gemfile.lock ./ 8 | RUN bundle install 9 | 10 | COPY . . 11 | 12 | CMD ["bundle exec ruby find_them_and_destroy_them.rb"] 13 | -------------------------------------------------------------------------------- /red-noc/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bitcoiner', github: "mikekelly/bitcoiner" 4 | gem 'pry', '~> 0.13.1' 5 | -------------------------------------------------------------------------------- /red-noc/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/mikekelly/bitcoiner.git 3 | revision: 7f514dffb181b4b94b17bdd8f42eaffca1c06f26 4 | specs: 5 | bitcoiner (0.2.1) 6 | addressable 7 | typhoeus (~> 1.3.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | addressable (2.7.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | coderay (1.1.3) 15 | ethon (0.12.0) 16 | ffi (>= 1.3.0) 17 | ffi (1.14.2) 18 | method_source (1.0.0) 19 | pry (0.13.1) 20 | coderay (~> 1.1) 21 | method_source (~> 1.0) 22 | public_suffix (4.0.6) 23 | typhoeus (1.3.1) 24 | ethon (>= 0.9.0) 25 | 26 | PLATFORMS 27 | x86_64-linux-musl 28 | 29 | DEPENDENCIES 30 | bitcoiner! 31 | pry (~> 0.13.1) 32 | 33 | BUNDLED WITH 34 | 2.2.3 35 | -------------------------------------------------------------------------------- /red-noc/find_them_and_destroy_them.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'bitcoiner' 3 | require 'pry' 4 | 5 | red_node_url = ENV.fetch("RED_NODE_URL") 6 | min_block_height = Integer(ENV.fetch("MIN_BLOCK_HEIGHT")) 7 | 8 | client = Bitcoiner.new('satoshi', 'waswrong', red_node_url) 9 | 10 | puts "Keeping an eye on the chain tip to kill off any honest blocks..." 11 | loop do 12 | blockchain_info = client.request('getblockchaininfo') 13 | chain_tip_hash = blockchain_info.fetch("bestblockhash") 14 | block = client.request("getblock", chain_tip_hash) 15 | if block.fetch("nTx") > 1 && block.fetch("height") > min_block_height 16 | puts "Honest block with transactions detected on chain tip with hash #{chain_tip_hash}" 17 | client.request("invalidateblock", chain_tip_hash) 18 | puts "Invalidated." 19 | end 20 | sleep 1 21 | end 22 | -------------------------------------------------------------------------------- /run_simulation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Bringing up everything apart from the miners..." 5 | docker-compose up -d blue-node jack-node jill-node 6 | docker-compose up -d explorer snitch runner 7 | 8 | echo "Waiting 20s for the runner to create the wallets" 9 | sleep 20 10 | 11 | echo "Bring up the blue team miner to confirm the seed transactions for Jack and Jill" 12 | docker-compose up -d blue-miner 13 | 14 | echo "Wait 30s for blue team miner to confirm a few blocks" 15 | sleep 30 16 | 17 | echo "Bring up the red team stuff and anything else that's left" 18 | docker-compose up -d 19 | -------------------------------------------------------------------------------- /runner/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /runner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.0-alpine3.13 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add --update build-base libffi-dev libcurl git 6 | 7 | COPY Gemfile Gemfile.lock ./ 8 | RUN bundle install 9 | 10 | COPY . . 11 | 12 | CMD ["load_the_jump_program.rb"] 13 | -------------------------------------------------------------------------------- /runner/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bitcoiner', github: "mikekelly/bitcoiner" 4 | gem 'pry', '~> 0.13.1' 5 | -------------------------------------------------------------------------------- /runner/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/mikekelly/bitcoiner.git 3 | revision: 7f514dffb181b4b94b17bdd8f42eaffca1c06f26 4 | specs: 5 | bitcoiner (0.2.1) 6 | addressable 7 | typhoeus (~> 1.3.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | addressable (2.7.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | coderay (1.1.3) 15 | ethon (0.12.0) 16 | ffi (>= 1.3.0) 17 | ffi (1.14.2) 18 | method_source (1.0.0) 19 | pry (0.13.1) 20 | coderay (~> 1.1) 21 | method_source (~> 1.0) 22 | public_suffix (4.0.6) 23 | typhoeus (1.3.1) 24 | ethon (>= 0.9.0) 25 | 26 | PLATFORMS 27 | x86_64-linux-musl 28 | 29 | DEPENDENCIES 30 | bitcoiner! 31 | pry (~> 0.13.1) 32 | 33 | BUNDLED WITH 34 | 2.2.3 35 | -------------------------------------------------------------------------------- /runner/bitcoin_user.rb: -------------------------------------------------------------------------------- 1 | class BitcoinUser 2 | def initialize(name:,node_url:,session_wallet_name: nil) 3 | @name = name 4 | @node_url = node_url 5 | @session_wallet_name = session_wallet_name 6 | end 7 | 8 | def balance 9 | raise "No session wallet available named #{session_wallet_name}" if session_wallet.nil? 10 | session_wallet.balance 11 | end 12 | 13 | def send_to(user:, amount:, fee_rate: 1) 14 | session_wallet.request('sendtoaddress', user.address, amount, "", "", false, true, nil, "unset", false, fee_rate) 15 | end 16 | 17 | def address 18 | @address ||= session_wallet.request('getnewaddress') 19 | end 20 | 21 | def session_wallet 22 | if @session_wallet_name.nil? 23 | @session_wallet_name = SecureRandom.uuid 24 | client.request('createwallet', @session_wallet_name) 25 | end 26 | @session_wallet ||= client.wallet_named(@session_wallet_name) 27 | end 28 | 29 | private 30 | 31 | def client 32 | @client ||= Bitcoiner.new(rpc_username, rpc_password, node_url) 33 | end 34 | 35 | def rpc_username 36 | "satoshi" 37 | end 38 | 39 | def rpc_password 40 | "waswrong" 41 | end 42 | 43 | attr_reader :name, :node_url, :session_wallet_name 44 | end 45 | 46 | -------------------------------------------------------------------------------- /runner/load_the_jump_program.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'bitcoiner' 3 | require 'pry' 4 | require_relative './bitcoin_user' 5 | 6 | red_node_url = ENV.fetch("RED_NODE_URL") 7 | blue_node_url = ENV.fetch("BLUE_NODE_URL") 8 | jack_node_url = ENV.fetch("JACK_NODE_URL") 9 | jill_node_url = ENV.fetch("JILL_NODE_URL") 10 | 11 | red_team = BitcoinUser.new(name: 'red_team', node_url: red_node_url, session_wallet_name: 'redwallet') 12 | blue_team = BitcoinUser.new(name: 'blue_team', node_url: blue_node_url, session_wallet_name: 'bluewallet') 13 | jack = BitcoinUser.new(name: 'jack', node_url: jack_node_url) 14 | jill = BitcoinUser.new(name: 'jill', node_url: jill_node_url) 15 | 16 | puts "Creating Jack's wallet + address" 17 | puts "Jack's address for this session: #{jack.address}" 18 | puts "Creating Jill's wallet + address" 19 | puts "Jill's address for this session: #{jill.address}" 20 | 21 | puts "Waiting for blue team to have funds for Jack and Jill..." 22 | loop do 23 | break if blue_team.balance > 0 24 | puts "Rechecking blue team balance in 5s..." 25 | sleep 5 26 | end 27 | 28 | puts 29 | 30 | number_of_uxtos = 100 31 | 32 | blue_team_balance = blue_team.balance - 2 # hold 2 BTC back to cover fees 33 | amount_to_remit = (blue_team_balance/2).round(8) / number_of_uxtos 34 | 35 | puts "Remitting to Jack and Jill..." 36 | number_of_uxtos.times do 37 | blue_team.send_to(user: jack, amount: amount_to_remit) 38 | blue_team.send_to(user: jill, amount: amount_to_remit) 39 | end 40 | puts "Done." 41 | 42 | puts 43 | 44 | puts "Waiting for Jack and Jill's funds to get confirmed..." 45 | loop do 46 | break if jack.balance > 0 && jill.balance > 0 47 | puts "Rechecking their balances in 5s..." 48 | sleep 5 49 | end 50 | 51 | puts 52 | 53 | amount_to_exchange = (amount_to_remit/100_000).round(8) 54 | 55 | puts "Jack and Jill begin happily exchanging bitcoin with each other..." 56 | loop do 57 | begin 58 | puts 59 | 60 | puts "Jack sending #{amount_to_exchange} to Jill..." 61 | jack.send_to(user: jill, amount: amount_to_exchange) 62 | puts "Done." 63 | 64 | puts "Jill sending #{amount_to_exchange} to Jack..." 65 | jill.send_to(user: jack, amount: amount_to_exchange) 66 | puts "Done." 67 | 68 | rescue Bitcoiner::Client::JSONRPCError => e 69 | puts "RPC Error:" 70 | puts e.message 71 | end 72 | 73 | puts "They both wait 10 seconds..." 74 | sleep 20 75 | end 76 | -------------------------------------------------------------------------------- /seed-network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ./cleandown.sh 5 | 6 | docker-compose up -d blue-node red-node 7 | 8 | # TODO: a proper healthcheck loop using docker-cli 9 | echo "Waiting 30 sec for nodes to come online" 10 | sleep 30 11 | 12 | echo "Creating blue wallet..." 13 | docker-compose exec blue-node bitcoin-cli createwallet bluewallet 14 | echo "Creating red wallet..." 15 | docker-compose exec red-node bitcoin-cli createwallet redwallet 16 | 17 | echo "Creating blue team coinbase address..." 18 | BLUE_COINBASE_ADDR=$(docker-compose exec blue-node bitcoin-cli getnewaddress) 19 | echo "${BLUE_COINBASE_ADDR}" 20 | echo "Creating red team coinbase address..." 21 | RED_COINBASE_ADDR=$(docker-compose exec red-node bitcoin-cli getnewaddress) 22 | echo "${RED_COINBASE_ADDR}" 23 | 24 | echo "Configuring coinbase addresses in docker-compose.yml..." 25 | sed -i -e "s/REPLACE_WITH_BLUE_COINBASE_ADDR/${BLUE_COINBASE_ADDR}/g" docker-compose.yml 26 | sed -i -e "s/REPLACE_WITH_RED_COINBASE_ADDR/${RED_COINBASE_ADDR}/g" docker-compose.yml 27 | 28 | echo "Creating initial block..." 29 | docker-compose exec blue-node bitcoin-cli -generate 30 | -------------------------------------------------------------------------------- /snitch/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /snitch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.0-alpine3.13 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add --update build-base libffi-dev libcurl git 6 | 7 | COPY Gemfile Gemfile.lock ./ 8 | RUN bundle install 9 | 10 | COPY . . 11 | 12 | CMD ["load_the_jump_program.rb"] 13 | -------------------------------------------------------------------------------- /snitch/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bitcoiner', github: "mikekelly/bitcoiner" 4 | gem 'pry', '~> 0.13.1' 5 | gem 'sinatra' 6 | gem 'sinatra-contrib' 7 | gem 'thin' 8 | gem 'tty-tree' 9 | -------------------------------------------------------------------------------- /snitch/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/mikekelly/bitcoiner.git 3 | revision: 7f514dffb181b4b94b17bdd8f42eaffca1c06f26 4 | specs: 5 | bitcoiner (0.2.1) 6 | addressable 7 | typhoeus (~> 1.3.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | addressable (2.7.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | coderay (1.1.3) 15 | daemons (1.3.1) 16 | ethon (0.12.0) 17 | ffi (>= 1.3.0) 18 | eventmachine (1.2.7) 19 | ffi (1.14.2) 20 | method_source (1.0.0) 21 | multi_json (1.15.0) 22 | mustermann (1.1.1) 23 | ruby2_keywords (~> 0.0.1) 24 | pry (0.13.1) 25 | coderay (~> 1.1) 26 | method_source (~> 1.0) 27 | public_suffix (4.0.6) 28 | rack (2.2.3) 29 | rack-protection (2.1.0) 30 | rack 31 | ruby2_keywords (0.0.4) 32 | sinatra (2.1.0) 33 | mustermann (~> 1.0) 34 | rack (~> 2.2) 35 | rack-protection (= 2.1.0) 36 | tilt (~> 2.0) 37 | sinatra-contrib (2.1.0) 38 | multi_json 39 | mustermann (~> 1.0) 40 | rack-protection (= 2.1.0) 41 | sinatra (= 2.1.0) 42 | tilt (~> 2.0) 43 | thin (1.8.0) 44 | daemons (~> 1.0, >= 1.0.9) 45 | eventmachine (~> 1.0, >= 1.0.4) 46 | rack (>= 1, < 3) 47 | tilt (2.0.10) 48 | tty-tree (0.4.0) 49 | typhoeus (1.3.1) 50 | ethon (>= 0.9.0) 51 | 52 | PLATFORMS 53 | ruby 54 | x86_64-linux-musl 55 | 56 | DEPENDENCIES 57 | bitcoiner! 58 | pry (~> 0.13.1) 59 | sinatra 60 | sinatra-contrib 61 | thin 62 | tty-tree 63 | 64 | BUNDLED WITH 65 | 2.2.3 66 | -------------------------------------------------------------------------------- /snitch/blorkchain.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'bitcoiner' 3 | require 'pry' 4 | 5 | class Blorkchain 6 | def initialize(blocks:Hash.new, blocks_by_height:Hash.new, orphaned_blocks:Array.new) 7 | @blocks = blocks 8 | @blocks_by_height = blocks_by_height 9 | @orphaned_blocks = orphaned_blocks 10 | end 11 | 12 | def to_h 13 | build_block_tree! 14 | genesis_block.to_h 15 | end 16 | 17 | def branches_at_depth(depth) 18 | build_block_tree! 19 | blocks_by_height[depth].to_a 20 | end 21 | 22 | def mempool_count 23 | client.request('getmempoolinfo')["size"] 24 | end 25 | 26 | attr_reader :orphaned_blocks 27 | 28 | private 29 | 30 | def build_block_tree! 31 | chain_tip_hashes.each do |block_hash| 32 | walk_to_genesis(block_hash) 33 | end 34 | end 35 | 36 | def chain_tip_hashes 37 | client.request('getchaintips').map do |chain_tip| 38 | chain_tip.fetch("hash") 39 | end 40 | end 41 | 42 | def walk_to_genesis(hash) 43 | blocks[hash] ||= fetch_block(hash) 44 | end 45 | 46 | def fetch_block(hash) 47 | fetched_block = client.request("getblock", hash) rescue nil 48 | return if fetched_block.nil? 49 | Block.new(fetched_block).tap { |block| 50 | (blocks_by_height[block.height] ||= Array.new).push(block) 51 | orphaned_blocks.push(block) if block.confirmations < 0 52 | if block.previousblockhash 53 | block.attach_to_parent(walk_to_genesis(block.previousblockhash)) 54 | else 55 | @genesis_block = block 56 | end 57 | } 58 | end 59 | 60 | def client 61 | @client ||= Bitcoiner.new(rpc_username, rpc_password, node_url) 62 | end 63 | 64 | def rpc_username 65 | "satoshi" 66 | end 67 | 68 | def rpc_password 69 | "waswrong" 70 | end 71 | 72 | def node_url 73 | "http://blue-node:8332" 74 | end 75 | 76 | attr_reader :blocks, :blocks_by_height, :genesis_block 77 | 78 | class Block < OpenStruct 79 | def attach_to_parent(block) 80 | block.children_as_hash ||= Hash.new 81 | block.children_as_hash[self.hash] = self 82 | end 83 | 84 | def children 85 | children_as_hash.to_h.values 86 | end 87 | 88 | def to_h 89 | object = {} 90 | description = hash + " :: #{nTx.to_i} txns" 91 | if nTx.to_i < 2 92 | description << " (EMPTY BLOCK!)" 93 | end 94 | description << " :: height=#{height}" 95 | object[description] = children.map(&:to_h) 96 | object 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /snitch/there_is_no_spoon.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/json' 3 | require 'tty-tree' 4 | require_relative 'blorkchain' 5 | 6 | class ThereIsNoSpoon < Sinatra::Base 7 | set :bad_block_list, [] 8 | set :bind, '0.0.0.0' 9 | 10 | get '/' do 11 | blorkchain = Blorkchain.new 12 | raw_block_trees = blorkchain.branches_at_depth(51).map(&:to_h) 13 | @block_trees = raw_block_trees.map { |raw_block_tree| TTY::Tree.new(raw_block_tree) } 14 | @orphaned_blocks = blorkchain.orphaned_blocks 15 | @mempool_count = blorkchain.mempool_count 16 | erb :rubber_dinghy_rapids_bro 17 | end 18 | 19 | private 20 | 21 | run! if app_file == $0 22 | end 23 | -------------------------------------------------------------------------------- /snitch/views/rubber_dinghy_rapids_bro.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |10 | Number of finalised transactions since attack started: 0 11 | Number of double-spendable transactions trapped in mempool: <%= @mempool_count %> 12 | Number of honest blocks orphaned by heavier chain of empty blocks: <%= @orphaned_blocks.count %> 13 | Estimated cost to miners of failed defence attempts at today's block reward value: $<%= (@orphaned_blocks.count * 312_500).to_s.gsub(/\d(?=(...)+$)/, '\0,') %> (6.25 BTC @ $50k) 14 |15 | <% @block_trees.each do |block_tree| %> 16 |
17 | <%= block_tree.render %> 18 |19 | <% end %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /start_from_scratch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ./seed-network.sh 5 | ./run_simulation.sh 6 | --------------------------------------------------------------------------------