PythonのLogging

シンプルなログ出力はお馴染みのprint文

print("I am the simplest log.")

ファイル出力、メール送信、HTTP通信など動作ログをよりフレキシブルに出力したい場合にはloggingモジュールを使用してログを出力する。

### ログレベル
DEBUG, INFO, WARNING, ERROR, CRITICAL

出力

import logging 

logging.info("info log")
logging.warning("warning log")

$ python3 test.py
WARNING:root:warning log

info はコマンドラインに出力されない
loggerオブジェクトを取得してログを出力する

import logging 

logger = logging.getLogger("sample")
logger.info("info log")
logger.warning("warning log")

$ python3 test.py
warning log

import logging 

logger = logging.getLogger("sample")
logger.setLevel(logging.DEBUG)
logger.info("info log")
logger.warning("warning log")

### ハンドラ
StreamHandler, FileHandler, SMTPHandler, HTTPHandlerがある

import logging 

logger = logging.getLogger("sample")
logger.setLevel(logging.DEBUG)


st_handler = logging.StreamHandler()

logger.addHandler(st_handler)
logger.info("info log")
logger.warning("warning log")

handlerで指定することでログ出力をカスタマイズできる。

ECDSAの原理

eは秘密鍵、pは公開鍵
eG = P

ランダムな256ビットの数字k
k * G = R (x座標をr)

uG + vP = kG
kはランダムに選ばれる数字で、u, vは署名者が選ぶ

vP = (k – u)G
P = ((k-u)/v)G

eG = ((k-u)/v)G のため、 e = (k – v) / u
つまり、秘密鍵 e = (k – v) / u を満たす(u, v)の組み合わせであればどれでも良い
eは秘密鍵を知っている人のみわかる

目的は署名ハッシュと呼ぶ。署名ハッシュはzで表す。
uG + vPに当てはめると、 u = z/s, v = z/s

uG + vP = R(目的) = kG
uG + veP = kG
u + ve = k
z/s + re/s = k
(z + re)/s = k
s = (z + re)/k

k は256ビットを保証するためhash256を2回繰り返す

1. 署名(r, s)、署名対象のハッシュをz, 公開鍵をPとする
2. u = z/s, v = r/sを計算する
3. uG + vP = Rを計算する
4. Rのx座標がrと同じであれば署名は有効

標準Script p2pk(pay to public key)

ScriptPubKeyの形式がp2pkであるトランザクションアウトプット
ECDSAの署名を検証するには、メッセージz, 公開鍵P, 署名r, sが必要
p2pkでは、ビットコインは公開鍵に送られ、秘密鍵の所有者は署名を作成することによりビットコインをアンロックできる。
ScriptPubKeyはビットコインを秘密鍵の所有者の管理下に置く。
P は公開鍵(x,y) 256bit
e は秘密鍵

e.g.
ScriptPubKey
41 – length of pubkey
0411…a3 pubkey
ac – OP_CHECKSIG

ScriptSig
47 – length of signature
3044 .. 01 – signature

OP_CHECKSIG, pubkey + signature のコマンドセットにする
スタックの先頭エレメントがゼロ以外のときは、有効なscript sigとみなされる

stackにpubkey, signatureをpushし、OP_CHECKSIGで有効であればstackに1をpush, 有効でなければ0をpushする
0以外であれば処理を終了する
0の場合は、スクリプトは無効となり、トランザクション自体が無効になる
秘密鍵を知っている人だけが有効なScriptSigを作り出すことができる

Scriptのパースとシリアライズ

各コマンドがオプコードかスタックにプッシュされるオプコードかを判定する
1~77まではエレメントでそれ以上(78以上)がオプコードとみなす

class Script:

    def __init__(self, cmds=None):
        if cmds is None:
            self.cmds = []
        else:
            self.cmds = cmds
    
    // omission

    @classmethod
    def parse(cls, s):
        length = read_varint(s)
        cmds = []
        count = 0
        while count < length:
            current = s.read(1)
            count += 1
            current_byte = current[0]
            if current_byte >= 1 and current_byte <= 75:
                n = current_byte
                cmds.append(s.read(n))
                count += n
            elif current_byte == 76:
                data_length = little_endian_to_int(s.read(1))
                count += data_length + 1
            elif current_byte == 77:
                data_length = little_endian_to_int(s.read(2))
                cmds.append(s.read(data_length))
                count += data_length + 2
            else:
                op_code = current_byte
                cmds.append(op_code)
        if count != length:
            raise SyntaxError('parsing script failed')
        return cls(cmds)

### シリアライズ
整数の場合はオプコード、それ以外はエレメント

class Script:

    // 

    def raw_serialize(self):
        result = b''
        for cmd in self.cmds:
            if type(cmd) == int:
                result += into_to_little_endian(cmd, 1)
            else:
                length = len(cmd)
                if length < 75:
                    result += int_to_little_endian(length, 1)
                elif length > 75 and length < 0x100:
                    result += int_to_little_endian(76, 1)
                    result += int_to_little_endian(length, 1)
                elif length >= 0x100 and length <= 520:
                    result += int_to_little_endian(77, 1)
                    result += int_to_little_endian(length, 2)
                else:
                    raise ValueError('too long an cmd')
                result += cmd
        return result
    
    def serialize(self):
        result = self.raw_serialize()
        total = len(result)
        return encode_varint(total) + result

### Script Fieldの連結
scriptオブジェクトは評価が必要なコマンドのセットを表す
スクリプを評価するにあたり、ロックボックスのScriptPubKey(outputsの2番目)とアンロックするScriptSig(inputsの3番目)のフィールドを連結する必要がある。
ScriptPubKeyはPrevious Transaction, ScriptSigはCurrent Transaction
ScriptSigがScriptPubKeyをunlockする
ScriptSigからのコマンドはScriptPubKeyのコマンドの上に配置し、処理するコマンドがなくなるまで1つづつ処理される

class Script:
    def __add__(self, other):
        
        return Script(self.cmds + other.cmds)

標準スクリプト(Base58, Bech32)
p2pk, p2pkh, p2sh, p2wpkh, p2wsh

ウォレットは様々なタイプを解釈する方法を知っており、適切なScriptPubKeyを作成する

スクリプトフィールドのパース

ScriptPubKeyとScriptSigはどちらも同じ方法でパースされる
バイトが0x01から0x4b(10進数の75)の間の場合(nと呼ぶ)、次のnバイトを1つのエレメントとして読み取る。その範囲の値ではないときは、そのバイトはオペレーションを表している

0x00 – OP_0
0x51 – OP_1
0x60 – OP_16
0x76 – OP_DUP
0x93 – OP_ADD
0xa9 – OP_HASH160
0xac – OP_CHECHSIG

0x4bより長いエレメントの場合は、特別なオプコードOP_PUSHDATA1(76-255), OP_PUSHDATA2(256-520), OP_PUSHDATA4がある。

Scriptのエレメントとオペレーション

エレメントはデータのことで、signature, pubkeyなど
オペレーションはデータに対して操作をする OP_CHECKSIG, OP_DUP, OP_HASH160
OP_DUPはスタックの先頭エレメントを複製し、新しく生成したエレメントにpush

重要なオペレーションはOP_CHECKSIG
stackから pubkey, signatureをpopし、その署名が公開鍵に対して有効であるか確認する。有効な場合、OP_CHECHSIGは1をスタックにpush

op_dup 118(10進数) -> 0x76(16進数)

def op_dup(stack):
    if len(stack) < 1:
        return False
    stack.append(stack[-1])
    return True

hash256 170(10進数) -> 0xaa(16進数)

def op_hash256(stack):
    if len(stack) < 1:
        return False
    element = stack.pop()
    stack.append(hash256(element))
    return True

hash160だと同様に以下のようになる。

def op_hash160(stack):
    if len(stack) < 1:
        return False
    element = stack.pop()
    stack.append(hash160(element))
    return True

Python stackのpopとpush

class MyStack:
    def __init__(self):
        self.stack = []
    
    def push(self, item):
        self.stack.append(item)

    def pop(self):
        pass

mystack = MyStack()
mystack.push(0)
mystack.push(1)
mystack.push(2)
print(mystack.stack)

$ python3 test.py
[0, 1, 2]

pop

    def pop(self):
        result = self.stack[-1]
        del self.stack[-1]
        return result

print(mystack.pop())
print(mystack.pop())

取り出して削除なので、[-1]の後にdelを実行している。
上記と同じことをpop()と書くことができる。

    def pop(self):
        return self.stack.pop()

stackが0の場合があるので、例外処理も書かなければならない。

    def pop(self):
        if len(self.stack) == 0:
            return None
        return self.stack.pop()

Transactionの手数料

トランザクションの手数料はinputsからoutputsを引いたもの
マイナーへの報酬となるトランザクション手数料のため、inputsはoutputsの合計以上の額にならなければならない。

class TxFetcher:
    cache = {}

    @classmethod
    def get_url(cls, testnet=False):
        if testnet:
            return ''
        else:
            return ''

    @classmethod
    def fetch(cls, tx_id, testnet=False, fresh=False):
        if fresh or (tx_id not in cls.cache):
            url = '{}/tx/{}.hex'.format(cls.get_url(testnet), tx_id)
            response = requests.get(url)
            try:
                raw = bytes.fromhex(response.text.strip())
            except ValueError:
                raise ValueError('unexpected response: {}'.format(response.text))
            if raw[4] == 0:
                raw = raw[:4] + raw[6:]
                tx = Tx.parse(BytesIO(raw), testnet=testnet)
                tx.locktime = little_endian_to_int(raw[-4:])
            else:
                tx = Tx.parse(BytesIO(raw), testnet=testnet)
            if tx.id() != tx_id:
                raise ValueError('not the same id: {} vs {}'.format(tx.id(), tx_id))
            cls.cache[tx_id] = tx
        cls.cache[tx_id].testnet = testnet
        return cls.cache[tx_id]
    def fetch_tx(self, testnet=False):
        return TxFetcher.fetch(self.prev_tx.hex(), testnet=testnet)

    def value(self, testnet=False):
        tx = self.fetch_tx(testnet=testnet)
        return tx.tx_outs[self.prev_index].amount
    
    def script_pubkey(self, testnet=False):
        tx = self.fetch_tx(testnet=test)
        return tx.tx_outs[self.prev_index].script_pubkey

transaction idで特定し、前のトランザクションindexから対象outputのamout, script_pubkeyを取得している

    def fee(self, testnet=False):
        input_sum, output_sum = 0, 0
        for tx_in in self.tx_ins:
            input_sum += tx_in.value(testnet=testnet)
        for tx_out in self.tx_outs:
            output_sum += tx_out.amount
        return input_sum - output_sum

transactionのserialize

class TxOut:
    def serialize(self):
        result = int_to_little_endian(self.amount, 8)
        result += self.script_pubkey.serialize()
        return result


class TxIn:
    def serialize(self):
        result = self.prev_tx[::-1]
        result += int_to_little_endian(self.prev_index, 4)
        result += self.script_sig.serialize()
        result += int_to_little_endian(self.sequence, 4)
        return result 

class Tx:
    def serialize(self)
        result = int_to_little_endian(self.version, 4)
        result += encode_varint(len(self.tx_ins))
        for tx_in in self.tx_ins:
            result += tx_in.serialize()
        result += encode_varint(len(self.tx_outs))
        for tx_out in self.tx_outs:
            result += tx_out.serialize()
        result += int_to_little_endian(self.locktime, 4)
        return result