def test_verify_signature_mismatch_of_signatures(): state = pruning_state() fees_authenticator = FeesAuthNr(state, None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } # 1 2 3 4 4 # 12345678901234567890123456789012345678901234567890 address = '2JMyZgFBFyp5YMsBta4gCFA5TMdUzMXrWbRvMFkW7KDNbaMrk1' inputs = [{ADDRESS: address, SEQNO: 1}, {ADDRESS: address, SEQNO: 1}] outputs = [ { ADDRESS: address, AMOUNT: 4 }, { ADDRESS: address, AMOUNT: 5 }, ] signatures = [ '2z34R9vCyohVS2V7SXf8VnkHuAm8a224D6hopKJBMbdV4z8meR8aTdLMbMiknRXnKD4YkGtZnYY1D6jSQz23FziG' ] setattr(msg, "fees", [inputs, outputs, signatures]) with pytest.raises(InsufficientCorrectSignatures): fees_authenticator.verify_signature(msg)
def test_verify_signature_success(): state = pruning_state() fees_authenticator = FeesAuthNr(state, None) msg = FeeData() msg.signatures = { 'V4SGRU86Z58d6TV7PBUe6f': 'dYZZFV6Fk59bFaNFaKwXfY9AqP6cr3wqTSPjoRLoLcgAi28RNErweRXRskzZ4cwRyzBCpZyzewmSPdrQb1oES83' } # 1 2 3 4 4 # 12345678901234567890123456789012345678901234567890 inputs = [{ ADDRESS: '2gWVuNmy8rdJrDHzQ9PDDpHkfcypyUZRsSHaqA9q1K3Gti3YXw', SEQNO: 153 }] outputs = [{ ADDRESS: '2gWVuNmy8rdJrDHzQ9PDDpHkfcypyUZRsSHaqA9q1K3Gti3YXw', AMOUNT: 9 }] signatures = [ '5cEsNP3tfVLG5hdKfW5dCNVyM6gtoUAgGDsTUsikfNw6ZEJXme4v6KxZPP6wvniwg6FZbeTMtkcQ5TY4uf3ihWsG' ] setattr(msg, "fees", [inputs, outputs, signatures]) fees_authenticator.verify_signature(msg)
def test_verify_signature_incorrect_signatures(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } inputs = [{ ADDRESS: '2jS4PHWQJKcawRxdW6GVsjnZBa1ecGdCssn7KhWYJZGTXgL7Es', SEQNO: 2 }] outputs = [{ ADDRESS: '2jS4PHWQJKcawRxdW6GVsjnZBa1ecGdCssn7KhWYJZGTXgL7Es', AMOUNT: 10 }] signatures = [ '5wuXGeWyrM2xcp68rRsYEaegaguEJBBTDQioeSgDv5jMFeaeSLPAcMs4XwcxNXBwoUAUWgxSMN9WUnZ7ADctdPyQ' ] setattr(msg, "fees", [inputs, outputs, signatures]) with pytest.raises(InsufficientCorrectSignatures): fees_authenticator.verify_signature(PreSigVerification(msg))
def test_authenticate_success(node): state = node[0].getState(DOMAIN_LEDGER_ID) fees_authenticator = FeesAuthNr(state, None) req_data = { 'signatures': { 'E7QRhdcnhAwA6E46k9EtZo': '32H37GfuchojdbNeMxwNUhZJwWyJCz48aP5u1AvN3xhNramVqrq74H4pE8LKMgZw7rAdyrPvHUWzWAZdB243fqhA', 'CA4bVFDU4GLbX8xZju811o': '3tkZU65KeybmkUcrA6HuovVTDD8vsVm2VvB7bpUhPt2MLpez6eRrRvysUutusJz6xryCtk7g1b115pKhNGcqRTss', 'M9BJDuS24bqbJNvBRsoGg3': '4QXHdWTeWYRpoyCAZgpM7qA7Ms2MRYu6NpasPagPQRGoUukE2NdSXGonu6dWgsPNgybqW7fotw9BjccXAy7BzMsY', 'B8fV7naUqLATYocqu7yZ8W': '2bq55hTSBafovXS9gTNkW1GM9vVF6Y4s2fLEsmew9DaN95rmZ5ZwXj74NgTmGeGszPomWXPsRr5QGNZb6GsG57PV' }, 'reqId': 1524500821797147, 'operation': { 'fees': { '10001': 8, '1': 4 }, 'type': '20000' }, 'protocolVersion': 1 } value = fees_authenticator.authenticate(req_data) assert value is not None assert 4 == len(value)
def test_verify_signature_success(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) msg = FeeData() msg.signatures = { 'V4SGRU86Z58d6TV7PBUe6f': 'dYZZFV6Fk59bFaNFaKwXfY9AqP6cr3wqTSPjoRLoLcgAi28RNErweRXRskzZ4cwRyzBCpZyzewmSPdrQb1oES83' } # 1 2 3 4 4 # 12345678901234567890123456789012345678901234567890 inputs = [{ ADDRESS: '2gWVuNmy8rdJrDHzQ9PDDpHkfcypyUZRsSHaqA9q1K3Gti3YXw', SEQNO: 153 }] outputs = [{ ADDRESS: '2gWVuNmy8rdJrDHzQ9PDDpHkfcypyUZRsSHaqA9q1K3Gti3YXw', AMOUNT: 9 }] signatures = [ '5cEsNP3tfVLG5hdKfW5dCNVyM6gtoUAgGDsTUsikfNw6ZEJXme4v6KxZPP6wvniwg6FZbeTMtkcQ5TY4uf3ihWsG' ] setattr(msg, "fees", [inputs, outputs, signatures]) msg.payload_digest = msg.digest fees_authenticator.verify_signature(PreSigVerification(msg))
def test_verify_signature_invalid_signature_format(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].getState(DOMAIN_LEDGER_ID), None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } inputs = [{ ADDRESS: '2jS4PHWQJKcawRxdW6GVsjnZBa1ecGdCssn7KhWYJZGTXgL7Es', SEQNO: 2 }] outputs = [{ ADDRESS: '2jS4PHWQJKcawRxdW6GVsjnZBa1ecGdCssn7KhWYJZGTXgL7Es', AMOUNT: 10 }] signatures = [ '000kKoLEAP1YCYULYqxSNKvcYigGG1fHRMbZ6N1byFhaRut4P5RDF2KGR73ffgQoyzMHabrcTvrRGHhEfQ6ZdzxB' ] setattr(msg, "fees", [inputs, outputs, signatures]) with pytest.raises(InvalidSignatureFormat): fees_authenticator.verify_signature(PreSigVerification(msg))
def test_authenticate_errors_on_invalid_inputs(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) req_data = { 'signatures': { 'E7QRhdcnhAwA6E46k9EtZo': '32H37GfuchojdbNeMxwNUhZJwWyJCz48aP5u1AvN3xhNramVqrq74H4pE8LKMgZw7rAdyrPvHUWzWAZdB243fqhA', 'CA4bVFDU4GLbX8xZju811o': '3tkZU65KeybmkUcrA6HuovVTDD8vsVm2VvB7bpUhPt2MLpez6eRrRvysUutusJz6xryCtk7g1b115pKhNGcqRTss', 'M9BJDuS24bqbJNvBRsoGg3': '00XHdWTeWYRpoyCAZgpM7qA7Ms2MRYu6NpasPagPQRGoUukE2NdSXGonu6dWgsPNgybqW7fotw9BjccXAy7BzMsY', 'B8fV7naUqLATYocqu7yZ8W': '00q55hTSBafovXS9gTNkW1GM9vVF6Y4s2fLEsmew9DaN95rmZ5ZwXj74NgTmGeGszPomWXPsRr5QGNZb6GsG57PV' }, 'reqId': 1524500821797147, 'operation': { 'fees': { '10001': 8, '1': 4 }, 'type': '20000' }, 'protocolVersion': 1 } with pytest.raises(InvalidSignatureFormat): fees_authenticator.authenticate(req_data)
def test_authenticate_success_one_signature(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) req_data = { 'signatures': { 'E7QRhdcnhAwA6E46k9EtZo': '32H37GfuchojdbNeMxwNUhZJwWyJCz48aP5u1AvN3xhNramVqrq74H4pE8LKMgZw7rAdyrPvHUWzWAZdB243fqhA' }, 'reqId': 1524500821797147, 'operation': { 'fees': { '10001': 8, '1': 4 }, 'type': '20000' }, 'protocolVersion': 1 } with pytest.raises(InsufficientCorrectSignatures) as ex: fees_authenticator.authenticate(req_data) assert ex.value.args == (1, 0, req_data['signatures'])
def test_verify_signature_no_fees(): # should just run, no exceptions state = pruning_state() fees_authenticator = FeesAuthNr(state, None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } fees_authenticator.verify_signature(msg)
def test_verify_signature_no_fees(node): # should just run, no exceptions fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } fees_authenticator.verify_signature(PreSigVerification(msg))
def test_authenticate_invalid(): state = pruning_state() fees_authenticator = FeesAuthNr(state, None) req_data = { 'signatures': SIGNATURES, 'reqId': VALID_REQID, 'operation': { 'type': 'INVALID_TXN_TYPE', 'fees': { '1': 4, '10001': 8 } } } with pytest.raises(InvalidClientRequest): fees_authenticator.authenticate(req_data, VALID_IDENTIFIER)
def integrate_plugin_in_node(node): from sovtokenfees.client_authnr import FeesAuthNr from sovtokenfees.static_fee_req_handler import StaticFeesReqHandler from sovtokenfees.three_phase_commit_handling import \ ThreePhaseCommitHandler from sovtoken import TOKEN_LEDGER_ID from sovtoken.client_authnr import TokenAuthNr token_authnr = node.clientAuthNr.get_authnr_by_type(TokenAuthNr) if not token_authnr: raise ImportError('sovtoken plugin should be loaded, ' # noqa 'authenticator not found') token_req_handler = node.get_req_handler(ledger_id=TOKEN_LEDGER_ID) if not token_req_handler: raise ImportError('sovtoken plugin should be loaded, request ' # noqa 'handler not found') # `handle_xfer_public_txn` in `TokenReqHandler` checks if the sum of inputs match # exactly the sum of outputs. Since the check to match inputs and outputs is done # during fees handling the check is avoided in `TokenReqHandler` by monkeypatching # `handle_xfer_public_txn` to do nothing. token_req_handler.handle_xfer_public_txn = lambda _: None token_ledger = token_req_handler.ledger token_state = token_req_handler.state utxo_cache = token_req_handler.utxo_cache fees_authnr = FeesAuthNr(node.getState(DOMAIN_LEDGER_ID), token_authnr) fees_req_handler = StaticFeesReqHandler(node.configLedger, node.getState(CONFIG_LEDGER_ID), token_ledger, token_state, utxo_cache, node.getState(DOMAIN_LEDGER_ID), node.bls_bft.bls_store, token_req_handler.tracker) node.clientAuthNr.register_authenticator(fees_authnr) node.register_req_handler(fees_req_handler, CONFIG_LEDGER_ID) node.register_hook(NodeHooks.PRE_SIG_VERIFICATION, fees_authnr.verify_signature) node.register_hook(NodeHooks.PRE_DYNAMIC_VALIDATION, fees_req_handler.can_pay_fees) node.register_hook(NodeHooks.POST_REQUEST_APPLICATION, fees_req_handler.deduct_fees) node.register_hook(NodeHooks.POST_REQUEST_COMMIT, fees_req_handler.commit_fee_txns) node.register_hook(NodeHooks.POST_BATCH_CREATED, fees_req_handler.post_batch_created) node.register_hook(NodeHooks.POST_BATCH_REJECTED, fees_req_handler.post_batch_rejected) node.register_hook(NodeHooks.POST_BATCH_COMMITTED, fees_req_handler.post_batch_committed) three_pc_handler = ThreePhaseCommitHandler(node.master_replica, token_ledger, token_state, fees_req_handler) node.master_replica.register_hook(ReplicaHooks.CREATE_PPR, three_pc_handler.add_to_pre_prepare) node.master_replica.register_hook(ReplicaHooks.CREATE_PR, three_pc_handler.add_to_prepare) node.master_replica.register_hook(ReplicaHooks.CREATE_ORD, three_pc_handler.add_to_ordered) node.master_replica.register_hook(ReplicaHooks.APPLY_PPR, three_pc_handler.check_recvd_pre_prepare) return node
def test_authenticate_invalid(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) req_data = { 'signatures': SIGNATURES, 'reqId': VALID_REQID, 'operation': { 'type': 'INVALID_TXN_TYPE', 'fees': { '1': 4, '10001': 8 } } } with pytest.raises(InvalidClientRequest): fees_authenticator.authenticate(req_data, VALID_IDENTIFIER)
def register_authentication(node): utxo_cache = node.db_manager.get_store(UTXO_CACHE_LABEL) token_authnr = node.clientAuthNr.get_authnr_by_type(TokenAuthNr) if not token_authnr: raise ImportError('sovtoken plugin should be loaded, ' # noqa 'authenticator not found') fees_authnr = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node.db_manager.idr_cache, token_authnr) node.clientAuthNr.register_authenticator(fees_authnr) fees_authorizer = FeesAuthorizer(config_state=node.getState(CONFIG_LEDGER_ID), utxo_cache=utxo_cache) node.write_req_validator.register_authorizer(fees_authorizer) return fees_authnr
def test_authenticate_success_one_signature(node): state = node[0].getState(DOMAIN_LEDGER_ID) fees_authenticator = FeesAuthNr(state, None) req_data = { 'signatures': { 'E7QRhdcnhAwA6E46k9EtZo': '32H37GfuchojdbNeMxwNUhZJwWyJCz48aP5u1AvN3xhNramVqrq74H4pE8LKMgZw7rAdyrPvHUWzWAZdB243fqhA' }, 'reqId': 1524500821797147, 'operation': { 'fees': { '10001': 8, '1': 4 }, 'type': '20000' }, 'protocolVersion': 1 } value = fees_authenticator.authenticate(req_data) assert value is not None assert 1 == len(value)
def test_authenticate_success_one_signature(node): state = node[0].getState(DOMAIN_LEDGER_ID) fees_authenticator = FeesAuthNr(state, None) req_data = { 'signatures': { 'E7QRhdcnhAwA6E46k9EtZo': '32H37GfuchojdbNeMxwNUhZJwWyJCz48aP5u1AvN3xhNramVqrq74H4pE8LKMgZw7rAdyrPvHUWzWAZdB243fqhA' }, 'reqId': 1524500821797147, 'operation': { 'fees': { '10001': 8, '1': 4 }, 'type': '20000' }, 'protocolVersion': 1 } with pytest.raises(InsufficientCorrectSignatures) as ex: fees_authenticator.authenticate(req_data) assert ex.value.args == (0, 1)
def test_verify_signature_sequence_order_wrong(node): fees_authenticator = FeesAuthNr(ACCEPTABLE_WRITE_TYPES_FEE, ACCEPTABLE_QUERY_TYPES_FEE, ACCEPTABLE_ACTION_TYPES_FEE, node[0].db_manager.idr_cache, None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } # 1 2 3 4 4 # 12345678901234567890123456789012345678901234567890 address = '2JMyZgFBFyp5YMsBta4gCFA5TMdUzMXrWbRvMFkW7KDNbaMrk1' inputs = [{ADDRESS: address, SEQNO: 2}] outputs = [{ADDRESS: address, AMOUNT: 9}] signatures = [ '2z34R9vCyohVS2V7SXf8VnkHuAm8a224D6hopKJBMbdV4z8meR8aTdLMbMiknRXnKD4YkGtZnYY1D6jSQz23FziG' ] setattr(msg, "fees", [inputs, outputs, signatures]) with pytest.raises(InsufficientCorrectSignatures): fees_authenticator.verify_signature(PreSigVerification(msg))
def test_verify_signature_incorrect_signatures(): state = pruning_state() fees_authenticator = FeesAuthNr(state, None) msg = FeeData() msg.signatures = { 'MSjKTWkPLtYoPEaTF1TUDb': '61PUc8K8aAkhmCjWLstxwRREBAJKbVMRuGiUXxSo1tiRwXgUfVT4TY47NJtbQymcDW3paXPWNqqD4cziJjoPQSSX' } inputs = [{ ADDRESS: '2jS4PHWQJKcawRxdW6GVsjnZBa1ecGdCssn7KhWYJZGTXgL7Es', SEQNO: 2 }] outputs = [{ ADDRESS: '2jS4PHWQJKcawRxdW6GVsjnZBa1ecGdCssn7KhWYJZGTXgL7Es', AMOUNT: 10 }] signatures = [ '5wuXGeWyrM2xcp68rRsYEaegaguEJBBTDQioeSgDv5jMFeaeSLPAcMs4XwcxNXBwoUAUWgxSMN9WUnZ7ADctdPyQ' ] setattr(msg, "fees", [inputs, outputs, signatures]) with pytest.raises(InsufficientCorrectSignatures): fees_authenticator.verify_signature(msg)
def integrate_plugin_in_node(node): from sovtokenfees.client_authnr import FeesAuthNr from sovtokenfees.static_fee_req_handler import StaticFeesReqHandler from sovtokenfees.three_phase_commit_handling import \ ThreePhaseCommitHandler from sovtoken import TOKEN_LEDGER_ID from sovtoken.client_authnr import TokenAuthNr def postCatchupCompleteClb(origin_clb): if origin_clb: origin_clb() fees_req_handler.postCatchupCompleteClbk() node.write_req_validator.auth_map.update(sovtokenfees_auth_map) token_authnr = node.clientAuthNr.get_authnr_by_type(TokenAuthNr) if not token_authnr: raise ImportError('sovtoken plugin should be loaded, ' # noqa 'authenticator not found') token_req_handler = node.get_req_handler(ledger_id=TOKEN_LEDGER_ID) if not token_req_handler: raise ImportError('sovtoken plugin should be loaded, request ' # noqa 'handler not found') # `handle_xfer_public_txn` in `TokenReqHandler` checks if the sum of inputs match # exactly the sum of outputs. Since the check to match inputs and outputs is done # during fees handling the check is avoided in `TokenReqHandler` by monkeypatching # `handle_xfer_public_txn` to do nothing. token_req_handler.handle_xfer_public_txn = lambda _: None token_ledger = token_req_handler.ledger token_state = token_req_handler.state utxo_cache = token_req_handler.utxo_cache fees_authnr = FeesAuthNr(node.getState(DOMAIN_LEDGER_ID), token_authnr) fees_req_handler = StaticFeesReqHandler( node.configLedger, node.getState(CONFIG_LEDGER_ID), token_ledger, token_state, utxo_cache, node.getState(DOMAIN_LEDGER_ID), node.bls_bft.bls_store, node, node.write_req_validator, ts_store=node.getStateTsDbStorage()) origin_token_clb = node.ledgerManager.ledgerRegistry[ TOKEN_LEDGER_ID].postCatchupCompleteClbk node.ledgerManager.ledgerRegistry[TOKEN_LEDGER_ID].postCatchupCompleteClbk = \ functools.partial(postCatchupCompleteClb, origin_token_clb) origin_token_post_added_clb = node.ledgerManager.ledgerRegistry[ TOKEN_LEDGER_ID].postTxnAddedToLedgerClbk def filter_fees(ledger_id: int, txn: Any): origin_token_post_added_clb( ledger_id, txn, get_type(txn) != FeesTransactions.FEES.value) node.ledgerManager.ledgerRegistry[ TOKEN_LEDGER_ID].postTxnAddedToLedgerClbk = filter_fees node.clientAuthNr.register_authenticator(fees_authnr) node_config_req_handler = node.get_req_handler(ledger_id=CONFIG_LEDGER_ID) node.unregister_req_handler(node_config_req_handler, CONFIG_LEDGER_ID) node.register_req_handler(fees_req_handler, CONFIG_LEDGER_ID) fees_authorizer = FeesAuthorizer( config_state=node.getState(CONFIG_LEDGER_ID), utxo_cache=utxo_cache) node.write_req_validator.register_authorizer(fees_authorizer) node.register_hook(NodeHooks.PRE_SIG_VERIFICATION, fees_authnr.verify_signature) node.register_hook(NodeHooks.POST_REQUEST_APPLICATION, fees_req_handler.deduct_fees) node.register_hook(NodeHooks.POST_REQUEST_COMMIT, fees_req_handler.commit_fee_txns) node.register_hook(NodeHooks.POST_BATCH_CREATED, fees_req_handler.post_batch_created) node.register_hook(NodeHooks.POST_BATCH_REJECTED, fees_req_handler.post_batch_rejected) node.register_hook(NodeHooks.POST_BATCH_COMMITTED, fees_req_handler.post_batch_committed) node.register_hook(NodeHooks.POST_NODE_STOPPED, token_req_handler.on_node_stopping) three_pc_handler = ThreePhaseCommitHandler(node.master_replica, token_ledger, token_state, fees_req_handler) node.master_replica.register_hook(ReplicaHooks.CREATE_PPR, three_pc_handler.add_to_pre_prepare) node.master_replica.register_hook(ReplicaHooks.CREATE_PR, three_pc_handler.add_to_prepare) node.master_replica.register_hook(ReplicaHooks.CREATE_ORD, three_pc_handler.add_to_ordered) node.master_replica.register_hook(ReplicaHooks.APPLY_PPR, three_pc_handler.check_recvd_pre_prepare) return node