Skip to content

Commit

Permalink
Merge branch 'main' into f/6500-cose-service-identity-endorsements
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets authored Oct 2, 2024
2 parents 199c0e8 + e6f00b7 commit affb9f7
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 27 deletions.
2 changes: 1 addition & 1 deletion js/ccf-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"author": "Microsoft",
"license": "Apache-2.0",
"devDependencies": {
"@types/chai": "^4.2.15",
"@types/chai": "^5.0.0",
"@types/mocha": "^10.0.0",
"@types/node": "^22.0.0",
"@types/node-forge": "^1.0.0",
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ endgroup

group "Python format"
if [ $FIX -ne 0 ]; then
git ls-files tests/ python/ scripts/ .cmake-format.py | grep -e '\.py$' | xargs black
git ls-files tests/ python/ scripts/ tla/ .cmake-format.py | grep -e '\.py$' | xargs black
else
git ls-files tests/ python/ scripts/ .cmake-format.py | grep -e '\.py$' | xargs black --check
git ls-files tests/ python/ scripts/ tla/ .cmake-format.py | grep -e '\.py$' | xargs black --check
fi
endgroup

Expand Down
23 changes: 23 additions & 0 deletions src/node/historical_queries_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,29 @@ namespace ccf::historical
}
}

// If recovery in progress, prohibit any historical queries for previous
// epochs, because the service does not yet have access to the
// ledger secrets necessary to produce commit evidence.
auto service = args.tx.template ro<ccf::Service>(Tables::SERVICE);
auto active_service = service->get();
if (active_service && active_service->status != ServiceStatus::OPEN)
{
if (
active_service->current_service_create_txid &&
target_tx_id.view < active_service->current_service_create_txid->view)
{
auto reason = fmt::format(
"Historical transaction {} is not signed by the current service "
"identity key and cannot be retrieved until recovery is complete.",
target_tx_id.to_str());
ehandler(
HistoricalQueryErrorCode::TransactionInvalid,
std::move(reason),
args);
return;
}
}

// We need a handle to determine whether this request is the 'same' as a
// previous one. For simplicity we use target_tx_id.seqno. This means we
// keep a lot of state around for old requests! It should be cleaned up
Expand Down
13 changes: 11 additions & 2 deletions tests/e2e_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ def verify_endorsements_openssl(service_cert, receipt):


def verify_receipt(
receipt, service_cert, claims=None, generic=True, skip_endorsement_check=False
receipt,
service_cert,
claims=None,
generic=True,
skip_endorsement_check=False,
is_signature_tx=False,
):
"""
Raises an exception on failure
Expand Down Expand Up @@ -115,7 +120,7 @@ def verify_receipt(
.digest()
.hex()
)
else:
elif not is_signature_tx:
assert "leaf_components" in receipt, receipt
assert "write_set_digest" in receipt["leaf_components"]
write_set_digest = bytes.fromhex(receipt["leaf_components"]["write_set_digest"])
Expand All @@ -133,6 +138,10 @@ def verify_receipt(
.digest()
.hex()
)
else:
assert is_signature_tx
leaf = receipt["leaf"]

root = ccf.receipt.root(leaf, receipt["proof"])
ccf.receipt.verify(root, receipt["signature"], node_cert)

Expand Down
69 changes: 69 additions & 0 deletions tests/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
from loguru import logger as LOG


def shifted_tx(tx, view_diff, seq_dif):
return ccf.tx_id.TxID(tx.view + view_diff, tx.seqno + seq_dif)


def get_and_verify_historical_receipt(network, ref_msg):
primary, _ = network.find_primary()
if not ref_msg:
Expand Down Expand Up @@ -165,6 +169,14 @@ def test_recover_service_with_wrong_identity(network, args):
network.save_service_identity(args)
first_service_identity_file = args.previous_service_identity_file

with old_primary.client() as c:
before_recovery_tx_id = ccf.tx_id.TxID.from_str(
c.get("/node/commit").body.json()["transaction_id"]
)
previous_service_created_tx_id = ccf.tx_id.TxID.from_str(
c.get("/node/network").body.json()["current_service_create_txid"]
)

network.stop_all_nodes()

current_ledger_dir, committed_ledger_dirs = old_primary.get_ledger()
Expand Down Expand Up @@ -254,8 +266,65 @@ def test_recover_service_with_wrong_identity(network, args):
snapshots_dir=snapshots_dir,
)

# Must fail with a dedicated error message if requesting a receipt for a TX
# from past epochs, since ledger secrets are not yet available,
# therefore no receipt can be generated.
primary, _ = recovered_network.find_primary()
with primary.client() as cli:
curr_tx_id = ccf.tx_id.TxID.from_str(
cli.get("/node/commit").body.json()["transaction_id"]
)

response = cli.get(f"/node/receipt?transaction_id={str(before_recovery_tx_id)}")
assert response.status_code == http.HTTPStatus.NOT_FOUND, response
assert (
"not signed by the current service"
in response.body.json()["error"]["message"]
), response

current_service_created_tx_id = ccf.tx_id.TxID.from_str(
cli.get("/node/network").body.json()["current_service_create_txid"]
)

# TX from the current epoch though can be verified, as soon as the caller
# trusts the current service identity.
receipt = primary.get_receipt(curr_tx_id.view, curr_tx_id.seqno).json()
verify_receipt(receipt, recovered_network.cert, is_signature_tx=True)

recovered_network.recover(args)

# Needs refreshing, recovery has completed.
with primary.client() as cli:
curr_tx_id = ccf.tx_id.TxID.from_str(
cli.get("/node/commit").body.json()["transaction_id"]
)

# Check receipts for transactions after multiple recoveries
txids = [
# Last TX before previous recovery
shifted_tx(previous_service_created_tx_id, -2, -1),
# First after previous recovery
previous_service_created_tx_id,
# Random TX before previous and last recovery
shifted_tx(current_service_created_tx_id, -2, -5),
# Last TX before last recovery
shifted_tx(current_service_created_tx_id, -2, -1),
# First TX after last recovery
current_service_created_tx_id,
# Random TX after last recovery
shifted_tx(curr_tx_id, 0, -3),
]

for tx in txids:
receipt = primary.get_receipt(tx.view, tx.seqno).json()

try:
verify_receipt(receipt, recovered_network.cert)
except AssertionError:
# May fail due to missing leaf components if it's a signature TX,
# try again with a flag to force skip leaf components verification.
verify_receipt(receipt, recovered_network.cert, is_signature_tx=True)

return recovered_network


Expand Down
8 changes: 5 additions & 3 deletions tla/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"""

TOP=5
TOP = 5

if __name__ == "__main__":
ts = defaultdict(list)
Expand All @@ -40,8 +40,10 @@
top_TOP = [x[0] for x in col_max[-TOP:]]
df_TOP = df[top_TOP]
print(df_TOP)
plot = df_TOP.plot.line(y=df_TOP.columns, width=1200, height=600, title=f"Top {TOP} distinct actions")
plot = df_TOP.plot.line(
y=df_TOP.columns, width=1200, height=600, title=f"Top {TOP} distinct actions"
)
if len(sys.argv) > 1:
hvplot.save(plot, sys.argv[1])
else:
hvplot.show(plot)
hvplot.show(plot)
4 changes: 3 additions & 1 deletion tla/install_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ def _parse_args() -> argparse.Namespace:

def install_tlc():
java = "java"
tlaplus_path = "~/.vscode-remote/extensions/tlaplus.vscode-ide-*/tools/tla2tools.jar"
tlaplus_path = (
"~/.vscode-remote/extensions/tlaplus.vscode-ide-*/tools/tla2tools.jar"
)
copy_tlaplus = f"-cp {tlaplus_path} tlc2.TLC"

set_alias("tlcrepl", f"{java} -cp {tlaplus_path} tlc2.REPL")
Expand Down
17 changes: 14 additions & 3 deletions tla/loc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dataclasses
import sys


@dataclasses.dataclass
class LOC:
code: int = 0
Expand All @@ -21,9 +22,10 @@ def __add__(self, other):
return LOC(
self.code + other.code,
self.comment + other.comment,
self.blank + other.blank
self.blank + other.blank,
)


def is_comment(line: str) -> bool:
if line.startswith(r"\*"):
return True
Expand All @@ -33,15 +35,19 @@ def is_comment(line: str) -> bool:
return True
return False


def is_multiline_comment_start(line: str) -> bool:
return line.startswith("(*")


def is_footer(line: str) -> bool:
return all(c == "=" for c in line)


def is_multiline_comment_end(line: str) -> bool:
return line.endswith("*)")


def count_loc(lines) -> LOC:
loc = LOC()
in_comment = False
Expand All @@ -68,6 +74,7 @@ def count_loc(lines) -> LOC:
loc.code += 1
return loc


JUST_CODE = r"""
---------- MODULE MCccfraft ----------
EXTENDS ccfraft, StatsFile, MCAliases
Expand Down Expand Up @@ -127,6 +134,7 @@ def count_loc(lines) -> LOC:
/\ msg.prevLogIndex = logline.msg.packet.prev_idx
"""


def test_loc():
"""
Run with py.test loc.py
Expand All @@ -135,7 +143,10 @@ def test_loc():
assert count_loc(CODE_AND_COMMENTS.splitlines()) == LOC(code=2, comment=2, blank=1)
assert count_loc(FOOTER.splitlines()) == LOC(code=0, comment=9, blank=1)
assert count_loc(MULTILINE_COMMENT.splitlines()) == LOC(code=2, comment=4, blank=2)
assert count_loc(MULTILINE_COMMENT_2.splitlines()) == LOC(code=7, comment=12, blank=1)
assert count_loc(MULTILINE_COMMENT_2.splitlines()) == LOC(
code=7, comment=12, blank=1
)


if __name__ == "__main__":
locs = []
Expand All @@ -144,4 +155,4 @@ def test_loc():
lines = f.readlines()
locs.append(count_loc(lines))
print(f"{arg} {locs[-1]}")
print(f"Total {sum(locs, LOC())}")
print(f"Total {sum(locs, LOC())}")
59 changes: 44 additions & 15 deletions tla/trace2scen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,41 @@
import json
import os


def comment(action):
return f"# {action['name']} {action['location']['module']}:{action['location']['beginLine']}"


def term(ctx, pre):
return str(pre["currentTerm"][ctx['i']])
return str(pre["currentTerm"][ctx["i"]])


def noop(ctx, pre, post):
return ["# Noop"]


MAP = {
"ClientRequest": lambda ctx, pre, post: ["replicate", term(ctx, pre), "42"],
"MCClientRequest": lambda ctx, pre, post: ["replicate", term(ctx, pre), "42"],
"CheckQuorum": lambda ctx, pre, post: ["periodic_one", ctx['i'], "110"],
"Timeout": lambda ctx, pre, post: ["periodic_one", ctx['i'], "110"],
"MCTimeout": lambda ctx, pre, post: ["periodic_one", ctx['i'], "110"],
"CheckQuorum": lambda ctx, pre, post: ["periodic_one", ctx["i"], "110"],
"Timeout": lambda ctx, pre, post: ["periodic_one", ctx["i"], "110"],
"MCTimeout": lambda ctx, pre, post: ["periodic_one", ctx["i"], "110"],
"RequestVote": noop,
"AppendEntries": lambda _, __, ___: ["dispatch_all"],
"BecomeLeader": noop,
"SignCommittableMessages": lambda ctx, pre, post: ["emit_signature", term(ctx, pre)],
"MCSignCommittableMessages": lambda ctx, pre, post: ["emit_signature", term(ctx, pre)],
"ChangeConfigurationInt": lambda ctx, pre, post: ["replicate_new_configuration", term(ctx, pre), *ctx["newConfiguration"]],
"SignCommittableMessages": lambda ctx, pre, post: [
"emit_signature",
term(ctx, pre),
],
"MCSignCommittableMessages": lambda ctx, pre, post: [
"emit_signature",
term(ctx, pre),
],
"ChangeConfigurationInt": lambda ctx, pre, post: [
"replicate_new_configuration",
term(ctx, pre),
*ctx["newConfiguration"],
],
"AdvanceCommitIndex": noop,
"HandleRequestVoteRequest": lambda _, __, ___: ["dispatch_all"],
"HandleRequestVoteResponse": noop,
Expand All @@ -38,31 +52,46 @@ def noop(ctx, pre, post):
"RcvRequestVoteResponse": noop,
}


def post_commit(post):
return [["assert_commit_idx", node, str(idx)] for node, idx in post["commitIndex"].items()]
return [
["assert_commit_idx", node, str(idx)]
for node, idx in post["commitIndex"].items()
]


def post_state(post):
entries = []
for node, state in post["state"].items():
entries.append(["assert_detail", "leadership_state", node, state])
entries.append(["assert_detail", "leadership_state", node, state])
return entries


def step_to_action(pre_state, action, post_state):
return os.linesep.join([
comment(action),
','.join(MAP[action['name']](action['context'], pre_state[1], post_state[1]))])
return os.linesep.join(
[
comment(action),
",".join(
MAP[action["name"]](action["context"], pre_state[1], post_state[1])
),
]
)


def asserts(pre_state, action, post_state, assert_gen):
return os.linesep.join([','.join(assertion) for assertion in assert_gen(post_state[1])])
return os.linesep.join(
[",".join(assertion) for assertion in assert_gen(post_state[1])]
)


if __name__ == "__main__":
with open(sys.argv[1]) as trace:
steps = json.load(trace)["action"]
initial_state = steps[0][0][1]
initial_node, = [node for node, log in initial_state["log"].items() if log]
(initial_node,) = [node for node, log in initial_state["log"].items() if log]
print(f"start_node,{initial_node}")
print(f"emit_signature,2")
for step in steps:
print(step_to_action(*step))
print(asserts(*step, post_state))
print(asserts(*steps[-1], post_commit))
print(asserts(*steps[-1], post_commit))

0 comments on commit affb9f7

Please sign in to comment.