コード例 #1
0
    def test_calculate(self):

        nw = {
            'NAME': 'MAINNET',
            'NB_FREEZE_CYCLE': 5,
            'BLOCK_TIME_IN_SEC': 60,
            'BLOCKS_PER_CYCLE': 4096,
            'BLOCKS_PER_ROLL_SNAPSHOT': 256
        }

        api = ProviderFactory(provider='prpc').newRewardApi(nw, BAKING_ADDRESS, '')
        model = api.get_rewards_for_cycle_map(153)

        phase0 = CalculatePhase0(model)
        reward_data, total_rewards = phase0.calculate()

        delegate_staking_balance = int(model.delegate_staking_balance)

        # total reward ratio is 1
        self.assertTrue(1.0, sum(r.ratio0 for r in reward_data))

        # check that ratio calculations are correct
        delegators_balances_dict = model.delegator_balance_dict

        # check ratios
        for (address, delegator_info), reward in zip(delegators_balances_dict.items(), reward_data):
            # ratio must be equal to stake/total staking balance
            delegator_staking_balance = int(delegator_info["staking_balance"])
            self.assertEqual(delegator_staking_balance / delegate_staking_balance, reward.ratio0)

        # last one is owners record
        self.assertTrue(reward_data[-1].type == reward_log.TYPE_OWNERS_PARENT)
コード例 #2
0
    def test_calculate(self):

        nw = DEFAULT_NETWORK_CONFIG_MAP["MAINNET"]

        api = ProviderFactory(provider="tzkt").newRewardApi(
            nw, BAKING_ADDRESS, "")

        model = api.get_rewards_for_cycle_map(CYCLE, REWARDS_TYPE)

        phase0 = CalculatePhase0(model)
        reward_data = phase0.calculate()

        delegate_staking_balance = int(model.delegate_staking_balance)

        # total reward ratio is 1
        self.assertEqual(1.0, sum(r.ratio0 for r in reward_data))

        # check that ratio calculations are correct
        delegators_balances_dict = model.delegator_balance_dict

        # check ratios
        for (address,
             delegator_info), reward in zip(delegators_balances_dict.items(),
                                            reward_data):
            # ratio must be equal to stake/total staking balance
            delegator_staking_balance = int(delegator_info["staking_balance"])
            self.assertEqual(
                delegator_staking_balance / delegate_staking_balance,
                reward.ratio0)

        # last one is owners record
        self.assertTrue(reward_data[-1].type == TYPE_OWNERS_PARENT)
コード例 #3
0
    def __init__(self,
                 name,
                 initial_payment_cycle,
                 network_config,
                 payments_dir,
                 calculations_dir,
                 run_mode,
                 service_fee_calc,
                 release_override,
                 payment_offset,
                 baking_cfg,
                 payments_queue,
                 life_cycle,
                 dry_run,
                 wllt_clnt_mngr,
                 node_url,
                 provider,
                 verbose=False):
        super(PaymentProducer, self).__init__()
        self.baking_address = baking_cfg.get_baking_address()
        self.owners_map = baking_cfg.get_owners_map()
        self.founders_map = baking_cfg.get_founders_map()
        self.excluded_delegators_set = baking_cfg.get_excluded_delegators_set()
        self.min_delegation_amt = baking_cfg.get_min_delegation_amount()
        self.pymnt_scale = baking_cfg.get_payment_scale()
        self.prcnt_scale = baking_cfg.get_percentage_scale()

        self.name = name

        rc = RoundingCommand(self.prcnt_scale)

        provider_factory = ProviderFactory(provider)
        self.reward_api = provider_factory.newRewardApi(
            network_config, self.baking_address, wllt_clnt_mngr, node_url)
        self.block_api = provider_factory.newBlockApi(network_config,
                                                      wllt_clnt_mngr, node_url)
        self.reward_calculator_api = provider_factory.newCalcApi(
            self.founders_map, self.min_delegation_amt,
            self.excluded_delegators_set, rc)

        self.fee_calc = service_fee_calc
        self.initial_payment_cycle = initial_payment_cycle
        self.nw_config = network_config
        self.payments_root = payments_dir
        self.calculations_dir = calculations_dir
        self.run_mode = run_mode
        self.exiting = False

        self.release_override = release_override
        self.payment_offset = payment_offset
        self.verbose = verbose
        self.payments_queue = payments_queue
        self.life_cycle = life_cycle
        self.dry_run = dry_run
        logger.debug('Producer started')
コード例 #4
0
def load_config_file(wllt_clnt_mngr, network_config, master_cfg):
    provider_factory = ProviderFactory(args.reward_data_provider)
    parser = BakingYamlConfParser(None,
                                  wllt_clnt_mngr,
                                  provider_factory,
                                  network_config,
                                  args.node_addr,
                                  api_base_url=args.api_base_url)
    parser.parse()
    parser.validate()
    parser.process()
    cfg_dict = parser.get_conf_obj()

    # dictionary to BakingConf object, for a bit of type safety
    cfg = BakingConf(cfg_dict, master_cfg)

    logger.info("Baking Configuration {}".format(cfg))

    baking_address = cfg.get_baking_address()
    payment_address = cfg.get_payment_address()
    logger.info(LINER)
    logger.info("BAKING ADDRESS is {}".format(baking_address))
    logger.info("PAYMENT ADDRESS is {}".format(payment_address))
    logger.info(LINER)

    # 7- get reporting directories
    reports_dir = os.path.expanduser(args.reports_base)

    reports_dir = os.path.join(reports_dir, baking_address)

    payments_root = get_payment_root(reports_dir, create=True)
    get_successful_payments_dir(payments_root, create=True)
    get_failed_payments_dir(payments_root, create=True)
コード例 #5
0
    def do_build_parser(self, e):
        provider_factory = ProviderFactory(self.args.reward_data_provider)

        self.__parser = BakingYamlConfParser(
            yaml_text=self.__config_text,
            clnt_mngr=self.__node_client,
            provider_factory=provider_factory,
            network_config=self.__nw_cfg,
            node_url=self.args.node_endpoint,
            api_base_url=self.args.api_base_url)
コード例 #6
0
def test_disk_full_payment_producer(args, caplog):
    # Issue: https://github.com/tezos-reward-distributor-organization/tezos-reward-distributor/issues/504
    client_manager = ClientManager(args.node_endpoint, args.signer_endpoint)
    network_config_map = init_network_config(args.network, client_manager)
    factory = ProviderFactory(provider="prpc")
    parser = BakingYamlConfParser(
        baking_config, None, None, None, None, block_api=factory, api_base_url=None
    )
    parser.parse()
    parser.process()

    cfg_dict = parser.get_conf_obj()
    baking_cfg = BakingConf(cfg_dict)
    baking_dirs = BakingDirs(args, baking_cfg.get_baking_address())
    srvc_fee_calc = ServiceFeeCalculator(
        baking_cfg.get_full_supporters_set(),
        baking_cfg.get_specials_map(),
        baking_cfg.get_service_fee(),
    )
    payments_queue = queue.Queue(50)
    plc = ProcessLifeCycle(None)
    pp = PaymentProducer(
        name="producer",
        network_config=network_config_map[args.network],
        payments_dir=baking_dirs.payments_root,
        calculations_dir=baking_dirs.calculations_root,
        run_mode=RunMode(args.run_mode),
        service_fee_calc=srvc_fee_calc,
        release_override=args.release_override,
        payment_offset=args.payment_offset,
        baking_cfg=baking_cfg,
        life_cycle=plc,
        payments_queue=payments_queue,
        dry_run=args.dry_run,
        client_manager=client_manager,
        node_url=args.node_endpoint,
        reward_data_provider=args.reward_data_provider,
        node_url_public=args.node_addr_public,
        api_base_url=args.api_base_url,
        retry_injected=args.retry_injected,
        initial_payment_cycle=args.initial_cycle,
    )
    assert disk_is_full()

    try:
        pp.daemon = True
        pp.start()

    finally:
        pp.stop()

    assert (
        "Disk is becoming full. Only 0.50 Gb left from 10.00 Gb. Please clean up disk to continue saving logs and reports."
        in caplog.text
    )
コード例 #7
0
def test_disk_full_payment_consumer(args, caplog):
    # Issue: https://github.com/tezos-reward-distributor-organization/tezos-reward-distributor/issues/504
    client_manager = ClientManager(args.node_endpoint, args.signer_endpoint)
    network_config_map = init_network_config(args.network, client_manager)
    factory = ProviderFactory(provider="prpc")
    parser = BakingYamlConfParser(
        baking_config, None, None, None, None, block_api=factory, api_base_url=None
    )
    parser.parse()
    parser.process()

    cfg_dict = parser.get_conf_obj()
    baking_cfg = BakingConf(cfg_dict)
    baking_dirs = BakingDirs(args, baking_cfg.get_baking_address())
    payments_queue = queue.Queue(50)
    plugins_manager = plugins.PluginManager(baking_cfg.get_plugins_conf(), args.dry_run)
    pc = PaymentConsumer(
        name="consumer0",
        payments_dir=baking_dirs.payments_root,
        key_name=baking_cfg.get_payment_address(),
        payments_queue=payments_queue,
        node_addr=args.node_endpoint,
        client_manager=client_manager,
        plugins_manager=plugins_manager,
        rewards_type=baking_cfg.get_rewards_type(),
        args=args,
        dry_run=args.dry_run,
        reactivate_zeroed=baking_cfg.get_reactivate_zeroed(),
        delegator_pays_ra_fee=baking_cfg.get_delegator_pays_ra_fee(),
        delegator_pays_xfer_fee=baking_cfg.get_delegator_pays_xfer_fee(),
        dest_map=baking_cfg.get_dest_map(),
        network_config=network_config_map[args.network],
        publish_stats=not args.do_not_publish_stats,
    )

    assert disk_is_full()

    try:
        pc.daemon = True
        pc.start()

    finally:
        pc.stop()

    assert (
        "Disk is becoming full. Only 0.30 Gb left from 11.00 Gb. Please clean up disk to continue saving logs and reports."
        in caplog.text
    )
コード例 #8
0
def onbakingaddress(input):
    try:
        AddressValidator("bakingaddress").validate(input)
    except Exception as e:
        printe("Invalid baking address: " + traceback.format_exc())
        return

    if not input.startswith("tz"):
        printe("Only tz addresses are allowed")
        return
    provider_factory = ProviderFactory(args.reward_data_provider)
    global parser
    parser = BakingYamlConfParser(None, wllt_clnt_mngr, provider_factory, network_config, args.node_addr)
    parser.set(BAKING_ADDRESS, input)
    messages['paymentaddress']=messages['paymentaddress'].format([v['alias'] for k,v in wllt_clnt_mngr.get_addr_dict().items() if v['sk']])+" (without quotes)"
    fsm.go()
コード例 #9
0
def onbakingaddress(input):
    try:
        AddressValidator("baking address").validate(input)
    except Exception as e:
        printe(f"Invalid baking address: {str(e)}")
        return

    if not input.startswith("tz"):
        printe("Only tz addresses are allowed")
        return
    provider_factory = ProviderFactory(args.reward_data_provider)
    global parser
    parser = BakingYamlConfParser(None, client_manager, provider_factory, network_config, args.node_endpoint,
                                  api_base_url=args.api_base_url)
    parser.set(BAKING_ADDRESS, input)
    fsm.go()
コード例 #10
0
def main(args):
    logger.info("TRD version {} is running in {} mode.".format(
        version.version,
        "daemon" if args.background_service else "interactive"))
    logger.info("Arguments Configuration = {}".format(
        json.dumps(args.__dict__, indent=1)))

    # 1- find where configuration is
    config_dir = os.path.expanduser(args.config_dir)

    # create configuration directory if it is not present
    # so that user can easily put his configuration there
    if config_dir and not os.path.exists(config_dir):
        os.makedirs(config_dir)

    # 2- Load master configuration file if it is present
    master_config_file_path = os.path.join(config_dir, "master.yaml")

    master_cfg = {}
    if os.path.isfile(master_config_file_path):
        logger.debug("Loading master configuration file {}".format(
            master_config_file_path))

        master_parser = YamlConfParser(
            ConfigParser.load_file(master_config_file_path))
        master_cfg = master_parser.parse()
    else:
        logger.debug("master configuration file not present.")

    managers = None
    contracts_by_alias = None
    addresses_by_pkh = None
    if 'managers' in master_cfg:
        managers = master_cfg['managers']
    if 'contracts_by_alias' in master_cfg:
        contracts_by_alias = master_cfg['contracts_by_alias']
    if 'addresses_by_pkh' in master_cfg:
        addresses_by_pkh = master_cfg['addresses_by_pkh']

    # 3- get client path

    client_path = get_client_path(
        [x.strip() for x in args.executable_dirs.split(',')], args.docker,
        args.network, args.verbose)

    logger.debug("Dune client path is {}".format(client_path))

    # 4. get network config
    config_client_manager = SimpleClientManager(client_path)
    network_config_map = init_network_config(args.network,
                                             config_client_manager,
                                             args.node_addr)
    network_config = network_config_map[args.network]

    logger.debug("Network config {}".format(network_config))

    # 5- load baking configuration file
    config_file_path = get_baking_configuration_file(config_dir)

    logger.info(
        "Loading baking configuration file {}".format(config_file_path))

    wllt_clnt_mngr = WalletClientManager(client_path,
                                         contracts_by_alias,
                                         addresses_by_pkh,
                                         managers,
                                         verbose=args.verbose)

    provider_factory = ProviderFactory(args.reward_data_provider,
                                       verbose=args.verbose)
    parser = BakingYamlConfParser(ConfigParser.load_file(config_file_path),
                                  wllt_clnt_mngr,
                                  provider_factory,
                                  network_config,
                                  args.node_addr,
                                  verbose=args.verbose)
    parser.parse()
    parser.validate()
    parser.process()
    cfg_dict = parser.get_conf_obj()

    # dictionary to BakingConf object, for a bit of type safety
    cfg = BakingConf(cfg_dict, master_cfg)

    logger.info("Baking Configuration {}".format(cfg))

    baking_address = cfg.get_baking_address()
    payment_address = cfg.get_payment_address()
    logger.info(LINER)
    logger.info("BAKING ADDRESS is {}".format(baking_address))
    logger.info("PAYMENT ADDRESS is {}".format(payment_address))
    logger.info(LINER)

    # 6- is it a reports run
    dry_run = args.dry_run_no_consumers or args.dry_run
    if args.dry_run_no_consumers:
        global NB_CONSUMERS
        NB_CONSUMERS = 0

    # 7- get reporting directories
    reports_base = os.path.expanduser(args.reports_base)
    # if in reports run mode, do not create consumers
    # create reports in reports directory
    if dry_run:
        reports_base = os.path.expanduser("./reports")

    reports_dir = os.path.join(reports_base, baking_address)

    payments_root = get_payment_root(reports_dir, create=True)
    calculations_root = get_calculations_root(reports_dir, create=True)
    get_successful_payments_dir(payments_root, create=True)
    get_failed_payments_dir(payments_root, create=True)

    # 8- start the life cycle
    life_cycle.start(not dry_run)

    # 9- service fee calculator
    srvc_fee_calc = ServiceFeeCalculator(cfg.get_full_supporters_set(),
                                         cfg.get_specials_map(),
                                         cfg.get_service_fee())

    if args.initial_cycle is None:
        recent = get_latest_report_file(payments_root)
        # if payment logs exists set initial cycle to following cycle
        # if payment logs does not exists, set initial cycle to 0, so that payment starts from last released rewards
        args.initial_cycle = 0 if recent is None else int(recent) + 1

        logger.info("initial_cycle set to {}".format(args.initial_cycle))

    p = PaymentProducer(name='producer',
                        initial_payment_cycle=args.initial_cycle,
                        network_config=network_config,
                        payments_dir=payments_root,
                        calculations_dir=calculations_root,
                        run_mode=RunMode(args.run_mode),
                        service_fee_calc=srvc_fee_calc,
                        release_override=args.release_override,
                        payment_offset=args.payment_offset,
                        baking_cfg=cfg,
                        life_cycle=life_cycle,
                        payments_queue=payments_queue,
                        dry_run=dry_run,
                        wllt_clnt_mngr=wllt_clnt_mngr,
                        node_url=args.node_addr,
                        provider_factory=provider_factory,
                        verbose=args.verbose)
    p.start()

    publish_stats = not args.do_not_publish_stats
    for i in range(NB_CONSUMERS):
        c = PaymentConsumer(
            name='consumer' + str(i),
            payments_dir=payments_root,
            key_name=payment_address,
            client_path=client_path,
            payments_queue=payments_queue,
            node_addr=args.node_addr,
            wllt_clnt_mngr=wllt_clnt_mngr,
            args=args,
            verbose=args.verbose,
            dry_run=dry_run,
            delegator_pays_xfer_fee=cfg.get_delegator_pays_xfer_fee(),
            dest_map=cfg.get_dest_map(),
            network_config=network_config,
            publish_stats=publish_stats)
        time.sleep(1)
        c.start()

        logger.info("Application start completed")
        logger.info(LINER)
    try:
        while life_cycle.is_running():
            time.sleep(10)
    except KeyboardInterrupt:
        logger.info("Interrupted.")
        life_cycle.stop()
コード例 #11
0
def main(args):
    logger.info("Arguments Configuration = {}".format(
        json.dumps(args.__dict__, indent=1)))

    # 1- find where configuration is
    config_dir = os.path.expanduser(args.config_dir)

    # create configuration directory if it is not present
    # so that user can easily put his configuration there
    if config_dir and not os.path.exists(config_dir):
        os.makedirs(config_dir)

    # 2- Load master configuration file if it is present
    master_config_file_path = os.path.join(config_dir, "master.yaml")

    master_cfg = {}
    if os.path.isfile(master_config_file_path):
        logger.info("Loading master configuration file {}".format(
            master_config_file_path))
        master_parser = YamlConfParser(
            ConfigParser.load_file(master_config_file_path))
        master_cfg = master_parser.parse()
    else:
        logger.info("master configuration file not present.")

    managers = None
    contracts_by_alias = None
    addresses_by_pkh = None

    if 'managers' in master_cfg:
        managers = master_cfg['managers']
    if 'contracts_by_alias' in master_cfg:
        contracts_by_alias = master_cfg['contracts_by_alias']
    if 'addresses_by_pkh' in master_cfg:
        addresses_by_pkh = master_cfg['addresses_by_pkh']

    # 3- get client path

    client_path = get_client_path(
        [x.strip() for x in args.executable_dirs.split(',')], args.docker,
        args.network, args.verbose)

    logger.debug("Tezos client path is {}".format(client_path))

    # 4. get network config
    config_client_manager = SimpleClientManager(client_path, args.node_addr)
    network_config_map = init_network_config(args.network,
                                             config_client_manager,
                                             args.node_addr)
    network_config = network_config_map[args.network]

    # 5- load baking configuration file
    config_file_path = get_baking_configuration_file(config_dir)

    logger.info(
        "Loading baking configuration file {}".format(config_file_path))

    wllt_clnt_mngr = WalletClientManager(client_path,
                                         contracts_by_alias,
                                         addresses_by_pkh,
                                         managers,
                                         verbose=args.verbose)

    provider_factory = ProviderFactory(args.reward_data_provider,
                                       verbose=args.verbose)
    parser = BakingYamlConfParser(ConfigParser.load_file(config_file_path),
                                  wllt_clnt_mngr,
                                  provider_factory,
                                  network_config,
                                  args.node_addr,
                                  verbose=args.verbose,
                                  api_base_url=args.api_base_url)
    parser.parse()
    parser.validate()
    parser.process()
    cfg_dict = parser.get_conf_obj()

    # dictionary to BakingConf object, for a bit of type safety
    cfg = BakingConf(cfg_dict, master_cfg)

    logger.info("Baking Configuration {}".format(cfg))

    baking_address = cfg.get_baking_address()
    payment_address = cfg.get_payment_address()
    logger.info(LINER)
    logger.info("BAKING ADDRESS is {}".format(baking_address))
    logger.info("PAYMENT ADDRESS is {}".format(payment_address))
    logger.info(LINER)

    # 6- is it a reports run
    dry_run = args.dry_run_no_consumers or args.dry_run
    if args.dry_run_no_consumers:
        global NB_CONSUMERS
        NB_CONSUMERS = 0

    # 7- get reporting directories
    reports_dir = os.path.expanduser(args.reports_base)

    # if in reports run mode, do not create consumers
    # create reports in reports directory
    if dry_run:
        reports_dir = os.path.expanduser("./reports")

    reports_dir = os.path.join(reports_dir, baking_address)

    payments_root = get_payment_root(reports_dir, create=True)
    calculations_root = get_calculations_root(reports_dir, create=True)
    get_successful_payments_dir(payments_root, create=True)
    get_failed_payments_dir(payments_root, create=True)

    # 8- start the life cycle
    life_cycle.start(False)

    # 9- service fee calculator
    srvc_fee_calc = ServiceFeeCalculator(cfg.get_full_supporters_set(),
                                         cfg.get_specials_map(),
                                         cfg.get_service_fee())

    try:

        p = PaymentProducer(name='producer',
                            initial_payment_cycle=None,
                            network_config=network_config,
                            payments_dir=payments_root,
                            calculations_dir=calculations_root,
                            run_mode=RunMode.ONETIME,
                            service_fee_calc=srvc_fee_calc,
                            release_override=0,
                            payment_offset=0,
                            baking_cfg=cfg,
                            life_cycle=life_cycle,
                            payments_queue=payments_queue,
                            dry_run=dry_run,
                            wllt_clnt_mngr=wllt_clnt_mngr,
                            node_url=args.node_addr,
                            provider_factory=provider_factory,
                            verbose=args.verbose,
                            api_base_url=args.api_base_url)

        p.retry_failed_payments(args.retry_injected)

        c = PaymentConsumer(
            name='consumer_retry_failed',
            payments_dir=payments_root,
            key_name=payment_address,
            client_path=client_path,
            payments_queue=payments_queue,
            node_addr=args.node_addr,
            wllt_clnt_mngr=wllt_clnt_mngr,
            verbose=args.verbose,
            dry_run=dry_run,
            delegator_pays_xfer_fee=cfg.get_delegator_pays_xfer_fee(),
            network_config=network_config)
        time.sleep(1)
        c.start()
        p.exit()
        c.join()

        logger.info("Application start completed")
        logger.info(LINER)

        sleep(5)

    except KeyboardInterrupt:
        logger.info("Interrupted.")
コード例 #12
0
def test_batch_payer_total_payout_amount():
    factory = ProviderFactory(provider="prpc")
    parser = BakingYamlConfParser(
        baking_config, None, None, None, None, block_api=factory, api_base_url=None
    )
    parser.parse()
    parser.process()

    cfg_dict = parser.get_conf_obj()
    baking_cfg = BakingConf(cfg_dict)

    srvc_fee_calc = ServiceFeeCalculator(
        baking_cfg.get_full_supporters_set(),
        baking_cfg.get_specials_map(),
        baking_cfg.get_service_fee(),
    )
    rules_model = RulesModel(
        baking_cfg.get_excluded_set_tob(),
        baking_cfg.get_excluded_set_toe(),
        baking_cfg.get_excluded_set_tof(),
        baking_cfg.get_dest_map(),
    )
    payment_calc = PhasedPaymentCalculator(
        baking_cfg.get_founders_map(),
        baking_cfg.get_owners_map(),
        srvc_fee_calc,
        int(baking_cfg.get_min_delegation_amount() * MUTEZ_PER_TEZ),
        rules_model,
    )

    rewardApi = factory.newRewardApi(
        default_network_config_map[CURRENT_TESTNET], baking_cfg.get_baking_address(), ""
    )

    # Simulate logic in payment_producer
    reward_logs = []
    attempts = 0
    exiting = False
    while not exiting and attempts < 2:
        attempts += 1

        # Reward data
        # Fetch cycle 51 of granadanet for tz1gtHbmBF3TSebsgJfJPvUB2e9x8EDeNm6V
        reward_model = rewardApi.get_rewards_for_cycle_map(
            PAYOUT_CYCLE, RewardsType.ACTUAL
        )

        # Calculate rewards - payment_producer.py
        reward_model.computed_reward_amount = reward_model.total_reward_amount
        reward_logs, total_amount = payment_calc.calculate(reward_model)

        # Check total reward amount matches sums of records
        # diff of 1 expected due to floating point arithmetic
        assert (
            total_amount - sum([rl.adjusted_amount for rl in reward_logs if rl.payable])
            <= 1
        )
        exiting = True

    # Merge payments to same address
    phaseMerge = CalculatePhaseMerge()
    reward_logs = phaseMerge.calculate(reward_logs)

    # Handle remapping of payment to alternate address
    phaseMapping = CalculatePhaseMapping()
    reward_logs = phaseMapping.calculate(reward_logs, baking_cfg.get_dest_map())

    # Filter zero-balance addresses based on config
    phaseZeroBalance = CalculatePhaseZeroBalance()
    reward_logs = phaseZeroBalance.calculate(
        reward_logs, baking_cfg.get_reactivate_zeroed()
    )

    # Filter out non-payable items
    reward_logs = [payment_item for payment_item in reward_logs if payment_item.payable]
    reward_logs.sort(key=cmp_to_key(cmp_by_type_balance))

    batch_payer = BatchPayer(
        node_url=node_endpoint,
        pymnt_addr="tz1N4UfQCahHkRShBanv9QP9TnmXNgCaqCyZ",
        clnt_mngr=ClientManager(node_endpoint, PRIVATE_SIGNER_URL),
        delegator_pays_ra_fee=True,
        delegator_pays_xfer_fee=True,
        network_config=network,
        plugins_manager=PluginManager(baking_cfg.get_plugins_conf(), dry_run=True),
        dry_run=True,
    )

    # Do the payment
    (
        _,
        total_attempts,
        total_payout_amount,
        number_future_payable_cycles,
    ) = batch_payer.pay(reward_logs, dry_run=True)

    # Payment does not have status done, paid or injected thus the total payout amount is zero
    assert total_payout_amount == 0
    assert number_future_payable_cycles == 2
    assert total_attempts == 3

    # Check the adjusted amount
    assert reward_logs[0].adjusted_amount == 40418486
    assert reward_logs[1].adjusted_amount == 10581272
    assert reward_logs[2].adjusted_amount == 109732835
    assert reward_logs[3].adjusted_amount == 48362127
    assert reward_logs[4].adjusted_amount == 29116310
コード例 #13
0
    def __init__(self,
                 name,
                 initial_payment_cycle,
                 network_config,
                 payments_dir,
                 calculations_dir,
                 run_mode,
                 service_fee_calc,
                 release_override,
                 payment_offset,
                 baking_cfg,
                 payments_queue,
                 life_cycle,
                 dry_run,
                 client_manager,
                 node_url,
                 reward_data_provider,
                 node_url_public='',
                 api_base_url=None,
                 retry_injected=False):
        super(PaymentProducer, self).__init__()

        self.rules_model = RulesModel(baking_cfg.get_excluded_set_tob(),
                                      baking_cfg.get_excluded_set_toe(),
                                      baking_cfg.get_excluded_set_tof(),
                                      baking_cfg.get_dest_map())
        self.baking_address = baking_cfg.get_baking_address()
        self.owners_map = baking_cfg.get_owners_map()
        self.founders_map = baking_cfg.get_founders_map()
        self.min_delegation_amt_in_mutez = baking_cfg.get_min_delegation_amount(
        ) * MUTEZ
        self.delegator_pays_xfer_fee = baking_cfg.get_delegator_pays_xfer_fee()
        self.provider_factory = ProviderFactory(reward_data_provider)
        self.name = name

        self.node_url = node_url
        self.client_manager = client_manager
        self.reward_api = self.provider_factory.newRewardApi(
            network_config, self.baking_address, self.node_url,
            node_url_public, api_base_url)
        self.block_api = self.provider_factory.newBlockApi(
            network_config, self.node_url, api_base_url)

        dexter_contracts_set = baking_cfg.get_contracts_set()
        if len(dexter_contracts_set) > 0 and not (self.reward_api.name
                                                  == 'tzstats'):
            logger.warning(
                "The Dexter functionality is currently only supported using tzstats."
                "The contract address will be treated as a normal delegator.")
        else:
            self.reward_api.set_dexter_contracts_set(dexter_contracts_set)

        self.rewards_type = baking_cfg.get_rewards_type()
        self.fee_calc = service_fee_calc
        self.initial_payment_cycle = initial_payment_cycle

        if self.initial_payment_cycle is None:
            recent = get_latest_report_file(payments_dir)
            # if payment logs exists set initial cycle to following cycle
            # if payment logs does not exists, set initial cycle to 0, so that payment starts from last released rewards
            self.initial_payment_cycle = 0 if recent is None else int(
                recent) + 1

        logger.info("initial_cycle set to {}".format(
            self.initial_payment_cycle))

        self.nw_config = network_config
        self.payments_root = payments_dir
        self.calculations_dir = calculations_dir
        self.run_mode = run_mode
        self.exiting = False

        self.release_override = release_override
        self.payment_offset = payment_offset
        self.payments_queue = payments_queue
        self.life_cycle = life_cycle
        self.dry_run = dry_run

        self.payment_calc = PhasedPaymentCalculator(
            self.founders_map, self.owners_map, self.fee_calc,
            self.min_delegation_amt_in_mutez, self.rules_model)

        self.retry_fail_thread = threading.Thread(target=self.retry_fail_run,
                                                  name=self.name +
                                                  "_retry_fail")
        self.retry_fail_event = threading.Event()
        self.retry_injected = retry_injected

        self.retry_producer = RetryProducer(self.payments_queue,
                                            self.reward_api, self,
                                            self.payments_root,
                                            self.retry_injected)

        logger.info('Producer "{}" started'.format(self.name))
コード例 #14
0
class PaymentProducer(threading.Thread, PaymentProducerABC):
    def __init__(self,
                 name,
                 initial_payment_cycle,
                 network_config,
                 payments_dir,
                 calculations_dir,
                 run_mode,
                 service_fee_calc,
                 release_override,
                 payment_offset,
                 baking_cfg,
                 payments_queue,
                 life_cycle,
                 dry_run,
                 client_manager,
                 node_url,
                 reward_data_provider,
                 node_url_public='',
                 api_base_url=None,
                 retry_injected=False):
        super(PaymentProducer, self).__init__()

        self.rules_model = RulesModel(baking_cfg.get_excluded_set_tob(),
                                      baking_cfg.get_excluded_set_toe(),
                                      baking_cfg.get_excluded_set_tof(),
                                      baking_cfg.get_dest_map())
        self.baking_address = baking_cfg.get_baking_address()
        self.owners_map = baking_cfg.get_owners_map()
        self.founders_map = baking_cfg.get_founders_map()
        self.min_delegation_amt_in_mutez = baking_cfg.get_min_delegation_amount(
        ) * MUTEZ
        self.delegator_pays_xfer_fee = baking_cfg.get_delegator_pays_xfer_fee()
        self.provider_factory = ProviderFactory(reward_data_provider)
        self.name = name

        self.node_url = node_url
        self.client_manager = client_manager
        self.reward_api = self.provider_factory.newRewardApi(
            network_config, self.baking_address, self.node_url,
            node_url_public, api_base_url)
        self.block_api = self.provider_factory.newBlockApi(
            network_config, self.node_url, api_base_url)

        dexter_contracts_set = baking_cfg.get_contracts_set()
        if len(dexter_contracts_set) > 0 and not (self.reward_api.name
                                                  == 'tzstats'):
            logger.warning(
                "The Dexter functionality is currently only supported using tzstats."
                "The contract address will be treated as a normal delegator.")
        else:
            self.reward_api.set_dexter_contracts_set(dexter_contracts_set)

        self.rewards_type = baking_cfg.get_rewards_type()
        self.fee_calc = service_fee_calc
        self.initial_payment_cycle = initial_payment_cycle

        if self.initial_payment_cycle is None:
            recent = get_latest_report_file(payments_dir)
            # if payment logs exists set initial cycle to following cycle
            # if payment logs does not exists, set initial cycle to 0, so that payment starts from last released rewards
            self.initial_payment_cycle = 0 if recent is None else int(
                recent) + 1

        logger.info("initial_cycle set to {}".format(
            self.initial_payment_cycle))

        self.nw_config = network_config
        self.payments_root = payments_dir
        self.calculations_dir = calculations_dir
        self.run_mode = run_mode
        self.exiting = False

        self.release_override = release_override
        self.payment_offset = payment_offset
        self.payments_queue = payments_queue
        self.life_cycle = life_cycle
        self.dry_run = dry_run

        self.payment_calc = PhasedPaymentCalculator(
            self.founders_map, self.owners_map, self.fee_calc,
            self.min_delegation_amt_in_mutez, self.rules_model)

        self.retry_fail_thread = threading.Thread(target=self.retry_fail_run,
                                                  name=self.name +
                                                  "_retry_fail")
        self.retry_fail_event = threading.Event()
        self.retry_injected = retry_injected

        self.retry_producer = RetryProducer(self.payments_queue,
                                            self.reward_api, self,
                                            self.payments_root,
                                            self.retry_injected)

        logger.info('Producer "{}" started'.format(self.name))

    def exit(self):
        if not self.exiting:
            self.payments_queue.put(
                PaymentBatch(self, 0, [self.create_exit_payment()]))
            self.exiting = True

            if self.life_cycle.is_running() and threading.current_thread(
            ) is not threading.main_thread():
                _thread.interrupt_main()

            if self.retry_fail_event:
                self.retry_fail_event.set()

    def retry_fail_run(self):
        logger.info('Retry Fail thread "{}" started'.format(
            self.retry_fail_thread.name))

        sleep(60)  # producer thread already tried once, wait for next try

        while not self.exiting and self.life_cycle.is_running():
            self.retry_producer.retry_failed_payments()

            try:
                # prepare to wait on event
                self.retry_fail_event.clear()

                # this will either return with timeout or set from parent producer thread
                self.retry_fail_event.wait(60 * 60)  # 1 hour
            except RuntimeError:
                pass

    def run(self):
        # call first retry if not in onetime mode.
        # retry_failed script is more suitable for one time cases.
        if not self.run_mode == RunMode.ONETIME:
            self.retry_producer.retry_failed_payments()

            if self.run_mode == RunMode.RETRY_FAILED:
                sleep(5)
                self.exit()
                return

        # first retry is done by producer thread, start retry thread for further retries
        if self.run_mode == RunMode.FOREVER:
            self.retry_fail_thread.start()

        try:
            current_cycle = self.block_api.get_current_cycle()
            pymnt_cycle = self.initial_payment_cycle
        except ApiProviderException as a:
            logger.error(
                "Unable to fetch current cycle, {:s}. Exiting.".format(str(a)))
            self.exit()
            return

        # if non-positive initial_payment_cycle, set initial_payment_cycle to
        # 'current cycle - abs(initial_cycle) - (NB_FREEZE_CYCLE+1)'
        if self.initial_payment_cycle <= 0:
            pymnt_cycle = current_cycle - abs(self.initial_payment_cycle) - (
                self.nw_config['NB_FREEZE_CYCLE'] + 1)
            logger.debug("Payment cycle is set to {}".format(pymnt_cycle))

        get_verbose_log_helper().reset(pymnt_cycle)

        while not self.exiting and self.life_cycle.is_running():

            # take a breath
            sleep(5)

            try:

                # Check if local node is bootstrapped; sleep if needed; restart loop
                if not self.node_is_bootstrapped():
                    logger.info(
                        "Local node, {}, is not in sync with the Tezos network. Will sleep for {} blocks and check again."
                        .format(self.node_url, BOOTSTRAP_SLEEP))
                    self.wait_for_blocks(BOOTSTRAP_SLEEP)
                    continue

                # Local node is ready
                current_level = self.block_api.get_current_level()
                current_cycle = self.block_api.level_to_cycle(current_level)
                level_in_cycle = self.block_api.level_in_cycle(current_level)

                # create reports dir
                if self.calculations_dir and not os.path.exists(
                        self.calculations_dir):
                    os.makedirs(self.calculations_dir)

                logger.debug(
                    "Checking for pending payments : payment_cycle <= current_cycle - (self.nw_config['NB_FREEZE_CYCLE'] + 1) - self.release_override"
                )
                logger.info(
                    "Checking for pending payments : checking {} <= {} - ({} + 1) - {}"
                    .format(pymnt_cycle, current_cycle,
                            self.nw_config['NB_FREEZE_CYCLE'],
                            self.release_override))

                # payments should not pass beyond last released reward cycle
                if pymnt_cycle <= current_cycle - (
                        self.nw_config['NB_FREEZE_CYCLE'] +
                        1) - self.release_override:
                    if not self.payments_queue.full():

                        # Paying upcoming cycles (-R in [-6, -11] )
                        if pymnt_cycle >= current_cycle:
                            logger.warn(
                                "Please note that you are doing payouts for future rewards!!! These rewards are not earned yet, they are an estimation."
                            )
                            if not self.rewards_type.isIdeal():
                                logger.error(
                                    "For future rewards payout, you must configure the payout type to 'Ideal', see documentation"
                                )
                                self.exit()
                                break

                        # Paying cycles with frozen rewards (-R in [-1, -5] )
                        elif pymnt_cycle >= current_cycle - self.nw_config[
                                'NB_FREEZE_CYCLE']:
                            logger.warn(
                                "Please note that you are doing payouts for frozen rewards!!!"
                            )
                            if (not self.rewards_type.isIdeal()
                                ) and self.reward_api.name == 'RPC':
                                logger.error(
                                    "Paying out frozen rewards with Node RPC API and rewards type 'Actual' is unsupported, you must use TzKT or tzstats API"
                                )
                                self.exit()
                                break

                        # If user wants to offset payments within a cycle, check here
                        if level_in_cycle < self.payment_offset:
                            wait_offset_blocks = self.payment_offset - level_in_cycle
                            logger.info(
                                "Current level within the cycle is {}; Requested offset is {}; Waiting for {} more blocks."
                                .format(level_in_cycle, self.payment_offset,
                                        wait_offset_blocks))
                            self.wait_for_blocks(wait_offset_blocks)
                            continue  # Break/Repeat loop

                        else:
                            result = self.try_to_pay(
                                pymnt_cycle,
                                expected_rewards=self.rewards_type.isIdeal())

                        if result:
                            # single run is done. Do not continue.
                            if self.run_mode == RunMode.ONETIME:
                                logger.info(
                                    "Run mode ONETIME satisfied. Terminating ..."
                                )
                                self.exit()
                                break
                            else:
                                pymnt_cycle = pymnt_cycle + 1
                                get_verbose_log_helper().reset(pymnt_cycle)

                    # end of queue size check
                    else:
                        logger.debug("Wait a few minutes, queue is full")
                        # wait a few minutes to let payments finish
                        sleep(60 * 3)

                # end of payment cycle check
                else:
                    logger.info(
                        "No pending payments for cycle {}, current cycle is {}"
                        .format(pymnt_cycle, current_cycle))

                    # pending payments done. Do not wait any more.
                    if self.run_mode == RunMode.PENDING:
                        logger.info(
                            "Run mode PENDING satisfied. Terminating ...")
                        self.exit()
                        break

                    sleep(10)

                    # calculate number of blocks until end of current cycle
                    nb_blocks_remaining = (
                        current_cycle +
                        1) * self.nw_config['BLOCKS_PER_CYCLE'] - current_level

                    # plus offset. cycle beginnings may be busy, move payments forward
                    nb_blocks_remaining = nb_blocks_remaining + self.payment_offset

                    logger.debug(
                        "Waiting until next cycle; {} blocks remaining".format(
                            nb_blocks_remaining))

                    # wait until current cycle ends
                    self.wait_for_blocks(nb_blocks_remaining)

            except (ApiProviderException, ReadTimeout, ConnectTimeout) as e:
                logger.debug(
                    "{:s} error at payment producer loop: '{:s}'".format(
                        self.reward_api.name, str(e)),
                    exc_info=True)
                logger.error(
                    "{:s} error at payment producer loop: '{:s}', will try again."
                    .format(self.reward_api.name, str(e)))

            except Exception as e:
                logger.debug(
                    "Unknown error in payment producer loop: {:s}".format(
                        str(e)),
                    exc_info=True)
                logger.error(
                    "Unknown error in payment producer loop: {:s}, will try again."
                    .format(str(e)))

        # end of endless loop
        logger.info("Producer returning ...")

        # ensure consumer exits
        self.exit()

        return

    def try_to_pay(self, pymnt_cycle, expected_rewards=False):
        try:
            logger.info("Payment cycle is " + str(pymnt_cycle))

            # 0- check for past payment evidence for current cycle
            past_payment_state = check_past_payment(self.payments_root,
                                                    pymnt_cycle)

            if not self.dry_run and past_payment_state:
                logger.warn(past_payment_state)
                return True

            # 1- get reward data
            if expected_rewards:
                logger.info(
                    "Using expected/ideal rewards for payouts calculations")
            else:
                logger.info("Using actual rewards for payouts calculations")

            reward_model = self.reward_api.get_rewards_for_cycle_map(
                pymnt_cycle, expected_rewards)

            # 2- calculate rewards
            reward_logs, total_amount = self.payment_calc.calculate(
                reward_model)

            # 3- set cycle info
            for rl in reward_logs:
                rl.cycle = pymnt_cycle
            total_amount_to_pay = sum(
                [rl.amount for rl in reward_logs if rl.payable])

            # 4- if total_rewards > 0, proceed with payment
            if total_amount_to_pay > 0:
                report_file_path = get_calculation_report_file(
                    self.calculations_dir, pymnt_cycle)

                # 5- send to payment consumer
                self.payments_queue.put(
                    PaymentBatch(self, pymnt_cycle, reward_logs))

                # logger.info("Total payment amount is {:,} mutez. %s".format(total_amount_to_pay),
                #            "" if self.delegator_pays_xfer_fee else "(Transfer fee is not included)")

                logger.debug("Creating calculation report (%s)",
                             report_file_path)

                sleep(5.0)

                # 6- create calculations report file. This file contains calculations details
                self.create_calculations_report(reward_logs, report_file_path,
                                                total_amount, expected_rewards)

                # 7- processing of cycle is done
                logger.info(
                    "Reward creation is done for cycle {}, created {} rewards."
                    .format(pymnt_cycle, len(reward_logs)))

            elif total_amount_to_pay == 0:
                logger.info("Total payment amount is 0. Nothing to pay!")

        except ApiProviderException as a:
            logger.error("[try_to_pay] API provider error {:s}".format(str(a)))
            raise a from a
        except Exception as e:
            logger.error("[try_to_pay] Generic exception {:s}".format(str(e)))
            raise e from e

        # Either succeeded or raised exception
        return True

    def wait_for_blocks(self, nb_blocks_remaining):
        for x in range(nb_blocks_remaining):
            sleep(self.nw_config['BLOCK_TIME_IN_SEC'])

            # if shutting down, exit
            if not self.life_cycle.is_running():
                self.exit()
                break

    def node_is_bootstrapped(self):
        # Get RPC node's (-A) bootstrap time. If bootstrap time + 2 minutes is
        # before local time, node is not bootstrapped.
        #
        # clnt_mngr is a super class of SimpleClientManager which interfaces
        # with the tezos-node used for txn forging/signing/injection. This is the
        # node which we need to determine bootstrapped state
        try:
            boot_time = self.client_manager.get_bootstrapped()
            utc_time = datetime.utcnow()
            if (boot_time + timedelta(minutes=2)) < utc_time:
                logger.info(
                    "Current time is '{}', latest block of local node is '{}'."
                    .format(utc_time, boot_time))
                return False
        except ValueError:
            logger.error(
                "Unable to determine local node's bootstrap status. Continuing..."
            )
        return True

    def create_calculations_report(self, payment_logs, report_file_path,
                                   total_rewards, expected_rewards):

        rt = "I" if expected_rewards else "A"

        # Open reports file and write; auto-closes file
        with open(report_file_path, 'w', newline='') as f:

            writer = csv.writer(f,
                                delimiter=',',
                                quotechar='"',
                                quoting=csv.QUOTE_MINIMAL)

            # write headers and total rewards
            writer.writerow([
                "address", "type", "staked_balance", "current_balance",
                "ratio", "fee_ratio", "amount", "fee_amount", "fee_rate",
                "payable", "skipped", "atphase", "desc", "payment_address",
                "rewards_type"
            ])

            # First row is for the baker
            writer.writerow([
                self.baking_address, "B",
                sum([pl.staking_balance for pl in payment_logs]),
                "{0:f}".format(1.0), "{0:f}".format(1.0), "{0:f}".format(0.0),
                "{0:f}".format(total_rewards), "{0:f}".format(0.0),
                "{0:f}".format(0.0), "0", "0", "-1", "Baker", "None", rt
            ])

            for pymnt_log in payment_logs:
                # write row to csv file
                array = [
                    pymnt_log.address, pymnt_log.type,
                    pymnt_log.staking_balance, pymnt_log.current_balance,
                    "{0:.10f}".format(pymnt_log.ratio),
                    "{0:.10f}".format(pymnt_log.service_fee_ratio),
                    "{0:f}".format(pymnt_log.amount),
                    "{0:f}".format(pymnt_log.service_fee_amount),
                    "{0:f}".format(pymnt_log.service_fee_rate),
                    "1" if pymnt_log.payable else "0",
                    "1" if pymnt_log.skipped else "0",
                    pymnt_log.skippedatphase if pymnt_log.skipped else "-1",
                    pymnt_log.desc if pymnt_log.desc else "None",
                    pymnt_log.paymentaddress, rt
                ]
                writer.writerow(array)

                logger.debug(
                    "Reward created for {:s} type: {:s}, stake bal: {:>10.2f}, cur bal: {:>10.2f}, ratio: {:.6f}, fee_ratio: {:.6f}, "
                    "amount: {:>10.6f}, fee_amount: {:>4.6f}, fee_rate: {:.2f}, payable: {:s}, skipped: {:s}, at-phase: {:d}, "
                    "desc: {:s}, pay_addr: {:s}, type: {:s}".format(
                        pymnt_log.address, pymnt_log.type,
                        pymnt_log.staking_balance / MUTEZ,
                        pymnt_log.current_balance / MUTEZ, pymnt_log.ratio,
                        pymnt_log.service_fee_ratio, pymnt_log.amount / MUTEZ,
                        pymnt_log.service_fee_amount / MUTEZ,
                        pymnt_log.service_fee_rate,
                        "Y" if pymnt_log.payable else "N",
                        "Y" if pymnt_log.skipped else "N",
                        pymnt_log.skippedatphase, pymnt_log.desc,
                        pymnt_log.paymentaddress, rt))

        logger.info(
            "Calculation report is created at '{}'".format(report_file_path))

    @staticmethod
    def create_exit_payment():
        return RewardLog.ExitInstance()

    def notify_retry_fail_thread(self):
        self.retry_fail_event.set()

    # upon success retry failed payments if present
    # success may indicate what went wrong in past is fixed.
    def on_success(self, pymnt_batch):
        self.notify_retry_fail_thread()

    def on_fail(self, pymnt_batch):
        pass
class PaymentProducer(threading.Thread, PaymentProducerABC):
    def __init__(
        self,
        name,
        initial_payment_cycle,
        network_config,
        payments_dir,
        calculations_dir,
        run_mode,
        service_fee_calc,
        release_override,
        payment_offset,
        baking_cfg,
        payments_queue,
        life_cycle,
        dry_run,
        client_manager,
        node_url,
        reward_data_provider,
        node_url_public="",
        api_base_url=None,
        retry_injected=False,
    ):
        super(PaymentProducer, self).__init__()
        self.event = threading.Event()
        self.rules_model = RulesModel(
            baking_cfg.get_excluded_set_tob(),
            baking_cfg.get_excluded_set_toe(),
            baking_cfg.get_excluded_set_tof(),
            baking_cfg.get_dest_map(),
        )
        self.baking_address = baking_cfg.get_baking_address()
        self.owners_map = baking_cfg.get_owners_map()
        self.founders_map = baking_cfg.get_founders_map()
        self.min_delegation_amt_in_mutez = (
            baking_cfg.get_min_delegation_amount() * MUTEZ)
        self.delegator_pays_xfer_fee = baking_cfg.get_delegator_pays_xfer_fee()
        self.provider_factory = ProviderFactory(reward_data_provider)
        self.name = name

        self.node_url = node_url
        self.client_manager = client_manager
        self.reward_api = self.provider_factory.newRewardApi(
            network_config,
            self.baking_address,
            self.node_url,
            node_url_public,
            api_base_url,
        )
        self.block_api = self.provider_factory.newBlockApi(
            network_config, self.node_url, api_base_url)

        dexter_contracts_set = baking_cfg.get_contracts_set()
        if len(dexter_contracts_set) > 0 and not (self.reward_api.name
                                                  == "tzstats"):
            logger.warning(
                "The Dexter functionality is currently only supported using tzstats."
                "The contract address will be treated as a normal delegator.")
        else:
            self.reward_api.set_dexter_contracts_set(dexter_contracts_set)

        self.rewards_type = baking_cfg.get_rewards_type()
        self.pay_denunciation_rewards = baking_cfg.get_pay_denunciation_rewards(
        )
        self.fee_calc = service_fee_calc
        self.initial_payment_cycle = initial_payment_cycle

        logger.info("Initial cycle set to {}".format(
            self.initial_payment_cycle))

        self.nw_config = network_config
        self.payments_root = payments_dir
        self.calculations_dir = calculations_dir
        self.run_mode = run_mode
        self.exiting = False

        self.release_override = release_override
        self.payment_offset = payment_offset
        self.payments_queue = payments_queue
        self.life_cycle = life_cycle
        self.dry_run = dry_run

        self.payment_calc = PhasedPaymentCalculator(
            self.founders_map,
            self.owners_map,
            self.fee_calc,
            self.min_delegation_amt_in_mutez,
            self.rules_model,
        )

        self.retry_fail_thread = threading.Thread(target=self.retry_fail_run,
                                                  name=self.name +
                                                  "_retry_fail")
        self.retry_fail_event = threading.Event()
        self.retry_injected = retry_injected

        self.retry_producer = RetryProducer(
            self.payments_queue,
            self.reward_api,
            self,
            self.payments_root,
            self.initial_payment_cycle,
            self.retry_injected,
        )

        logger.debug('Producer "{}" started'.format(self.name))

    def exit(self):
        if not self.exiting:
            self.payments_queue.put(
                PaymentBatch(self, 0, [self.create_exit_payment()]))
            self.exiting = True

            if (self.life_cycle.is_running() and threading.current_thread()
                    is not threading.main_thread()):
                _thread.interrupt_main()

            if self.retry_fail_event:
                self.retry_fail_event.set()

    def retry_fail_run(self):
        logger.debug('Retry Fail thread "{}" started'.format(
            self.retry_fail_thread.name))

        sleep(60)  # producer thread already tried once, wait for next try

        while not self.exiting and self.life_cycle.is_running():
            self.retry_producer.retry_failed_payments()

            try:
                # prepare to wait on event
                self.retry_fail_event.clear()

                # this will either return with timeout or set from parent producer thread
                self.retry_fail_event.wait(60 * 60)  # 1 hour
            except RuntimeError:
                pass

    def run(self):
        # call first retry if not in onetime mode.
        # retry_failed script is more suitable for one time cases.
        if not self.run_mode == RunMode.ONETIME:
            self.retry_producer.retry_failed_payments()

            if self.run_mode == RunMode.RETRY_FAILED:
                sleep(5)
                self.exit()
                return

        # first retry is done by producer thread, start retry thread for further retries
        if self.run_mode == RunMode.FOREVER:
            self.retry_fail_thread.start()

        try:
            (
                current_cycle,
                current_level,
            ) = self.block_api.get_current_cycle_and_level()
        except ApiProviderException as a:
            logger.error(
                "Unable to fetch current cycle, {:s}. Exiting.".format(str(a)))
            self.exit()
            return

        # if initial_payment_cycle has the default value of -1 resulting in the last released cycle
        if self.initial_payment_cycle == -1:
            pymnt_cycle = (current_cycle -
                           (self.nw_config["NB_FREEZE_CYCLE"] + 1) -
                           self.release_override)
            if pymnt_cycle < 0:
                logger.error(
                    "Payment cycle cannot be < 0 but configuration results to {}"
                    .format(pymnt_cycle))
            else:
                logger.debug(
                    "Payment cycle is set to last released cycle {}".format(
                        pymnt_cycle))
        else:
            pymnt_cycle = self.initial_payment_cycle

        get_verbose_log_helper().reset(pymnt_cycle)

        while not self.exiting and self.life_cycle.is_running():

            # take a breath
            sleep(5)

            try:

                # Exit if disk is full
                # https://github.com/tezos-reward-distributor-organization/tezos-reward-distributor/issues/504
                if disk_is_full():
                    self.exit()
                    break

                # Check if local node is bootstrapped; sleep if needed; restart loop
                if not self.node_is_bootstrapped():
                    logger.info(
                        "Local node {} is not in sync with the Tezos network. Will sleep for {} blocks and check again."
                        .format(self.node_url, BOOTSTRAP_SLEEP))
                    self.wait_for_blocks(BOOTSTRAP_SLEEP)
                    continue

                # Local node is ready
                (
                    current_cycle,
                    current_level,
                ) = self.block_api.get_current_cycle_and_level()
                level_in_cycle = self.block_api.level_in_cycle(current_level)

                # create reports dir
                if self.calculations_dir and not os.path.exists(
                        self.calculations_dir):
                    os.makedirs(self.calculations_dir)

                logger.debug(
                    "Checking for pending payments: payment_cycle <= current_cycle - (self.nw_config['NB_FREEZE_CYCLE'] + 1) - self.release_override"
                )
                logger.info(
                    "Checking for pending payments: checking {} <= {} - ({} + 1) - {}"
                    .format(
                        pymnt_cycle,
                        current_cycle,
                        self.nw_config["NB_FREEZE_CYCLE"],
                        self.release_override,
                    ))

                # payments should not pass beyond last released reward cycle
                if (pymnt_cycle <= current_cycle -
                    (self.nw_config["NB_FREEZE_CYCLE"] + 1) -
                        self.release_override):
                    if not self.payments_queue.full():
                        if (not self.pay_denunciation_rewards
                            ) and self.reward_api.name == "RPC":
                            logger.info(
                                "Error: pay_denunciation_rewards=False requires an indexer since it is not possible to distinguish reward source using RPC"
                            )
                            e = "You must set 'pay_denunciation_rewards' to True when using RPC provider."
                            logger.error(e)
                            self.exit()
                            break

                        # Paying upcoming cycles (-R in [-6, -11] )
                        if pymnt_cycle >= current_cycle:
                            logger.warn(
                                "Please note that you are doing payouts for future rewards!!! These rewards are not earned yet, they are an estimation."
                            )
                            if not self.rewards_type.isEstimated():
                                logger.error(
                                    "For future rewards payout, you must configure the payout type to 'Estimated', see documentation"
                                )
                                self.exit()
                                break

                        # Paying cycles with frozen rewards (-R in [-1, -5] )
                        elif (pymnt_cycle >= current_cycle -
                              self.nw_config["NB_FREEZE_CYCLE"]):
                            logger.warn(
                                "Please note that you are doing payouts for frozen rewards!!!"
                            )

                        # If user wants to offset payments within a cycle, check here
                        if level_in_cycle < self.payment_offset:
                            wait_offset_blocks = self.payment_offset - level_in_cycle
                            wait_offset_minutes = (
                                wait_offset_blocks *
                                self.nw_config["MINIMAL_BLOCK_DELAY"]) / 60
                            logger.info(
                                "Current level within the cycle is {}; Requested offset is {}; Waiting for {} more blocks (~{} minutes)"
                                .format(
                                    level_in_cycle,
                                    self.payment_offset,
                                    wait_offset_blocks,
                                    wait_offset_minutes,
                                ))
                            self.wait_for_blocks(wait_offset_blocks)
                            continue  # Break/Repeat loop

                        else:
                            result = self.try_to_pay(pymnt_cycle,
                                                     self.rewards_type,
                                                     self.nw_config)

                        if result:
                            # single run is done. Do not continue.
                            if self.run_mode == RunMode.ONETIME:
                                logger.info(
                                    "Run mode ONETIME satisfied. Terminating..."
                                )
                                self.exit()
                                break
                            else:
                                pymnt_cycle = pymnt_cycle + 1
                                get_verbose_log_helper().reset(pymnt_cycle)

                    # end of queue size check
                    else:
                        logger.debug("Wait a few minutes, queue is full")
                        # wait a few minutes to let payments finish
                        sleep(60 * 3)

                # end of payment cycle check
                else:
                    logger.info(
                        "No pending payments for cycle {}, current cycle is {}"
                        .format(pymnt_cycle, current_cycle))

                    # pending payments done. Do not wait any more.
                    if self.run_mode == RunMode.PENDING:
                        logger.info(
                            "Run mode PENDING satisfied. Terminating...")
                        self.exit()
                        break

                    sleep(10)

                    # calculate number of blocks until end of current cycle plus user-defined offset
                    nb_blocks_remaining = (self.nw_config["BLOCKS_PER_CYCLE"] -
                                           level_in_cycle +
                                           self.payment_offset)
                    logger.debug(
                        "Waiting until next cycle; {} blocks remaining".format(
                            nb_blocks_remaining))

                    # wait until current cycle ends
                    self.wait_for_blocks(nb_blocks_remaining)

            except (ApiProviderException, ReadTimeout, ConnectTimeout) as e:
                logger.debug(
                    "{:s} error at payment producer loop: '{:s}'".format(
                        self.reward_api.name, str(e)),
                    exc_info=True,
                )
                logger.error(
                    "{:s} error at payment producer loop: '{:s}', will try again."
                    .format(self.reward_api.name, str(e)))

            except Exception as e:
                logger.debug(
                    "Unknown error in payment producer loop: {:s}".format(
                        str(e)),
                    exc_info=True,
                )
                logger.error(
                    "Unknown error in payment producer loop: {:s}, will try again."
                    .format(str(e)))

        # end of endless loop
        logger.debug("Producer returning...")

        # ensure consumer exits
        self.exit()

        return

    def stop(self):
        self.exit()
        self.event.set()

    def compute_rewards(self, reward_model, rewards_type, network_config):
        if rewards_type.isEstimated():
            logger.info("Using estimated rewards for payouts calculations")
            block_reward = network_config["BLOCK_REWARD"]
            endorsement_reward = network_config["ENDORSEMENT_REWARD"]
            total_estimated_block_reward = reward_model.num_baking_rights * block_reward
            total_estimated_endorsement_reward = (
                reward_model.num_endorsing_rights * endorsement_reward)
            computed_reward_amount = (total_estimated_block_reward +
                                      total_estimated_endorsement_reward)
        elif rewards_type.isActual():
            logger.info("Using actual rewards for payouts calculations")
            if self.pay_denunciation_rewards:
                computed_reward_amount = reward_model.total_reward_amount
            else:
                # omit denunciation rewards
                computed_reward_amount = (reward_model.rewards_and_fees -
                                          reward_model.equivocation_losses)
        elif rewards_type.isIdeal():
            logger.info("Using ideal rewards for payouts calculations")
            if self.pay_denunciation_rewards:
                computed_reward_amount = (reward_model.total_reward_amount +
                                          reward_model.offline_losses)
            else:
                # omit denunciation rewards and double baking loss
                computed_reward_amount = (reward_model.rewards_and_fees +
                                          reward_model.offline_losses)
        return computed_reward_amount

    def try_to_pay(self, pymnt_cycle, rewards_type, network_config):
        try:
            logger.info("Payment cycle is {:s}".format(str(pymnt_cycle)))

            # 0- check for past payment evidence for current cycle
            past_payment_state = check_past_payment(self.payments_root,
                                                    pymnt_cycle)

            if past_payment_state:
                logger.warn(past_payment_state)
                return True

            # 1- get reward data
            reward_model = self.reward_api.get_rewards_for_cycle_map(
                pymnt_cycle, rewards_type)

            # 2- compute reward amount to distribute based on configuration
            reward_model.computed_reward_amount = self.compute_rewards(
                reward_model, rewards_type, network_config)

            # 3- calculate rewards for delegators
            reward_logs, total_amount = self.payment_calc.calculate(
                reward_model)

            # 4- set cycle info
            for rl in reward_logs:
                rl.cycle = pymnt_cycle
            total_amount_to_pay = sum(
                [rl.amount for rl in reward_logs if rl.payable])

            # 5- if total_rewards > 0, proceed with payment
            if total_amount_to_pay > 0:

                # 6- send to payment consumer
                self.payments_queue.put(
                    PaymentBatch(self, pymnt_cycle, reward_logs))

                sleep(5.0)

                # 7- create calculations report file. This file contains calculations details
                report_file_path = get_calculation_report_file(
                    self.calculations_dir, pymnt_cycle)
                logger.debug("Creating calculation report (%s)",
                             report_file_path)
                self.create_calculations_report(reward_logs, report_file_path,
                                                total_amount, rewards_type)

                # 8- processing of cycle is done
                logger.info(
                    "Reward creation is done for cycle {}, created {} rewards."
                    .format(pymnt_cycle, len(reward_logs)))

            elif total_amount_to_pay == 0:
                logger.info("Total payment amount is 0. Nothing to pay!")

        except ApiProviderException as a:
            logger.error("[try_to_pay] API provider error {:s}".format(str(a)))
            raise a from a
        except Exception as e:
            logger.error("[try_to_pay] Generic exception {:s}".format(str(e)))
            raise e from e

        # Either succeeded or raised exception
        return True

    def wait_for_blocks(self, nb_blocks_remaining):
        for x in range(nb_blocks_remaining):
            sleep(self.nw_config["MINIMAL_BLOCK_DELAY"])

            # if shutting down, exit
            if not self.life_cycle.is_running():
                self.exit()
                break

    def node_is_bootstrapped(self):
        # Get RPC node's (-A) bootstrap time. If bootstrap time + 2 minutes is
        # before local time, node is not bootstrapped.
        #
        # clnt_mngr is a super class of SimpleClientManager which interfaces
        # with the tezos-node used for txn forging/signing/injection. This is the
        # node which we need to determine bootstrapped state
        try:
            boot_time = self.client_manager.get_bootstrapped()
            utc_time = datetime.utcnow()
            if (boot_time + timedelta(minutes=2)) < utc_time:
                logger.debug(
                    "Current time is '{}', latest block of local node is '{}'."
                    .format(utc_time, boot_time))
                return False
        except ValueError:
            logger.error(
                "Unable to determine local node's bootstrap status. Continuing..."
            )
        return True

    def create_calculations_report(self, payment_logs, report_file_path,
                                   total_rewards, rewards_type):

        if rewards_type.isEstimated():
            rt = "E"
        elif rewards_type.isActual():
            rt = "A"
        elif rewards_type.isIdeal():
            rt = "I"

        try:

            # Open reports file and write; auto-closes file
            with open(report_file_path, "w", newline="") as f:

                writer = csv.writer(f,
                                    delimiter=",",
                                    quotechar='"',
                                    quoting=csv.QUOTE_MINIMAL)

                # write headers and total rewards
                writer.writerow([
                    "address",
                    "type",
                    "staked_balance",
                    "current_balance",
                    "ratio",
                    "fee_ratio",
                    "amount",
                    "fee_amount",
                    "fee_rate",
                    "payable",
                    "skipped",
                    "atphase",
                    "desc",
                    "payment_address",
                    "rewards_type",
                ])

                # First row is for the baker
                writer.writerow([
                    self.baking_address,
                    "B",
                    sum([pl.staking_balance for pl in payment_logs]),
                    "{0:f}".format(1.0),
                    "{0:f}".format(1.0),
                    "{0:f}".format(0.0),
                    "{0:f}".format(total_rewards),
                    "{0:f}".format(0.0),
                    "{0:f}".format(0.0),
                    "0",
                    "0",
                    "-1",
                    "Baker",
                    "None",
                    rt,
                ])

                for pymnt_log in payment_logs:
                    # write row to csv file
                    array = [
                        pymnt_log.address,
                        pymnt_log.type,
                        pymnt_log.staking_balance,
                        pymnt_log.current_balance,
                        "{0:.10f}".format(pymnt_log.ratio),
                        "{0:.10f}".format(pymnt_log.service_fee_ratio),
                        "{0:f}".format(pymnt_log.amount),
                        "{0:f}".format(pymnt_log.service_fee_amount),
                        "{0:f}".format(pymnt_log.service_fee_rate),
                        "1" if pymnt_log.payable else "0",
                        "1" if pymnt_log.skipped else "0",
                        pymnt_log.skippedatphase
                        if pymnt_log.skipped else "-1",
                        pymnt_log.desc if pymnt_log.desc else "None",
                        pymnt_log.paymentaddress,
                        rt,
                    ]
                    writer.writerow(array)

                    logger.debug(
                        "Reward created for {:s} type: {:s}, stake bal: {:>10.2f}, cur bal: {:>10.2f}, ratio: {:.6f}, fee_ratio: {:.6f}, "
                        "amount: {:>10.6f}, fee_amount: {:>4.6f}, fee_rate: {:.2f}, payable: {:s}, skipped: {:s}, at-phase: {:d}, "
                        "desc: {:s}, pay_addr: {:s}, type: {:s}".format(
                            pymnt_log.address,
                            pymnt_log.type,
                            pymnt_log.staking_balance / MUTEZ,
                            pymnt_log.current_balance / MUTEZ,
                            pymnt_log.ratio,
                            pymnt_log.service_fee_ratio,
                            pymnt_log.amount / MUTEZ,
                            pymnt_log.service_fee_amount / MUTEZ,
                            pymnt_log.service_fee_rate,
                            "Y" if pymnt_log.payable else "N",
                            "Y" if pymnt_log.skipped else "N",
                            pymnt_log.skippedatphase,
                            pymnt_log.desc,
                            pymnt_log.paymentaddress,
                            rt,
                        ))

        except Exception as e:
            import errno

            print("Exception during write operation invoked: {}".format(e))
            if e.errno == errno.ENOSPC:
                print("Not enough space on device!")
            exit()

        logger.info(
            "Calculation report is created at '{}'".format(report_file_path))

    @staticmethod
    def create_exit_payment():
        return RewardLog.ExitInstance()

    def notify_retry_fail_thread(self):
        self.retry_fail_event.set()

    # upon success retry failed payments if present
    # success may indicate what went wrong in past is fixed.
    def on_success(self, pymnt_batch):
        self.notify_retry_fail_thread()

    def on_fail(self, pymnt_batch):
        pass
コード例 #16
0
def test_batch_payer_total_payout_amount():
    factory = ProviderFactory(provider="prpc")
    parser = BakingYamlConfParser(baking_config,
                                  None,
                                  None,
                                  None,
                                  None,
                                  block_api=factory,
                                  api_base_url=None)
    parser.parse()
    parser.process()

    cfg_dict = parser.get_conf_obj()
    baking_cfg = BakingConf(cfg_dict)

    srvc_fee_calc = ServiceFeeCalculator(
        baking_cfg.get_full_supporters_set(),
        baking_cfg.get_specials_map(),
        baking_cfg.get_service_fee(),
    )
    rules_model = RulesModel(
        baking_cfg.get_excluded_set_tob(),
        baking_cfg.get_excluded_set_toe(),
        baking_cfg.get_excluded_set_tof(),
        baking_cfg.get_dest_map(),
    )
    payment_calc = PhasedPaymentCalculator(
        baking_cfg.get_founders_map(),
        baking_cfg.get_owners_map(),
        srvc_fee_calc,
        baking_cfg.get_min_delegation_amount() * MUTEZ,
        rules_model,
    )

    rewardApi = factory.newRewardApi(
        default_network_config_map[CURRENT_TESTNET],
        baking_cfg.get_baking_address(), "")

    # Simulate logic in payment_producer
    reward_logs = []
    attempts = 0
    exiting = False
    while not exiting and attempts < 2:
        attempts += 1

        # Reward data
        # Fetch cycle 51 of granadanet for tz1gtHbmBF3TSebsgJfJPvUB2e9x8EDeNm6V
        reward_model = rewardApi.get_rewards_for_cycle_map(
            PAYOUT_CYCLE, RewardsType.ACTUAL)

        # Calculate rewards - payment_producer.py
        reward_model.computed_reward_amount = reward_model.total_reward_amount
        reward_logs, total_amount = payment_calc.calculate(reward_model)

        # Check total reward amount matches sums of records
        assert total_amount == sum(
            [rl.amount for rl in reward_logs if rl.payable])
        exiting = True

    # Merge payments to same address
    phaseMerge = CalculatePhaseMerge()
    reward_logs = phaseMerge.calculate(reward_logs)

    # Handle remapping of payment to alternate address
    phaseMapping = CalculatePhaseMapping()
    reward_logs = phaseMapping.calculate(reward_logs,
                                         baking_cfg.get_dest_map())

    # Filter zero-balance addresses based on config
    phaseZeroBalance = CalculatePhaseZeroBalance()
    reward_logs = phaseZeroBalance.calculate(
        reward_logs, baking_cfg.get_reactivate_zeroed())

    # Filter out non-payable items
    reward_logs = [
        payment_item for payment_item in reward_logs if payment_item.payable
    ]
    reward_logs.sort(key=cmp_to_key(cmp_by_type_balance))

    batch_payer = BatchPayer(
        node_url=node_endpoint,
        pymnt_addr="tz1gtHbmBF3TSebsgJfJPvUB2e9x8EDeNm6V",
        clnt_mngr=ClientManager(node_endpoint, PRIVATE_SIGNER_URL),
        delegator_pays_ra_fee=True,
        delegator_pays_xfer_fee=True,
        network_config=network,
        plugins_manager=PluginManager(baking_cfg.get_plugins_conf(),
                                      dry_run=True),
        dry_run=True,
    )

    # Fix the endpoint auto port assignment because
    # https://mainnet-tezos.giganode.io:8732 cannot be reached
    batch_payer.clnt_mngr.node_endpoint = node_endpoint

    # Do the payment
    (
        payment_logs,
        total_attempts,
        total_payout_amount,
        number_future_payable_cycles,
    ) = batch_payer.pay(reward_logs, dry_run=True)

    assert total_attempts == 3
    assert total_payout_amount == 238211030
    assert (PAYMENT_ADDRESS_BALANCE //
            total_payout_amount) - 1 == number_future_payable_cycles
コード例 #17
0
def main(args):
    logger.info("TRD version {} is running in {} mode.".format(
        VERSION, "daemon" if args.background_service else "interactive"))
    logger.info("Arguments Configuration = {}".format(
        json.dumps(args.__dict__, indent=1)))

    publish_stats = not args.do_not_publish_stats
    logger.info(
        "Anonymous statistics {} be collected. See docs/statistics.rst for more information."
        .format("will" if publish_stats else "will not"))

    # 1- find where configuration is
    config_dir = os.path.expanduser(args.config_dir)

    # create configuration directory if it is not present
    # so that user can easily put his configuration there
    if config_dir and not os.path.exists(config_dir):
        os.makedirs(config_dir)

    # 4. get network config
    client_manager = ClientManager(node_endpoint=args.node_endpoint,
                                   signer_endpoint=args.signer_endpoint)
    network_config_map = init_network_config(args.network, client_manager)
    network_config = network_config_map[args.network]
    logger.debug("Network config {}".format(network_config))

    # Setup provider to fetch RPCs
    provider_factory = ProviderFactory(args.reward_data_provider)

    # 5- load and verify baking configuration file
    config_file_path = None
    try:
        config_file_path = get_baking_configuration_file(config_dir)

        logger.info(
            "Loading baking configuration file {}".format(config_file_path))

        parser = BakingYamlConfParser(
            yaml_text=ConfigParser.load_file(config_file_path),
            clnt_mngr=client_manager,
            provider_factory=provider_factory,
            network_config=network_config,
            node_url=args.node_endpoint,
            api_base_url=args.api_base_url)
        parser.parse()
        parser.validate()
        parser.process()

        # dictionary to BakingConf object, for a bit of type safety
        cfg_dict = parser.get_conf_obj()
        cfg = BakingConf(cfg_dict)

    except ConfigurationException as e:
        logger.info(
            "Unable to parse '{}' config file.".format(config_file_path))
        logger.info(e)
        sys.exit(1)

    logger.info("Baking Configuration {}".format(cfg))

    baking_address = cfg.get_baking_address()
    payment_address = cfg.get_payment_address()

    logger.info(LINER)
    logger.info("BAKING ADDRESS is {}".format(baking_address))
    logger.info("PAYMENT ADDRESS is {}".format(payment_address))
    logger.info(LINER)

    # 6- is it a reports run
    dry_run = args.dry_run_no_consumers or args.dry_run
    if args.dry_run_no_consumers:
        global NB_CONSUMERS
        NB_CONSUMERS = 0

    # 7- get reporting directories
    reports_base = os.path.expanduser(args.reports_base)

    # if in reports run mode, do not create consumers
    # create reports in reports directory
    if dry_run:
        reports_base = os.path.expanduser("./reports")

    reports_dir = os.path.join(reports_base, baking_address)

    payments_root = get_payment_root(reports_dir, create=True)
    calculations_root = get_calculations_root(reports_dir, create=True)
    get_successful_payments_dir(payments_root, create=True)
    get_failed_payments_dir(payments_root, create=True)

    # 8- start the life cycle
    life_cycle.start(not dry_run)

    # 9- service fee calculator
    srvc_fee_calc = ServiceFeeCalculator(cfg.get_full_supporters_set(),
                                         cfg.get_specials_map(),
                                         cfg.get_service_fee())

    if args.initial_cycle is None:
        recent = get_latest_report_file(payments_root)
        # if payment logs exists set initial cycle to following cycle
        # if payment logs does not exists, set initial cycle to 0, so that payment starts from last released rewards
        args.initial_cycle = 0 if recent is None else int(recent) + 1

        logger.info("initial_cycle set to {}".format(args.initial_cycle))

    # 10- load plugins
    plugins_manager = plugins.PluginManager(cfg.get_plugins_conf(), dry_run)

    # 11- Start producer and consumer
    p = PaymentProducer(name='producer',
                        initial_payment_cycle=args.initial_cycle,
                        network_config=network_config,
                        payments_dir=payments_root,
                        calculations_dir=calculations_root,
                        run_mode=RunMode(args.run_mode),
                        service_fee_calc=srvc_fee_calc,
                        release_override=args.release_override,
                        payment_offset=args.payment_offset,
                        baking_cfg=cfg,
                        life_cycle=life_cycle,
                        payments_queue=payments_queue,
                        dry_run=dry_run,
                        client_manager=client_manager,
                        node_url=args.node_endpoint,
                        provider_factory=provider_factory,
                        node_url_public=args.node_addr_public,
                        api_base_url=args.api_base_url,
                        retry_injected=args.retry_injected)
    p.start()

    for i in range(NB_CONSUMERS):
        c = PaymentConsumer(
            name='consumer' + str(i),
            payments_dir=payments_root,
            key_name=payment_address,
            payments_queue=payments_queue,
            node_addr=args.node_endpoint,
            client_manager=client_manager,
            plugins_manager=plugins_manager,
            rewards_type=cfg.get_rewards_type(),
            args=args,
            dry_run=dry_run,
            reactivate_zeroed=cfg.get_reactivate_zeroed(),
            delegator_pays_ra_fee=cfg.get_delegator_pays_ra_fee(),
            delegator_pays_xfer_fee=cfg.get_delegator_pays_xfer_fee(),
            dest_map=cfg.get_dest_map(),
            network_config=network_config,
            publish_stats=publish_stats)

        sleep(1)
        c.start()

        logger.info("Application start completed")
        logger.info(LINER)

    # Run forever
    try:
        while life_cycle.is_running():
            sleep(10)
    except KeyboardInterrupt:
        logger.info("Interrupted.")
        life_cycle.stop()
コード例 #18
0
    def test_process_payouts(self):

        logger.debug("")  # Console formatting
        factory = ProviderFactory(provider='prpc')
        parser = BakingYamlConfParser(self.baking_config,
                                      None,
                                      None,
                                      None,
                                      None,
                                      block_api=factory,
                                      api_base_url=None)
        parser.parse()
        parser.process()

        cfg_dict = parser.get_conf_obj()
        baking_cfg = BakingConf(cfg_dict)

        srvc_fee_calc = ServiceFeeCalculator(
            baking_cfg.get_full_supporters_set(),
            baking_cfg.get_specials_map(), baking_cfg.get_service_fee())
        rules_model = RulesModel(baking_cfg.get_excluded_set_tob(),
                                 baking_cfg.get_excluded_set_toe(),
                                 baking_cfg.get_excluded_set_tof(),
                                 baking_cfg.get_dest_map())
        payment_calc = PhasedPaymentCalculator(
            baking_cfg.get_founders_map(), baking_cfg.get_owners_map(),
            srvc_fee_calc,
            baking_cfg.get_min_delegation_amount() * MUTEZ, rules_model)

        rewardApi = factory.newRewardApi(
            default_network_config_map[CURRENT_TESTNET],
            baking_cfg.get_baking_address(), "")

        # Simulate logic in payment_producer
        reward_logs = []
        attempts = 0
        exiting = False
        while not exiting and attempts < 2:
            attempts += 1
            try:
                # Reward data
                # Fetch cycle 90 of delphinet for tz1gtHbmBF3TSebsgJfJPvUB2e9x8EDeNm6V
                reward_model = rewardApi.get_rewards_for_cycle_map(
                    PAYOUT_CYCLE)

                # Calculate rewards - payment_producer.py
                reward_logs, total_amount = payment_calc.calculate(
                    reward_model)

                # Check total reward amount matches sums of records
                self.assertTrue(
                    total_amount,
                    sum([rl.amount for rl in reward_logs if rl.payable]))

                exiting = True

            except ApiProviderException as e:
                logger.error(
                    "{:s} error at payment producer loop: '{:s}', will try again."
                    .format("RPC", str(e)))
                sleep(5)

        #
        # The next 3 phases happen in payment_consumer.py
        #

        # Merge payments to same address
        phaseMerge = CalculatePhaseMerge()
        reward_logs = phaseMerge.calculate(reward_logs)

        # Handle remapping of payment to alternate address
        phaseMapping = CalculatePhaseMapping()
        reward_logs = phaseMapping.calculate(reward_logs,
                                             baking_cfg.get_dest_map())

        # Filter zero-balance addresses based on config
        phaseZeroBalance = CalculatePhaseZeroBalance()
        reward_logs = phaseZeroBalance.calculate(
            reward_logs, baking_cfg.get_reactivate_zeroed())

        # Filter out non-payable items
        reward_logs = [pi for pi in reward_logs if pi.payable]
        reward_logs.sort(key=cmp_to_key(cmp_by_type_balance))

        # TRD Calculated Results
        # tz1V9SpwXaGFiYdDfGJtWjA61EumAH3DwSyT type: D, stake bal:   62657.83, cur bal:   62657.83, ratio: 0.327420, fee_ratio: 0.000000, amount:   0.000000, fee_amount: 0.000000, fee_rate: 0.00, payable: N, skipped: Y, at-phase: 1, desc: Excluded by configuration, pay_addr: tz1V9SpwXaGFiYdDfGJtWjA61EumAH3DwSyT
        # tz1YTMY7Zewx6AMM2h9eCwc8TyXJ5wgn9ace type: D, stake bal:   55646.70, cur bal:   55646.70, ratio: 0.432340, fee_ratio: 0.000000, amount: 102.988160, fee_amount: 0.000000, fee_rate: 0.00, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1YTMY7Zewx6AMM2h9eCwc8TyXJ5wgn9ace
        # tz1T5woJN3r7SV5v2HGDyA5kurhbD9Y8ZKHZ type: D, stake bal:   25689.88, cur bal:   25689.88, ratio: 0.179635, fee_ratio: 0.019959, amount:  42.791010, fee_amount: 4.754557, fee_rate: 0.10, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1T5woJN3r7SV5v2HGDyA5kurhbD9Y8ZKHZ
        # tz1fgX6oRWQb4HYHUT6eRjW8diNFrqjEfgq7 type: D, stake bal:   24916.33, cur bal:   24916.33, ratio: 0.193584, fee_ratio: 0.000000, amount:  46.113902, fee_amount: 0.000000, fee_rate: 0.00, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1fgX6oRWQb4HYHUT6eRjW8diNFrqjEfgq7
        # tz1RRzfechTs3gWdM58y6xLeByta3JWaPqwP type: D, stake bal:    6725.43, cur bal:    6725.43, ratio: 0.047027, fee_ratio: 0.005225, amount:  11.202382, fee_amount: 1.244709, fee_rate: 0.10, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1RMmSzPSWPSSaKU193Voh4PosWSZx1C7Hs
        # tz1L1XQWKxG38wk1Ain1foGaEZj8zeposcbk type: D, stake bal:     981.64, cur bal:     981.64, ratio: 0.007627, fee_ratio: 0.000000, amount:   1.816762, fee_amount: 0.000000, fee_rate: 0.00, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1L1XQWKxG38wk1Ain1foGaEZj8zeposcbk
        # tz1L1XQWKxG38wk1Ain1foGaEZj8zeposcbk type: O, stake bal:   14750.53, cur bal:       0.00, ratio: 0.114602, fee_ratio: 0.000000, amount:  27.299548, fee_amount: 0.000000, fee_rate: 0.00, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1L1XQWKxG38wk1Ain1foGaEZj8zeposcbk
        # tz1fgX6oRWQb4HYHUT6eRjW8diNFrqjEfgq7 type: F, stake bal:       0.00, cur bal:       0.00, ratio: 0.006296, fee_ratio: 0.000000, amount:   1.499816, fee_amount: 0.000000, fee_rate: 0.00, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1fgX6oRWQb4HYHUT6eRjW8diNFrqjEfgq7
        # tz1YTMY7Zewx6AMM2h9eCwc8TyXJ5wgn9ace type: F, stake bal:       0.00, cur bal:       0.00, ratio: 0.018889, fee_ratio: 0.000000, amount:   4.499450, fee_amount: 0.000000, fee_rate: 0.00, payable: Y, skipped: N, at-phase: 0, desc: , pay_addr: tz1YTMY7Zewx6AMM2h9eCwc8TyXJ5wgn9ace

        # Final records before creating transactions
        # These values are known to be correct
        cr = {}
        cr["tz1T5woJN3r7SV5v2HGDyA5kurhbD9Y8ZKHZ"] = {
            "type": "D",
            "amount": 42791010,
            "pay_addr": "tz1T5woJN3r7SV5v2HGDyA5kurhbD9Y8ZKHZ"
        }
        cr["tz1RRzfechTs3gWdM58y6xLeByta3JWaPqwP"] = {
            "type": "D",
            "amount": 11202382,
            "pay_addr": "tz1RMmSzPSWPSSaKU193Voh4PosWSZx1C7Hs"
        }
        cr["tz1YTMY7Zewx6AMM2h9eCwc8TyXJ5wgn9ace"] = {
            "type": "M",
            "amount": 107487610,
            "pay_addr": "tz1YTMY7Zewx6AMM2h9eCwc8TyXJ5wgn9ace"
        }
        cr["tz1fgX6oRWQb4HYHUT6eRjW8diNFrqjEfgq7"] = {
            "type": "M",
            "amount": 47613718,
            "pay_addr": "tz1fgX6oRWQb4HYHUT6eRjW8diNFrqjEfgq7"
        }
        cr["tz1L1XQWKxG38wk1Ain1foGaEZj8zeposcbk"] = {
            "type": "M",
            "amount": 29116310,
            "pay_addr": "tz1L1XQWKxG38wk1Ain1foGaEZj8zeposcbk"
        }

        # Verify that TRD calculated matches known values
        for r in reward_logs:

            # We know this address should be skipped
            if r.address == "tz1V9SpwXaGFiYdDfGJtWjA61EumAH3DwSyT":
                self.assertEqual(r.skipped, 1)
                self.assertEqual(r.amount, 0)
                continue

            # All others we can compare normally
            cmp = cr[r.address]

            self.assertEqual(r.type, cmp["type"])
            self.assertEqual(r.amount, (cmp["amount"]))
            self.assertEqual(r.paymentaddress, cmp["pay_addr"])
コード例 #19
0
class PaymentProducer(threading.Thread, PaymentProducerABC):
    def __init__(
        self,
        name,
        initial_payment_cycle,
        network_config,
        payments_dir,
        calculations_dir,
        run_mode,
        service_fee_calc,
        release_override,
        payment_offset,
        baking_cfg,
        payments_queue,
        life_cycle,
        dry_run,
        client_manager,
        node_url,
        reward_data_provider,
        node_url_public="",
        api_base_url=None,
        retry_injected=False,
    ):
        super(PaymentProducer, self).__init__()
        self.event = threading.Event()
        self.rules_model = RulesModel(
            baking_cfg.get_excluded_set_tob(),
            baking_cfg.get_excluded_set_toe(),
            baking_cfg.get_excluded_set_tof(),
            baking_cfg.get_dest_map(),
        )
        self.baking_address = baking_cfg.get_baking_address()
        self.owners_map = baking_cfg.get_owners_map()
        self.founders_map = baking_cfg.get_founders_map()
        self.min_delegation_amt_in_mutez = int(
            baking_cfg.get_min_delegation_amount() * MUTEZ_PER_TEZ)
        self.delegator_pays_xfer_fee = baking_cfg.get_delegator_pays_xfer_fee()
        self.provider_factory = ProviderFactory(reward_data_provider)
        self.name = name

        self.node_url = node_url
        self.client_manager = client_manager
        self.reward_api = self.provider_factory.newRewardApi(
            network_config,
            self.baking_address,
            self.node_url,
            node_url_public,
            api_base_url,
        )
        self.block_api = self.provider_factory.newBlockApi(
            network_config, self.node_url, api_base_url)

        dexter_contracts_set = baking_cfg.get_contracts_set()
        if len(dexter_contracts_set) > 0 and not (self.reward_api.name
                                                  == "tzstats"):
            logger.warning(
                "The Dexter functionality is currently only supported using tzstats."
                "The contract address will be treated as a normal delegator.")
        else:
            self.reward_api.set_dexter_contracts_set(dexter_contracts_set)

        self.rewards_type = baking_cfg.get_rewards_type()
        self.pay_denunciation_rewards = baking_cfg.get_pay_denunciation_rewards(
        )
        self.fee_calc = service_fee_calc
        self.initial_payment_cycle = initial_payment_cycle

        logger.info("Initial cycle set to {}".format(
            self.initial_payment_cycle))

        self.nw_config = network_config
        self.payments_root = payments_dir
        self.calculations_dir = calculations_dir
        self.run_mode = run_mode
        self.exiting = False

        self.release_override = release_override
        self.payment_offset = payment_offset
        self.payments_queue = payments_queue
        self.life_cycle = life_cycle
        self.dry_run = dry_run

        self.payment_calc = PhasedPaymentCalculator(
            self.founders_map,
            self.owners_map,
            self.fee_calc,
            self.min_delegation_amt_in_mutez,
            self.rules_model,
        )

        self.retry_fail_thread = threading.Thread(target=self.retry_fail_run,
                                                  name=self.name +
                                                  "_retry_fail")
        self.retry_fail_event = threading.Event()
        self.retry_injected = retry_injected

        self.retry_producer = RetryProducer(
            self.payments_queue,
            self.reward_api,
            self,
            self.payments_root,
            self.initial_payment_cycle,
            self.retry_injected,
        )

        logger.debug('Producer "{}" started'.format(self.name))

    def exit(self):
        if not self.exiting:
            self.payments_queue.put(
                PaymentBatch(self, 0, [self.create_exit_payment()]))
            self.exiting = True

            if (self.life_cycle.is_running() and threading.current_thread()
                    is not threading.main_thread()):
                _thread.interrupt_main()
                logger.info("Sending KeyboardInterrupt signal.")

            if self.retry_fail_event:
                self.retry_fail_event.set()

    def retry_fail_run(self):
        logger.debug('Retry Fail thread "{}" started'.format(
            self.retry_fail_thread.name))

        sleep(60)  # producer thread already tried once, wait for next try

        while not self.exiting and self.life_cycle.is_running():
            self.retry_producer.retry_failed_payments()

            try:
                # prepare to wait on event
                self.retry_fail_event.clear()

                # this will either return with timeout or set from parent producer thread
                self.retry_fail_event.wait(60 * 60)  # 1 hour
            except RuntimeError:
                pass

    def run(self):
        # call first retry if not in onetime mode.
        # retry_failed script is more suitable for one time cases.
        if not self.run_mode == RunMode.ONETIME:
            self.retry_producer.retry_failed_payments()

            if self.run_mode == RunMode.RETRY_FAILED:
                sleep(5)
                self.exit()
                return

        # first retry is done by producer thread, start retry thread for further retries
        if self.run_mode == RunMode.FOREVER:
            self.retry_fail_thread.start()

        try:
            (
                current_cycle,
                current_level,
            ) = self.block_api.get_current_cycle_and_level()
        except ApiProviderException as a:
            logger.error(
                "Unable to fetch current cycle, {:s}. Exiting.".format(str(a)))
            self.exit()
            return

        # if initial_payment_cycle has the default value of -1 resulting in the last released cycle
        if self.initial_payment_cycle == -1:
            pymnt_cycle = (current_cycle -
                           (self.nw_config["NB_FREEZE_CYCLE"] + 1) -
                           self.release_override)
            if pymnt_cycle < 0:
                logger.error(
                    "Payment cycle cannot be < 0 but configuration results to {}"
                    .format(pymnt_cycle))
            else:
                logger.debug(
                    "Payment cycle is set to last released cycle {}".format(
                        pymnt_cycle))
        else:
            pymnt_cycle = self.initial_payment_cycle

        get_verbose_log_helper().reset(pymnt_cycle)

        while not self.exiting and self.life_cycle.is_running():

            # take a breath
            sleep(5)

            try:

                # Exit if disk is full
                # https://github.com/tezos-reward-distributor-organization/tezos-reward-distributor/issues/504
                if disk_is_full():
                    self.exit()
                    break

                # Check if local node is bootstrapped; sleep if needed; restart loop
                if not self.node_is_bootstrapped():
                    logger.info(
                        "Local node {} is not in sync with the Tezos network. Will sleep for {} blocks and check again."
                        .format(self.node_url, BOOTSTRAP_SLEEP))
                    self.wait_for_blocks(BOOTSTRAP_SLEEP)
                    continue

                # Local node is ready
                (
                    current_cycle,
                    current_level,
                ) = self.block_api.get_current_cycle_and_level()
                level_in_cycle = self.block_api.level_in_cycle(current_level)

                # create reports dir
                if self.calculations_dir and not os.path.exists(
                        self.calculations_dir):
                    os.makedirs(self.calculations_dir)

                logger.debug(
                    "Checking for pending payments: payment_cycle <= current_cycle - (self.nw_config['NB_FREEZE_CYCLE'] + 1) - self.release_override"
                )
                logger.info(
                    "Checking for pending payments: checking {} <= {} - ({} + 1) - {}"
                    .format(
                        pymnt_cycle,
                        current_cycle,
                        self.nw_config["NB_FREEZE_CYCLE"],
                        self.release_override,
                    ))

                # payments should not pass beyond last released reward cycle
                if (pymnt_cycle <= current_cycle -
                    (self.nw_config["NB_FREEZE_CYCLE"] + 1) -
                        self.release_override):
                    if not self.payments_queue.full():
                        if (not self.pay_denunciation_rewards
                            ) and self.reward_api.name == "RPC":
                            logger.info(
                                "Error: pay_denunciation_rewards=False requires an indexer since it is not possible to distinguish reward source using RPC"
                            )
                            e = "You must set 'pay_denunciation_rewards' to True when using RPC provider."
                            logger.error(e)
                            self.exit()
                            break

                        # Paying upcoming cycles (-R set to -11 )
                        if pymnt_cycle >= current_cycle:
                            logger.warn(
                                "Please note that you are doing payouts for future rewards!!! These rewards are not earned yet, they are an estimation."
                            )
                            logger.warn(
                                "TRD will attempt to adjust the amount after the cycle runs, but it may not work."
                            )

                        # Paying cycles with frozen rewards (-R set to -5 )
                        elif (pymnt_cycle >= current_cycle -
                              self.nw_config["NB_FREEZE_CYCLE"]):
                            logger.warn(
                                "Please note that you are doing payouts for frozen rewards!!!"
                            )

                        # If user wants to offset payments within a cycle, check here
                        if level_in_cycle < self.payment_offset:
                            wait_offset_blocks = self.payment_offset - level_in_cycle
                            wait_offset_minutes = (
                                wait_offset_blocks *
                                self.nw_config["MINIMAL_BLOCK_DELAY"]) / 60
                            logger.info(
                                "Current level within the cycle is {}; Requested offset is {}; Waiting for {} more blocks (~{} minutes)"
                                .format(
                                    level_in_cycle,
                                    self.payment_offset,
                                    wait_offset_blocks,
                                    wait_offset_minutes,
                                ))
                            self.wait_for_blocks(wait_offset_blocks)
                            continue  # Break/Repeat loop

                        else:
                            result = self.try_to_pay(
                                pymnt_cycle,
                                self.rewards_type,
                                self.nw_config,
                                current_cycle,
                            )

                        if result:
                            # single run is done. Do not continue.
                            if self.run_mode == RunMode.ONETIME:
                                logger.info(
                                    "Run mode ONETIME satisfied. Terminating..."
                                )
                                self.exit()
                                break
                            else:
                                pymnt_cycle = pymnt_cycle + 1
                                get_verbose_log_helper().reset(pymnt_cycle)

                    # end of queue size check
                    else:
                        logger.debug("Wait a few minutes, queue is full")
                        # wait a few minutes to let payments finish
                        sleep(60 * 3)

                # end of payment cycle check
                else:
                    logger.info(
                        "No pending payments for cycle {}, current cycle is {}"
                        .format(pymnt_cycle, current_cycle))

                    # pending payments done. Do not wait any more.
                    if self.run_mode == RunMode.PENDING:
                        logger.info(
                            "Run mode PENDING satisfied. Terminating...")
                        self.exit()
                        break

                    sleep(10)

                    # calculate number of blocks until end of current cycle plus user-defined offset
                    nb_blocks_remaining = (self.nw_config["BLOCKS_PER_CYCLE"] -
                                           level_in_cycle +
                                           self.payment_offset)
                    logger.debug(
                        "Waiting until next cycle; {} blocks remaining".format(
                            nb_blocks_remaining))

                    # wait until current cycle ends
                    self.wait_for_blocks(nb_blocks_remaining)

            except (ApiProviderException, ReadTimeout, ConnectTimeout) as e:
                logger.debug(
                    "{:s} error at payment producer loop: '{:s}'".format(
                        self.reward_api.name, str(e)),
                    exc_info=True,
                )
                logger.error(
                    "{:s} error at payment producer loop: '{:s}', will try again."
                    .format(self.reward_api.name, str(e)))

            except Exception as e:
                logger.debug(
                    "Unknown error in payment producer loop: {:s}".format(
                        str(e)),
                    exc_info=True,
                )
                logger.error(
                    "Unknown error in payment producer loop: {:s}, will try again."
                    .format(str(e)))

        # end of endless loop
        logger.debug("Producer returning...")

        # ensure consumer exits
        self.exit()

        return

    def stop(self):
        self.exit()
        self.event.set()

    def compute_rewards(self,
                        pymnt_cycle,
                        computation_type,
                        network_config,
                        adjustments={}):
        """
        Compute total rewards based on computation type and policy, then
        calls payment_call.calculate to calculate rewards per delegator.

        :param pymnt_cycle: the cycle for which rewards are being calculated
        :param computation_type: calculate estimated, actual or ideal rewards
        :param network_config: configuration of the current tezos network, needed to calc rewards.
        :param adjustments: a map of adjustments per address. We add to amount to compute adjusted_amount
        :return: rewards per delegator (reward logs) and total amount
        """
        # does calculation report file already exist for this cycle?

        logger.info("Computing rewards for cycle {:s}.".format(
            str(pymnt_cycle)))
        reward_model = self.reward_api.get_rewards_for_cycle_map(
            pymnt_cycle, computation_type)
        if computation_type.isEstimated():
            logger.info("Using estimated rewards for payouts calculations")
            block_reward = network_config["BLOCK_REWARD"]
            endorsement_reward = network_config["ENDORSEMENT_REWARD"]
            total_estimated_block_reward = reward_model.num_baking_rights * block_reward
            total_estimated_endorsement_reward = (
                reward_model.num_endorsing_rights * endorsement_reward)
            reward_model.computed_reward_amount = (
                total_estimated_block_reward +
                total_estimated_endorsement_reward)
        elif computation_type.isActual():
            logger.info("Using actual rewards for payouts calculations")
            if self.pay_denunciation_rewards:
                reward_model.computed_reward_amount = reward_model.total_reward_amount
            else:
                # omit denunciation rewards
                reward_model.computed_reward_amount = (
                    reward_model.rewards_and_fees -
                    reward_model.equivocation_losses)
        elif computation_type.isIdeal():
            logger.info("Using ideal rewards for payouts calculations")
            if self.pay_denunciation_rewards:
                reward_model.computed_reward_amount = (
                    reward_model.total_reward_amount +
                    reward_model.offline_losses)
            else:
                # omit denunciation rewards and double baking loss
                reward_model.computed_reward_amount = (
                    reward_model.rewards_and_fees +
                    reward_model.offline_losses)

        # 3- calculate rewards for delegators
        return self.payment_calc.calculate(reward_model, adjustments)

    def recompute_rewards(self, completed_cycle, computation_type,
                          network_config):
        """
        In case of early payout, the payout is already done when the cycle runs.
        After a cycle has run, we redo the computations. If we find overpayemnt
        (overestimate) or underpayment (negative overestimate), we record it in the
        calculation report csv file and return an adjustment map.

        :param completed_cycle: the cycle for which rewards are being recalculated
        :param computation_type: calculate estimated, actual or ideal rewards
        :param network_config: configuration of the current tezos network, needed to calc rewards.
        :return: adjustments map showing negative of overestimates per delegator
        """
        logger.info(
            "Checking for potential adjustment for recently completed cycle {:s}."
            .format(str(completed_cycle)))
        completed_cycle_report_file_path = get_calculation_report_file_path(
            self.calculations_dir, completed_cycle)
        adjustments = {}
        if os.path.isfile(completed_cycle_report_file_path):
            logger.info(
                "TRD ran for cycle: {:s}, calculating adjustments.".format(
                    str(completed_cycle)))
            (
                reward_logs_from_report,
                total_amount_from_report,
                rewards_type_from_report,
                _,
            ) = CsvCalculationFileParser().parse(
                completed_cycle_report_file_path, self.baking_address)
            # check that the overestimate has not been computed yet
            if sum([rl.overestimate or 0
                    for rl in reward_logs_from_report]) > 0:
                logger.info(
                    "Overestimate has already been calculated for cycle {:s}, not calculating it again."
                    .format(str(completed_cycle)))
                completed_cycle_reward_logs, completed_cycle_total_amount = (
                    reward_logs_from_report,
                    total_amount_from_report,
                )
            else:
                (
                    completed_cycle_reward_logs,
                    completed_cycle_total_amount,
                ) = self.compute_rewards(completed_cycle, computation_type,
                                         network_config)
            overestimate = int(total_amount_from_report -
                               completed_cycle_total_amount)
            logger.info(
                "We {:s}estimated payout for cycle {:s} by {:<,d} mutez, will attempt to adjust."
                .format(
                    ("over" if overestimate > 0 else "under"),
                    str(completed_cycle),
                    abs(overestimate),
                ))
            for rl in reward_logs_from_report:
                # overwrite only overestimate in report csv file, leave the rest alone
                rl.overestimate = int(
                    Decimal(rl.ratio * overestimate).to_integral_value(
                        rounding=ROUND_HALF_DOWN))
                # we adjust the cycle we are paying out with the overestimate of the
                # just completed cycle
                adjustments[rl.address] = rl.overestimate
                logger.debug(
                    f"Will try to recover {adjustments[rl.address]} mutez for {rl.address} based on past overpayment"
                )

            CsvCalculationFileParser().write(
                reward_logs_from_report,
                completed_cycle_report_file_path,
                total_amount_from_report,
                rewards_type_from_report,
                self.baking_address,
                False,
            )
        return adjustments

    def try_to_pay(self, pymnt_cycle, rewards_type, network_config,
                   current_cycle):
        try:
            logger.info("Payment cycle is {:s}".format(str(pymnt_cycle)))

            # 0- check for past payment evidence for current cycle
            past_payment_state = check_past_payment(self.payments_root,
                                                    pymnt_cycle)

            if past_payment_state:
                logger.warn(past_payment_state)
                return True

            adjustments = {}
            early_payout = False
            current_cycle_rewards_type = rewards_type
            # 1- adjust past cycle if necessary
            if self.release_override == -11 and pymnt_cycle >= current_cycle:
                early_payout = True
                completed_cycle = pymnt_cycle - 6
                adjustments = self.recompute_rewards(completed_cycle,
                                                     rewards_type,
                                                     network_config)
                # payout for current cycle will be estimated since we don't know actual rewards yet
                current_cycle_rewards_type = RewardsType.ESTIMATED

            # 2- get reward data and compute how to distribute them
            reward_logs, total_amount = self.compute_rewards(
                pymnt_cycle, current_cycle_rewards_type, network_config,
                adjustments)
            total_recovered_adjustments = int(
                sum([rl.adjustment for rl in reward_logs]))
            total_adjustments_to_recover = int(sum(adjustments.values()))
            if total_adjustments_to_recover > 0:
                logger.debug(
                    "Total adjustments to recover is {:<,d} mutez, total recovered adjustment is {:<,d} mutez."
                    .format(total_adjustments_to_recover,
                            total_recovered_adjustments))
                logger.info(
                    "After early payout of cycle {:s}, {:<,d} mutez were not recovered."
                    .format(
                        str(completed_cycle),
                        total_adjustments_to_recover +
                        total_recovered_adjustments,
                    ))

            # 3- create calculations report file. This file contains calculations details
            report_file_path = get_calculation_report_file_path(
                self.calculations_dir, pymnt_cycle)
            logger.debug("Creating calculation report (%s)", report_file_path)
            CsvCalculationFileParser().write(
                reward_logs,
                report_file_path,
                total_amount,
                current_cycle_rewards_type,
                self.baking_address,
                early_payout,
            )

            # 4- set cycle info
            for reward_log in reward_logs:
                reward_log.cycle = pymnt_cycle
            total_amount_to_pay = int(
                sum([
                    reward_log.adjusted_amount for reward_log in reward_logs
                    if reward_log.payable
                ]))

            # 5- if total_rewards > 0, proceed with payment
            if total_amount_to_pay > 0:

                self.payments_queue.put(
                    PaymentBatch(self, pymnt_cycle, reward_logs))

                sleep(5.0)

                # 6- processing of cycle is done
                logger.info(
                    "Reward creation is done for cycle {}, created {} rewards."
                    .format(pymnt_cycle, len(reward_logs)))

            elif total_amount_to_pay == 0:
                logger.info("Total payment amount is 0. Nothing to pay!")

        except ApiProviderException as a:
            logger.error("[try_to_pay] API provider error {:s}".format(str(a)))
            raise a from a
        except Exception as e:
            logger.error("[try_to_pay] Generic exception {:s}".format(str(e)))
            raise e from e

        # Either succeeded or raised exception
        return True

    def wait_for_blocks(self, nb_blocks_remaining):
        for x in range(nb_blocks_remaining):
            sleep(self.nw_config["MINIMAL_BLOCK_DELAY"])

            # if shutting down, exit
            if not self.life_cycle.is_running():
                self.exit()
                break

    def node_is_bootstrapped(self):
        # Get RPC node's (-A) bootstrap time. If bootstrap time + 2 minutes is
        # before local time, node is not bootstrapped.
        #
        # clnt_mngr is a super class of SimpleClientManager which interfaces
        # with the tezos-node used for txn forging/signing/injection. This is the
        # node which we need to determine bootstrapped state
        try:
            boot_time = self.client_manager.get_bootstrapped()
            utc_time = datetime.utcnow()
            if (boot_time + timedelta(minutes=2)) < utc_time:
                logger.debug(
                    "Current time is '{}', latest block of local node is '{}'."
                    .format(utc_time, boot_time))
                return False
        except ValueError:
            logger.error(
                "Unable to determine local node's bootstrap status. Continuing..."
            )
        return True

    @staticmethod
    def create_exit_payment():
        return RewardLog.ExitInstance()

    def notify_retry_fail_thread(self):
        self.retry_fail_event.set()

    # upon success retry failed payments if present
    # success may indicate what went wrong in past is fixed.
    def on_success(self, pymnt_batch):
        self.notify_retry_fail_thread()

    def on_fail(self, pymnt_batch):
        pass