def get_tx_info(tx_hex, block_parser=None, block_index=None): """Get the transaction info. Calls one of two subfunctions depending on signature type.""" if not block_index: block_index = util.CURRENT_BLOCK_INDEX try: if util.enabled('multisig_addresses', block_index=block_index): # Protocol change. tx_info = get_tx_info2(tx_hex, block_parser=block_parser) else: tx_info = get_tx_info1(tx_hex, block_index, block_parser=block_parser) except (DecodeError, SCHOnlyError) as e: # NOTE: For debugging, logger.debug('Could not decode: ' + str(e)) tx_info = b'', None, None, None, None return tx_info
def validate (db, source, possible_moves, wager, move_random_hash, expiration, block_index): problems = [] if util.enabled('disable_rps'): problems.append('rps disabled') if not isinstance(possible_moves, int): problems.append('possible_moves must be a integer') return problems if not isinstance(wager, int): problems.append('wager must be in satoshis') return problems if not isinstance(expiration, int): problems.append('expiration must be expressed as an integer block delta') return problems if not all(c in string.hexdigits for c in move_random_hash): problems.append('move_random_hash must be an hexadecimal string') return problems move_random_hash_bytes = binascii.unhexlify(move_random_hash) if possible_moves < 3: problems.append('possible moves must be at least 3') if possible_moves % 2 == 0: problems.append('possible moves must be odd') if wager <= 0: problems.append('non‐positive wager') if expiration < 0: problems.append('negative expiration') if expiration == 0 and not (block_index >= 317500 or config.TESTNET): # Protocol change. problems.append('zero expiration') if expiration > config.MAX_EXPIRATION: problems.append('expiration overflow') if len(move_random_hash_bytes) != 32: problems.append('move_random_hash must be 32 bytes in hexadecimal format') return problems
def validate (db, source, destination, asset, quantity, divisible, callable_, call_date, call_price, description, block_index): problems = [] fee = 0 if asset in (config.SCH, config.SHP): problems.append('cannot issue {} or {}'.format(config.SCH, config.SHP)) if call_date is None: call_date = 0 if call_price is None: call_price = 0.0 if description is None: description = "" if divisible is None: divisible = True if isinstance(call_price, int): call_price = float(call_price) #^ helps especially with calls from JS‐based clients, where parseFloat(15) returns 15 (not 15.0), which json takes as an int if not isinstance(quantity, int): problems.append('quantity must be in satoshis') return call_date, call_price, problems, fee, description, divisible, None if call_date and not isinstance(call_date, int): problems.append('call_date must be epoch integer') return call_date, call_price, problems, fee, description, divisible, None if call_price and not isinstance(call_price, float): problems.append('call_price must be a float') return call_date, call_price, problems, fee, description, divisible, None if quantity < 0: problems.append('negative quantity') if call_price < 0: problems.append('negative call price') if call_date < 0: problems.append('negative call date') # Callable, or not. if not callable_: if block_index >= 312500 or config.TESTNET: # Protocol change. call_date = 0 call_price = 0.0 elif block_index >= 310000: # Protocol change. if call_date: problems.append('call date for non‐callable asset') if call_price: problems.append('call price for non‐callable asset') # Valid re-issuance? cursor = db.cursor() cursor.execute('''SELECT * FROM issuances \ WHERE (status = ? AND asset = ?) ORDER BY tx_index ASC''', ('valid', asset)) issuances = cursor.fetchall() cursor.close() if issuances: reissuance = True last_issuance = issuances[-1] if last_issuance['issuer'] != source: problems.append('issued by another address') if bool(last_issuance['divisible']) != bool(divisible): problems.append('cannot change divisibility') if bool(last_issuance['callable']) != bool(callable_): problems.append('cannot change callability') if last_issuance['call_date'] > call_date and (call_date != 0 or (block_index < 312500 and not config.TESTNET)): problems.append('cannot advance call date') if last_issuance['call_price'] > call_price: problems.append('cannot reduce call price') if last_issuance['locked'] and quantity: problems.append('locked asset and non‐zero quantity') else: reissuance = False if description.lower() == 'lock': problems.append('cannot lock a non‐existent asset') if destination: problems.append('cannot transfer a non‐existent asset') # Check for existence of fee funds. if quantity or (block_index >= 315000 or config.TESTNET): # Protocol change. if not reissuance or (block_index < 310000 and not config.TESTNET): # Pay fee only upon first issuance. (Protocol change.) cursor = db.cursor() cursor.execute('''SELECT * FROM balances \ WHERE (address = ? AND asset = ?)''', (source, config.SHP)) balances = cursor.fetchall() cursor.close() if util.enabled('numeric_asset_names'): # Protocol change. if len(asset) >= 13: fee = 0 else: fee = int(0.5 * config.UNIT) elif block_index >= 291700 or config.TESTNET: # Protocol change. fee = int(0.5 * config.UNIT) elif block_index >= 286000 or config.TESTNET: # Protocol change. fee = 5 * config.UNIT elif block_index > 281236 or config.TESTNET: # Protocol change. fee = 5 if fee and (not balances or balances[0]['quantity'] < fee): problems.append('insufficient funds') if not (block_index >= 317500 or config.TESTNET): # Protocol change. if len(description) > 42: problems.append('description too long') # For SQLite3 call_date = min(call_date, config.MAX_INT) total = sum([issuance['quantity'] for issuance in issuances]) assert isinstance(quantity, int) if total + quantity > config.MAX_INT: problems.append('total quantity overflow') if destination and quantity: problems.append('cannot issue and transfer simultaneously') return call_date, call_price, problems, fee, description, divisible, reissuance
def get_tx_info2(tx_hex, block_parser=None): """Get multisig transaction info. The destinations, if they exists, always comes before the data output; the change, if it exists, always comes after. """ # Decode transaction binary. ctx = backend.deserialize(tx_hex) def arc4_decrypt(cyphertext): '''Un‐obfuscate. Initialise key once per attempt.''' key = ARC4.new(ctx.vin[0].prevout.hash[::-1]) return key.decrypt(cyphertext) def get_opreturn(asm): if len(asm) == 2 and asm[0] == 'OP_RETURN': pubkeyhash = asm[1] if type(pubkeyhash) == bytes: return pubkeyhash raise DecodeError('invalid OP_RETURN') def decode_opreturn(asm): chunk = get_opreturn(asm) chunk = arc4_decrypt(chunk) if chunk[:len(config.PREFIX)] == config.PREFIX: # Data destination, data = None, chunk[len(config.PREFIX):] else: raise DecodeError('unrecognised OP_RETURN output') return destination, data def decode_checksig(asm): pubkeyhash = script.get_checksig(asm) chunk = arc4_decrypt(pubkeyhash) if chunk[1:len(config.PREFIX) + 1] == config.PREFIX: # Data # Padding byte in each output (instead of just in the last one) so that encoding methods may be mixed. Also, it’s just not very much data. chunk_length = chunk[0] chunk = chunk[1:chunk_length + 1] destination, data = None, chunk[len(config.PREFIX):] else: # Destination pubkeyhash = binascii.hexlify(pubkeyhash).decode('utf-8') destination, data = script.base58_check_encode(pubkeyhash, config.ADDRESSVERSION), None return destination, data def decode_checkmultisig(asm): pubkeys, signatures_required = script.get_checkmultisig(asm) chunk = b'' for pubkey in pubkeys[:-1]: # (No data in last pubkey.) chunk += pubkey[1:-1] # Skip sign byte and nonce byte. chunk = arc4_decrypt(chunk) if chunk[1:len(config.PREFIX) + 1] == config.PREFIX: # Data # Padding byte in each output (instead of just in the last one) so that encoding methods may be mixed. Also, it’s just not very much data. chunk_length = chunk[0] chunk = chunk[1:chunk_length + 1] destination, data = None, chunk[len(config.PREFIX):] else: # Destination pubkeyhashes = [script.pubkey_to_pubkeyhash(pubkey) for pubkey in pubkeys] destination, data = script.construct_array(signatures_required, pubkeyhashes, len(pubkeyhashes)), None return destination, data # Ignore coinbase transactions. if ctx.is_coinbase(): raise DecodeError('coinbase transaction') # Get destinations and data outputs. destinations, shell_amount, fee, data = [], 0, 0, b'' for vout in ctx.vout: # Fee is the input values minus output values. output_value = vout.nValue fee -= output_value # Ignore transactions with invalid script. try: asm = script.get_asm(vout.scriptPubKey) except CScriptInvalidError as e: raise DecodeError(e) if asm[0] == 'OP_RETURN': new_destination, new_data = decode_opreturn(asm) elif asm[-1] == 'OP_CHECKSIG': new_destination, new_data = decode_checksig(asm) elif asm[-1] == 'OP_CHECKMULTISIG': new_destination, new_data = decode_checkmultisig(asm) else: raise DecodeError('unrecognised output type') assert not (new_destination and new_data) assert new_destination != None or new_data != None # `decode_*()` should never return `None, None`. if util.enabled('null_data_check'): if new_data == []: raise DecodeError('new destination is `None`') # All destinations come before all data. if not data and not new_data and destinations != [config.UNSPENDABLE,]: destinations.append(new_destination) shell_amount += output_value else: if new_destination: # Change. break else: # Data. data += new_data # Only look for source if data were found or destination is `UNSPENDABLE`, # for speed. if not data and destinations != [config.UNSPENDABLE,]: raise SCHOnlyError('no data and not unspendable') # Collect all (unique) source addresses. sources = [] for vin in ctx.vin[:]: # Loop through inputs. # Get the full transaction data for this input transaction. if block_parser: vin_tx = block_parser.read_raw_transaction(ib2h(vin.prevout.hash)) vin_ctx = backend.deserialize(vin_tx['__data__']) else: vin_tx = backend.getrawtransaction(ib2h(vin.prevout.hash)) vin_ctx = backend.deserialize(vin_tx) vout = vin_ctx.vout[vin.prevout.n] fee += vout.nValue asm = script.get_asm(vout.scriptPubKey) if asm[-1] == 'OP_CHECKSIG': new_source, new_data = decode_checksig(asm) if new_data or not new_source: raise DecodeError('data in source') elif asm[-1] == 'OP_CHECKMULTISIG': new_source, new_data = decode_checkmultisig(asm) if new_data or not new_source: raise DecodeError('data in source') else: raise DecodeError('unrecognised source type') # Collect unique sources. if new_source not in sources: sources.append(new_source) sources = '-'.join(sources) destinations = '-'.join(destinations) return sources, destinations, shell_amount, round(fee), data