def test_corrupted_signature(network, args): node = network.find_node_by_role() # Test each supported curve for curve in infra.network.EllipticCurve: LOG.info(f"Testing curve: {curve.name}") # Add a member so we have at least one on this curve member = network.consortium.generate_and_add_new_member( node, curve=curve, ) r = member.ack(node) with node.client() as nc: nc.wait_for_commit(r) with node.client(*member.auth(write=True)) as mc: # Override the auth provider with invalid ones for fn in (missing_signature, empty_signature, modified_signature): # pylint: disable=protected-access mc.client_impl._auth_provider = make_signature_corrupter(fn) r = mc.post("/gov/proposals", '{"actions": []}') assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code # Remove the new member once we're done with them network.consortium.remove_member(node, member) return network
def test_corrupted_signature(network, args): node = network.find_node_by_role() # Test each supported curve for curve in infra.network.ParticipantsCurve: LOG.info(f"Testing curve: {curve.name}") # Add a member so we have at least one on this curve member = network.consortium.generate_and_add_new_member( node, curve=curve, ) with node.client(*member.auth(write=True)) as mc: # pylint: disable=protected-access # Cache the original auth provider original_auth = ccf.clients.RequestClient._auth_provider # Override the auth provider with invalid ones for fn in (missing_signature, empty_signature, modified_signature): ccf.clients.RequestClient._auth_provider = make_signature_corrupter( fn) r = mc.post("/gov/proposals") assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code # Restore original auth provider for future calls! ccf.clients.RequestClient._auth_provider = original_auth # Remove the new member once we're done with them network.consortium.retire_member(node, member) return network
def test_signed_escapes(network, args): if args.package == "liblogging": node = network.find_node_by_role() with node.client("user0", "user0") as c: escaped_query_tests(c, "signed_request_query") return network
def test_service_principals(network, args): node = network.find_node_by_role() principal_id = "0xdeadbeef" # Initially, there is nothing in this table latest_public_tables, _ = network.get_latest_ledger_public_state() assert "public:ccf.gov.service_principals" not in latest_public_tables # Create and accept a proposal which populates an entry in this table principal_data = {"name": "Bob", "roles": ["Fireman", "Zookeeper"]} proposal = { "actions": [{ "name": "set_service_principal", "args": { "id": principal_id, "data": principal_data }, }] } ballot = { "ballot": "export function vote(proposal, proposer_id) { return true; }" } proposal = network.consortium.get_any_active_member().propose( node, proposal) network.consortium.vote_using_majority(node, proposal, ballot) # Confirm it can be read latest_public_tables, _ = network.get_latest_ledger_public_state() assert (json.loads( latest_public_tables["public:ccf.gov.service_principals"][ principal_id.encode()]) == principal_data) # Create and accept a proposal which removes an entry from this table proposal = { "actions": [{ "name": "remove_service_principal", "args": { "id": principal_id } }] } proposal = network.consortium.get_any_active_member().propose( node, proposal) network.consortium.vote_using_majority(node, proposal, ballot) # Confirm it is gone latest_public_tables, _ = network.get_latest_ledger_public_state() assert (principal_id.encode() not in latest_public_tables["public:ccf.gov.service_principals"]) return network
def test_missing_signature_header(network, args): node = network.find_node_by_role() member = network.consortium.get_any_active_member() with node.client(member.local_id) as mc: r = mc.post("/gov/proposals") assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code www_auth = "www-authenticate" assert www_auth in r.headers, r.headers auth_header = r.headers[www_auth] assert auth_header.startswith("Signature"), auth_header elements = { e[0].strip(): e[1] for e in (element.split("=") for element in auth_header.split(",")) } assert "headers" in elements, elements required_headers = elements["headers"] assert required_headers.startswith('"'), required_headers assert required_headers.endswith('"'), required_headers assert "(request-target)" in required_headers, required_headers assert "digest" in required_headers, required_headers return network
def test_governance(network, args): node = network.find_node_by_role() primary, _ = network.find_primary() LOG.info("Original members can ACK") network.consortium.get_any_active_member().ack(node) LOG.info("Network can be opened again, with no effect") network.consortium.transition_service_to_open(node) LOG.info("Unknown proposal is rejected on completion") unkwown_proposal = {"actions": [{"name": "unknown_action"}]} try: proposal = network.consortium.get_any_active_member().propose( primary, unkwown_proposal) assert False, "Unknown proposal should fail on validation" except infra.proposal.ProposalNotCreated: pass LOG.info("Proposal to add a new member (with different curve)") ( new_member_proposal, new_member, careful_vote, ) = network.consortium.generate_and_propose_new_member( remote_node=node, curve=infra.network.EllipticCurve(args.participants_curve).next(), ) LOG.info("Check proposal has been recorded in open state") with primary.client( network.consortium.get_any_active_member().local_id) as c: r = c.get(f"/gov/proposals/{new_member_proposal.proposal_id}") assert r.status_code == 200, r.body.text() assert r.body.json( )["state"] == infra.proposal.ProposalState.OPEN.value LOG.info("Rest of consortium accept the proposal") network.consortium.vote_using_majority(node, new_member_proposal, careful_vote) assert new_member_proposal.state == ProposalState.ACCEPTED # Manually add new member to consortium network.consortium.members.append(new_member) LOG.debug( "Further vote requests fail as the proposal has already been accepted") params_error = http.HTTPStatus.BAD_REQUEST.value assert (network.consortium.get_member_by_local_id("member0").vote( node, new_member_proposal, careful_vote).status_code == params_error) assert (network.consortium.get_member_by_local_id("member1").vote( node, new_member_proposal, careful_vote).status_code == params_error) LOG.debug("Accepted proposal cannot be withdrawn") response = network.consortium.get_member_by_local_id( new_member_proposal.proposer_id).withdraw(node, new_member_proposal) assert response.status_code == params_error LOG.info("New non-active member should get insufficient rights response") current_recovery_thresold = network.consortium.recovery_threshold try: proposal_recovery_threshold, careful_vote = network.consortium.make_proposal( "set_recovery_threshold", recovery_threshold=current_recovery_thresold) new_member.propose(node, proposal_recovery_threshold) assert False, "New non-active member should get insufficient rights response" except infra.proposal.ProposalNotCreated as e: assert e.response.status_code == http.HTTPStatus.FORBIDDEN.value LOG.debug("New member ACK") new_member.ack(node) LOG.info("New member is now active and send a proposal") proposal = new_member.propose(node, proposal_recovery_threshold) proposal.vote_for = careful_vote LOG.debug("Members vote for proposal") network.consortium.vote_using_majority(node, proposal, careful_vote) assert proposal.state == infra.proposal.ProposalState.ACCEPTED LOG.info("New member makes a new proposal") proposal_recovery_threshold, careful_vote = network.consortium.make_proposal( "set_recovery_threshold", recovery_threshold=current_recovery_thresold) proposal = new_member.propose(node, proposal_recovery_threshold) LOG.debug( "Other members (non proposer) are unable to withdraw new proposal") response = network.consortium.get_member_by_local_id("member1").withdraw( node, proposal) assert response.status_code == http.HTTPStatus.FORBIDDEN.value LOG.debug("Proposer withdraws their proposal") response = new_member.withdraw(node, proposal) assert response.status_code == http.HTTPStatus.OK.value assert proposal.state == infra.proposal.ProposalState.WITHDRAWN with primary.client( network.consortium.get_any_active_member().local_id) as c: r = c.get(f"/gov/proposals/{proposal.proposal_id}") assert r.status_code == 200, r.body.text() assert r.body.json( )["state"] == infra.proposal.ProposalState.WITHDRAWN.value LOG.debug("Further withdraw proposals fail") response = new_member.withdraw(node, proposal) assert response.status_code == params_error LOG.debug("Further votes fail") response = new_member.vote(node, proposal, careful_vote) assert response.status_code == params_error
def test_governance(network, args): node = network.find_node_by_role() primary, _ = network.find_primary() LOG.info("Original members can ACK") network.consortium.get_any_active_member().ack(node) LOG.info("Network cannot be opened twice") try: network.consortium.open_network(node) except infra.proposal.ProposalNotAccepted as e: assert e.proposal.state == infra.proposal.ProposalState.Failed LOG.info("Proposal to add a new member (with different curve)") ( new_member_proposal, new_member, careful_vote, ) = network.consortium.generate_and_propose_new_member( remote_node=node, curve=infra.network.ParticipantsCurve(args.participants_curve).next(), ) LOG.info("Check proposal has been recorded in open state") proposals = network.consortium.get_proposals(primary) proposal_entry = next( (p for p in proposals if p.proposal_id == new_member_proposal.proposal_id), None, ) assert proposal_entry assert proposal_entry.state == ProposalState.Open LOG.info("Rest of consortium accept the proposal") network.consortium.vote_using_majority(node, new_member_proposal, careful_vote) assert new_member_proposal.state == ProposalState.Accepted # Manually add new member to consortium network.consortium.members.append(new_member) LOG.debug( "Further vote requests fail as the proposal has already been accepted") params_error = http.HTTPStatus.BAD_REQUEST.value assert (network.consortium.get_member_by_local_id("member0").vote( node, new_member_proposal, careful_vote).status_code == params_error) assert (network.consortium.get_member_by_local_id("member1").vote( node, new_member_proposal, careful_vote).status_code == params_error) LOG.debug("Accepted proposal cannot be withdrawn") response = network.consortium.get_member_by_local_id( new_member_proposal.proposer_id).withdraw(node, new_member_proposal) assert response.status_code == params_error LOG.info("New non-active member should get insufficient rights response") current_recovery_thresold = network.consortium.recovery_threshold try: ( proposal_recovery_threshold, careful_vote, ) = ccf.proposal_generator.set_recovery_threshold( current_recovery_thresold) new_member.propose(node, proposal_recovery_threshold) assert False, "New non-active member should get insufficient rights response" except infra.proposal.ProposalNotCreated as e: assert e.response.status_code == http.HTTPStatus.FORBIDDEN.value LOG.debug("New member ACK") new_member.ack(node) LOG.info("New member is now active and send a proposal") proposal = new_member.propose(node, proposal_recovery_threshold) proposal.vote_for = careful_vote LOG.debug("Members vote for proposal") network.consortium.vote_using_majority(node, proposal, careful_vote) assert proposal.state == infra.proposal.ProposalState.Accepted LOG.info("New member makes a new proposal") ( proposal_recovery_threshold, careful_vote, ) = ccf.proposal_generator.set_recovery_threshold( current_recovery_thresold) proposal = new_member.propose(node, proposal_recovery_threshold) LOG.debug( "Other members (non proposer) are unable to withdraw new proposal") response = network.consortium.get_member_by_local_id("member1").withdraw( node, proposal) assert response.status_code == http.HTTPStatus.FORBIDDEN.value LOG.debug("Proposer withdraws their proposal") response = new_member.withdraw(node, proposal) assert response.status_code == http.HTTPStatus.OK.value assert proposal.state == infra.proposal.ProposalState.Withdrawn proposals = network.consortium.get_proposals(primary) proposal_entry = next( (p for p in proposals if p.proposal_id == proposal.proposal_id), None, ) assert proposal_entry assert proposal_entry.state == ProposalState.Withdrawn LOG.debug("Further withdraw proposals fail") response = new_member.withdraw(node, proposal) assert response.status_code == params_error LOG.debug("Further votes fail") response = new_member.vote(node, proposal, careful_vote) assert response.status_code == params_error
def test_service_principals(network, args): node = network.find_node_by_role() principal_id = "0xdeadbeef" ballot = {"ballot": {"text": "return true"}} def read_service_principal(): with node.client("member0") as mc: return mc.post( "/gov/read", { "table": "public:gov.service_principals", "key": principal_id }, ) # Initially, there is nothing in this table r = read_service_principal() assert r.status_code == http.HTTPStatus.NOT_FOUND.value # Create and accept a proposal which populates an entry in this table principal_data = {"name": "Bob", "roles": ["Fireman", "Zookeeper"]} proposal = { "script": { "text": 'tables, args = ...\nreturn Calls:call("set_service_principal", args)' }, "parameter": { "id": principal_id, "data": principal_data, }, } proposal = network.consortium.get_any_active_member().propose( node, proposal) network.consortium.vote_using_majority(node, proposal, ballot) # Confirm it can be read r = read_service_principal() assert r.status_code == http.HTTPStatus.OK.value j = r.body.json() assert j == principal_data # Create and accept a proposal which removes an entry from this table proposal = { "script": { "text": 'tables, args = ...\nreturn Calls:call("remove_service_principal", args)' }, "parameter": { "id": principal_id, }, } proposal = network.consortium.get_any_active_member().propose( node, proposal) network.consortium.vote_using_majority(node, proposal, ballot) # Confirm it is gone r = read_service_principal() assert r.status_code == http.HTTPStatus.NOT_FOUND.value return network
def test_historical_query_range(network, args): id_a = 2 id_b = 3 id_c = 4 # NB: Because we submit from multiple concurrent threads, the actual pattern # on the ledger will not match this but will be interleaved. But the final # ratio of transactions will match this id_pattern = [id_a, id_a, id_a, id_b, id_b, id_c] n_entries = 30000 format_width = len(str(n_entries)) jwt_issuer = infra.jwt_issuer.JwtIssuer() jwt_issuer.register(network) jwt = jwt_issuer.issue_jwt() primary, _ = network.find_primary() # Submit many transactions, overwriting the same IDs LOG.info(f"Submitting {n_entries} entries") submissions_per_job = 1000 assigned = 0 fs = [] with futures.ThreadPoolExecutor() as executor: while assigned < n_entries: start = assigned end = min(n_entries, assigned + submissions_per_job) fs.append( executor.submit(submit_range, primary, id_pattern, start, end, format_width)) assigned = end results = [f.result() for f in fs] first_seqno = min(res[0] for res in results) view = max(res[1] for res in results) last_seqno = max(res[2] for res in results) with primary.client("user0") as c: infra.commit.wait_for_commit(c, seqno=last_seqno, view=view, timeout=3) LOG.info( f"Total ledger contains {last_seqno} entries, of which we expect our transactions to be spread over a range of ~{last_seqno - first_seqno} transactions" ) # Total fetch time depends on number of entries. We expect to be much faster than this, but # to set a safe timeout allow for a rate as low as 100 fetches per second timeout = n_entries / 100 # Ensure all nodes have reached committed state before querying a backup for historical state network.wait_for_all_nodes_to_commit(primary=primary) entries = {} node = network.find_node_by_role(role=infra.network.NodeRole.BACKUP, log_capture=[]) with node.client(common_headers={"authorization": f"Bearer {jwt}"}) as c: # Index is currently built lazily to avoid impacting other perf tests using the same app # So pre-fetch to ensure index is fully constructed get_all_entries(c, id_a, timeout=timeout) get_all_entries(c, id_b, timeout=timeout) get_all_entries(c, id_c, timeout=timeout) entries[id_a], duration_a = get_all_entries(c, id_a, timeout=timeout) entries[id_b], duration_b = get_all_entries(c, id_b, timeout=timeout) entries[id_c], duration_c = get_all_entries(c, id_c, timeout=timeout) c.get("/node/memory") id_a_fetch_rate = len(entries[id_a]) / duration_a id_b_fetch_rate = len(entries[id_b]) / duration_b id_c_fetch_rate = len(entries[id_c]) / duration_c average_fetch_rate = (id_a_fetch_rate + id_b_fetch_rate + id_c_fetch_rate) / 3 with cimetrics.upload.metrics(complete=False) as metrics: upload_name = "hist_sgx_cft^" LOG.debug(f"Uploading metric: {upload_name} = {average_fetch_rate}") metrics.put(upload_name, average_fetch_rate) # NB: The similar test in e2e_logging checks correctness, so we make no duplicate # assertions here return network