diff --git a/algorithmic/problems/59/interactor.cpp b/algorithmic/problems/59/interactor.cpp index 4ebcf47e8..fcd1b29a8 100644 --- a/algorithmic/problems/59/interactor.cpp +++ b/algorithmic/problems/59/interactor.cpp @@ -9,6 +9,9 @@ int main(int argc, char* argv[]) { registerInteraction(argc, argv); int n = inf.readInt(1, 30000, "n"); + // Seed the generator from a per-test secret in the answer file so the + // hidden array is not a fixed function of the default rng seed (#208). + rnd.setSeed(ans.readLong()); cout << n << endl; cout.flush(); diff --git a/algorithmic/problems/59/testdata/1.ans b/algorithmic/problems/59/testdata/1.ans index e69de29bb..24ca6db22 100644 --- a/algorithmic/problems/59/testdata/1.ans +++ b/algorithmic/problems/59/testdata/1.ans @@ -0,0 +1 @@ +2718281828 diff --git a/algorithmic/problems/59/testdata/10.ans b/algorithmic/problems/59/testdata/10.ans index e69de29bb..591107c59 100644 --- a/algorithmic/problems/59/testdata/10.ans +++ b/algorithmic/problems/59/testdata/10.ans @@ -0,0 +1 @@ +1912931182 diff --git a/algorithmic/problems/59/testdata/2.ans b/algorithmic/problems/59/testdata/2.ans index e69de29bb..1faffd906 100644 --- a/algorithmic/problems/59/testdata/2.ans +++ b/algorithmic/problems/59/testdata/2.ans @@ -0,0 +1 @@ +3141592653 diff --git a/algorithmic/problems/59/testdata/3.ans b/algorithmic/problems/59/testdata/3.ans index e69de29bb..114735707 100644 --- a/algorithmic/problems/59/testdata/3.ans +++ b/algorithmic/problems/59/testdata/3.ans @@ -0,0 +1 @@ +1618033988 diff --git a/algorithmic/problems/59/testdata/4.ans b/algorithmic/problems/59/testdata/4.ans index e69de29bb..f449148fe 100644 --- a/algorithmic/problems/59/testdata/4.ans +++ b/algorithmic/problems/59/testdata/4.ans @@ -0,0 +1 @@ +1414213562 diff --git a/algorithmic/problems/59/testdata/5.ans b/algorithmic/problems/59/testdata/5.ans index e69de29bb..175aeb680 100644 --- a/algorithmic/problems/59/testdata/5.ans +++ b/algorithmic/problems/59/testdata/5.ans @@ -0,0 +1 @@ +2236067977 diff --git a/algorithmic/problems/59/testdata/6.ans b/algorithmic/problems/59/testdata/6.ans index e69de29bb..783918515 100644 --- a/algorithmic/problems/59/testdata/6.ans +++ b/algorithmic/problems/59/testdata/6.ans @@ -0,0 +1 @@ +1732050807 diff --git a/algorithmic/problems/59/testdata/7.ans b/algorithmic/problems/59/testdata/7.ans index e69de29bb..308fcaa08 100644 --- a/algorithmic/problems/59/testdata/7.ans +++ b/algorithmic/problems/59/testdata/7.ans @@ -0,0 +1 @@ +2645751311 diff --git a/algorithmic/problems/59/testdata/8.ans b/algorithmic/problems/59/testdata/8.ans index e69de29bb..4ca96a183 100644 --- a/algorithmic/problems/59/testdata/8.ans +++ b/algorithmic/problems/59/testdata/8.ans @@ -0,0 +1 @@ +1259921049 diff --git a/algorithmic/problems/59/testdata/9.ans b/algorithmic/problems/59/testdata/9.ans index e69de29bb..6b2b4cf57 100644 --- a/algorithmic/problems/59/testdata/9.ans +++ b/algorithmic/problems/59/testdata/9.ans @@ -0,0 +1 @@ +1442249570 diff --git a/algorithmic/tests/test_problem59.py b/algorithmic/tests/test_problem59.py new file mode 100644 index 000000000..a82ae2146 --- /dev/null +++ b/algorithmic/tests/test_problem59.py @@ -0,0 +1,103 @@ +"""The problem 59 interactor must not be solvable with zero queries (#208). + +Before the fix the interactor built its hidden array with testlib's rng at the +fixed default seed and never reseeded, so a program replaying that stream scored +100 without asking a single comparison. The fix reads a per-test secret seed from +the (hidden) answer file. These tests reconstruct the array the interactor will +use from the committed secret seed and feed it back with no queries: the array +from the committed seed is accepted, while the array from the old default seed is +rejected on every non-trivial test, proving the exploit is defeated. +""" +import os +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DATA = ROOT / "algorithmic/problems/59/testdata" +TESTS = list(range(1, 11)) + +# testlib random_t: java-style 48-bit LCG. +_MUL, _ADD, _MASK = 0x5DEECE66D, 0xB, (1 << 48) - 1 +_LLMAX = 9223372036854775807 +_DEFAULT_SEED = 3905348978240129619 + + +class _Rng: + def __init__(self, secret=None): + # registerInteraction leaves the default seed; setSeed(s) xors with the multiplier. + self.s = _DEFAULT_SEED if secret is None else ((secret ^ _MUL) & _MASK) + + def _bits(self, bits): + if bits <= 48: + self.s = (self.s * _MUL + _ADD) & _MASK + return self.s >> (48 - bits) + return (self._bits(31) << 32) ^ self._bits(32) # lowerBitCount = 32 (version = -1) + + def _next(self, n): + limit = _LLMAX // n * n + while True: + b = self._bits(63) + if b < limit: + return b % n + + def nrange(self, a, b): + return a + self._next(b - a + 1) + + +def _hidden_array(n, secret): + rng = _Rng(secret) + a = list(range(1, n + 1)) + for i in range(n): + j = rng.nrange(i, min(n - 1, i + 2)) + a[i], a[j] = a[j], a[i] + return a + + +@pytest.fixture(scope="module") +def interactor(tmp_path_factory): + exe = tmp_path_factory.mktemp("interactor59") / "interactor" + subprocess.run( + ["g++", "-std=gnu++17", "-O2", "-I" + str(ROOT / "algorithmic/judge/include"), + str(ROOT / "algorithmic/problems/59/interactor.cpp"), "-o", str(exe)], + check=True, + ) + return exe + + +def _play(interactor, t, array): + """Send `array` as the final answer with no queries; return (returncode, stderr).""" + inf, ans = DATA / f"{t}.in", DATA / f"{t}.ans" + proc = subprocess.Popen( + [str(interactor), str(inf), os.devnull, str(ans)], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + ) + n = int(proc.stdout.readline()) + proc.stdin.write("! " + " ".join(map(str, array)) + "\n") + proc.stdin.flush() + proc.stdin.close() + err = proc.stderr.read() + proc.wait() + return proc.returncode, n, err + + +@pytest.mark.parametrize("t", TESTS) +def test_correct_array_is_accepted(interactor, t): + secret = int((DATA / f"{t}.ans").read_text().split()[0]) + n = int((DATA / f"{t}.in").read_text().split()[0]) + rc, got_n, err = _play(interactor, t, _hidden_array(n, secret)) + assert got_n == n + assert rc == 7, err # testlib _points: a correct answer scores + assert "Ratio: 1.0" in err + + +@pytest.mark.parametrize("t", TESTS) +def test_default_seed_replay_is_rejected(interactor, t): + n = int((DATA / f"{t}.in").read_text().split()[0]) + if n < 2: + pytest.skip("n=1 has a unique array, so no seed can distinguish it") + rc, _, err = _play(interactor, t, _hidden_array(n, None)) + assert rc != 7, "zero-query default-seed replay must no longer score (#208)" + assert "Wrong final array" in err