зеркало из https://github.com/microsoft/CCF.git
216 строки
8.2 KiB
Python
216 строки
8.2 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the Apache 2.0 License.
|
|
import infra.e2e_args
|
|
import infra.network
|
|
import infra.proc
|
|
import infra.remote
|
|
import infra.crypto
|
|
import ccf.ledger
|
|
import infra.doc
|
|
from infra.proposal import ProposalState
|
|
import http
|
|
import os
|
|
import base64
|
|
import json
|
|
from loguru import logger as LOG
|
|
import suite.test_requirements as reqs
|
|
import ccf.read_ledger
|
|
import infra.logging_app as app
|
|
|
|
|
|
def check_operations(ledger, operations):
|
|
LOG.debug("Audit the ledger file for governance operations")
|
|
|
|
members = {}
|
|
for chunk in ledger:
|
|
for tr in chunk:
|
|
tables = tr.get_public_domain().get_tables()
|
|
if "public:ccf.gov.members.certs" in tables:
|
|
members_table = tables["public:ccf.gov.members.certs"]
|
|
for member_id, member_cert in members_table.items():
|
|
members[member_id] = member_cert
|
|
|
|
if "public:ccf.gov.history" in tables:
|
|
governance_history_table = tables["public:ccf.gov.history"]
|
|
for member_id, signed_request in governance_history_table.items():
|
|
assert member_id in members
|
|
signed_request = json.loads(signed_request)
|
|
|
|
cert = members[member_id]
|
|
sig = base64.b64decode(signed_request["sig"])
|
|
req = base64.b64decode(signed_request["req"])
|
|
request_body = base64.b64decode(signed_request["request_body"])
|
|
digest = signed_request["md"]
|
|
|
|
infra.crypto.verify_request_sig(
|
|
cert, sig, req, request_body, digest
|
|
)
|
|
request_target_line = req.decode().splitlines()[0]
|
|
if "/gov/proposals" in request_target_line:
|
|
vote_suffix = "/ballots"
|
|
elements = request_target_line.split("/")
|
|
if request_target_line.endswith(vote_suffix):
|
|
op = (elements[-2], member_id.decode(), "vote")
|
|
elif request_target_line.endswith("/withdraw"):
|
|
op = (elements[-2], member_id.decode(), "withdraw")
|
|
else:
|
|
(proposal_id,) = tables["public:ccf.gov.proposals"].keys()
|
|
op = (proposal_id.decode(), member_id.decode(), "propose")
|
|
|
|
if op in operations:
|
|
operations.remove(op)
|
|
|
|
assert operations == set(), operations
|
|
|
|
|
|
def check_all_tables_are_documented(ledger, doc_path):
|
|
# Check that all CCF tables present in the input ledger are documented.
|
|
# Tables marked as experimental in the doc must not be present in the ledger.
|
|
with open(doc_path, encoding="utf-8") as doc:
|
|
parsed_doc = infra.doc.parse(doc.read())
|
|
table_names = infra.doc.extract_table_names(parsed_doc)
|
|
|
|
experimental_table_names = [tn for tn in table_names if "(experimental)" in tn]
|
|
table_names = [tn for tn in table_names if tn not in experimental_table_names]
|
|
experimental_table_names = [tn.split(" ")[0] for tn in experimental_table_names]
|
|
|
|
table_names_in_ledger = set()
|
|
for chunk in ledger:
|
|
for tr in chunk:
|
|
table_names_in_ledger.update(tr.get_public_domain().get_tables().keys())
|
|
|
|
experimental_table_names_in_ledger = [
|
|
tn for tn in table_names_in_ledger if tn in experimental_table_names
|
|
]
|
|
if experimental_table_names_in_ledger:
|
|
raise ValueError(
|
|
f"Experimental tables {experimental_table_names_in_ledger} were present in ledger"
|
|
)
|
|
|
|
public_table_names_in_ledger = set(
|
|
[tn for tn in table_names_in_ledger if tn.startswith("public:ccf.")]
|
|
)
|
|
undocumented_tables = public_table_names_in_ledger - set(table_names)
|
|
assert undocumented_tables == set(), undocumented_tables
|
|
|
|
|
|
@reqs.description("Check tables are documented")
|
|
def test_tables_doc(network, args):
|
|
primary, _ = network.find_primary()
|
|
ledger_directories = primary.remote.ledger_paths()
|
|
ledger = ccf.ledger.Ledger(ledger_directories)
|
|
check_all_tables_are_documented(ledger, "../doc/audit/builtin_maps.rst")
|
|
return network
|
|
|
|
|
|
@reqs.description("Test that all nodes' ledgers can be read")
|
|
def test_ledger_is_readable(network, args):
|
|
primary, backups = network.find_nodes()
|
|
for node in (primary, *backups):
|
|
ledger_dirs = node.remote.ledger_paths()
|
|
LOG.info(f"Reading ledger from {ledger_dirs}")
|
|
ledger = ccf.ledger.Ledger(ledger_dirs)
|
|
for chunk in ledger:
|
|
for _ in chunk:
|
|
pass
|
|
return network
|
|
|
|
|
|
@reqs.description("Test that all nodes' ledgers can be read using read_ledger.py")
|
|
def test_read_ledger_utility(network, args):
|
|
def fmt_str(data: bytes) -> str:
|
|
return data.decode()
|
|
|
|
format_rule = [(".*records.*", {"key": fmt_str, "value": fmt_str})]
|
|
|
|
network.txs.issue(network, number_txs=args.snapshot_tx_interval)
|
|
network.get_latest_ledger_public_state()
|
|
|
|
primary, backups = network.find_nodes()
|
|
for node in (primary, *backups):
|
|
ledger_dirs = node.remote.ledger_paths()
|
|
assert ccf.read_ledger.run(paths=ledger_dirs, tables_format_rules=format_rule)
|
|
|
|
snapshot_dir = network.get_committed_snapshots(primary)
|
|
assert ccf.read_ledger.run(
|
|
paths=[os.path.join(snapshot_dir, os.listdir(snapshot_dir)[-1])],
|
|
is_snapshot=True,
|
|
tables_format_rules=format_rule,
|
|
)
|
|
return network
|
|
|
|
|
|
def run(args):
|
|
# Keep track of governance operations that happened in the test
|
|
governance_operations = set()
|
|
|
|
txs = app.LoggingTxs("user0")
|
|
with infra.network.network(
|
|
args.nodes,
|
|
args.binary_dir,
|
|
args.debug_nodes,
|
|
args.perf_nodes,
|
|
pdb=args.pdb,
|
|
txs=txs,
|
|
) as network:
|
|
network.start_and_open(args)
|
|
primary, _ = network.find_primary()
|
|
|
|
ledger_directories = primary.remote.ledger_paths()
|
|
LOG.info("Add new member proposal (implicit vote)")
|
|
(
|
|
new_member_proposal,
|
|
_,
|
|
careful_vote,
|
|
) = network.consortium.generate_and_propose_new_member(
|
|
primary, curve=infra.network.EllipticCurve.secp256r1
|
|
)
|
|
member = network.consortium.get_member_by_local_id(
|
|
new_member_proposal.proposer_id
|
|
)
|
|
governance_operations.add(
|
|
(new_member_proposal.proposal_id, member.service_id, "propose")
|
|
)
|
|
|
|
LOG.info("2/3 members accept the proposal")
|
|
p = network.consortium.vote_using_majority(
|
|
primary, new_member_proposal, careful_vote
|
|
)
|
|
for voter in p.voters:
|
|
governance_operations.add((p.proposal_id, voter, "vote"))
|
|
assert new_member_proposal.state == infra.proposal.ProposalState.ACCEPTED
|
|
|
|
LOG.info("Create new proposal but withdraw it before it is accepted")
|
|
new_member_proposal, _, _ = network.consortium.generate_and_propose_new_member(
|
|
primary, curve=infra.network.EllipticCurve.secp256r1
|
|
)
|
|
member = network.consortium.get_member_by_local_id(
|
|
new_member_proposal.proposer_id
|
|
)
|
|
governance_operations.add(
|
|
(new_member_proposal.proposal_id, member.service_id, "propose")
|
|
)
|
|
|
|
with primary.client() as c:
|
|
response = network.consortium.get_member_by_local_id(
|
|
new_member_proposal.proposer_id
|
|
).withdraw(primary, new_member_proposal)
|
|
infra.checker.Checker(c)(response)
|
|
assert response.status_code == http.HTTPStatus.OK.value
|
|
assert response.body.json()["state"] == ProposalState.WITHDRAWN.value
|
|
member = network.consortium.get_member_by_local_id(
|
|
new_member_proposal.proposer_id
|
|
)
|
|
governance_operations.add(
|
|
(new_member_proposal.proposal_id, member.service_id, "withdraw")
|
|
)
|
|
|
|
# Force ledger flush of all transactions so far
|
|
network.get_latest_ledger_public_state()
|
|
ledger = ccf.ledger.Ledger(ledger_directories)
|
|
check_operations(ledger, governance_operations)
|
|
|
|
test_ledger_is_readable(network, args)
|
|
test_read_ledger_utility(network, args)
|
|
test_tables_doc(network, args)
|