ビットコイン・マイニングゲーム・パイソン版

# 趣旨
以前ビットコインのマイニングゲームをC++で作ろうとした時にうまくできずに半ば中途半端な状態で放置してしまったんですが、pythonならすぐできんじゃね？と思ってやってみました。

ビットコインのマイニングプロセスを大体のレベルで理解した上で自然言語でGPT先生にお願いしてコードを書いてもらってます。コードを書いてもらった後にどのライブラリで実現してるのか、どういう順序でロジックを立ててるのか、ブロックの生成手順は合ってるかなどを確認しています。

多分どこかしらにエラーがあると思いますが、実際にマイニングに成功することはないと思うので、個別に詳細を確認することはあっても100％の完璧さは求めないことにします。

# コード
bitcoin.rpcで実際にノードとやり取りできるようにしてますし、requestsでブロックデータやtx取得してますね。pygameでゲーム画面を生成させてhashlibとrandomでナンス値とハッシュ計算やってるようです。

```python bitcoin_mining_game.py
import pygame
import requests
import hashlib
import time
import random
import bitcoin.rpc  # Make sure you install python-bitcoinlib

# Step 1: Fetch Latest Block Data and Unconfirmed Transactions
def fetch_blockchain_data():
    print("Fetching blockchain data...")
    latest_block_url = "https://blockchain.info/latestblock"
    latest_block_response = requests.get(latest_block_url)
    latest_block_data = latest_block_response.json()
    
    unconfirmed_tx_url = "https://blockchain.info/unconfirmed-transactions?format=json"
    unconfirmed_tx_response = requests.get(unconfirmed_tx_url)
    unconfirmed_tx_data = unconfirmed_tx_response.json()
    
    print("Fetched blockchain data successfully.")
    return latest_block_data, unconfirmed_tx_data["txs"]

# Step 2: Fetch Current Bitcoin Difficulty
def fetch_difficulty():
    print("Fetching current Bitcoin difficulty...")
    difficulty_url = "https://blockchain.info/q/getdifficulty"
    difficulty_response = requests.get(difficulty_url)
    difficulty_value = float(difficulty_response.text)
    print(f"Current difficulty: {difficulty_value}")
    return difficulty_value

# Step 3: Calculate Merkle Root from Transactions
def calculate_merkle_root(transactions):
    if len(transactions) == 1:
        return transactions[0]

    if len(transactions) % 2 != 0:
        transactions.append(transactions[-1])

    new_tx_list = []
    for i in range(0, len(transactions), 2):
        if isinstance(transactions[i], dict):
            tx_pair = transactions[i]["hash"] + transactions[i + 1]["hash"]
        else:
            tx_pair = transactions[i] + transactions[i + 1]
        
        new_tx_list.append(hashlib.sha256(tx_pair.encode()).hexdigest())

    return calculate_merkle_root(new_tx_list)

# Step 4: Construct Block Header
def construct_block_header(version, prev_block_hash, merkle_root, timestamp, bits, nonce):
    return (version + prev_block_hash + merkle_root + timestamp + bits + nonce)

# Step 5: Hash Calculation and Difficulty Check
def calculate_block_hash(header):
    header_bin = header.encode('utf-8')
    hash1 = hashlib.sha256(header_bin).hexdigest()
    hash2 = hashlib.sha256(hash1.encode('utf-8')).hexdigest()
    return hash2

def check_difficulty(block_hash, target_difficulty):
    return int(block_hash, 16) < int(target_difficulty, 16)

# Step 6: Submit the mined block to Bitcoin node via RPC
def submit_mined_block(block_hex):
    # Update with your node's configuration
    proxy = bitcoin.rpc.Proxy(service_url="http://yourusername:yourpassword@127.0.0.1:8332")
    
    try:
        # Submit the block
        proxy.call('submitblock', block_hex)
        print("Block successfully submitted to the network!")
    except Exception as e:
        print(f"Failed to submit block: {e}")

# Step 7: Pygame Setup for Player Interaction
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Bitcoin Mining Game")
clock = pygame.time.Clock()

cursor = pygame.Rect(100, 100, 50, 50)

# Function to generate bugs (representing nonces)
def generate_bugs():
    return [pygame.Rect(random.randint(0, 750), random.randint(0, 550), 20, 20) for _ in range(20)]  # 20 bugs

bugs = generate_bugs()

# Function to move the cursor
def move_cursor():
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        cursor.x -= 5
    if keys[pygame.K_RIGHT]:
        cursor.x += 5
    if keys[pygame.K_UP]:
        cursor.y -= 5
    if keys[pygame.K_DOWN]:
        cursor.y += 5

# Function to simulate eating a bug and returning the nonce
def eat_bug():
    for bug in bugs:
        if cursor.colliderect(bug):
            bugs.remove(bug)
            return random.randint(0, 2**64 - 1)  # Larger Nonce value
    return None

# Add a button for refreshing the bugs
refresh_button = pygame.Rect(350, 550, 100, 40)  # X, Y, Width, Height

# Step 8: Mining Simulation with Player Interaction and Refresh Button
def simulate_mining_game():
    print("Starting mining simulation...")
    
    # Fetch blockchain data
    block_data, transactions = fetch_blockchain_data()
    prev_block_hash = block_data["hash"]
    difficulty = fetch_difficulty()
    
    target_difficulty = '00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
    merkle_root = calculate_merkle_root(transactions)
    version = "20000000"
    timestamp = hex(int(time.time()))[2:]

    nonce = None
    mining_in_progress = True
    while mining_in_progress:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                return
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if refresh_button.collidepoint(event.pos):
                    print("Refreshing bugs!")
                    global bugs
                    bugs = generate_bugs()

        screen.fill((0, 0, 0))
        
        move_cursor()
        pygame.draw.rect(screen, (255, 0, 0), cursor)
        
        for bug in bugs:
            pygame.draw.rect(screen, (0, 255, 0), bug)
        
        # Draw the refresh button
        pygame.draw.rect(screen, (0, 0, 255), refresh_button)
        font = pygame.font.Font(None, 36)
        refresh_text = font.render('Refresh', True, (255, 255, 255))
        screen.blit(refresh_text, (refresh_button.x + 10, refresh_button.y + 5))

        # Check if the player has eaten a bug (nonce)
        eaten_nonce = eat_bug()
        if eaten_nonce:
            nonce = eaten_nonce
            print(f"Eaten nonce: {nonce}")
            
            # Construct block header
            block_header = construct_block_header(version, prev_block_hash, merkle_root, timestamp, target_difficulty, hex(nonce)[2:].zfill(8))
            block_hash = calculate_block_hash(block_header)
            
            # Check if block hash meets difficulty target
            if check_difficulty(block_hash, target_difficulty):
                print(f"Block mined successfully!")
                print(f"Block Hash: {block_hash}")
                print(f"Nonce: {nonce}")
                
                # Convert the block header into the full block and send it to the node
                block_hex = block_header  # Simplified; in real life, you'd serialize the entire block
                submit_mined_block(block_hex)
                
                mining_in_progress = False

        pygame.display.flip()
        clock.tick(60)

simulate_mining_game()
```

# 詳細確認
気になる部分のロジックを確認していきます。
## ブロックヘッダー
ブロックヘッダーもきちんと生成されているようですね。
```python
# Step 4: Construct Block Header
def construct_block_header(version, prev_block_hash, merkle_root, timestamp, bits, nonce):
    return (version + prev_block_hash + merkle_root + timestamp + bits + nonce)
```
ブロックヘッダーは以下の6つの要素で構成されます。
![Screenshot 2024-09-10 at 10.07.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670541/8211bb81-2a59-d277-ad5f-4709c56bd716.png)

## マークルツリー
取得したtxデータ群を繋ぎ合わせてますね。ただロジックが正しくないように見えます。
```python
# Step 3: Calculate Merkle Root from Transactions
def calculate_merkle_root(transactions):
    if len(transactions) == 1:
        return transactions[0]

    if len(transactions) % 2 != 0:
        transactions.append(transactions[-1])

    new_tx_list = []
    for i in range(0, len(transactions), 2):
        if isinstance(transactions[i], dict):
            tx_pair = transactions[i]["hash"] + transactions[i + 1]["hash"]
        else:
            tx_pair = transactions[i] + transactions[i + 1]
        
        new_tx_list.append(hashlib.sha256(tx_pair.encode()).hexdigest())

    return calculate_merkle_root(new_tx_list)
```

## マイニングに成功したブロックの送信
RPCで送信するようですが、もちろんノードと立てて繋いでいないので仮のURLが当てられていますね。
```python
# Step 6: Submit the mined block to Bitcoin node via RPC
def submit_mined_block(block_hex):
    # Update with your node's configuration
    proxy = bitcoin.rpc.Proxy(service_url="http://yourusername:yourpassword@127.0.0.1:8332")
    
    try:
        # Submit the block
        proxy.call('submitblock', block_hex)
        print("Block successfully submitted to the network!")
    except Exception as e:
        print(f"Failed to submit block: {e}")
```
## Difficulty
ブロックのハッシュ計算を2回実行してdifficultyのthreshold値を超えているか確認してますね。エンコードやダイジェストの詳細まで把握してないので、実際にこれで問題ないかわかりません。今度詳細まで学んでおこうと思います。
```python
# Step 5: Hash Calculation and Difficulty Check
def calculate_block_hash(header):
    header_bin = header.encode('utf-8')
    hash1 = hashlib.sha256(header_bin).hexdigest()
    hash2 = hashlib.sha256(hash1.encode('utf-8')).hexdigest()
    return hash2

def check_difficulty(block_hash, target_difficulty):
    return int(block_hash, 16) < int(target_difficulty, 16)
```

## ゲーム実装部分
せっかくフェッチしたデータを無視してplaceholder的にデータ当てはめてますね。GPT先生も詳細まできちんと全て指示を出さないと仕事する時に手を抜くみたいです笑。
```python
# Step 8: Mining Simulation with Player Interaction and Refresh Button
def simulate_mining_game():
    print("Starting mining simulation...")
    
    # Fetch blockchain data
    block_data, transactions = fetch_blockchain_data()
    prev_block_hash = block_data["hash"]
    difficulty = fetch_difficulty()
    
    target_difficulty = '00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
    merkle_root = calculate_merkle_root(transactions)
    version = "20000000"
    timestamp = hex(int(time.time()))[2:]
```

# 感想
マークルルートの計算とディフィカルティーの計算の部分を把握できていないということが判明したので、詳細まで学んでおきます。

そしてやっぱりC++難しい。Pythonにした途端、急に難易度が下がります。