PiggyBank x Pwnagotchi

# WiFiをクラックするツールで貯satsしよう！

PwnagotchiのWaveshare 2.13 inch V4バージョンを使ってビットコイン貯金箱を作りました。再現するのちょっと難しいと思いますが、僕は楽しかったです。

# 必要なハード
- Raspberry Pi Zero 2W
- Waveshare 2.13 Inch e-Paper Display HAT V4
- microSDカード
- ラズパイ用バッテリー

# ライブラリ一覧
ライブラリのインストールが一番難しいです。本番環境だと入れられないライブラリがあるので必ず仮想環境に必要なライブラリを全てインストールして実行する必要があります。インストール方法は事前にある程度調べてから実行することをお勧めします。本当に難しいです。

```
(myenv) pi@raspberry:~/e-Paper/RaspberryPi_JetsonNano/python/examples $ pip list
Package             Version
------------------- ------------
asn1crypto          1.5.1
bip_utils           2.9.3
cbor2               5.6.4
certifi             2024.8.30
cffi                1.17.1
charset-normalizer  3.3.2
coincurve           19.0.1 #cmake then pip install coincurve==19.0.1
colorzero           2.0
crcmod              1.7
ecdsa               0.19.0
ed25519-blake2b     1.4.1
gpiozero            2.0.1
idna                3.8
Jetson.GPIO         2.1.6
lgpio               0.2.2.0 # pip install lgpio
pillow              10.4.0
pip                 23.0.1
py-sr25519-bindings 0.2.0
pycparser           2.22
pycryptodome        3.20.0
PyNaCl              1.5.0
pypng               0.20220715.0
qrcode              7.4.2 # sudo apt install python3-pil python3-qrcode
requests            2.32.3
RPi.GPIO            0.7.1 # sudo apt install python3-rpi.gpio
setuptools          74.1.2 # pip install setuptools
six                 1.16.0
spidev              3.6 # pip install spidev RPi.GPIO
typing_extensions   4.12.2
urllib3             2.2.2
waveshare-epd       0.0.0
```

cmakeとcoincurveをインストールした際のコマンド（多分）coincurveは19.0.1だったらいけました。ここかなり苦戦しました。qrcodeもちょっと大変でしたね。
```
# cmake
sudo apt-get update && sudo apt-get upgrade
sudo apt-get install cmake
which cmake # /usr/bin/cmake
/usr/bin/cmake --version # cmake version 3.25.1
pip install coincurve==19.0.1
```

Waveshare 2.13 inch V4を動かすためのドライバもインストールします。慣れてないと難しいかもです。
```
pip install setuptools
cd ~/e-Paper/RaspberryPi_JetsonNano/python/setup.py
sudo python3 setup.py install
```


## pip freeze
被りますけど、一応。
```
(myenv) pi@raspberry:~/e-Paper/RaspberryPi_JetsonNano/python/examples $ pip freeze
asn1crypto==1.5.1
bip_utils==2.9.3
cbor2==5.6.4
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.3.2
coincurve==19.0.1
colorzero==2.0
crcmod==1.7
ecdsa==0.19.0
ed25519-blake2b==1.4.1
gpiozero==2.0.1
idna==3.8
Jetson.GPIO==2.1.6
lgpio==0.2.2.0
pillow==10.4.0
py-sr25519-bindings==0.2.0
pycparser==2.22
pycryptodome==3.20.0
PyNaCl==1.5.0
pypng==0.20220715.0
qrcode==7.4.2
requests==2.32.3
RPi.GPIO==0.7.1
six==1.16.0
spidev==3.6
typing_extensions==4.12.2
urllib3==2.2.2
waveshare-epd==0.0.0
```
# 実際のコード
Xpubを読みこんでそこからアドレスを生成してノードにそれぞれのアドレスが既にusedかunusedか、合計でどれくらい中にsatsが入っているかなどを照会してそれをディスプレイに表示しています。またunusedのアドレスのうち最もインデックス番号の若いアドレスをQRコードとして表示してsats情報と同時に表示するようにしています。

```python
#!/usr/bin/python
# -*- coding:utf-8 -*-
import sys, os, logging, platform, time, requests, json
from waveshare_epd import epd2in13_V4  # Import the Waveshare E-Ink display driver
from PIL import Image, ImageDraw, ImageFont
from bip_utils import Bip84, Bip84Coins, Bip44Changes
import qrcode

# Initialize paths, logging, and display driver
picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic')
libdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib')
if os.path.exists(libdir): sys.path.append(libdir)

logging.basicConfig(level=logging.DEBUG)
time.sleep(30)  # Delay before fetching data

# ==========================
# Helper Functions
# ==========================
def load_json(file):
    if os.path.exists(file):
        with open(file, 'r') as f:
            return json.load(f)
    raise FileNotFoundError(f"{file} not found. Please make sure the file exists.")

def api_get(url):
    response = requests.get(url)
    return response.json() if response.status_code == 200 else None

# ==========================
# Bitcoin Functions
# ==========================
def get_utxos(address): return api_get(f"https://blockstream.info/api/address/{address}/utxo")

def get_balance(address):
    data = api_get(f"https://blockstream.info/api/address/{address}")
    if data:
        balance = data.get('chain_stats', {}).get('funded_txo_sum', 0) - data.get('chain_stats', {}).get('spent_txo_sum', 0)
        mempool_balance = data.get('mempool_stats', {}).get('funded_txo_sum', 0) - data.get('mempool_stats', {}).get('spent_txo_sum', 0)
        return balance + mempool_balance
    return None

def collect_utxos(addresses):
    all_utxos, total_satoshis = [], 0
    for address in addresses:
        utxos = get_utxos(address)
        if utxos:
            for utxo in utxos:
                all_utxos.append(utxo)
                total_satoshis += utxo['value']
    return all_utxos, total_satoshis

# ==========================
# Display Functions
# ==========================
def display_text(draw, font, total_satoshis):
    draw.text((10, 10), "I'm full! Break me to take out sats", font=font, fill=0)
    draw.text((10, 30), "Total Balance:", font=font, fill=0)
    draw.text((10, 50), f"{total_satoshis} satoshi", font=font, fill=0)
    draw.text((10, 80), "Please use your seed directly.", font=font, fill=0)
    draw.text((10, 95), "I'm really proud of you. Good job!", font=font, fill=0)

def display_full_status(total_satoshis):
    epd = epd2in13_V4.EPD()
    epd.init(), epd.Clear(0xFF)
    eink_image = Image.new('1', (epd.height, epd.width), 255)
    draw = ImageDraw.Draw(eink_image)
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 12)
    display_text(draw, font, total_satoshis)
    epd.display(epd.getbuffer(eink_image.rotate(90, expand=True)))
    epd.sleep()

def display_on_eink(index, balance, addr, count):
    epd = epd2in13_V4.EPD()
    epd.init(), epd.Clear(0xFF)
    img = Image.new('1', (epd.height, epd.width), 255)
    draw = ImageDraw.Draw(img)
    font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 11)

    qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=3, border=1)
    qr.add_data(addr), qr.make(fit=True)
    qr_img = qr.make_image(fill="black", back_color="white").resize((100, 100))
    img.paste(qr_img, (5, 10))

    draw.text((110, 10), "Bitcoin PiggyBank", font=font, fill=0)
    draw.text((110, 40), "Total Balance:", font=font, fill=0)
    draw.text((110, 60), f"{balance} sats", font=font, fill=0)
    draw.text((110, 90), f"You saved {count} times!", font=font, fill=0)

    epd.display(epd.getbuffer(img.rotate(90, expand=True)))
    epd.sleep()

# ==========================
# Main Execution Loop
# ==========================
zpub = load_json("zpub.json").get("zpub")
bip84_ctx = Bip84.FromExtendedKey(zpub, Bip84Coins.BITCOIN)
addresses = [bip84_ctx.Change(Bip44Changes.CHAIN_EXT).AddressIndex(i).PublicKey().ToAddress() for i in range(21)]

while True:
    total_balance, found_unused, count, i = 0, False, 0, 0

    while not found_unused:
        addr = addresses[i]
        balance = get_balance(addr)
        print(f"Checking address {i}: {addr}, Balance: {balance} sats")

        if balance is None:
            print(f"Skipping address {i} due to rate limit.")
            continue

        if balance == 0 and not found_unused:
            found_unused, current_index = True, i

        if balance > 0:
            total_balance += balance
            count += 1

        i += 1

    display_on_eink(current_index, total_balance, addr, count)
    print(f"Unused address: {addr} (Index: {current_index})")
    print(f"Total Balance: {total_balance} sats")
    print(f"Used Addresses Count: {count}")

    if count >= 21:
        print("Used Addresses Count reached 21. Initiating break.")
        utxos, total_satoshis = collect_utxos(addresses)
        display_full_status(total_satoshis)
        break

    time.sleep(30)
```

ここにxpubを置かないと動きません。

```zpub.json
{
  "zpub": "replace this with xpub"
}
```

Xpubは流出してもsatsを奪われることはないんですが、全アドレスの動きが奪取者にわかってしまうのでもしPiggyBankを無くしたら、中にあるsatsはCoinJonするか取引所に送るかAtomicSwapして一旦追えなくする必要が出てしまいます。Raspberry Pi Zero 2WはSSHで総当たりできるのがデフォルトなので、セキュリティ気にするなら捨てシードを使うか、SSHやBluetoothを使用不可にしておく必要がありますね。あとはノードを自分で立ててそこに接続する方が良いです。


# 追記
もしかしたら、これで一発でインストールできるかも知れないです。試行錯誤しながらインストールしてたのでどうやって全部インストールしたか把握してないです。すみません。あと以下のライブラリはBreak_PiggyBankの方に使われるものも含まれています。

```bash
nano setup.sh

chmod +x setup.sh

./setup.sh
```

```bash
#!/bin/bash

# Update and upgrade the system
sudo apt-get update && sudo apt-get upgrade -y

# Install CMake and other system dependencies
sudo apt-get install -y cmake build-essential pigpio libjpeg-dev zlib1g-dev python3-dev python3-pip git \
libfreetype6-dev liblcms2-dev libopenjp2-7 libtiff5 libwebp-dev tcl8.6 tk8.6

# Verify CMake installation
cmake_version=$(cmake --version)
echo "CMake version installed: $cmake_version"

# Install coincurve Python package (version 19.0.1)
pip install coincurve==19.0.1

# Clone the e-Paper repository if not already cloned
if [ ! -d "~/e-Paper" ]; then
    git clone https://github.com/waveshareteam/e-Paper.git ~/e-Paper
fi

# Install setuptools (for Python package management)
pip install setuptools

# Run the Waveshare e-Paper setup (includes Jetson.GPIO setup)
cd ~/e-Paper/RaspberryPi_JetsonNano/python/
sudo python3 setup.py install

# Install Pillow and qrcode with the necessary dependencies
pip install Pillow qrcode[pil]

# Install bip_utils via pip
pip install bip_utils==2.9.3

# Install all Python packages listed in the requirements
pip install asn1crypto==1.5.1 \
base58check==1.0.2 \
cbor2==5.6.4 \
certifi==2024.8.30 \
cffi==1.17.1 \
charset-normalizer==3.3.2 \
colorzero==2.0 \
crcmod==1.7 \
ecdsa==0.19.0 \
ed25519-blake2b==1.4.1 \
gpiozero==2.0.1 \
idna==3.8 \
lgpio==0.2.2.0 \
mpmath==1.3.0 \
py-sr25519-bindings==0.2.0 \
pycparser==2.22 \
pycryptodome==3.20.0 \
PyNaCl==1.5.0 \
pypng==0.20220715.0 \
requests==2.32.3 \
RPi.GPIO==0.7.1 \
six==1.16.0 \
spidev==3.6 \
sympy==1.13.2 \
typing_extensions==4.12.2 \
urllib3==2.2.2

# Install python-bitcointx from GitHub
if [ ! -d "~/python-bitcointx" ]; then
    git clone https://github.com/Simplexum/python-bitcointx.git ~/python-bitcointx
fi
cd ~/python-bitcointx
python3 setup.py install

# Final message
echo "Setup complete!"
```