Skip to content

Commit 4b33d50

Browse files
committed
feat(lwe): add structured recovery challenge
1 parent 07500f9 commit 4b33d50

20 files changed

Lines changed: 3340 additions & 0 deletions

2.0/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,14 @@ tail-frame (≥60) LPIPS-vs-GT over the unpatched baseline, gated by a wall-cloc
126126
guardrail (so drift can't be bought with more compute). A history-stabilization
127127
reference reliably beats baseline (validated t≈2.5/22 clips); beating it
128128
substantially is the open challenge.
129+
130+
## Structured-LWE Public Witness Recovery
131+
132+
This cryptanalysis task publishes 200 structured-LWE instances spanning ten
133+
balanced matrix/secret structure families. Its problem ID is
134+
`lwe_structured_recovery`. Agents recover any public-valid secret for as many
135+
instances as possible and submit an immediately updated cumulative JSON ledger;
136+
each solved instance contributes one point. The evaluator holds no planted
137+
secret or private checking key: it deterministically reconstructs the public
138+
matrix and checks the submitted vector's public secret and centered-error
139+
predicates.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
tag: security
2+
runtime:
3+
language: python
4+
timeout_seconds: 10800
5+
environment: "Public structured-LWE instances; Python 3.12 helper library; CPU only"
6+
apt_packages:
7+
- build-essential
8+
- ca-certificates
9+
- fplll-tools
10+
- git
11+
- libgmp-dev
12+
- libmpfr-dev
13+
- pkg-config
14+
- python3
15+
- python3-dev
16+
docker:
17+
image: ubuntu:24.04
18+
environment:
19+
cpus: 8
20+
memory_mb: 32768
21+
storage_mb: 32768
22+
build_timeout_seconds: 1800
23+
submission:
24+
kind: file
25+
path: /app/solution.json
26+
max_queue_size: 3
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5+
if [[ -n "${FCS_PYTHON:-}" ]]; then
6+
PYTHON_CANDIDATES=("$FCS_PYTHON")
7+
else
8+
PYTHON_CANDIDATES=(python3.12 python3.11 python3)
9+
fi
10+
11+
PYTHON_BIN=""
12+
for CANDIDATE in "${PYTHON_CANDIDATES[@]}"; do
13+
if command -v "$CANDIDATE" >/dev/null 2>&1 \
14+
&& "$CANDIDATE" -c 'import sys; raise SystemExit(sys.version_info < (3, 11))' \
15+
>/dev/null 2>&1; then
16+
PYTHON_BIN="$CANDIDATE"
17+
break
18+
fi
19+
done
20+
21+
if [[ -z "$PYTHON_BIN" ]]; then
22+
if command -v apt-get >/dev/null 2>&1; then
23+
export DEBIAN_FRONTEND=noninteractive
24+
apt-get update -qq
25+
apt-get install -y -qq --no-install-recommends python3 >/dev/null
26+
fi
27+
for CANDIDATE in "${PYTHON_CANDIDATES[@]}"; do
28+
if command -v "$CANDIDATE" >/dev/null 2>&1 \
29+
&& "$CANDIDATE" -c 'import sys; raise SystemExit(sys.version_info < (3, 11))' \
30+
>/dev/null 2>&1; then
31+
PYTHON_BIN="$CANDIDATE"
32+
break
33+
fi
34+
done
35+
if [[ -z "$PYTHON_BIN" ]]; then
36+
echo "Error: Python 3.11 or newer is required" >&2
37+
exit 1
38+
fi
39+
fi
40+
41+
if [[ $# -gt 0 ]]; then
42+
SOLUTION="$1"
43+
else
44+
SOLUTION="/work/execution_env/solution_env/solution.json"
45+
CI_REFERENCE="/work/execution_env/solution_env/solution.py"
46+
if [[ ! -f "$SOLUTION" && -f "$CI_REFERENCE" ]]; then
47+
"$PYTHON_BIN" "$CI_REFERENCE" > "$SOLUTION"
48+
fi
49+
fi
50+
51+
exec "$PYTHON_BIN" "$SCRIPT_DIR/evaluator.py" "$SOLUTION"
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Frontier-CS entry point for the public structured-LWE evaluator."""
2+
3+
from __future__ import annotations
4+
5+
import hashlib
6+
import math
7+
import os
8+
import stat
9+
import sys
10+
from collections.abc import Mapping
11+
from functools import lru_cache
12+
from pathlib import Path
13+
from typing import TypeAlias
14+
15+
16+
JsonScalar: TypeAlias = None | bool | int | float | str
17+
PlainData: TypeAlias = JsonScalar | list["PlainData"] | dict[str, "PlainData"]
18+
19+
try:
20+
_SOURCE_PUBLIC_DIR = (
21+
Path(__file__).resolve().parent / "harbor" / "app" / "public"
22+
)
23+
_INITIAL_CATALOG_OVERRIDE = os.environ.get("FCS_STRUCTURED_LWE_CATALOG")
24+
_IMPORT_PUBLIC_DIR = Path(
25+
_SOURCE_PUBLIC_DIR
26+
if _INITIAL_CATALOG_OVERRIDE is not None
27+
else os.environ.get("FRONTIER_PUBLIC_DIR", _SOURCE_PUBLIC_DIR)
28+
).resolve()
29+
_IMPORT_PUBLIC_DIR_TEXT = str(_IMPORT_PUBLIC_DIR)
30+
while _IMPORT_PUBLIC_DIR_TEXT in sys.path:
31+
sys.path.remove(_IMPORT_PUBLIC_DIR_TEXT)
32+
sys.path.insert(0, _IMPORT_PUBLIC_DIR_TEXT)
33+
34+
from lwe_challenge.evaluator_core import evaluate_path # noqa: E402
35+
from lwe_challenge.schema import MAX_CATALOG_BYTES, Catalog # noqa: E402
36+
except Exception:
37+
if __name__ == "__main__":
38+
print("infrastructure_error", file=sys.stderr)
39+
raise SystemExit(1) from None
40+
raise
41+
42+
43+
_SIDECAR_BYTES = len(f"{'0' * 64} catalog.jsonl\n".encode("ascii"))
44+
45+
46+
def _catalog_path() -> Path:
47+
override = os.environ.get("FCS_STRUCTURED_LWE_CATALOG")
48+
if override is not None:
49+
return Path(override).resolve()
50+
public_dir = Path(
51+
os.environ.get("FRONTIER_PUBLIC_DIR", _SOURCE_PUBLIC_DIR)
52+
).resolve()
53+
return (public_dir / "catalog.jsonl").resolve()
54+
55+
56+
def _catalog() -> Catalog:
57+
path = _catalog_path()
58+
production = os.environ.get("FCS_STRUCTURED_LWE_CATALOG") is None
59+
catalog_bytes = _read_regular_bytes(path, max_bytes=MAX_CATALOG_BYTES)
60+
digest = hashlib.sha256(catalog_bytes).hexdigest()
61+
if production:
62+
expected_sidecar = f"{digest} catalog.jsonl\n".encode("ascii")
63+
try:
64+
sidecar = _read_regular_bytes(
65+
path.with_name("catalog.sha256"), max_bytes=_SIDECAR_BYTES
66+
)
67+
except ValueError:
68+
raise ValueError("catalog.sha256 is not a valid sidecar") from None
69+
if sidecar != expected_sidecar:
70+
raise ValueError("catalog.sha256 does not match catalog.jsonl")
71+
return _load_catalog(str(path), digest, production)
72+
73+
74+
def _read_regular_bytes(path: Path, *, max_bytes: int) -> bytes:
75+
flags = (
76+
os.O_RDONLY
77+
| os.O_NONBLOCK
78+
| getattr(os, "O_CLOEXEC", 0)
79+
| getattr(os, "O_NOFOLLOW", 0)
80+
)
81+
fd = os.open(path, flags)
82+
try:
83+
if not stat.S_ISREG(os.fstat(fd).st_mode):
84+
raise ValueError("catalog assets must be regular files")
85+
data = bytearray()
86+
limit = max_bytes + 1
87+
while len(data) < limit:
88+
chunk = os.read(fd, min(1024 * 1024, limit - len(data)))
89+
if not chunk:
90+
break
91+
data.extend(chunk)
92+
finally:
93+
os.close(fd)
94+
if len(data) > max_bytes:
95+
raise ValueError("catalog asset exceeds its byte limit")
96+
return bytes(data)
97+
98+
99+
@lru_cache(maxsize=8)
100+
def _load_catalog(path: str, digest: str, production: bool) -> Catalog:
101+
catalog = Catalog.load(path)
102+
if catalog.catalog_id != digest:
103+
raise RuntimeError("catalog changed while it was being verified")
104+
if production and len(catalog.instances) != 200:
105+
raise ValueError("production catalog must contain exactly 200 instances")
106+
return catalog
107+
108+
109+
def _plain_data(value: object) -> PlainData:
110+
if isinstance(value, float) and not math.isfinite(value):
111+
raise TypeError("public metrics require finite float values")
112+
if value is None or isinstance(value, (bool, int, float, str)):
113+
return value
114+
if isinstance(value, Mapping):
115+
copied: dict[str, PlainData] = {}
116+
for key, item in value.items():
117+
if not isinstance(key, str):
118+
raise TypeError("public metrics mappings require string keys")
119+
copied[key] = _plain_data(item)
120+
return copied
121+
if isinstance(value, tuple):
122+
return [_plain_data(item) for item in value]
123+
raise TypeError("public metrics contain a non-JSON value")
124+
125+
126+
def prepare() -> dict[str, object]:
127+
catalog = _catalog()
128+
return {
129+
"instance_count": len(catalog.instances),
130+
"catalog_id": catalog.catalog_id,
131+
}
132+
133+
134+
def evaluate(solution_path: str) -> tuple[float, float, str, dict[str, object]]:
135+
catalog = _catalog()
136+
result = evaluate_path(solution_path, catalog=catalog)
137+
metrics = _plain_data(result.metrics)
138+
if not isinstance(metrics, dict):
139+
raise TypeError("evaluator metrics must be a mapping")
140+
return result.score, result.score_unbounded, result.message, metrics
141+
142+
143+
def main(argv: list[str]) -> int:
144+
if len(argv) != 2:
145+
print("usage: evaluator.py SOLUTION_JSON", file=sys.stderr)
146+
return 2
147+
try:
148+
score, score_unbounded, message, _metrics = evaluate(argv[1])
149+
except Exception:
150+
print("infrastructure_error", file=sys.stderr)
151+
return 1
152+
print(message, file=sys.stderr)
153+
print(f"{score:.12f} {score_unbounded:.12f}")
154+
return 0
155+
156+
157+
if __name__ == "__main__":
158+
raise SystemExit(main(sys.argv))
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env python3
2+
"""Merge one candidate witness into the cumulative solution ledger."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import sys
8+
from collections.abc import Sequence
9+
from pathlib import Path
10+
from typing import NoReturn
11+
12+
13+
def _load_ledger_module():
14+
public_path = str(Path(__file__).resolve().parent / "public")
15+
while public_path in sys.path:
16+
sys.path.remove(public_path)
17+
sys.path.insert(0, public_path)
18+
import lwe_challenge.ledger as ledger_module
19+
20+
return ledger_module
21+
22+
23+
if __name__ != "__main__":
24+
ledger = _load_ledger_module()
25+
26+
27+
def merge_solution(
28+
ledger_path: str | Path,
29+
instance_id: str,
30+
secret: Sequence[int],
31+
) -> int:
32+
"""Merge one new witness through the canonical locked transaction."""
33+
34+
updated = ledger.merge_witness_transaction(
35+
ledger_path,
36+
instance_id=instance_id,
37+
secret=secret,
38+
replace=False,
39+
)
40+
return len(updated.solutions)
41+
42+
43+
class _SanitizedArgumentParser(argparse.ArgumentParser):
44+
def error(self, _message: str) -> NoReturn:
45+
self.exit(2, "error: invalid command line\n")
46+
47+
48+
def _parser() -> argparse.ArgumentParser:
49+
parser = _SanitizedArgumentParser()
50+
parser.add_argument("instance_id", metavar="INSTANCE_ID")
51+
parser.add_argument("secret", metavar="SECRET", nargs="?")
52+
parser.add_argument("--ledger", default="/app/solution.json")
53+
parser.add_argument("--replace", action="store_true")
54+
return parser
55+
56+
57+
def main() -> int:
58+
parser = _parser()
59+
args, unknown = parser.parse_known_args()
60+
if args.secret is None and len(unknown) == 1:
61+
args.secret = unknown[0]
62+
unknown = []
63+
if args.secret is None or unknown:
64+
parser.error("INSTANCE_ID and SECRET are required")
65+
try:
66+
secret = tuple(int(component, 10) for component in args.secret.split(","))
67+
except ValueError:
68+
parser.error("SECRET must be a comma-separated integer vector")
69+
70+
try:
71+
if args.replace:
72+
updated = ledger.merge_witness_transaction(
73+
args.ledger,
74+
instance_id=args.instance_id,
75+
secret=secret,
76+
replace=True,
77+
)
78+
solved_count = len(updated.solutions)
79+
else:
80+
solved_count = merge_solution(args.ledger, args.instance_id, secret)
81+
except (OSError, TypeError, ValueError):
82+
parser.exit(2, "error: unable to update ledger\n")
83+
84+
print(solved_count)
85+
return 0
86+
87+
88+
if __name__ == "__main__":
89+
try:
90+
ledger = _load_ledger_module()
91+
except Exception:
92+
sys.stderr.write("error: unable to update ledger\n")
93+
raise SystemExit(2)
94+
raise SystemExit(main())

0 commit comments

Comments
 (0)