Example #1
0
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
Example #2
0
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
Example #3
0
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
Example #4
0
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
Example #5
0
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
Example #6
0
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
Example #7
0
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
Example #8
0
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
Example #9
0
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