diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4f2ebc4306..cd91a06b73f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0] + +### Added + +- The service certificate is now returned as part of the `/node/network/` endpoint response. + +### Removed + +- `/gov/query` and `/gov/read` governance endpoints are removed. + ## [0.99.0] This is a bridging release to simplify the upgrade to 1.0. It includes the new JS constitution, but also supports the existing Lua governance so that users can upgrade in 2 steps - first implementing all of the changes below with their existing Lua governance, then upgrading to the JS governance. Lua governance will be removed in CCF 1.0. See [temporary docs](https://microsoft.github.io/CCF/ccf-0.99.0/governance/js_gov.html) for help with transitioning from Lua to JS. diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index d5c851f1b646..715ebd73b7d7 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -161,21 +161,6 @@ ], "type": "object" }, - "KVRead__In": { - "properties": { - "key": { - "$ref": "#/components/schemas/json" - }, - "table": { - "$ref": "#/components/schemas/string" - } - }, - "required": [ - "table", - "key" - ], - "type": "object" - }, "Proposal": { "properties": { "actions": { @@ -912,68 +897,6 @@ ] } }, - "/query": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Script" - } - } - }, - "description": "Auto-generated request body schema" - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/json" - } - } - }, - "description": "Default response description" - } - }, - "security": [ - { - "member_signature": [] - } - ] - } - }, - "/read": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KVRead__In" - } - } - }, - "description": "Auto-generated request body schema" - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/json" - } - } - }, - "description": "Default response description" - } - }, - "security": [ - { - "member_signature": [] - } - ] - } - }, "/receipt": { "get": { "parameters": [ diff --git a/doc/schemas/node_openapi.json b/doc/schemas/node_openapi.json index a7de64bb5124..76e602ccfe15 100644 --- a/doc/schemas/node_openapi.json +++ b/doc/schemas/node_openapi.json @@ -111,12 +111,16 @@ "primary_id": { "$ref": "#/components/schemas/EntityId" }, + "service_certificate": { + "$ref": "#/components/schemas/Pem" + }, "service_status": { "$ref": "#/components/schemas/ServiceStatus" } }, "required": [ "service_status", + "service_certificate", "current_view", "primary_id" ], @@ -252,6 +256,9 @@ ], "type": "string" }, + "Pem": { + "type": "string" + }, "Quote": { "properties": { "endorsements": { diff --git a/python/ccf/ledger.py b/python/ccf/ledger.py index a7eefb26a363..e5613166fa7e 100644 --- a/python/ccf/ledger.py +++ b/python/ccf/ledger.py @@ -5,7 +5,7 @@ import struct import os -from typing import BinaryIO, NamedTuple, Optional +from typing import BinaryIO, NamedTuple, Optional, Tuple, Dict import json import base64 @@ -31,6 +31,9 @@ SIGNATURE_TX_TABLE_NAME = "public:ccf.internal.signatures" NODES_TABLE_NAME = "public:ccf.gov.nodes.info" +# Key used by CCF to record single-key tables +WELL_KNOWN_SINGLETON_TABLE_KEY = bytes(bytearray(8)) + def to_uint_32(buffer): return struct.unpack("@I", buffer)[0] @@ -411,18 +414,15 @@ def __iter__(self): def __next__(self): if self._next_offset == self._file_size: raise StopIteration() - try: - self._complete_read() - self._read_header() - # Adds every transaction to the ledger validator - # LedgerValidator does verification for every added transaction and throws when it finds any anomaly. - self._ledger_validator.add_transaction(self) + self._complete_read() + self._read_header() - return self - except Exception as exception: - LOG.exception(f"Encountered exception: {exception}") - raise + # Adds every transaction to the ledger validator + # LedgerValidator does verification for every added transaction and throws when it finds any anomaly. + self._ledger_validator.add_transaction(self) + + return self class LedgerChunk: @@ -466,10 +466,14 @@ class Ledger: _current_chunk: LedgerChunk _ledger_validator: LedgerValidator + def _reset_iterators(self): + self._fileindex = -1 + # Initialize LedgerValidator instance which will be passed to LedgerChunks. + self._ledger_validator = LedgerValidator() + def __init__(self, directory: str): self._filenames = [] - self._fileindex = -1 ledgers = os.listdir(directory) # Sorts the list based off the first number after ledger_ so that @@ -485,8 +489,7 @@ def __init__(self, directory: str): if os.path.isfile(os.path.join(directory, chunk)): self._filenames.append(os.path.join(directory, chunk)) - # Initialize LedgerValidator instance which will be passed to LedgerChunks. - self._ledger_validator = LedgerValidator() + self._reset_iterators() def __next__(self) -> LedgerChunk: self._fileindex += 1 @@ -513,6 +516,70 @@ def last_verified_txid(self): self._ledger_validator.last_verified_seqno, ) + def get_transaction(self, seqno: int) -> Transaction: + """ + Returns the :py:class:`ccf.Ledger.Transaction` recorded in the ledger at the given sequence number. + + Note that the transaction returned may not yet be verified by a + signature transaction nor committed by the service. + + :param int seqno: Sequence number of the transaction to fetch. + + :return: :py:class:`ccf.Ledger.Transaction` + """ + if seqno < 1: + raise ValueError("Ledger first seqno is 1") + + self._reset_iterators() + + transaction = None + try: + # Note: This is slower than it really needs to as this will walk through + # all transactions from the start of the ledger. + for chunk in self: + for tx in chunk: + public_transaction = tx.get_public_domain() + if public_transaction.get_seqno() == seqno: + return tx + finally: + self._reset_iterators() + + if transaction is None: + raise UnknownTransaction( + f"Transaction at seqno {seqno} does not exist in ledger" + ) + return transaction + + def get_latest_public_state(self) -> Tuple[dict, int]: + """ + Returns the current public state of the service. + + Note that the public state returned may not yet be verified by a + signature transaction nor committed by the service. + + :return: Tuple[Dict, int]: Tuple containing a dictionary of public tables and their values and the seqno of the state read from the ledger. + """ + self._reset_iterators() + + public_tables: Dict[str, Dict] = {} + latest_seqno = 0 + for chunk in self: + for tx in chunk: + latest_seqno = tx.get_public_domain().get_seqno() + for table_name, records in tx.get_public_domain().get_tables().items(): + if table_name in public_tables: + public_tables[table_name].update(records) + # Remove deleted keys + public_tables[table_name] = { + k: v + for k, v in public_tables[table_name].items() + if v is not None + } + else: + public_tables[table_name] = records + + return public_tables, latest_seqno + class InvalidRootException(Exception): """MerkleTree root doesn't match with the root reported in the signature's table""" @@ -528,3 +595,7 @@ class CommitIdRangeException(Exception): class UntrustedNodeException(Exception): """The signing node wasn't part of the network""" + + +class UnknownTransaction(Exception): + """The transaction at seqno does not exist in ledger""" diff --git a/python/ledger_tutorial.py b/python/ledger_tutorial.py index 55a9c76f1ce9..fdcd2411a947 100644 --- a/python/ledger_tutorial.py +++ b/python/ledger_tutorial.py @@ -4,6 +4,7 @@ import sys from loguru import logger as LOG import json +import random # Note: It is safer to run the ledger tutorial when the service has stopped # as all ledger files will have been written to. @@ -44,3 +45,16 @@ # In this case, the target table 'public:ccf.gov.nodes.info' is raw bytes to JSON. LOG.info(f"{key.decode()} : {json.loads(value)}") # SNIPPET_END: iterate_over_ledger + +# Read state of ledger +latest_state, latest_seqno = ledger.get_latest_public_state() + +seqnos = [1, 2, 3, latest_seqno // 2, latest_seqno] +random.shuffle(seqnos) +for seqno in seqnos: + transaction = ledger.get_transaction(seqno) + +# Confirm latest state can still be accessed, and is unchanged +latest_state1, latest_seqno1 = ledger.get_latest_public_state() +assert latest_seqno == latest_seqno1 +assert latest_state == latest_state1 diff --git a/src/crypto/pem.h b/src/crypto/pem.h index b2565c449f41..59b932eca65e 100644 --- a/src/crypto/pem.h +++ b/src/crypto/pem.h @@ -116,6 +116,16 @@ namespace crypto fmt::format("Unable to parse pem from this JSON: {}", j.dump())); } } + + inline std::string schema_name(const Pem&) + { + return "Pem"; + } + + inline void fill_json_schema(nlohmann::json& schema, const Pem&) + { + schema["type"] = "string"; + } } namespace std diff --git a/src/node/constitution.h b/src/node/constitution.h index 1f58df30b0c7..d19fb0ff0c2a 100644 --- a/src/node/constitution.h +++ b/src/node/constitution.h @@ -2,7 +2,9 @@ // Licensed under the Apache 2.0 License. #pragma once +#include "service_map.h" + namespace ccf { - using Constitution = kv::JsonSerialisedMap; + using Constitution = ServiceMap; } \ No newline at end of file diff --git a/src/node/rpc/call_types.h b/src/node/rpc/call_types.h index bd07c098334a..a8527b9f0a34 100644 --- a/src/node/rpc/call_types.h +++ b/src/node/rpc/call_types.h @@ -54,6 +54,7 @@ namespace ccf struct Out { ServiceStatus service_status; + crypto::Pem service_certificate; std::optional current_view; std::optional primary_id; }; diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 4a1b23dd93af..9a6777f3bc31 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -1010,12 +1010,6 @@ namespace ccf return check_member_status(tx, id, {MemberStatus::ACTIVE}); } - bool check_member_accepted(kv::ReadOnlyTx& tx, const MemberId& id) - { - return check_member_status( - tx, id, {MemberStatus::ACTIVE, MemberStatus::ACCEPTED}); - } - bool check_member_status( kv::ReadOnlyTx& tx, const MemberId& id, @@ -1114,85 +1108,6 @@ namespace ccf const AuthnPolicies member_cert_or_sig = {member_cert_auth_policy, member_signature_auth_policy}; - auto read = [this](auto& ctx, nlohmann::json&& params) { - const auto member_id = get_caller_member_id(ctx); - if (!member_id.has_value()) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is unknown."); - } - - if (!check_member_status( - ctx.tx, - member_id.value(), - {MemberStatus::ACTIVE, MemberStatus::ACCEPTED})) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active or accepted."); - } - - const auto in = params.get(); - - const ccf::Script read_script(R"xxx( - local tables, table_name, key = ... - return tables[table_name]:get(key) or {} - )xxx"); - - auto value = tsr.run( - ctx.tx, - {read_script, {}, WlIds::MEMBER_CAN_READ, {}}, - in.table, - in.key); - if (value.empty()) - { - return make_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::KeyNotFound, - fmt::format( - "Key {} does not exist in table {}.", in.key.dump(), in.table)); - } - - return make_success(value); - }; - make_endpoint("read", HTTP_POST, json_adapter(read), member_cert_or_sig) - // This can be executed locally, but can't currently take ReadOnlyTx due - // to restrictions in our lua wrappers - .set_forwarding_required(endpoints::ForwardingRequired::Sometimes) - .set_auto_schema() - .install(); - - auto query = [this](auto& ctx, nlohmann::json&& params) { - const auto member_id = get_caller_member_id(ctx); - if (!member_id.has_value()) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is unknown."); - } - if (!check_member_accepted(ctx.tx, member_id.value())) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not accepted."); - } - - const auto script = params.get(); - return make_success(tsr.run( - ctx.tx, {script, {}, WlIds::MEMBER_CAN_READ, {}})); - }; - make_endpoint("query", HTTP_POST, json_adapter(query), member_cert_or_sig) - // This can be executed locally, but can't currently take ReadOnlyTx due - // to restrictions in our lua wrappers - .set_forwarding_required(endpoints::ForwardingRequired::Sometimes) - .set_auto_schema() - .install(); - auto propose = [this](auto& ctx, nlohmann::json&& params) { const auto& caller_identity = ctx.template get_caller(); diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index a914f8f2a1bb..813f3f6a41b0 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -429,7 +429,9 @@ namespace ccf auto service_state = service->get(0); if (service_state.has_value()) { - out.service_status = service_state.value().status; + const auto& service_value = service_state.value(); + out.service_status = service_value.status; + out.service_certificate = service_value.cert; if (consensus != nullptr) { out.current_view = consensus->get_view(); diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index c86f4b253b6f..0217be0ce196 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -70,7 +70,11 @@ namespace ccf DECLARE_JSON_TYPE(GetNetworkInfo::Out) DECLARE_JSON_REQUIRED_FIELDS( - GetNetworkInfo::Out, service_status, current_view, primary_id) + GetNetworkInfo::Out, + service_status, + service_certificate, + current_view, + primary_id) DECLARE_JSON_TYPE(GetNode::NodeInfo) DECLARE_JSON_REQUIRED_FIELDS( diff --git a/src/node/rpc/test/frontend_test_infra.h b/src/node/rpc/test/frontend_test_infra.h index 8e509cb3651b..03a683bd3f39 100644 --- a/src/node/rpc/test/frontend_test_infra.h +++ b/src/node/rpc/test/frontend_test_infra.h @@ -128,26 +128,6 @@ std::vector create_signed_request( return r.build_request(); } -template -auto query_params(T script, bool compile) -{ - json params; - if (compile) - params["bytecode"] = lua::compile(script); - else - params["text"] = script; - return params; -} - -template -auto read_params(const T& key, const string& table_name) -{ - json params; - params["key"] = key; - params["table"] = table_name; - return params; -} - auto frontend_process( MemberRpcFrontend& frontend, const std::vector& serialized_request, diff --git a/src/node/rpc/test/member_voting_test.cpp b/src/node/rpc/test/member_voting_test.cpp index 3af028789f61..32c7849a1c74 100644 --- a/src/node/rpc/test/member_voting_test.cpp +++ b/src/node/rpc/test/member_voting_test.cpp @@ -2,126 +2,6 @@ // Licensed under the Apache 2.0 License. #include "node/rpc/test/frontend_test_infra.h" -DOCTEST_TEST_CASE("Member query/read") -{ - // initialize the network state - NetworkState network; - auto gen_tx = network.tables->create_tx(); - GenesisGenerator gen(network, gen_tx); - gen.init_values(); - gen.create_service({}); - ShareManager share_manager(network); - StubNodeContext context; - MemberRpcFrontend frontend(network, context, share_manager); - frontend.open(); - const auto member_id = gen.add_member(member_cert); - DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - - const enclave::SessionContext member_session( - enclave::InvalidSessionId, member_cert.raw()); - - // put value to read - constexpr auto key = 123; - constexpr auto value = 456; - auto tx = network.tables->create_tx(); - tx.rw(network.values)->put(key, value); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - static constexpr auto query = R"xxx( - local tables = ... - return tables["public:ccf.internal.values"]:get(123) - )xxx"; - - DOCTEST_SUBCASE("Query: bytecode/script allowed access") - { - // set member ACL so that the VALUES table is accessible - auto tx = network.tables->create_tx(); - tx.rw(network.whitelists)->put(WlIds::MEMBER_CAN_READ, {Tables::VALUES}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - bool compile = true; - do - { - const auto req = create_request(query_params(query, compile), "query"); - const auto r = frontend_process(frontend, req, member_cert); - const auto result = parse_response_body(r); - DOCTEST_CHECK(result == value); - compile = !compile; - } while (!compile); - } - - DOCTEST_SUBCASE("Query: table not in ACL") - { - // set member ACL so that no table is accessible - auto tx = network.tables->create_tx(); - tx.rw(network.whitelists)->put(WlIds::MEMBER_CAN_READ, {}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - auto req = create_request(query_params(query, true), "query"); - const auto response = frontend_process(frontend, req, member_cert); - - check_error(response, HTTP_STATUS_INTERNAL_SERVER_ERROR); - } - - DOCTEST_SUBCASE("Read: allowed access, key exists") - { - auto tx = network.tables->create_tx(); - tx.rw(network.whitelists)->put(WlIds::MEMBER_CAN_READ, {Tables::VALUES}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - auto read_call = - create_request(read_params(key, Tables::VALUES), "read"); - const auto r = frontend_process(frontend, read_call, member_cert); - const auto result = parse_response_body(r); - DOCTEST_CHECK(result == value); - } - - DOCTEST_SUBCASE("Read: allowed access, key doesn't exist") - { - constexpr auto wrong_key = 321; - auto tx = network.tables->create_tx(); - tx.rw(network.whitelists)->put(WlIds::MEMBER_CAN_READ, {Tables::VALUES}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - auto read_call = - create_request(read_params(wrong_key, Tables::VALUES), "read"); - const auto response = frontend_process(frontend, read_call, member_cert); - - check_error(response, HTTP_STATUS_NOT_FOUND); - } - - DOCTEST_SUBCASE("Read: access not allowed") - { - auto tx = network.tables->create_tx(); - tx.rw(network.whitelists)->put(WlIds::MEMBER_CAN_READ, {}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - auto read_call = - create_request(read_params(key, Tables::VALUES), "read"); - const auto response = frontend_process(frontend, read_call, member_cert); - - check_error(response, HTTP_STATUS_INTERNAL_SERVER_ERROR); - } - - DOCTEST_SUBCASE("Read: member is removed") - { - auto gen_tx = network.tables->create_tx(); - GenesisGenerator gen(network, gen_tx); - gen.remove_member(member_id); - DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - - auto tx = network.tables->create_tx(); - tx.rw(network.whitelists)->put(WlIds::MEMBER_CAN_READ, {Tables::VALUES}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - - auto read_call = - create_request(read_params(key, Tables::VALUES), "read"); - const auto response = frontend_process(frontend, read_call, member_cert); - - check_error(response, HTTP_STATUS_UNAUTHORIZED); - } -} - DOCTEST_TEST_CASE("Proposer ballot") { NetworkState network; @@ -312,238 +192,6 @@ struct TestNewMember crypto::Pem cert; }; -DOCTEST_TEST_CASE("Add new members until there are 7 then reject") -{ - logger::config::level() = logger::INFO; - - constexpr auto initial_members = 3; - constexpr auto n_new_members = 7; - constexpr auto max_members = 8; - NetworkState network; - network.ledger_secrets = std::make_shared(); - network.ledger_secrets->init(); - init_network(network); - auto gen_tx = network.tables->create_tx(); - GenesisGenerator gen(network, gen_tx); - gen.init_values(); - gen.create_service({}); - gen.init_configuration({1}); - ShareManager share_manager(network); - StubNodeContext context; - // add three initial active members - // the proposer - auto proposer_id = gen.add_member({member_cert, dummy_enc_pubk}); - gen.activate_member(proposer_id); - - // the voters - const auto voter_a_cert = get_cert(1, kp); - auto voter_a = gen.add_member({voter_a_cert, dummy_enc_pubk}); - gen.activate_member(voter_a); - const auto voter_b_cert = get_cert(2, kp); - auto voter_b = gen.add_member({voter_b_cert, dummy_enc_pubk}); - gen.activate_member(voter_b); - - set_whitelists(gen); - gen.set_gov_scripts(lua::Interpreter().invoke(gov_script_file)); - gen.open_service(); - DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - MemberRpcFrontend frontend(network, context, share_manager); - frontend.open(); - - vector new_members(n_new_members); - - auto i = 0ul; - for (auto& new_member : new_members) - { - new_member.local_id = initial_members + i++; - - // new member certificate - auto cert_pem = new_member.kp->self_sign( - fmt::format("CN=new member{}", new_member.local_id)); - auto encryption_pub_key = dummy_enc_pubk; - auto cert_der = crypto::make_verifier(cert_pem)->cert_der(); - new_member.service_id = crypto::Sha256Hash(cert_der).hex_str(); - new_member.cert = cert_pem; - - // check new_member id does not work before member is added - const auto read_next_req = create_request( - read_params(new_member.service_id, Tables::MEMBER_ACKS), "read"); - const auto r = frontend_process(frontend, read_next_req, new_member.cert); - check_error(r, HTTP_STATUS_UNAUTHORIZED); - - // propose new member, as proposer - Propose::In proposal; - proposal.script = std::string(R"xxx( - tables, member_info = ... - return Calls:call("new_member", member_info) - )xxx"); - proposal.parameter["cert"] = cert_pem; - proposal.parameter["encryption_pub_key"] = dummy_enc_pubk; - - const auto propose = - create_signed_request(proposal, "proposals", kp, member_cert); - - ProposalId proposal_id; - { - const auto r = frontend_process(frontend, propose, member_cert); - const auto result = parse_response_body(r); - - // the proposal should be accepted, but not succeed immediately - proposal_id = result.proposal_id; - DOCTEST_CHECK(result.state == ProposalState::OPEN); - } - - { - // vote for own proposal - Script vote_yes("return true"); - const auto vote = create_signed_request( - Vote{vote_yes}, - fmt::format("proposals/{}/votes", proposal_id), - kp, - member_cert); - const auto r = frontend_process(frontend, vote, member_cert); - const auto result = parse_response_body(r); - DOCTEST_CHECK(result.state == ProposalState::OPEN); - } - - // read initial proposal, as second member - const Proposal initial_read = - get_proposal(frontend, proposal_id, voter_a_cert); - DOCTEST_CHECK(initial_read.proposer == proposer_id); - DOCTEST_CHECK(initial_read.script == proposal.script); - DOCTEST_CHECK(initial_read.parameter == proposal.parameter); - - // vote as second member - Script vote_ballot(fmt::format( - R"xxx( - local tables, calls = ... - local n = 0 - tables["public:ccf.gov.members.info"]:foreach( function(k, v) n = n + 1 end ) - if n < {} then - return true - else - return false - end - )xxx", - max_members)); - - const auto vote = create_signed_request( - Vote{vote_ballot}, - fmt::format("proposals/{}/votes", proposal_id), - kp, - voter_a_cert); - - { - const auto r = frontend_process(frontend, vote, voter_a_cert); - const auto result = parse_response_body(r); - - if (new_member.local_id < max_members) - { - // vote should succeed - DOCTEST_CHECK(result.state == ProposalState::ACCEPTED); - // check that member with the new new_member cert can make RPCs now - auto r = frontend_process(frontend, read_next_req, new_member.cert); - DOCTEST_CHECK(r.status == HTTP_STATUS_OK); - - // successful proposals are removed from the kv, so we can't confirm - // their final state - } - else - { - // vote should not succeed - DOCTEST_CHECK(result.state == ProposalState::OPEN); - // check that member with the new new_member cert can make RPCs now - check_error( - frontend_process(frontend, read_next_req, new_member.cert), - HTTP_STATUS_UNAUTHORIZED); - - // re-read proposal, as second member - const Proposal final_read = - get_proposal(frontend, proposal_id, voter_a_cert); - DOCTEST_CHECK(final_read.proposer == proposer_id); - DOCTEST_CHECK(final_read.script == proposal.script); - DOCTEST_CHECK(final_read.parameter == proposal.parameter); - - const auto my_vote = final_read.votes.find(voter_a); - DOCTEST_CHECK(my_vote != final_read.votes.end()); - DOCTEST_CHECK(my_vote->second == vote_ballot); - } - } - } - - DOCTEST_SUBCASE("ACK from newly added members") - { - // iterate over all new_members, except for the last one - for (auto new_member = new_members.cbegin(); new_member != - new_members.cend() - (initial_members + n_new_members - max_members); - new_member++) - { - // (1) read ack entry - const auto read_state_digest_req = create_request( - read_params(new_member->service_id, Tables::MEMBER_ACKS), "read"); - const auto ack0 = parse_response_body( - frontend_process(frontend, read_state_digest_req, new_member->cert)); - DOCTEST_REQUIRE(std::all_of( - ack0.state_digest.begin(), ack0.state_digest.end(), [](uint8_t i) { - return i == 0; - })); - - { - // make sure that there is a signature in the signatures table since - // ack's depend on that - auto tx = network.tables->create_tx(); - auto signatures = tx.rw(network.signatures); - PrimarySignature sig_value; - signatures->put(0, sig_value); - DOCTEST_REQUIRE(tx.commit() == kv::CommitResult::SUCCESS); - } - - // (2) ask for a fresher digest of state - const auto freshen_state_digest_req = - create_request(nullptr, "ack/update_state_digest"); - const auto freshen_state_digest = parse_response_body( - frontend_process(frontend, freshen_state_digest_req, new_member->cert)); - DOCTEST_CHECK(freshen_state_digest.state_digest != ack0.state_digest); - - // (3) read ack entry again and check that the state digest has changed - const auto ack1 = parse_response_body( - frontend_process(frontend, read_state_digest_req, new_member->cert)); - DOCTEST_CHECK(ack0.state_digest != ack1.state_digest); - DOCTEST_CHECK(freshen_state_digest.state_digest == ack1.state_digest); - - // (4) sign stale state and send it - StateDigest params; - params.state_digest = ack0.state_digest; - const auto send_stale_sig_req = - create_signed_request(params, "ack", new_member->kp, new_member->cert); - check_error( - frontend_process(frontend, send_stale_sig_req, new_member->cert), - HTTP_STATUS_BAD_REQUEST); - - // (5) sign new state digest and send it - params.state_digest = ack1.state_digest; - const auto send_good_sig_req = - create_signed_request(params, "ack", new_member->kp, new_member->cert); - const auto good_response = - frontend_process(frontend, send_good_sig_req, new_member->cert); - DOCTEST_CHECK(good_response.status == HTTP_STATUS_NO_CONTENT); - - // (6) read own member information - const auto read_cert_req = create_request( - read_params(new_member->service_id, Tables::MEMBER_CERTS), "read"); - const auto cert = parse_response_body( - frontend_process(frontend, read_cert_req, new_member->cert)); - DOCTEST_CHECK(cert == new_member->cert); - - const auto read_status_req = create_request( - read_params(new_member->service_id, Tables::MEMBER_INFO), "read"); - const auto mi = parse_response_body( - frontend_process(frontend, read_status_req, new_member->cert)); - DOCTEST_CHECK(mi.status == MemberStatus::ACTIVE); - } - } -} - DOCTEST_TEST_CASE("Accept node") { NetworkState network; @@ -581,12 +229,11 @@ DOCTEST_TEST_CASE("Accept node") // check node exists with status pending { - auto read_values = - create_request(read_params(node_id, Tables::NODES), "read"); - const auto r = parse_response_body( - frontend_process(frontend, read_values, member_0_cert)); - - DOCTEST_CHECK(r.status == NodeStatus::PENDING); + auto tx = network.tables->create_tx(); + auto nodes = tx.ro(network.nodes); + auto node = nodes->get(node_id); + DOCTEST_CHECK(node.has_value()); + DOCTEST_CHECK(node->status == NodeStatus::PENDING); } // m0 proposes adding new node @@ -634,13 +281,13 @@ DOCTEST_TEST_CASE("Accept node") frontend_process(frontend, vote, member_1_cert), ProposalState::ACCEPTED); } - // check node exists with status pending + // check node exists with status trusted { - const auto read_values = - create_request(read_params(node_id, Tables::NODES), "read"); - const auto r = parse_response_body( - frontend_process(frontend, read_values, member_0_cert)); - DOCTEST_CHECK(r.status == NodeStatus::TRUSTED); + auto tx = network.tables->create_tx(); + auto nodes = tx.ro(network.nodes); + auto node = nodes->get(node_id); + DOCTEST_CHECK(node.has_value()); + DOCTEST_CHECK(node->status == NodeStatus::TRUSTED); } // m0 proposes retire node @@ -686,11 +333,11 @@ DOCTEST_TEST_CASE("Accept node") // check that node exists with status retired { - auto read_values = - create_request(read_params(node_id, Tables::NODES), "read"); - const auto r = parse_response_body( - frontend_process(frontend, read_values, member_0_cert)); - DOCTEST_CHECK(r.status == NodeStatus::RETIRED); + auto tx = network.tables->create_tx(); + auto nodes = tx.ro(network.nodes); + auto node = nodes->get(node_id); + DOCTEST_CHECK(node.has_value()); + DOCTEST_CHECK(node->status == NodeStatus::RETIRED); } // check that retired node cannot be trusted @@ -1361,13 +1008,11 @@ DOCTEST_TEST_CASE("Passing operator change" * doctest::test_suite("operator")) const ccf::Script vote_against("return false"); { - DOCTEST_INFO("Check node exists with status pending"); - auto read_values = - create_request(read_params(node_id, Tables::NODES), "read"); - const auto r = parse_response_body( - frontend_process(frontend, read_values, operator_cert)); - - DOCTEST_CHECK(r.status == NodeStatus::PENDING); + auto tx = network.tables->create_tx(); + auto nodes = tx.ro(network.nodes); + auto node = nodes->get(node_id); + DOCTEST_CHECK(node.has_value()); + DOCTEST_CHECK(node->status == NodeStatus::PENDING); } { @@ -1550,11 +1195,11 @@ DOCTEST_TEST_CASE( { DOCTEST_INFO("Check node exists with status pending"); - const auto read_values = - create_request(read_params(node_id, Tables::NODES), "read"); - const auto r = parse_response_body( - frontend_process(frontend, read_values, proposer_cert)); - DOCTEST_CHECK(r.status == NodeStatus::PENDING); + auto tx = network.tables->create_tx(); + auto nodes = tx.ro(network.nodes); + auto node = nodes->get(node_id); + DOCTEST_CHECK(node.has_value()); + DOCTEST_CHECK(node->status == NodeStatus::PENDING); } { @@ -1656,21 +1301,18 @@ DOCTEST_TEST_CASE("User data") frontend.open(); ccf::UserId user_id; - std::vector read_user_info; DOCTEST_SUBCASE("No initial user data") { user_id = gen.add_user({user_cert}); DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - read_user_info = - create_request(read_params(user_id, Tables::USER_INFO), "read"); - { DOCTEST_INFO("user data is not initially set"); - check_error( - frontend_process(frontend, read_user_info, member_cert), - HTTP_STATUS_NOT_FOUND); + auto tx = network.tables->create_tx(); + auto users = tx.ro(network.user_info); + auto user = users->get(user_id); + DOCTEST_CHECK(!user.has_value()); } } @@ -1680,14 +1322,13 @@ DOCTEST_TEST_CASE("User data") user_id = gen.add_user({user_cert, user_data_string}); DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - read_user_info = - create_request(read_params(user_id, Tables::USER_INFO), "read"); - { DOCTEST_INFO("initial user data object can be read"); - const auto read_response = parse_response_body( - frontend_process(frontend, read_user_info, member_cert)); - DOCTEST_CHECK(read_response.user_data == user_data_string); + auto tx = network.tables->create_tx(); + auto users = tx.ro(network.user_info); + auto user = users->get(user_id); + DOCTEST_CHECK(user.has_value()); + DOCTEST_CHECK(user->user_data == user_data_string); } } @@ -1726,9 +1367,12 @@ DOCTEST_TEST_CASE("User data") } DOCTEST_INFO("user data object can be read"); - const auto read_response = parse_response_body( - frontend_process(frontend, read_user_info, member_cert)); - DOCTEST_CHECK(read_response.user_data == user_data_object); + + auto tx = network.tables->create_tx(); + auto users = tx.ro(network.user_info); + auto user = users->get(user_id); + DOCTEST_CHECK(user.has_value()); + DOCTEST_CHECK(user->user_data == user_data_object); } { @@ -1762,9 +1406,11 @@ DOCTEST_TEST_CASE("User data") } DOCTEST_INFO("user data object can be read"); - const auto response = parse_response_body( - frontend_process(frontend, read_user_info, member_cert)); - DOCTEST_CHECK(response.user_data == user_data_string); + auto tx = network.tables->create_tx(); + auto users = tx.ro(network.user_info); + auto user = users->get(user_id); + DOCTEST_CHECK(user.has_value()); + DOCTEST_CHECK(user->user_data == user_data_string); } } diff --git a/tests/ca_certs.py b/tests/ca_certs.py index 04edaf6b002d..ba53c303130b 100644 --- a/tests/ca_certs.py +++ b/tests/ca_certs.py @@ -2,7 +2,6 @@ # Licensed under the Apache 2.0 License. import os import tempfile -import http import infra.network import infra.path import infra.proc @@ -10,6 +9,7 @@ import infra.e2e_args import suite.test_requirements as reqs import ccf.proposal_generator +import json from loguru import logger as LOG @@ -21,6 +21,7 @@ def test_cert_store(network, args): primary, _ = network.find_nodes() cert_name = "mycert" + raw_cert_name = cert_name.encode() LOG.info("Member builds a ca cert update proposal with malformed cert") with tempfile.NamedTemporaryFile("w") as f: @@ -55,29 +56,29 @@ def test_cert_store(network, args): cert_pem_fp.write(cert_pem) cert_pem_fp.write(cert2_pem) cert_pem_fp.flush() - network.consortium.set_ca_cert_bundle(primary, cert_name, cert_pem_fp.name) + set_proposal = network.consortium.set_ca_cert_bundle( + primary, cert_name, cert_pem_fp.name + ) - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", - {"table": "public:ccf.gov.tls.ca_cert_bundles", "key": cert_name}, + stored_cert = json.loads( + primary.get_ledger_public_state_at(set_proposal.completed_seqno)[ + "public:ccf.gov.tls.ca_cert_bundles" + ][raw_cert_name] ) - assert r.status_code == http.HTTPStatus.OK.value, r.status_code cert_ref = cert_pem + cert2_pem - cert_kv = r.body.json() assert ( - cert_ref == cert_kv - ), f"stored cert not equal to input certs: {cert_ref} != {cert_kv}" + cert_ref == stored_cert + ), f"input certs not equal to stored cert: {cert_ref} != {stored_cert}" LOG.info("Member removes a ca cert") - network.consortium.remove_ca_cert_bundle(primary, cert_name) - - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", - {"table": "public:ccf.gov.tls.ca_cert_bundles", "key": cert_name}, - ) - assert r.status_code == http.HTTPStatus.NOT_FOUND.value, r.status_code + remove_proposal = network.consortium.remove_ca_cert_bundle(primary, cert_name) + + assert ( + primary.get_ledger_public_state_at(remove_proposal.completed_seqno)[ + "public:ccf.gov.tls.ca_cert_bundles" + ][raw_cert_name] + == None + ), "CA bundle was not removed" return network diff --git a/tests/governance.py b/tests/governance.py index ea6b49a7dbb3..89aa5a4f6ced 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -12,6 +12,7 @@ import infra.e2e_args import suite.test_requirements as reqs import infra.logging_app as app +import json from loguru import logger as LOG @@ -113,23 +114,22 @@ def test_no_quote(network, args): def test_member_data(network, args): assert args.initial_operator_count > 0 primary, _ = network.find_nodes() - with primary.client("member0") as mc: - - def member_info(mid): - return mc.post( - "/gov/read", {"table": "public:ccf.gov.members.info", "key": mid} - ).body.json() - - md_count = 0 - for member in network.get_members(): - if member.member_data: - assert ( - member_info(member.service_id)["member_data"] == member.member_data - ) - md_count += 1 - else: - assert "member_data" not in member_info(member.service_id) - assert md_count == args.initial_operator_count + + latest_public_tables, _ = primary.get_latest_ledger_public_state() + members_info = latest_public_tables["public:ccf.gov.members.info"] + + md_count = 0 + for member in network.get_members(): + stored_member_info = json.loads(members_info[member.service_id.encode()]) + if member.member_data: + assert ( + stored_member_info["member_data"] == member.member_data + ), f'stored member data "{stored_member_info["member_data"]}" != expected "{member.member_data} "' + md_count += 1 + else: + assert "member_data" not in stored_member_info + + assert md_count == args.initial_operator_count return network @@ -154,16 +154,9 @@ def test_service_principals(network, args): principal_id = "0xdeadbeef" - def read_service_principal(): - with node.client("member0") as mc: - return mc.post( - "/gov/read", - {"table": "public:ccf.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 + latest_public_tables, _ = node.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"]} @@ -194,10 +187,15 @@ def read_service_principal(): 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 + latest_public_tables, _ = node.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 if os.getenv("JS_GOVERNANCE"): @@ -219,9 +217,11 @@ def read_service_principal(): 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 - + latest_public_tables, _ = node.get_latest_ledger_public_state() + assert ( + principal_id.encode() + not in latest_public_tables["public:ccf.gov.service_principals"] + ) return network diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 785ac861d325..ca3b1f189ac5 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -14,6 +14,7 @@ import infra.crypto import infra.member import ccf.proposal_generator +import ccf.ledger from infra.proposal import ProposalState from loguru import logger as LOG @@ -75,46 +76,36 @@ def __init__( f"Successfully recovered member {local_id}: {new_member.service_id}" ) + # Retrieve state of service directly from ledger + latest_public_state, _ = remote_node.get_latest_ledger_public_state() + self.recovery_threshold = json.loads( + latest_public_state["public:ccf.gov.service.config"][ + ccf.ledger.WELL_KNOWN_SINGLETON_TABLE_KEY + ] + )["recovery_threshold"] + if not self.members: LOG.warning("No consortium member to recover") return - with remote_node.client(self.members[0].local_id) as c: - r = c.post( - "/gov/query", - { - "text": """tables = ... - members = {} - tables["public:ccf.gov.members.info"]:foreach(function(service_id, info) - table.insert(members, {service_id, info}) - end) - return members - """ - }, - ) - for member_service_id, info in r.body.json(): - status = info["status"] - member = self.get_member_by_service_id(member_service_id) - if member: - if ( - infra.member.MemberStatus(status) - == infra.member.MemberStatus.ACTIVE - ): - member.set_active() - else: - LOG.warning( - f"Keys and certificates for consortium member {member_service_id} do not exist locally" - ) - - r = c.post( - "/gov/query", - { - "text": """tables = ... - return tables["public:ccf.gov.service.config"]:get(0) - """ - }, - ) - self.recovery_threshold = r.body.json()["recovery_threshold"] + for id_bytes, info_bytes in latest_public_state[ + "public:ccf.gov.members.info" + ].items(): + member_id = id_bytes.decode() + member_info = json.loads(info_bytes) + + status = member_info["status"] + member = self.get_member_by_service_id(member_id) + if member: + if ( + infra.member.MemberStatus(status) + == infra.member.MemberStatus.ACTIVE + ): + member.set_active() + else: + LOG.warning( + f"Keys and certificates for consortium member {member_id} do not exist locally" + ) def set_authenticate_session(self, flag): self.authenticate_session = flag @@ -271,34 +262,12 @@ def vote_using_majority( view = response.view ccf.commit.wait_for_commit(c, seqno, view, timeout=timeout) - if proposal.state != ProposalState.ACCEPTED: + if proposal.state == ProposalState.ACCEPTED: + proposal.set_completed(seqno, view) + else: raise infra.proposal.ProposalNotAccepted(proposal) - return proposal - def get_proposals(self, remote_node): - script = """ - tables = ... - local proposals = {} - tables["public:ccf.gov.proposals"]:foreach( function(k, v) - proposals[tostring(k)] = v; - end ) - return proposals; - """ - - proposals = [] - member = self.get_any_active_member() - with remote_node.client(*member.auth()) as c: - r = c.post("/gov/query", {"text": script}) - assert r.status_code == http.HTTPStatus.OK.value - for proposal_id, attr in r.body.json().items(): - proposals.append( - infra.proposal.Proposal( - proposal_id=proposal_id, - proposer_id=attr["proposer"], - state=infra.proposal.ProposalState(attr["state"]), - ) - ) - return proposals + return proposal def retire_node(self, remote_node, node_to_retire): LOG.info(f"Retiring node {node_to_retire.local_node_id}") @@ -313,12 +282,8 @@ def retire_node(self, remote_node, node_to_retire): proposal = self.get_any_active_member().propose(remote_node, proposal_body) self.vote_using_majority(remote_node, proposal, careful_vote) - member = self.get_any_active_member() - with remote_node.client(*member.auth(write=True)) as c: - r = c.post( - "/gov/read", - {"table": "public:ccf.gov.nodes.info", "key": node_to_retire.node_id}, - ) + with remote_node.client() as c: + r = c.get(f"/node/network/nodes/{node_to_retire.node_id}") assert r.body.json()["status"] == infra.node.NodeStatus.RETIRED.value def trust_node(self, remote_node, node_id, timeout=3): @@ -413,6 +378,11 @@ def set_js_app(self, remote_node, app_bundle_path): # Large apps take a long time to process - wait longer than normal for commit return self.vote_using_majority(remote_node, proposal, careful_vote, timeout=10) + def remove_js_app(self, remote_node): + proposal_body, careful_vote = ccf.proposal_generator.remove_js_app() + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + def set_jwt_issuer(self, remote_node, json_path): proposal_body, careful_vote = self.make_proposal("set_jwt_issuer", json_path) proposal = self.get_any_active_member().propose(remote_node, proposal_body) @@ -521,39 +491,13 @@ def retire_code(self, remote_node, code_id): def check_for_service(self, remote_node, status): """ - Check via the member frontend of the given node that the certificate - associated with current CCF service signing key has been recorded in + Check the certificate associated with current CCF service signing key has been recorded in the KV store with the appropriate status. """ - # When opening the service in BFT, the first transaction to be - # completed when f = 1 takes a significant amount of time - member = self.get_any_active_member() - with remote_node.client(*member.auth()) as c: - r = c.post( - "/gov/query", - { - "text": """tables = ... - service = tables["public:ccf.gov.service.info"]:get(0) - if service == nil then - LOG_DEBUG("Service is nil") - else - LOG_DEBUG("Service version: ", tostring(service.version)) - LOG_DEBUG("Service status: ", tostring(service.status_code)) - cert_len = #service.cert - LOG_DEBUG("Service cert len: ", tostring(cert_len)) - LOG_DEBUG("Service cert bytes: " .. - tostring(service.cert[math.ceil(cert_len / 4)]) .. " " .. - tostring(service.cert[math.ceil(cert_len / 3)]) .. " " .. - tostring(service.cert[math.ceil(cert_len / 2)]) - ) - end - return service - """ - }, - timeout=3, - ) - current_status = r.body.json()["status"] - current_cert = r.body.json()["cert"] + with remote_node.client() as c: + r = c.get("/node/network") + current_status = r.body.json()["service_status"] + current_cert = r.body.json()["service_certificate"] expected_cert = open( os.path.join(self.common_dir, "networkcert.pem"), "rb" @@ -567,11 +511,8 @@ def check_for_service(self, remote_node, status): ), f"Service status {current_status} (expected {status.value})" def _check_node_exists(self, remote_node, node_id, node_status=None): - member = self.get_any_active_member() - with remote_node.client(*member.auth()) as c: - r = c.post( - "/gov/read", {"table": "public:ccf.gov.nodes.info", "key": node_id} - ) + with remote_node.client() as c: + r = c.get(f"/node/network/nodes/{node_id}") if r.status_code != http.HTTPStatus.OK.value or ( node_status and r.body.json()["status"] != node_status.value diff --git a/tests/infra/node.py b/tests/infra/node.py index ab0edf13270f..c9517cb82224 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -8,9 +8,11 @@ import infra.net import infra.path import ccf.clients +import ccf.ledger import os import socket import re +import time from loguru import logger as LOG @@ -316,6 +318,33 @@ def wait_for_node_to_join(self, timeout=3): f"Node {self.local_node_id} failed to join the network" ) from e + def get_ledger_public_state_at(self, seqno, timeout=3): + end_time = time.time() + timeout + while time.time() < end_time: + try: + ledger = ccf.ledger.Ledger(self.remote.ledger_path()) + tx = ledger.get_transaction(seqno) + return tx.get_public_domain().get_tables() + except Exception: + time.sleep(0.1) + + raise TimeoutError( + f"Could not read transaction at seqno {seqno} from ledger {self.remote.ledger_path()}" + ) + + def get_latest_ledger_public_state(self, timeout=3): + end_time = time.time() + timeout + while time.time() < end_time: + try: + ledger = ccf.ledger.Ledger(self.remote.ledger_path()) + return ledger.get_latest_public_state() + except Exception: + time.sleep(0.1) + + raise TimeoutError( + f"Could not read latest state from ledger {self.remote.ledger_path()}" + ) + def get_ledger(self, include_read_only_dirs=False): """ Triage committed and un-committed (i.e. current) ledger files diff --git a/tests/infra/proposal.py b/tests/infra/proposal.py index 10d13df65303..fd98b26e8259 100644 --- a/tests/infra/proposal.py +++ b/tests/infra/proposal.py @@ -41,5 +41,12 @@ def __init__( self.view = view self.seqno = seqno + self.completed_view = view if state == ProposalState.ACCEPTED else None + self.completed_seqno = seqno if state == ProposalState.ACCEPTED else None + + def set_completed(self, seqno, view): + self.completed_seqno = seqno + self.completed_view = view + def increment_votes_for(self): self.votes_for += 1 diff --git a/tests/js-modules/modules.py b/tests/js-modules/modules.py index 4692852f3699..307a4541d995 100644 --- a/tests/js-modules/modules.py +++ b/tests/js-modules/modules.py @@ -15,7 +15,6 @@ import infra.e2e_args import infra.crypto import suite.test_requirements as reqs -import ccf.proposal_generator import openapi_spec_validator from loguru import logger as LOG @@ -62,16 +61,21 @@ def test_app_bundle(network, args): # Testing the bundle archive support of the Python client here. # Plain bundle folders are tested in the npm-based app tests. bundle_dir = os.path.join(PARENT_DIR, "js-app-bundle") + raw_module_name = "/math.js".encode() with tempfile.TemporaryDirectory(prefix="ccf") as tmp_dir: bundle_path = shutil.make_archive( os.path.join(tmp_dir, "bundle"), "zip", bundle_dir ) - network.consortium.set_js_app(primary, bundle_path) + set_js_proposal = network.consortium.set_js_app(primary, bundle_path) - LOG.info("Verifying that modules and endpoints were added") - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post("/gov/read", {"table": "public:ccf.gov.modules", "key": "/math.js"}) - assert r.status_code == http.HTTPStatus.OK, r.status_code + assert ( + raw_module_name + in primary.get_ledger_public_state_at(set_js_proposal.completed_seqno)[ + "public:ccf.gov.modules" + ] + ), "Module was not added" + + LOG.info("Verifying that app was deployed") with primary.client("user0") as c: valid_body = {"op": "sub", "left": 82, "right": 40} @@ -89,20 +93,19 @@ def test_app_bundle(network, args): validate_openapi(c) LOG.info("Removing js app") - proposal_body, careful_vote = ccf.proposal_generator.remove_js_app() - proposal = network.consortium.get_any_active_member().propose( - primary, proposal_body - ) - network.consortium.vote_using_majority(primary, proposal, careful_vote) + remove_js_proposal = network.consortium.remove_js_app(primary) LOG.info("Verifying that modules and endpoints were removed") with primary.client("user0") as c: r = c.post("/app/compute", valid_body) assert r.status_code == http.HTTPStatus.NOT_FOUND, r.status_code - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post("/gov/read", {"table": "public:ccf.gov.modules", "key": "/math.js"}) - assert r.status_code == http.HTTPStatus.NOT_FOUND, r.status_code + assert ( + primary.get_ledger_public_state_at(remove_js_proposal.completed_seqno)[ + "public:ccf.gov.modules" + ][raw_module_name] + is None + ), "Module was not removed" return network diff --git a/tests/jwt_test.py b/tests/jwt_test.py index 72b376645287..7b6ad3fb47fd 100644 --- a/tests/jwt_test.py +++ b/tests/jwt_test.py @@ -5,7 +5,6 @@ import json import time import base64 -import http from http.server import HTTPServer, BaseHTTPRequestHandler from http import HTTPStatus import ssl @@ -40,6 +39,7 @@ def test_jwt_without_key_policy(network, args): cert_pem = infra.crypto.generate_cert(key_priv_pem) kid = "my_kid" issuer = "my_issuer" + raw_kid = kid.encode() LOG.info("Try to add JWT signing key without matching issuer") with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp: @@ -77,50 +77,43 @@ def test_jwt_without_key_policy(network, args): with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp: json.dump(create_jwks(kid, cert_pem), jwks_fp) jwks_fp.flush() - network.consortium.set_jwt_public_signing_keys(primary, issuer, jwks_fp.name) - - LOG.info("Check if JWT signing key was stored correctly") - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", {"table": "public:ccf.gov.jwt.public_signing_keys", "key": kid} + set_jwt_proposal = network.consortium.set_jwt_public_signing_keys( + primary, issuer, jwks_fp.name ) - assert r.status_code == http.HTTPStatus.OK.value, r.status_code - # Note that /gov/read returns all data as JSON. - # Here, the stored data is a uint8 array, therefore it - # is returned as an array of integers. - cert_kv_der = base64.b64decode(r.body.json()) - cert_kv_pem = infra.crypto.cert_der_to_pem(cert_kv_der) + + stored_jwt_signing_key = primary.get_ledger_public_state_at( + set_jwt_proposal.completed_seqno + )["public:ccf.gov.jwt.public_signing_keys"][raw_kid] + + stored_cert = infra.crypto.cert_der_to_pem(stored_jwt_signing_key) assert infra.crypto.are_certs_equal( - cert_pem, cert_kv_pem - ), "stored cert not equal to input cert" + cert_pem, stored_cert + ), "input cert is not equal to stored cert" LOG.info("Remove JWT issuer") - network.consortium.remove_jwt_issuer(primary, issuer) + remove_jwt_proposal = network.consortium.remove_jwt_issuer(primary, issuer) - LOG.info("Check if JWT signing key was deleted") - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", {"table": "public:ccf.gov.jwt.public_signing_keys", "key": kid} - ) - assert r.status_code == http.HTTPStatus.NOT_FOUND.value, r.status_code + assert ( + primary.get_ledger_public_state_at(remove_jwt_proposal.completed_seqno)[ + "public:ccf.gov.jwt.public_signing_keys" + ][raw_kid] + is None + ), "JWT issuer was not removed" LOG.info("Add JWT issuer with initial keys") with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp: json.dump({"issuer": issuer, "jwks": create_jwks(kid, cert_pem)}, metadata_fp) metadata_fp.flush() - network.consortium.set_jwt_issuer(primary, metadata_fp.name) + set_jwt_issuer = network.consortium.set_jwt_issuer(primary, metadata_fp.name) - LOG.info("Check if JWT signing key was stored correctly") - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", {"table": "public:ccf.gov.jwt.public_signing_keys", "key": kid} - ) - assert r.status_code == http.HTTPStatus.OK.value, r.status_code - cert_kv_der = base64.b64decode(r.body.json()) - cert_kv_pem = infra.crypto.cert_der_to_pem(cert_kv_der) + stored_jwt_signing_key = primary.get_ledger_public_state_at( + set_jwt_issuer.completed_seqno + )["public:ccf.gov.jwt.public_signing_keys"][raw_kid] + + stored_cert = infra.crypto.cert_der_to_pem(stored_jwt_signing_key) assert infra.crypto.are_certs_equal( - cert_pem, cert_kv_pem - ), "stored cert not equal to input cert" + cert_pem, stored_cert + ), "input cert is not equal to stored cert" return network @@ -233,20 +226,16 @@ def test_jwt_with_sgx_key_filter(network, args): jwks = {"keys": non_oe_jwks["keys"] + oe_jwks["keys"]} json.dump(jwks, jwks_fp) jwks_fp.flush() - network.consortium.set_jwt_public_signing_keys(primary, issuer, jwks_fp.name) - - LOG.info("Check that only SGX cert was added") - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", - {"table": "public:ccf.gov.jwt.public_signing_keys", "key": non_oe_kid}, + set_jwt_proposal = network.consortium.set_jwt_public_signing_keys( + primary, issuer, jwks_fp.name ) - assert r.status_code == http.HTTPStatus.NOT_FOUND.value, r.status_code - r = c.post( - "/gov/read", - {"table": "public:ccf.gov.jwt.public_signing_keys", "key": oe_kid}, - ) - assert r.status_code == http.HTTPStatus.OK.value, r.status_code + + stored_jwt_signing_keys = primary.get_ledger_public_state_at( + set_jwt_proposal.completed_seqno + )["public:ccf.gov.jwt.public_signing_keys"] + + assert non_oe_kid.encode() not in stored_jwt_signing_keys + assert oe_kid.encode() in stored_jwt_signing_keys return network @@ -308,23 +297,19 @@ def __exit__(self, exc_type, exc_value, traceback): def check_kv_jwt_key_matches(network, kid, cert_pem): primary, _ = network.find_nodes() - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/read", - {"table": "public:ccf.gov.jwt.public_signing_keys", "key": kid}, - ) - if cert_pem is None: - assert r.status_code == http.HTTPStatus.NOT_FOUND.value, r.status_code - else: - assert r.status_code == http.HTTPStatus.OK.value, r.status_code - # Note that /gov/read returns all data as JSON. - # Here, the stored data is a uint8 array, therefore it - # is returned as an array of integers. - cert_kv_der = base64.b64decode(r.body.json()) - cert_kv_pem = infra.crypto.cert_der_to_pem(cert_kv_der) - assert infra.crypto.are_certs_equal( - cert_pem, cert_kv_pem - ), "stored cert not equal to input cert" + + latest_public_state, _ = primary.get_latest_ledger_public_state() + latest_jwt_signing_key = latest_public_state[ + "public:ccf.gov.jwt.public_signing_keys" + ] + + if cert_pem is None: + assert kid.encode() not in latest_jwt_signing_key + else: + stored_cert = infra.crypto.cert_der_to_pem(latest_jwt_signing_key[kid.encode()]) + assert infra.crypto.are_certs_equal( + cert_pem, stored_cert + ), "input cert is not equal to stored cert" def get_jwt_refresh_endpoint_metrics(network) -> dict: @@ -378,11 +363,12 @@ def test_jwt_key_auto_refresh(network, args): metadata_fp.flush() network.consortium.set_jwt_issuer(primary, metadata_fp.name) - LOG.info("Check that keys got refreshed") - # Note: refresh interval is set to 1s, see network args below. - with_timeout( - lambda: check_kv_jwt_key_matches(network, kid, cert_pem), timeout=5 - ) + LOG.info("Check that keys got refreshed") + # Note: refresh interval is set to 1s, see network args below. + with_timeout( + lambda: check_kv_jwt_key_matches(network, kid, cert_pem), + timeout=5, + ) LOG.info("Check that JWT refresh endpoint has no failures") m = get_jwt_refresh_endpoint_metrics(network) diff --git a/tests/memberclient.py b/tests/memberclient.py index 2d88b090bf34..fcc22b6abefc 100644 --- a/tests/memberclient.py +++ b/tests/memberclient.py @@ -154,15 +154,6 @@ def test_governance(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == infra.proposal.ProposalState.OPEN.value - else: - 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 @@ -239,14 +230,6 @@ def test_governance(network, args): assert ( r.body.json()["state"] == infra.proposal.ProposalState.WITHDRAWN.value ) - else: - 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) diff --git a/tests/suite/test_requirements.py b/tests/suite/test_requirements.py index 478806924d1d..d88f286df381 100644 --- a/tests/suite/test_requirements.py +++ b/tests/suite/test_requirements.py @@ -98,23 +98,16 @@ def check(network, args, *nargs, **kwargs): def can_kill_n_nodes(nodes_to_kill_count): def check(network, args, *nargs, **kwargs): primary, _ = network.find_primary() - with primary.client(network.consortium.get_any_active_member().local_id) as c: - r = c.post( - "/gov/query", - { - "text": """tables = ... - trusted_nodes_count = 0 - tables["public:ccf.gov.nodes.info"]:foreach(function(node_id, details) - if details["status"] == "Trusted" then - trusted_nodes_count = trusted_nodes_count + 1 - end - end) - return trusted_nodes_count - """ - }, + with primary.client() as c: + r = c.get("/node/network/nodes") + + trusted_nodes_count = len( + [ + node + for node in r.body.json()["nodes"] + if node["status"] == infra.node.NodeStatus.TRUSTED.value + ] ) - - trusted_nodes_count = r.body.json() running_nodes_count = len(network.get_joined_nodes()) would_leave_nodes_count = running_nodes_count - nodes_to_kill_count minimum_nodes_to_run_count = ceil((trusted_nodes_count + 1) / 2)