From 3c62401874f1654097ff585e9bafa8628a3adebc Mon Sep 17 00:00:00 2001 From: Ardwin Date: Thu, 18 Jan 2024 02:49:04 -0800 Subject: [PATCH] Perpetual Toking Mining ARC20 with Bitwork --- electrumx/lib/coins.py | 4 +- electrumx/lib/util_atomicals.py | 140 ++++++++- electrumx/server/block_processor.py | 206 +++++++++---- electrumx/server/db.py | 2 - electrumx/server/session.py | 9 +- tests/lib/test_atomicals_utils.py | 461 ++++++++++++++++++++++++++++ 6 files changed, 747 insertions(+), 75 deletions(-) create mode 100644 tests/lib/test_atomicals_utils.py diff --git a/electrumx/lib/coins.py b/electrumx/lib/coins.py index bdfcfbcb..d59deac6 100644 --- a/electrumx/lib/coins.py +++ b/electrumx/lib/coins.py @@ -665,7 +665,7 @@ class Bitcoin(BitcoinMixin, Coin): ATOMICALS_ACTIVATION_HEIGHT = 808080 ATOMICALS_ACTIVATION_HEIGHT_DMINT = 819181 ATOMICALS_ACTIVATION_HEIGHT_COMMITZ = 822800 - # ATOMICALS_ACTIVATION_HEIGHT_DENSITY = 828000 + ATOMICALS_ACTIVATION_HEIGHT_DENSITY = 828128 @classmethod def warn_old_client_on_tx_broadcast(cls, client_ver): @@ -932,7 +932,7 @@ class BitcoinTestnet(BitcoinTestnetMixin, Coin): ATOMICALS_ACTIVATION_HEIGHT = 2505238 ATOMICALS_ACTIVATION_HEIGHT_DMINT = 2540296 ATOMICALS_ACTIVATION_HEIGHT_COMMITZ = 2543936 - # ATOMICALS_ACTIVATION_HEIGHT_DENSITY = 2572729 + ATOMICALS_ACTIVATION_HEIGHT_DENSITY = 2572729 @classmethod def warn_old_client_on_tx_broadcast(cls, client_ver): diff --git a/electrumx/lib/util_atomicals.py b/electrumx/lib/util_atomicals.py index 6be329a5..958662f4 100644 --- a/electrumx/lib/util_atomicals.py +++ b/electrumx/lib/util_atomicals.py @@ -37,6 +37,7 @@ import base64 import krock32 import pickle +import math from electrumx.lib.hash import sha256, double_sha256 from cbor2 import dumps, loads, CBORDecodeError from collections.abc import Mapping @@ -90,8 +91,8 @@ class AtomicalsValidationError(Exception): DFT_MINT_MAX_MIN_COUNT = 1 # The maximum number (legacy) of DFT max_mints. Set at 500,000 mints mainly for efficieny reasons in legacy. DFT_MINT_MAX_MAX_COUNT_LEGACY = 500000 -# The maximum number of DFT max_mints (after legacy 'DENSITY' update). Set at 21,000,000 max mints. -# DFT_MINT_MAX_MAX_COUNT_DENSITY = 21000000 +# The maximum number of DFT max_mints (after legacy 'DENSITY' update). Set at 10,000,000 max mints. +DFT_MINT_MAX_MAX_COUNT_DENSITY = 10000000 # This would never change, but we put it as a constant for clarity DFT_MINT_HEIGHT_MIN = 0 @@ -257,7 +258,7 @@ def is_validate_pow_prefix_string(pow_prefix, pow_prefix_ext): m = re.compile(r'^[a-f0-9]{1,64}$') if pow_prefix: if pow_prefix_ext: - if isinstance(pow_prefix_ext, int) and pow_prefix_ext >= 0 or pow_prefix_ext <= 15 and m.match(pow_prefix): + if isinstance(pow_prefix_ext, int) and pow_prefix_ext >= 0 and pow_prefix_ext <= 15 and m.match(pow_prefix): return True else: return False @@ -691,14 +692,15 @@ def populate_args_meta_ctx_init(mint_info, op_found_payload): logger.warning(f'DFT init has invalid max_mints {hash_to_hex_str(tx_hash)}, {max_mints}. Skipping...') return None, None - if max_mints > DFT_MINT_MAX_MAX_COUNT_LEGACY: - logger.warning(f'DFT init has invalid max_mints legacy {hash_to_hex_str(tx_hash)}, {max_mints}. Skipping...') - return None, None - - # elif height >= coin.ATOMICALS_ACTIVATION_HEIGHT_DENSITY: - # if max_mints > DFT_MINT_MAX_MAX_COUNT_DENSITY: - # logger.warning(f'DFT init has invalid max_mints {hash_to_hex_str(tx_hash)}, {max_mints}. Skipping...') - # return None, None + if height < coin.ATOMICALS_ACTIVATION_HEIGHT_DENSITY: + if max_mints > DFT_MINT_MAX_MAX_COUNT_LEGACY: + logger.warning(f'DFT init has invalid max_mints legacy {hash_to_hex_str(tx_hash)}, {max_mints}. Skipping...') + return None, None + + elif height >= coin.ATOMICALS_ACTIVATION_HEIGHT_DENSITY: + if max_mints > DFT_MINT_MAX_MAX_COUNT_DENSITY: + logger.warning(f'DFT init has invalid max_mints {hash_to_hex_str(tx_hash)}, {max_mints}. Skipping...') + return None, None mint_info['$mint_height'] = mint_height mint_info['$mint_amount'] = mint_amount @@ -714,6 +716,7 @@ def populate_args_meta_ctx_init(mint_info, op_found_payload): else: logger.warning(f'DFT mint has invalid mint_bitworkc. Skipping...') return None, None + # If set it requires the mint reveal tx to have POW matching the mint_reveal_powprefix to claim a mint mint_pow_reveal = mint_info['args'].get('mint_bitworkr') if mint_pow_reveal: @@ -729,7 +732,65 @@ def populate_args_meta_ctx_init(mint_info, op_found_payload): if is_immutable: logger.warning(f'DFT cannot be is_immutable is invalid {tx_hash}, {ticker}. Skipping...') return None, None + + dft_mode = mint_info['args'].get('md') + if dft_mode != 1 and dft_mode != None: + logger.warning(f'DFT init has invalid md {hash_to_hex_str(tx_hash)}, {dft_mode}. Skipping...') + return None, None + + # Perpetual mint mode available on activation + if height >= coin.ATOMICALS_ACTIVATION_HEIGHT_DENSITY and dft_mode == 1: + bv = mint_info['args'].get('bv') + bci = mint_info['args'].get('bci') + bri = mint_info['args'].get('bri') + bcs = mint_info['args'].get('bcs', 64) + brs = mint_info['args'].get('brs', 64) + if (not bci and not bri) or not bv: + return None, None + + if not is_hex_string(bv) or len(bv) < 4: + logger.warning(f'DFT init has invalid bv must be at least length 4 hex {hash_to_hex_str(tx_hash)}, {bv}. Skipping...') + return None, None + + if mint_info.get('$mint_bitworkr'): + logger.warning(f'DFT init has invalid because mint_bitworkr cannot be set when perpetual mode {hash_to_hex_str(tx_hash)}. Skipping...') + return None, None + + if mint_info.get('$mint_bitworkc'): + logger.warning(f'DFT init has invalid because mint_bitworkc cannot be set when perpetual mode {hash_to_hex_str(tx_hash)}. Skipping...') + return None, None + + if bci and (not isinstance(bci, int) or bci < 1 or bci > 64): + logger.warning(f'DFT init has invalid bci {hash_to_hex_str(tx_hash)}, {bci}. Skipping...') + return None, None + if bri and (not isinstance(bri, int) or bri < 1 or bri > 64): + logger.warning(f'DFT init has invalid bri {hash_to_hex_str(tx_hash)}, {bri}. Skipping...') + return None, None + + if bcs and (not isinstance(bcs, int) or bcs < 64 or bcs > 256): + logger.warning(f'DFT init has invalid bcs {hash_to_hex_str(tx_hash)}, {bcs}. Skipping...') + return None, None + + if brs and (not isinstance(brs, int) or brs < 64 or brs > 256): + logger.warning(f'DFT init has invalid brs {hash_to_hex_str(tx_hash)}, {brs}. Skipping...') + return None, None + + mint_info['$mint_mode'] = 'infinite' + mint_info['$mint_bitworkc_inc'] = bci + mint_info['$mint_bitworkr_inc'] = bri + mint_info['$mint_bitworkc_start'] = bcs + mint_info['$mint_bitworkr_start'] = brs + mint_info['$mint_bitwork_vec'] = bv + + # When in infinite minting mode limit the max mints per phase + max_mints = mint_info['$max_mints'] + if max_mints > 100000: + logger.warning(f'DFT init has invalid max_mints must be <= 100000 with infinite mining {hash_to_hex_str(tx_hash)}, {max_mints}. Skipping...') + return None, None + else: + mint_info['$mint_mode'] = 'fixed' + if not mint_info or not mint_info.get('type'): return None, None @@ -1579,6 +1640,63 @@ def get_subname_request_candidate_status(current_height, atomical_info, status, 'pending_candidate_atomical_id': candidate_id_compact } +def calculate_expected_bitwork(bitwork_vec, actual_mints, max_mints, target_increment, starting_target): + if starting_target < 64 or starting_target > 256: + raise Exception(f'Invalid starting target {starting_target}') + if max_mints < 1 or max_mints > 100000: + raise Exception(f'Invalid max_mints {starting_target}') + if target_increment < 1 or target_increment > 64: + raise Exception(f'Invalid target_increment {target_increment}') + target_steps = int(math.floor(actual_mints / max_mints)) + current_target = starting_target + (target_steps * target_increment) + return derive_bitwork_prefix_from_target(bitwork_vec, current_target) + +# Derive a bitwork string based on purely using an increment difficulty factor +def derive_bitwork_prefix_from_target(base_bitwork_prefix, target): + if target < 16: + raise Exception(f'increments must be at least 16. Provided: {target}') + base_bitwork_padded = base_bitwork_prefix.ljust(32, '0') + multiples = target / 16 + full_amount = int(math.floor(multiples)) + modulo = target % 16 + + bitwork_prefix = base_bitwork_padded[:full_amount] + if modulo > 0: + return bitwork_prefix + '.' + str(modulo) + return bitwork_prefix + +def decode_bitwork_target_from_prefix(bitwork_string): + fullstr, parts = is_valid_bitwork_string(bitwork_string) + if not fullstr: + raise Exception(f'Invalid bitwork string {bitwork_string}') + return len(parts['prefix']) * 16 + int(parts['ext'] or 0) + +def is_bitwork_subset(first_bitwork, second_bitwork): + first_fullstr, first_parts = is_valid_bitwork_string(first_bitwork) + if not first_fullstr: + raise Exception(f'Invalid bitwork string {first_bitwork}') + second_fullstr, second_parts = is_valid_bitwork_string(second_bitwork) + if not second_fullstr: + raise Exception(f'Invalid bitwork string {second_bitwork}') + + if second_parts['prefix'].startswith(first_parts['prefix']): + print(f'second_parts={second_parts} first_parts={first_parts}') + if len(second_parts['prefix']) > len(first_parts['prefix']): + return True + if len(second_parts['prefix']) == len(first_parts['prefix']) and ((second_parts['ext'] or 0) >= (first_parts['ext'] or 0)): + return True + return False + +def is_mint_pow_valid(txid, mint_pow_commit): + valid_commit_str, bitwork_commit_parts = is_valid_bitwork_string(mint_pow_commit) + if not valid_commit_str: + return False + mint_bitwork_prefix = bitwork_commit_parts['prefix'] + mint_bitwork_ext = bitwork_commit_parts['ext'] + if is_proof_of_work_prefix_match(txid, mint_bitwork_prefix, mint_bitwork_ext): + return True + return False + def expand_spend_utxo_data(data): value, = unpack_le_uint64(data[HASHX_LEN + SCRIPTHASH_LEN : HASHX_LEN + SCRIPTHASH_LEN + 8]) exponent, = unpack_le_uint16_from(data[HASHX_LEN + SCRIPTHASH_LEN + 8: HASHX_LEN + SCRIPTHASH_LEN + 8 + 2]) diff --git a/electrumx/server/block_processor.py b/electrumx/server/block_processor.py index baa0b91d..40829cb1 100644 --- a/electrumx/server/block_processor.py +++ b/electrumx/server/block_processor.py @@ -21,6 +21,7 @@ from electrumx.lib.util import ( chunks, class_logger, pack_le_uint32, unpack_le_uint32, pack_le_uint64, unpack_le_uint64, pack_be_uint64, unpack_be_uint64, OldTaskGroup, pack_byte, pack_le_uint16, unpack_le_uint16_from ) +import math from electrumx.lib.tx import Tx from electrumx.server.db import FlushData, COMP_TXID_LEN, DB from electrumx.server.history import TXNUM_LEN @@ -39,6 +40,7 @@ pad_bytes_n, has_requested_proof_of_work, is_valid_container_string_name, + calculate_expected_bitwork, expand_spend_utxo_data, encode_tx_hash_hex, SUBREALM_MINT_PATH, @@ -64,7 +66,8 @@ validate_dmitem_mint_args_with_container_dmint, is_seal_operation, is_event_operation, - encode_atomical_ids_hex + encode_atomical_ids_hex, + is_mint_pow_valid ) from electrumx.lib.atomicals_blueprint_builder import AtomicalsTransferBlueprintBuilder @@ -87,8 +90,6 @@ ATOMICAL_ID_LEN = 36 LOCATION_ID_LEN = 36 TX_OUTPUT_IDX_LEN = 4 -SANITY_CHECK_ATOMICAL = 'd3805673d1080bd6f527b3153dd5f8f7584731dec04b332e6285761b5cdbf171i0' -SANITY_CHECK_ATOMICAL_MAX_SUPPLY = 2000 class Prefetcher: '''Prefetches blocks (in the forward direction only).''' @@ -530,7 +531,7 @@ def validate_ft_rules_raw_tx(self, raw_tx): if blueprint_builder.get_are_fts_burned() or not blueprint_builder.cleanly_assigned: encoded_atomicals_spent_at_inputs = encode_atomical_ids_hex(atomicals_spent_at_inputs) encoded_ft_output_blueprint = encode_atomical_ids_hex(ft_output_blueprint) - raise AtomicalsValidationError(f'detected invalid ft token inputs and outputs for tx_hash={hash_to_hex_str(tx_hash)}, operations_found_at_inputs={operations_found_at_inputs}, atomicals_spent_at_inputs={encoded_atomicals_spent_at_inputs}, ft_output_blueprint={encoded_ft_output_blueprint}') + raise AtomicalsValidationError(f'detected invalid ft token inputs and outputs for tx_hash={hash_to_hex_str(tx_hash)}, operations_found_at_inputs={operations_found_at_inputs}, atomicals_spent_at_inputs={encoded_atomicals_spent_at_inputs}, ft_output_blueprint.outputs={encoded_ft_output_blueprint.outputs} ft_output_blueprint.fts_burned={encoded_ft_output_blueprint.fts_burned}') # Query general data including the cache def get_general_data_with_cache(self, key): @@ -817,6 +818,23 @@ def put_atomicals_utxo(self, location_id, atomical_id, value): } self.atomicals_utxo_cache[location_id] = cache + def get_distmints_by_atomical_id(self, atomical_id, limit, offset): + def lookup_gi_entries(atomical_id): + # Query all the gi key in the db for the atomical + prefix = b'gi' + atomical_id + location_ids = [] + limit_counter = 0 + offset_counter = 0 + for atomical_gi_db_key, atomical_gi_db_value in self.db.utxo_db.iterator(prefix=prefix): + if offset_counter >= offset: + location_ids.append(atomical_gi_db_value.hex()) + limit_counter += 1 + if limit_counter >= limit: + break + offset_counter += 1 + return location_ids + return lookup_gi_entries(atomical_id) + # Get the total number of distributed mints for an atomical id and check the cache and db # This can be a heavy operation with many 10's of thousands in the db def get_distmints_count_by_atomical_id(self, height, atomical_id, use_block_db_cache): @@ -843,7 +861,7 @@ def lookup_db_count(atomical_id): # We got the db count as of the latest block db_count = lookup_db_count(atomical_id) self.atomicals_dft_mint_count_cache[atomical_id] = db_count - self.logger.info(f'height={height}, dft_atomical_id={location_id_bytes_to_compact(atomical_id)}, db_count={db_count}, cache_count={cache_count}') + self.logger.debug(f'height={height}, dft_atomical_id={location_id_bytes_to_compact(atomical_id)}, db_count={db_count}, cache_count={cache_count}') else: # No block db cache was used, grab it from the db now db_count = lookup_db_count(atomical_id) @@ -1437,7 +1455,11 @@ def create_or_delete_atomical(self, operations_found_at_inputs, atomicals_spent_ elif valid_create_op_type == 'FT': # Add $max_supply informative property if mint_info['subtype'] == 'decentralized': - mint_info['$max_supply'] = mint_info['$mint_amount'] * mint_info['$max_mints'] + # For perpetual mints the max supply is unbounded + if mint_info.get('$mint_mode') == 'infinite': + mint_info['$max_supply'] = -1 + else: + mint_info['$max_supply'] = mint_info['$mint_amount'] * mint_info['$max_mints'] else: mint_info['$max_supply'] = txout.value if not self.create_or_delete_ticker_entry_if_requested(mint_info, height, Delete): @@ -1959,11 +1981,53 @@ async def get_dft_mint_info_rpc_format_by_atomical_id(self, atomical_id): atomical_result['dft_info'] = { 'mint_count': 0 } - atomical_dft_mint_info_key = b'gi' + atomical_id - mint_count = 0 - for location_key, location_result_value in self.db.utxo_db.iterator(prefix=atomical_dft_mint_info_key): - mint_count += 1 + mint_count = self.get_distmints_count_by_atomical_id(self.height, atomical_id, True) atomical_result['dft_info']['mint_count'] = mint_count + if atomical_result.get('$mint_mode') == 'infinite': + self.logger.debug(f'atomical_result={atomical_result}') + mint_bitwork_vec = atomical_result.get('$mint_bitwork_vec') + mint_bitworkc_inc = atomical_result.get('$mint_bitworkc_inc') + mint_bitworkr_inc = atomical_result.get('$mint_bitworkr_inc') + max_mints = atomical_result.get('$max_mints') + if mint_bitworkc_inc: + mint_bitworkc_start = atomical_result['$mint_bitworkc_start'] + remaining = max_mints - (mint_count % max_mints) + upcoming_bitworks = [ + { + 'label': '_current', + 'mints': mint_count + }, + { + 'label': '_next', + 'mints': mint_count + remaining, + }, + { + 'label': '_next_next', + 'mints': mint_count + remaining + max_mints + } + ] + for elem in upcoming_bitworks: + atomical_result['dft_info']['mint_bitworkc' + elem['label']] = calculate_expected_bitwork(mint_bitwork_vec, elem['mints'], max_mints, mint_bitworkc_inc, mint_bitworkc_start) + if mint_bitworkr_inc: + mint_bitworkr_start = atomical_result['$mint_bitworkr_start'] + remaining = max_mints - (mint_count % max_mints) + upcoming_bitworks = [ + { + 'label': '_current', + 'mints': mint_count + }, + { + 'label': '_next', + 'mints': mint_count + remaining, + }, + { + 'label': '_next_next', + 'mints': mint_count + remaining + max_mints + } + ] + for elem in upcoming_bitworks: + atomical_result['dft_info']['mint_bitworkr' + elem['label']] = calculate_expected_bitwork(mint_bitwork_vec, elem['mints'], max_mints, mint_bitworkr_inc, mint_bitworkr_start) + atomical_result['location_summary'] = {} self.populate_location_info_summary(atomical_id, atomical_result['location_summary']) self.atomicals_rpc_general_cache[b'dft_info' + atomical_id] = atomical_result @@ -1976,7 +2040,6 @@ def populate_location_info_summary(self, atomical_id, atomical_result): active_supply = 0 atomical_active_location_key_prefix = b'a' + atomical_id for atomical_active_location_key, atomical_active_location_value in self.db.utxo_db.iterator(prefix=atomical_active_location_key_prefix): - location = atomical_active_location_key[1 + ATOMICAL_ID_LEN : 1 + ATOMICAL_ID_LEN + ATOMICAL_ID_LEN] location_value, = unpack_le_uint64(atomical_active_location_value[HASHX_LEN + SCRIPTHASH_LEN : HASHX_LEN + SCRIPTHASH_LEN + 8]) active_supply += location_value scripthash = atomical_active_location_value[HASHX_LEN : HASHX_LEN + SCRIPTHASH_LEN] @@ -2095,9 +2158,23 @@ def get_base_mint_info_by_atomical_id(self, atomical_id): subtype = init_mint_info.get('subtype') atomical['subtype'] = subtype if subtype == 'decentralized': - atomical['$max_supply'] = init_mint_info['$max_supply'] + # The mint mode can be fixed with a known max_supply + # Or the mode mint can be perpetual with an unbounded max_supply + atomical['$mint_mode'] = init_mint_info['$mint_mode'] + if init_mint_info['$mint_mode'] == 'infinite': + atomical['$max_supply'] = -1 + atomical['$mint_bitwork_vec'] = init_mint_info['$mint_bitwork_vec'] + atomical['$mint_bitworkc_inc'] = init_mint_info.get('$mint_bitworkc_inc') + atomical['$mint_bitworkc_start'] = init_mint_info.get('$mint_bitworkc_start') + atomical['$mint_bitworkr_inc'] = init_mint_info.get('$mint_bitworkr_inc') + atomical['$mint_bitworkr_start'] = init_mint_info.get('$mint_bitworkr_start') + else: + atomical['$max_supply'] = init_mint_info['$max_supply'] + atomical['$mint_height'] = init_mint_info['$mint_height'] atomical['$mint_amount'] = init_mint_info['$mint_amount'] + # The semantics of max_mints is total number of mints when mint_mode=fixed and it is the + # max mints per epoch step increment with perpetual mint_mode atomical['$max_mints'] = init_mint_info['$max_mints'] # The decentralized FT also has a proof of work option such that it requires some proof of work # To be minted by users. The deployer can determine if the proof of work must appear in the @@ -2436,6 +2513,8 @@ def create_or_delete_decentralized_mint_output(self, atomicals_operations_found_ self.logger.info(f'create_or_delete_decentralized_mint_outputs: Detected invalid mint attempt in {hash_to_hex_str(tx_hash)} for ticker {ticker} which is not a decentralized mint type. Ignoring...') return None + # For perpetual mints mint_mode=perpetual otherwise it is fixed (None or 'fixed') + mint_mode = mint_info_for_ticker.get('$mint_mode') max_mints = mint_info_for_ticker['$max_mints'] mint_amount = mint_info_for_ticker['$mint_amount'] mint_height = mint_info_for_ticker['$mint_height'] @@ -2468,43 +2547,52 @@ def create_or_delete_decentralized_mint_output(self, atomicals_operations_found_ if mint_amount == txout.value: # Count the number of existing b'gi' entries and ensure it is strictly less than max_mints decentralized_mints = self.get_distmints_count_by_atomical_id(height, dmt_mint_atomical_id, True) - if decentralized_mints > max_mints: - raise IndexError(f'create_or_delete_decentralized_mint_outputs :Fatal IndexError decentralized_mints > max_mints for {location_id_bytes_to_compact(dmt_mint_atomical_id)}. Too many mints detected in db') - if decentralized_mints < max_mints: - self.logger.debug(f'create_or_delete_decentralized_mint_outputs: found mint request in {hash_to_hex_str(tx_hash)} for {ticker}. Checking for any POW in distributed mint record...') - # If this was a POW mint, then validate that the POW is valid + # Assess whether we allow the mint based on 'fixed' or 'infinite' mint modes + # The perpetual mint mode will derive the minimum expected bitworkr/c needed given the quantity of already minted units + allow_mint = False + if mint_mode == 'infinite': + # In the 'infinite' mint mode an unbounded number of tokens can be minted according to the ever increasing bitworkc/r + mint_bitwork_vec = mint_info_for_ticker.get('$mint_bitwork_vec') + mint_bitworkc_inc = mint_info_for_ticker.get('$mint_bitworkc_inc') + mint_bitworkr_inc = mint_info_for_ticker.get('$mint_bitworkr_inc') + + # If there was a commit bitwork required, then assess the stage of the minimum we expect to allow the mint + if mint_bitworkc_inc: + mint_bitworkc_start = mint_info_for_ticker.get('$mint_bitworkc_start') + expected_minimum_bitworkc = calculate_expected_bitwork(mint_bitwork_vec, decentralized_mints, max_mints, mint_bitworkc_inc, mint_bitworkc_start) + if not is_mint_pow_valid(atomicals_operations_found_at_inputs['commit_txid'], expected_minimum_bitworkc): + self.logger.warning(f'create_or_delete_decentralized_mint_output: mint_bitworkc_inc not is_mint_pow_valid {hash_to_hex_str(tx_hash)}, expected_minimum_bitworkc={expected_minimum_bitworkc}, atomicals_operations_found_at_inputs={atomicals_operations_found_at_inputs}...') + return None + # If there was a reveal bitwork required, then assess the stage of the minimum we expect to allow the mint + if mint_bitworkr_inc: + mint_bitworkr_start = mint_info_for_ticker.get('$mint_bitworkr_start') + expected_minimum_bitworkr = calculate_expected_bitwork(mint_bitwork_vec, decentralized_mints, max_mints, mint_bitworkr_inc, mint_bitworkr_start) + if not is_mint_pow_valid(atomicals_operations_found_at_inputs['reveal_location_txid'], expected_minimum_bitworkr): + self.logger.warning(f'create_or_delete_decentralized_mint_output: mint_bitworkr_inc not is_mint_pow_valid {hash_to_hex_str(tx_hash)}, expected_minimum_bitworkr={expected_minimum_bitworkr}, atomicals_operations_found_at_inputs={atomicals_operations_found_at_inputs}...') + return None + allow_mint = True + else: + # It is the 'fixed' mint mode and the bitworkc/r is static mint_pow_commit = mint_info_for_ticker.get('$mint_bitworkc') mint_pow_reveal = mint_info_for_ticker.get('$mint_bitworkr') - if mint_pow_commit: - # It required commit proof of work - commit_txid = atomicals_operations_found_at_inputs['commit_txid'] - valid_commit_str, bitwork_commit_parts = is_valid_bitwork_string(mint_pow_commit) - if not valid_commit_str: - self.logger.warning(f'create_or_delete_decentralized_mint_output: not valid_commit_str {hash_to_hex_str(tx_hash)}...') - return None - mint_bitwork_prefix = bitwork_commit_parts['prefix'] - mint_bitwork_ext = bitwork_commit_parts['ext'] - if is_proof_of_work_prefix_match(commit_txid, mint_bitwork_prefix, mint_bitwork_ext): - self.logger.debug(f'create_or_delete_decentralized_mint_outputs: has VALID mint_bitworkc {valid_commit_str} for {hash_to_hex_str(commit_txid)} for {ticker}. Continuing to mint...') - else: - self.logger.warning(f'create_or_delete_decentralized_mint_outputs: has INVALID mint_bitworkc {valid_commit_str} because the pow is invalid for {hash_to_hex_str(commit_txid)} for {ticker}. Skipping invalid mint attempt...') - return None - - if mint_pow_reveal: - # It required reveal proof of work - reveal_txid = atomicals_operations_found_at_inputs['reveal_location_txid'] - valid_reveal_str, bitwork_reveal_parts = is_valid_bitwork_string(mint_pow_reveal) - if not valid_reveal_str: - self.logger.debug(f'create_or_delete_decentralized_mint_output: not valid_reveal_str {hash_to_hex_str(tx_hash)}...') - return None - mint_bitwork_prefix = bitwork_reveal_parts['prefix'] - mint_bitwork_ext = bitwork_reveal_parts['ext'] - if is_proof_of_work_prefix_match(reveal_txid, mint_bitwork_prefix, mint_bitwork_ext): - self.logger.debug(f'create_or_delete_decentralized_mint_outputs: has VALID mint_bitworkr {valid_reveal_str} for {hash_to_hex_str(reveal_txid)} for {ticker}. Continuing to mint...') - else: - self.logger.warning(f'create_or_delete_decentralized_mint_outputs: has INVALID mint_bitworkr {valid_reveal_str} because the pow is invalid for {hash_to_hex_str(reveal_txid)} for {ticker}. Skipping invalid mint attempt...') - return None - + # In the fixed mode there is a max number of mints allowed and then no more + if decentralized_mints > max_mints: + raise IndexError(f'create_or_delete_decentralized_mint_outputs: Fatal IndexError decentralized_mints > max_mints for {location_id_bytes_to_compact(dmt_mint_atomical_id)}. Too many mints detected in db') + + if decentralized_mints < max_mints: + self.logger.debug(f'create_or_delete_decentralized_mint_outputs: found mint request in {hash_to_hex_str(tx_hash)} for {ticker}. Checking for any POW in distributed mint record...') + # If this was a POW mint, then validate that the POW is valid + if mint_pow_commit: + if not is_mint_pow_valid(atomicals_operations_found_at_inputs['commit_txid'], mint_pow_commit): + self.logger.warning(f'create_or_delete_decentralized_mint_output: not is_mint_pow_valid {hash_to_hex_str(tx_hash)}, mint_pow_commit={mint_pow_commit}, atomicals_operations_found_at_inputs={atomicals_operations_found_at_inputs}...') + return None + if mint_pow_reveal: + if not is_mint_pow_valid(atomicals_operations_found_at_inputs['reveal_location_txid'], mint_pow_reveal): + self.logger.warning(f'create_or_delete_decentralized_mint_output: not is_mint_pow_valid {hash_to_hex_str(tx_hash)}, mint_pow_reveal={mint_pow_reveal}, atomicals_operations_found_at_inputs={atomicals_operations_found_at_inputs}...') + return None + allow_mint = True + + if allow_mint: the_key = b'po' + location if Delete: atomicals_found_list = self.spend_atomicals_utxo(tx_hash, expected_output_index, True) @@ -2538,6 +2626,8 @@ def is_dmint_activated(self, height): return False def is_density_activated(self, height): + if height >= self.coin.ATOMICALS_ACTIVATION_HEIGHT_DENSITY: + return True return False # Builds a map of the atomicals spent at a tx @@ -2631,7 +2721,7 @@ def advance_txs( atomicals_spent_at_inputs[txin_index] = atomicals_transferred_list for atomical_spent in atomicals_transferred_list: atomical_id = atomical_spent['atomical_id'] - self.logger.debug(f'atomicals_transferred_list - tx_hash={hash_to_hex_str(tx_hash)}, txin_index={txin_index}, txin_hash={hash_to_hex_str(txin.prev_hash)}, txin_previdx={txin.prev_idx}, atomical_id_spent={atomical_id.hex()}') + self.logger.debug(f'atomicals_transferred_list - tx_hash={hash_to_hex_str(tx_hash)}, txin_index={txin_index}, txin_hash={hash_to_hex_str(txin.prev_hash)}, txin_previdx={txin.prev_idx}, atomical_id_spent={location_id_bytes_to_compact(atomical_id)}') # Get the undo format for the spent atomicals reformatted_for_undo_entries = [] for atomicals_entry in atomicals_transferred_list: @@ -2736,7 +2826,7 @@ def advance_txs( if has_at_least_one_valid_atomicals_operation: put_general_data(b'th' + pack_le_uint32(height) + pack_le_uint64(tx_num) + tx_hash, tx_hash) - + append_hashXs(hashXs) update_touched(hashXs) tx_num += 1 @@ -2758,7 +2848,7 @@ def advance_txs( current_height_atomicals_block_hash = self.coin.header_hash(b''.join(concatenation_of_tx_hashes_with_valid_atomical_operation)) put_general_data(b'tt' + pack_le_uint32(height), current_height_atomicals_block_hash) self.logger.info(f'height={height}, atomicals_block_hash={hash_to_hex_str(current_height_atomicals_block_hash)}') - + return undo_info, atomicals_undo_info # Sanity safety check method to call at end of block processing to ensure no dft token inflation @@ -2767,17 +2857,18 @@ def validate_no_dft_inflation(self, atomical_id_map, height): # Get the max mints allowed for the dft ticker (if set) mint_info_for_ticker = self.get_atomicals_id_mint_info(atomical_id_of_dft_ticker, False) max_mints = mint_info_for_ticker['$max_mints'] + dft_mode = mint_info_for_ticker.get('$mint_mode') + if dft_mode == 'infinite': + continue # Count the number of existing b'gi' entries and ensure it is strictly less than max_mints decentralized_mints = self.get_distmints_count_by_atomical_id(height, atomical_id_of_dft_ticker, False) if decentralized_mints > max_mints: raise IndexError(f'validate_no_dft_inflation - inflation_bug_found: atomical_id_of_dft_ticker={location_id_bytes_to_compact(atomical_id_of_dft_ticker)} decentralized_mints={decentralized_mints} max_mints={max_mints}') def create_or_delete_subname_payment_output_if_valid(self, tx_hash, tx, tx_num, height, operations_found_at_inputs, atomicals_spent_at_inputs, db_prefix, subname_data_cache, get_expected_subname_payment_info, Delete=False): - atomical_id_for_payment, payment_marker_idx, entity_type = AtomicalsTransferBlueprintBuilder.get_atomical_id_for_payment_marker_if_found(tx) if not atomical_id_for_payment: return None - # Make sure the payment type for the right type subrealm or dmitem is correct if entity_type == 'subrealm' and db_prefix != b'spay': return None @@ -2982,6 +3073,7 @@ def backup_txs( self.atomicals_id_cache.clear() self.atomicals_rpc_format_cache.clear() self.atomicals_rpc_general_cache.clear() + self.atomicals_dft_mint_count_cache.clear() # Delete the Atomicals hash for the current height as we are rolling back self.delete_general_data(b'tt' + pack_le_uint32(self.height)) @@ -3006,9 +3098,9 @@ def backup_txs( m = len(atomicals_undo_info) atomicals_undo_entry_len = ATOMICAL_ID_LEN + ATOMICAL_ID_LEN + HASHX_LEN + SCRIPTHASH_LEN + 8 + 2 + TXNUM_LEN atomicals_count = m / atomicals_undo_entry_len - has_undo_info_for_atomicals = False - if m > 0: - has_undo_info_for_atomicals = True + # has_undo_info_for_atomicals = False + # if m > 0: + # has_undo_info_for_atomicals = True c = m atomicals_undo_info_map = {} # Build a map of atomicals location to atomicals located there counted_atomicals_count = 0 @@ -3047,11 +3139,8 @@ def backup_txs( put_utxo = self.utxo_cache.__setitem__ spend_utxo = self.spend_utxo - put_general_data = self.general_data_cache.__setitem__ - touched = self.touched undo_entry_len = HASHX_LEN + TXNUM_LEN + 8 - to_le_uint64 = pack_le_uint64 tx_num = self.tx_count atomical_num = self.atomical_count atomicals_minted = 0 @@ -3069,7 +3158,6 @@ def backup_txs( # Get the hashX cache_value = spend_utxo(tx_hash, idx) hashX = cache_value[:HASHX_LEN] - txout_value = cache_value[-8:] touched.add(hashX) # Rollback the atomicals that were created at the output hashXs_spent, spent_atomicals = self.rollback_spend_atomicals(tx_hash, tx, idx, tx_num, self.height, operations_found_at_inputs) diff --git a/electrumx/server/db.py b/electrumx/server/db.py index b1608eb9..369fb276 100644 --- a/electrumx/server/db.py +++ b/electrumx/server/db.py @@ -1666,11 +1666,9 @@ def get_name_entries_template_limited(self, db_prefix, subject_encoded, Reverse= start_count += 1 continue tx_numb = db_key[-8:] - atomical_id = db_value tx_num, = unpack_le_uint64(tx_numb) name_len, = unpack_le_uint16_from(db_key[-10:-8]) db_prefix_len = len(db_prefix) - self.logger.info(f'db_key, {db_key} {db_prefix_len} {name_len}') entries.append({ 'name': db_key[db_prefix_len : db_prefix_len + name_len].decode('latin-1'), # Extract the name portion 'atomical_id': db_value, diff --git a/electrumx/server/session.py b/electrumx/server/session.py index c6cc58c1..8798040b 100644 --- a/electrumx/server/session.py +++ b/electrumx/server/session.py @@ -274,6 +274,7 @@ async def _start_servers(self, services): app.router.add_get('/proxy/blockchain.atomicals.get_by_container_item_validate', handler.atomicals_get_by_container_item_validation) app.router.add_get('/proxy/blockchain.atomicals.get_container_items', handler.atomicals_get_container_items) app.router.add_get('/proxy/blockchain.atomicals.get_ft_info', handler.atomicals_get_ft_info) + app.router.add_get('/proxy/blockchain.atomicals.get_dft_mints', handler.atomicals_get_dft_mints) app.router.add_get('/proxy/blockchain.atomicals.find_tickers', handler.atomicals_search_tickers) app.router.add_get('/proxy/blockchain.atomicals.find_realms', handler.atomicals_search_realms) app.router.add_get('/proxy/blockchain.atomicals.find_subrealms', handler.atomicals_search_subrealms) @@ -1379,7 +1380,7 @@ async def atomical_id_get(self, compact_atomical_id): if atomical_in_mempool == None: raise RPCError(BAD_REQUEST, f'"{compact_atomical_id}" is not found') return atomical_in_mempool - + async def atomical_id_get_ft_info(self, compact_atomical_id): atomical_id = compact_to_location_id_bytes(compact_atomical_id) atomical = await self.session_mgr.bp.get_base_mint_info_rpc_format_by_atomical_id(atomical_id) @@ -1537,6 +1538,11 @@ async def atomicals_dump(self): self.db.dump() return {'result': True} + async def atomicals_get_dft_mints(self, compact_atomical_id, limit=100, offset=0): + atomical_id = compact_to_location_id_bytes(compact_atomical_id) + entries = self.session_mgr.bp.get_distmints_by_atomical_id(atomical_id, limit, offset) + return {'global': await self.get_summary_info(), 'result': entries} + async def atomicals_get_ft_info(self, compact_atomical_id_or_atomical_number): compact_atomical_id = self.atomical_resolve_id(compact_atomical_id_or_atomical_number) return {'global': await self.get_summary_info(), 'result': await self.atomical_id_get_ft_info(compact_atomical_id)} @@ -2737,6 +2743,7 @@ def set_request_handlers(self, ptuple): 'blockchain.atomicals.get_by_container_item_validate': self.atomicals_get_by_container_item_validation, 'blockchain.atomicals.get_container_items': self.atomicals_get_container_items, 'blockchain.atomicals.get_ft_info': self.atomicals_get_ft_info, + 'blockchain.atomicals.get_dft_mints': self.atomicals_get_dft_mints, 'blockchain.atomicals.find_tickers': self.atomicals_search_tickers, 'blockchain.atomicals.find_realms': self.atomicals_search_realms, 'blockchain.atomicals.find_subrealms': self.atomicals_search_subrealms, diff --git a/tests/lib/test_atomicals_utils.py b/tests/lib/test_atomicals_utils.py new file mode 100644 index 00000000..0adbaabc --- /dev/null +++ b/tests/lib/test_atomicals_utils.py @@ -0,0 +1,461 @@ +import pytest + +from electrumx.lib.atomicals_blueprint_builder import AtomicalsTransferBlueprintBuilder, get_nominal_token_value +from electrumx.lib.coins import Bitcoin +from electrumx.lib.hash import HASHX_LEN, hex_str_to_hash, hash_to_hex_str +from electrumx.lib.tx import Tx, TxInput, TxOutput + +from electrumx.lib.util_atomicals import ( + location_id_bytes_to_compact, + derive_bitwork_prefix_from_target, + decode_bitwork_target_from_prefix, + is_bitwork_subset, + calculate_expected_bitwork +) + +coin = Bitcoin + +class MockLogger: + def debug(self, msg): + return + def info(self, msg): + return + def warning(self, msg): + return + +def test_derive_bitwork_prefix_from_target_exception(): + with pytest.raises(Exception): + derive_bitwork_prefix_from_target('', 0) + derive_bitwork_prefix_from_target('', 15) + +def test_derive_bitwork_prefix_from_target_from_empty(): + testvec = [ + { + 'base': '', + 'inc': 16, + 'exp': '0' + }, + { + 'base': '', + 'inc': 17, + 'exp': '0.1' + }, + { + 'base': '', + 'inc': 18, + 'exp': '0.2' + }, + { + 'base': '', + 'inc': 19, + 'exp': '0.3' + }, + { + 'base': '', + 'inc': 20, + 'exp': '0.4' + }, + { + 'base': '', + 'inc': 21, + 'exp': '0.5' + }, + { + 'base': '', + 'inc': 22, + 'exp': '0.6' + }, + { + 'base': '', + 'inc': 23, + 'exp': '0.7' + }, + { + 'base': '', + 'inc': 24, + 'exp': '0.8' + }, + { + 'base': '', + 'inc': 25, + 'exp': '0.9' + }, + { + 'base': '', + 'inc': 26, + 'exp': '0.10' + }, + { + 'base': '', + 'inc': 27, + 'exp': '0.11' + }, + { + 'base': '', + 'inc': 28, + 'exp': '0.12', + }, + { + 'base': '', + 'inc': 29, + 'exp': '0.13' + }, + { + 'base': '', + 'inc': 30, + 'exp': '0.14' + }, + { + 'base': '', + 'inc': 31, + 'exp': '0.15' + }, + { + 'base': '', + 'inc': 32, + 'exp': '00' + }, + { + 'base': '', + 'inc': 33, + 'exp': '00.1' + }, + { + 'base': '', + 'inc': 34, + 'exp': '00.2' + }, + { + 'base': '', + 'inc': 35, + 'exp': '00.3' + }, + { + 'base': '', + 'inc': 36, + 'exp': '00.4' + }, + { + 'base': '', + 'inc': 37, + 'exp': '00.5' + }, + { + 'base': '', + 'inc': 38, + 'exp': '00.6' + }, + { + 'base': '', + 'inc': 39, + 'exp': '00.7' + }, + { + 'base': '', + 'inc': 40, + 'exp': '00.8' + }, + { + 'base': '', + 'inc': 41, + 'exp': '00.9' + }, + { + 'base': '', + 'inc': 42, + 'exp': '00.10' + }, + { + 'base': '', + 'inc': 43, + 'exp': '00.11' + }, + { + 'base': '', + 'inc': 44, + 'exp': '00.12' + }, + { + 'base': '', + 'inc': 45, + 'exp': '00.13' + }, + { + 'base': '', + 'inc': 46, + 'exp': '00.14' + }, + { + 'base': '', + 'inc': 47, + 'exp': '00.15' + }, + { + 'base': '', + 'inc': 48, + 'exp': '000' + }, + { + 'base': '', + 'inc': 49, + 'exp': '000.1' + }, + { + 'base': '', + 'inc': 50, + 'exp': '000.2' + }, + { + 'base': '', + 'inc': 51, + 'exp': '000.3' + }, + { + 'base': '', + 'inc': 52, + 'exp': '000.4' + }, + { + 'base': '', + 'inc': 53, + 'exp': '000.5' + }, + { + 'base': '', + 'inc': 54, + 'exp': '000.6' + }, + { + 'base': '', + 'inc': 55, + 'exp': '000.7' + }, + { + 'base': '', + 'inc': 56, + 'exp': '000.8' + }, + { + 'base': '', + 'inc': 57, + 'exp': '000.9' + }, + { + 'base': '', + 'inc': 58, + 'exp': '000.10' + }, + { + 'base': '', + 'inc': 59, + 'exp': '000.11' + }, + { + 'base': '', + 'inc': 60, + 'exp': '000.12' + }, + { + 'base': '', + 'inc': 61, + 'exp': '000.13' + }, + { + 'base': '', + 'inc': 62, + 'exp': '000.14' + }, + { + 'base': '', + 'inc': 63, + 'exp': '000.15' + }, + { + 'base': '', + 'inc': 64, + 'exp': '0000' + }, + { + 'base': '', + 'inc': 65, + 'exp': '0000.1' + } + ] + + for x in testvec: + assert(derive_bitwork_prefix_from_target(x['base'], x['inc']) == x['exp']) + +def test_derive_bitwork_prefix_from_target_misc(): + testvec = [ + { + 'base': 'abc', + 'inc': 64, + 'exp': 'abc0' + }, + { + 'base': 'abcd', + 'inc': 64, + 'exp': 'abcd' + }, + { + 'base': 'abcd', + 'inc': 65, + 'exp': 'abcd.1' + }, + { + 'base': 'abcd', + 'inc': 80, + 'exp': 'abcd0' + }, + { + 'base': 'abcd', + 'inc': 83, + 'exp': 'abcd0.3' + }, + { + 'base': '0123456789abcdef', + 'inc': 128, + 'exp': '01234567' + }, + { + 'base': '0123456789abcdef', + 'inc': 129, + 'exp': '01234567.1' + }, + { + 'base': '0123456789abcdef', + 'inc': 256, + 'exp': '0123456789abcdef' + }, + { + 'base': '0123456789abcdef', + 'inc': 257, + 'exp': '0123456789abcdef.1' + }, + { + 'base': '0123456789abcdef', + 'inc': 273, + 'exp': '0123456789abcdef0.1' + } + ] + + for x in testvec: + assert(derive_bitwork_prefix_from_target(x['base'], x['inc']) == x['exp']) + +def test_decode_bitwork_target_from_prefix_empty(): + with pytest.raises(Exception): + decode_bitwork_target_from_prefix('z') + with pytest.raises(Exception): + decode_bitwork_target_from_prefix('.') + with pytest.raises(Exception): + decode_bitwork_target_from_prefix('0.') + with pytest.raises(Exception): + decode_bitwork_target_from_prefix('0.17') + with pytest.raises(Exception): + decode_bitwork_target_from_prefix('') + with pytest.raises(Exception): + decode_bitwork_target_from_prefix('00..0') + +def test_decode_bitwork_target_from_prefix_valid(): + testvec = [ + { + 'bitwork': 'a', + 'target': 16 + }, + { + 'bitwork': 'a.0', + 'target': 16 + }, + { + 'bitwork': 'a.1', + 'target': 17 + }, + { + 'bitwork': 'a.2', + 'target': 18 + }, + { + 'bitwork': 'a.15', + 'target': 31 + }, + { + 'bitwork': 'ab', + 'target': 32 + }, + { + 'bitwork': 'ab.0', + 'target': 32 + }, + { + 'bitwork': 'abcd', + 'target': 64 + }, + { + 'bitwork': 'abcd.1', + 'target': 65 + }, + { + 'bitwork': 'abcd0123', + 'target': 128 + }, + { + 'bitwork': 'abcd0123.5', + 'target': 133 + } + ] + + for x in testvec: + assert(decode_bitwork_target_from_prefix(x['bitwork']) == x['target']) + +def test_is_bitwork_subset_fail(): + with pytest.raises(Exception): + is_bitwork_subset('', '') + + assert(is_bitwork_subset('a', 'b') == False) + assert(is_bitwork_subset('a', 'a') == True) + assert(is_bitwork_subset('a', 'ab') == True) + assert(is_bitwork_subset('a', 'a.1') == True) + assert(is_bitwork_subset('ab', 'ab') == True) + assert(is_bitwork_subset('ab', 'ab.1') == True) + assert(is_bitwork_subset('ab.1', 'ab.2') == True) + assert(is_bitwork_subset('ab.14', 'ab.15') == True) + assert(is_bitwork_subset('ab.15', 'ab0') == True) + assert(is_bitwork_subset('ab', 'ab') == True) + assert(is_bitwork_subset('ab', 'ab.15') == True) + assert(is_bitwork_subset('ab.15', 'ab') == False) + assert(is_bitwork_subset('0000', '000') == False) + assert(is_bitwork_subset('0000', '0000') == True) + assert(is_bitwork_subset('0000', '00000') == True) + assert(is_bitwork_subset('0000.5', '0000.6') == True) + assert(is_bitwork_subset('0000.5', '0000.15') == True) + assert(is_bitwork_subset('0000.5', '00008888') == True) + +def test_calculate_expected_bitwork_base(): + with pytest.raises(Exception): + calculate_expected_bitwork('', 0, 1, 1, 63) + + with pytest.raises(Exception): + calculate_expected_bitwork('', 0, 1, 0, 64) + + assert(calculate_expected_bitwork('', 0, 1, 1, 64) == '0000') + assert(calculate_expected_bitwork('a', 0, 1, 1, 64) == 'a000') + assert(calculate_expected_bitwork('a', 1, 1, 1, 64) == 'a000.1') + assert(calculate_expected_bitwork('a', 2, 1, 1, 64) == 'a000.2') + assert(calculate_expected_bitwork('a', 2, 1, 2, 64) == 'a000.4') + assert(calculate_expected_bitwork('abcd', 0, 1000, 1, 64) == 'abcd') + assert(calculate_expected_bitwork('abcd', 1, 1000, 1, 64) == 'abcd') + assert(calculate_expected_bitwork('abcd', 999, 1000, 1, 64) == 'abcd') + assert(calculate_expected_bitwork('abcd', 1000, 1000, 1, 64) == 'abcd.1') + assert(calculate_expected_bitwork('abcd', 1001, 1000, 1, 64) == 'abcd.1') + assert(calculate_expected_bitwork('abcd', 1999, 1000, 1, 64) == 'abcd.1') + assert(calculate_expected_bitwork('abcd', 2000, 1000, 1, 64) == 'abcd.2') + assert(calculate_expected_bitwork('abcd', 15999, 1000, 1, 64) == 'abcd.15') + assert(calculate_expected_bitwork('abcd', 16000, 1000, 1, 64) == 'abcd0') + assert(calculate_expected_bitwork('abcd', 16001, 1000, 1, 64) == 'abcd0') + assert(calculate_expected_bitwork('abcdef', 32000, 1000, 1, 64) == 'abcdef') + assert(calculate_expected_bitwork('abcdefe', 32001, 1000, 2, 64) == 'abcdefe0') + assert(calculate_expected_bitwork('abcdefe', 33000, 1000, 2, 64) == 'abcdefe0.2') + assert(calculate_expected_bitwork('abcdefe', 33000, 1000, 3, 64) == 'abcdefe000.3') + assert(calculate_expected_bitwork('abcdefe', 33000, 1000, 1, 127) == 'abcdefe000') + assert(calculate_expected_bitwork('abcdefe', 33000, 1000, 3, 127) == 'abcdefe0000000.2') +