Esempio n. 1
0
    def get(self):
        coind_type = self.get_request_coind_type()
        height = self.get_request_int('height', None)
        hash = self.request.get('hash', None)

        db = CloudSQL(coind_type)
        with db as c:
            # ブロックの取得
            if height is not None:
                c.execute('SELECT * FROM blockheader WHERE height = %s',
                          (height, ))
            elif hash is not None:
                c.execute('SELECT * FROM blockheader WHERE hash = %s',
                          (hash, ))
            else:
                raise ValidationError('condition',
                                      'there is no height and hash')

            blockheader = c.fetchone()

            if blockheader is None:
                # 存在しない場合
                raise exc.HTTPNotFound()

            # 現在のブロック高を取得する
            c.execute('SELECT MAX(height) FROM blockheader')
            chain_height = c.fetchone()['MAX(height)']

            # 後続ブロックのハッシュ値を取得する
            c.execute('SELECT hash FROM blockheader WHERE height = %s',
                      (blockheader['height'] + 1, ))
            nextblockhash = c.fetchone()
            if nextblockhash is not None:
                nextblockhash = nextblockhash['hash']

            # 直前ブロックのハッシュ値を取得する
            c.execute('SELECT hash FROM blockheader WHERE height = %s',
                      (blockheader['height'] - 1, ))
            previousblockhash = c.fetchone()
            if previousblockhash is not None:
                previousblockhash = previousblockhash['hash']

        # コインノードからの生データをパース
        json_data = json.loads(
            bz2.decompress(base64.b64decode(blockheader['json'])))

        # 確認数を更新する
        json_data['confirmations'] = chain_height - blockheader['height'] + 1

        # 同期時に握りつぶした情報を補完する
        json_data['nextblockhash'] = nextblockhash
        json_data['previousblockhash'] = previousblockhash

        # JSON 形式でシリアライズして返す
        self.write_json(json_data)
Esempio n. 2
0
    def get(self):
        coind_type = self.get_request_coind_type()
        addresses = self.request.get('addresses')
        offset = self.get_request_int('offset', 0)
        limit = self.get_request_int('limit', None)

        # アドレス json を解析する
        try:
            addresses = ' '.join(json.loads(addresses))
        except ValueError as e:
            raise ValidationError('addresses', e.message)

        db = CloudSQL(coind_type)
        with db as c:
            # 現在の残高を取得する
            c.execute(
                'SELECT balance, serial FROM balance WHERE addresses = %s ORDER BY serial DESC LIMIT 1',
                (addresses, ))
            r = c.fetchone()

            if r is None:
                raise exc.HTTPNotFound()

            balance = r['balance'] / SATOSHI_COIN
            max_serial = r['serial']

            # 取得範囲を限定する
            if limit is not None:
                under_serial = max_serial - offset - limit + 1
            else:
                under_serial = 0
            over_serial = max_serial - offset + 1

            # 履歴順に辞書を配列で格納する
            history = []
            c.execute(
                'SELECT height, txid, time, gain, balance FROM balance WHERE addresses = %s AND serial >= %s AND serial < %s ORDER BY serial',
                (addresses, under_serial, over_serial))
            for e in c.fetchall():
                history.append({
                    'height': e['height'],
                    'txid': e['txid'],
                    'time': int(time.mktime(e['time'].timetuple())),
                    'gain': e['gain'] / SATOSHI_COIN,
                    'balance': e['balance'] / SATOSHI_COIN
                })

        self.write_json({'balance': balance, 'history': history})
Esempio n. 3
0
def run(coind_type, max_leng):
    logging.getLogger().setLevel(logging.DEBUG)

    try:
        # 指定された coind のデータベースへ接続する
        db = CloudSQL(coind_type)
    except MySQLdb.OperationalError:
        # 接続に失敗した場合、データベースの作成から行う
        db = init_db(coind_type)
        logging.debug(coind_type + ': DB initialized.')

    # 同時実行を防ぐため、ロックをかける
    with db as c:
        # 同時実行がない場合、もしくは指定秒数以上経過していれば UPDATE に成功する
        c.execute(
            'UPDATE state SET running_flag = 1, running_time = NOW() WHERE running_flag = 0 OR TIMESTAMPADD( SECOND, %d, running_time ) < NOW()'
            % LOCK_TIMEOUT)
        if c.rowcount != 1:
            # ロック確保に失敗したらここで止める
            raise Exception('running another!!')

    # 開始時のポイントを覚えておく
    with db as c:
        c.execute('SELECT IFNULL(MAX(height)+1,0) FROM blockheader')
        start_block_height = c.fetchone()['IFNULL(MAX(height)+1,0)']

    # ここで DB 更新作業を行う
    if check_db_state(db, coind_type):
        # 巻き戻しを行わなかった場合のみ更新に進む
        sync(db, coind_type, max_leng)

    # 終了時のポイントを取得する
    with db as c:
        c.execute('SELECT IFNULL(MAX(height)+1,0) FROM blockheader')
        end_block_height = c.fetchone()['IFNULL(MAX(height)+1,0)']

    # ロック解除
    with db as c:
        c.execute('UPDATE state SET running_flag = 0, running_time = NULL')

    # 実行結果をログと戻り値の双方に記述する
    result = 'update_db: %d => %d' % (start_block_height, end_block_height)

    logging.debug('%s: %s' % (coind_type, result))
    return {'result': result}
Esempio n. 4
0
	def get( self ):
		coind_type = self.get_request_coind_type()
		n = self.get_request_int('n', 10)

		db = CloudSQL( coind_type )
		with db as c:
			# 最新のものから順に取り出して応答を作成
			r = []
			c.execute( 'SELECT height, hash, miners FROM blockheader ORDER BY height DESC LIMIT %s', {n,} )
			for e in c.fetchall():
				r.append({
					'height': e['height'],
					'hash': e['hash'],
					'miners': e['miners'].split(' '),
				})

		# JSON にシリアライズして返却
		self.write_json( r )
Esempio n. 5
0
    def get(self):
        coind_type = self.get_request_coind_type()
        offset = self.get_request_int('offset', 0)
        limit = self.get_request_int('limit', 10)

        db = CloudSQL(coind_type)
        with db as c:
            # 現在の残高をソートして取得する
            r = []
            c.execute(
                'SELECT * FROM current_balance ORDER BY balance DESC LIMIT %s OFFSET %s',
                (limit, offset))
            for e in c.fetchall():
                r.append({
                    'addresses': e['addresses'].split(' '),
                    'balance': e['balance'] / SATOSHI_COIN
                })

        self.write_json(r)
Esempio n. 6
0
    def get(self):
        coind_type = self.get_request_coind_type()
        n = self.get_request_int('n', 10)

        db = CloudSQL(coind_type)
        with db as c:
            c.execute(
                'SELECT txid, time, height, total_output FROM transaction ORDER BY time DESC LIMIT %s',
                (n, ))

            r = []
            for e in c.fetchall():
                # 応答の要素を作成して追加
                r.append({
                    'height': e['height'],
                    'txid': e['txid'],
                    'time': int(time.mktime(e['time'].timetuple())),
                    'value': e['total_output'] / SATOSHI_COIN
                })

        # JSON にシリアライズして返却
        self.write_json(r)
Esempio n. 7
0
    def get(self):
        coind_type = self.get_request_coind_type()
        txid = self.request.get('txid')
        height = self.request.get('height', None)

        db = CloudSQL(coind_type)
        with db as c:
            # 現在のブロック高を取得する
            c.execute('SELECT MAX(height) FROM blockheader')
            chain_height = c.fetchone()['MAX(height)']

            # トランザクション情報を取得
            if height is None:
                c.execute('SELECT * FROM transaction WHERE txid = %s',
                          (txid, ))
            else:
                c.execute(
                    'SELECT * FROM transaction WHERE txid = %s AND height = %s',
                    (txid, height))

            r = []
            for e in c.fetchall():
                # コインノードからの生データをパース
                json_txdata = json.loads(
                    bz2.decompress(base64.b64decode(e['json'])))

                # vin_n の想定数を数える
                vin_n = 0
                for ee in json_txdata['vin']:
                    if ee.has_key('txid'):
                        vin_n += 1

                # vin の情報を追加する
                c.execute(
                    'SELECT * FROM transaction_link WHERE vin_height = %s AND vin_txid = %s ORDER BY vin_idx',
                    (e['height'], e['txid']))
                vin_link = c.fetchall()
                if len(vin_link) != vin_n:
                    raise Exception(e['height'], e['txid'], len(vin_link),
                                    vin_n, 'mismatch and vin_link and vin_n')

                for i in range(vin_n):
                    json_txdata['vin'][i][
                        'value'] = vin_link[i]['value'] / SATOSHI_COIN
                    json_txdata['vin'][i]['height'] = vin_link[i][
                        'vout_height']
                    json_txdata['vin'][i]['txid'] = vin_link[i]['vout_txid']
                    if json_txdata['vin'][i].has_key('scriptSig'):
                        json_txdata['vin'][i]['scriptSig'][
                            'addresses'] = vin_link[i]['addresses'].split(' ')

                # vout 方向のリンクを追加する
                c.execute(
                    'SELECT * FROM transaction_link WHERE vout_height = %s AND vout_txid = %s ORDER BY vout_idx',
                    (e['height'], e['txid']))
                vout_link = c.fetchall()
                if len(vout_link) != e['vout_n']:
                    raise Exception(e['height'], e['txid'], len(vout_link),
                                    e['vout_n'],
                                    'mismatch and vout_link and vout_n')

                for i in range(e['vout_n']):
                    json_txdata['vout'][i]['scriptPubKey']['txid'] = vout_link[
                        i]['vin_txid']
                    json_txdata['vout'][i]['scriptPubKey'][
                        'height'] = vout_link[i]['vin_height']

                # 確認数を追加する
                json_txdata['confirmations'] = chain_height - e['height'] + 1

                # 高さを追加
                json_txdata['height'] = e['height']

                r.append(json_txdata)

        if len(r) == 0:
            # 存在しない場合
            raise exc.HTTPNotFound()

        # JSON 形式でシリアライズして返す
        if height is None:
            self.write_json(r)
        else:
            self.write_json(r[0])
Esempio n. 8
0
    def get(self):
        coind_type = self.get_request_coind_type()

        # パラメータを取得する
        try:
            params = json.loads(self.request.get('params'))
        except ValueError as e:
            raise ValidationError('params', e.message)

        # パラメータを分解する
        params_from = params['from']
        params_to = params['to']
        req_sigs = params['req_sigs']
        value = params['value']
        fee = params['fee']
        data = params.get('data', None)

        # from を検査してからハッシュ値に変換する
        from_hash = []
        from_addr = []
        from_pk = []
        if not isinstance(params_from, list):
            raise ValidationError('from', 'list')
        for e in params_from:
            try:
                # コインアドレスとしてパースを試みる
                from_hash.append(decode_coin_address(e, coind_type, 'from'))
                from_addr.append(e)
                from_pk.append(None)
            except ValidationError:
                # ダメだったら公開鍵としてパースを試みる
                pk = parse_pub_key(e, 'from')
                addr = encode_coin_address(pk, coind_type)
                from_hash.append(decode_coin_address(addr, coind_type, 'from'))
                from_addr.append(addr)
                from_pk.append(pk)

        # MULTISIG の場合は公開鍵がなければならない
        if len(params_from) != 1:
            for e in from_pk:
                if e is None:
                    raise ValidationError('from', 'pub_key')

        # to を検査してからハッシュ値に変換する
        to_hash = []
        to_addr = []
        to_pk = []
        if not isinstance(params_to, list):
            raise ValidationError('to', 'list')
        for e in params_to:
            try:
                # コインアドレスとしてパースを試みる
                to_hash.append(decode_coin_address(e, coind_type, 'to'))
                to_addr.append(e)
                to_pk.append(None)
            except ValidationError:
                # ダメだったら公開鍵としてパースを試みる
                pk = parse_pub_key(e, 'to')
                addr = encode_coin_address(pk, coind_type)
                to_hash.append(decode_coin_address(addr, coind_type, 'to'))
                to_addr.append(addr)
                to_pk.append(pk)

        # MULTISIG の場合は公開鍵がなければならない
        if len(params_to) != 1:
            for e in to_pk:
                if e is None:
                    raise ValidationError('to', 'pub_key')

        # req_sigs の検査
        if not isinstance(req_sigs, int):
            raise ValidationError('req_sigs', 'int')
        if req_sigs > len(params_to):
            raise ValidationError('req_sigs', 'len')

        # 送金額の確認
        if value < 0.0:
            raise ValidationError('value', '0')

        # トランザクション手数料の確認
        if fee <= 0.0:
            raise ValidationError('fee', '0')

        # OP_RETURN データの検査
        if data is not None:
            try:
                # 16進数文字列で...
                data = unhexlify(data)

                # ...75 Byte 以下
                if len(data) > 75:
                    raise ValidationError('data', 'len')
            except TypeError as e:
                raise ValidationError('data', e.message)

        # 入力トランザクションの検索
        input_value = 0
        vin_txid = []
        vin_idx = []
        vin_type = ''
        vin_reqSigs = []
        db = CloudSQL(coind_type)
        with db as c:
            c.execute(
                'SELECT * FROM transaction_link WHERE addresses = %s AND ISNULL(vin_txid)',
                (' '.join(from_addr), ))
            for e in c.fetchall():
                # トランザクションの生データ取得
                c.execute(
                    'SELECT json FROM transaction WHERE txid = %s AND height = %s',
                    (e['vout_txid'], e['vout_height']))
                raw = json.loads(
                    bz2.decompress(base64.b64decode(c.fetchone()['json'])))

                # P2PKH か MULTISIG でなければスキップ
                type = raw['vout'][e['vout_idx']]['scriptPubKey']['type']
                if len(from_addr) == 1:
                    if type != 'pubkeyhash':
                        continue
                else:
                    if type != 'multisig':
                        continue

                input_value += e['value']
                vin_txid.append(e['vout_txid'])
                vin_idx.append(e['vout_idx'])
                vin_type = type
                vin_reqSigs.append(
                    raw['vout'][e['vout_idx']]['scriptPubKey']['reqSigs'])

                if input_value >= (value + fee) * SATOSHI_COIN:
                    break

        # 残高が足りるか確認
        if input_value < (value + fee) * SATOSHI_COIN:
            raise Exception("You don't have enough money.")

        vout_n = 0
        vout_lt = bytearray()

        # vout[0] (送金出力) の作成
        vout_lt = vout_lt + self.make_vout(
            self.make_script(to_hash, to_pk, req_sigs), value * SATOSHI_COIN)
        vout_n += 1

        # vout[1] (おつり出力) の作成
        rem = long(input_value - (value + fee) * SATOSHI_COIN)
        if rem != 0:
            vout_lt = vout_lt + self.make_vout(
                self.make_script(from_hash, from_pk, max(vin_reqSigs)), rem)
            vout_n += 1

        # vout[2] (OP_RETURN の作成)
        if data is not None:
            vout_lt = vout_lt + self.make_vout(self.make_data_script(data), 0)
            vout_n += 1

        # vout~locktime 区間のバイナリ組み立て
        vout_lt = var_int(vout_n) + vout_lt + bytearray(pack('<I', 0))

        # 入力トランザクションごとに署名対象のハッシュ値を求める
        sign_hash = []
        for i in range(0, len(vin_txid)):
            # 入力トランザクションの出力スクリプト
            # - 送金元アドレスから P2PKH を仮定して生成する
            script = self.make_script(from_hash, from_pk, vin_reqSigs[i])

            # 先頭は vin の個数
            vin = bytearray(var_int(len(vin_txid)))

            # 全入力トランザクションを結合
            for j in range(0, len(vin_txid)):
                # 入力トランザクションを追加
                vin = vin + bytearray(
                    unhexlify(vin_txid[j])[::-1] + pack('<I', vin_idx[j]))

                # 該当する入力のときだけスクリプトを挿入
                if i == j:
                    vin = vin + var_int(len(script)) + script
                else:
                    vin = vin + var_int(0)

                # シーケンスは 0 固定
                vin = vin + bytearray(pack('<I', 0))

            # txCopy を完成させる
            tx = bytearray(pack('<i', 2)) + vin + vout_lt + bytearray(
                pack('<I', 1))

            # SHA256 ハッシュを計算して署名対象のハッシュ値とする
            sign_hash.append({
                'txid': vin_txid[i],
                'hash': sha256(sha256(tx).digest()).hexdigest(),
                'reqSigs': vin_reqSigs[i]
            })

        b64_pk = []
        for e in from_pk:
            if e is not None:
                b64_pk.append(b64encode(e))
            else:
                b64_pk.append(None)

        # 後半ステージに送る情報をペイロードにまとめる
        payload_body = json.dumps({
            'hash': sign_hash,
            'from_pk': b64_pk,
            'vin_txid': vin_txid,
            'vin_idx': vin_idx,
            'vin_type': vin_type,
            'vin_reqSigs': vin_reqSigs,
            'vout_lt': hexlify(vout_lt),
            'log_data': {
                'params_from': params_from,
                'params_to': params_to,
                'sign_hash': sign_hash
            }
        })

        # さらにハッシュをつけて包む
        payload = {
            'body': payload_body,
            'hash': sha256(payload_body).hexdigest()
        }

        self.write_json({
            'sign': sign_hash,
            'payload': b64encode(bz2.compress(json.dumps(payload)))
        })
Esempio n. 9
0
def init_db(coind_type):
    # 一旦標準データベースに接続してデータベースの作成を行う
    with CloudSQL('mysql') as c:
        # 本来であればプレイスホルダを使用したいところだが、
        # DB 名は文字列ではないらしい MySQL のクソ仕様のため、
        # % で SQL 文を組み立てる。coind_type は入力チェックをパスしているため、
        # SQL インジェクションにはならないはず。
        c.execute('CREATE DATABASE %s' % coind_type)

    # 作成したデータベースに接続して、テーブルを作成する
    # MySQL のクソ仕様により、CREATE TABLE は暗黙コミットされるので
    # トランザクションの意味はまったくないが、失敗したら手動で
    # DB ごと消せばいいのでとりあえずこれで
    db = CloudSQL(coind_type)
    with db as c:
        c.execute('''
			CREATE TABLE state (
				running_flag BOOL NOT NULL,
				running_time DATETIME
			)
		''')
        c.execute('''
			CREATE TABLE blockheader (
				height BIGINT UNSIGNED NOT NULL PRIMARY KEY,
				hash VARCHAR(128) NOT NULL,
				time DATETIME NOT NULL,
				miners TEXT,
				json LONGTEXT NOT NULL,
				INDEX( hash )
			)
		''')
        c.execute('''
			CREATE TABLE transaction (
				txid VARCHAR(128) NOT NULL,
				height BIGINT UNSIGNED NOT NULL,
				blockhash VARCHAR(128) NOT NULL,
				time DATETIME NOT NULL,
				vin_n INT NOT NULL,
				vout_n INT NOT NULL,
				total_output BIGINT UNSIGNED NOT NULL,
				json LONGTEXT NOT NULL,
				INDEX( time ),
				INDEX( txid ),
				PRIMARY KEY( height, txid )
			)
		''')
        c.execute('''
			CREATE TABLE transaction_link (
				vin_height BIGINT UNSIGNED,
				vin_txid VARCHAR(128),
				vin_idx INT,
				vout_height BIGINT UNSIGNED NOT NULL,
				vout_txid VARCHAR(128) NOT NULL,
				vout_idx INT NOT NULL,
				addresses VARCHAR(1300) CHARACTER SET ASCII,
				value BIGINT UNSIGNED NOT NULL,
				INDEX( addresses, vin_txid ),
				INDEX( vin_height, vin_txid, vin_idx, vout_txid, vout_idx ),
				INDEX( vin_height, vin_txid, vin_idx ),
				INDEX( vout_height, vout_txid, vout_idx )
			)
		''')
        c.execute('''
			CREATE TABLE balance (
				addresses VARCHAR(1300) CHARACTER SET ASCII NOT NULL,
				height BIGINT UNSIGNED NOT NULL,
				txid VARCHAR(128) NOT NULL,
				serial BIGINT UNSIGNED NOT NULL,
				time DATETIME NOT NULL,
				balance BIGINT UNSIGNED NOT NULL,
				gain BIGINT NOT NULL,
				INDEX( height, txid ),
				INDEX( addresses, serial ),
				PRIMARY KEY( addresses, height, txid )
			)
		''')
        c.execute('''
			CREATE TABLE current_balance (
				addresses VARCHAR(1300) CHARACTER SET ASCII NOT NULL PRIMARY KEY,
				balance BIGINT UNSIGNED NOT NULL,
				INDEX( balance )
			)
		''')
        c.execute('INSERT INTO state VALUES ( 0, NULL )')

    return db