`qvm-app` server mishandles `rng-seed`s greater than $2^32-1$
The qvm-app accepts an rng-seed parameter to seed the QVM's PRNG. The documentation states that it should be greater than 0, but any value in $\{ 2^{32+n}, n>0 \} = 2^{32}, 2^{33}, .., 2^{63}, 2^{64}, ..$ causes the server to return a 500 response code with this message:
"The value\n 0\nis not of type\n (INTEGER 1 4294967295)\nwhen binding MT19937::SEED"
However, large values aren't flatly rejected--instead, they're truncated to their LSB (at least on the machine I'm testing on; it may be architecture dependent) (see below for the program that tested this):
---- These have the same results ----
{"response":200,"result":[[1],[0],[0],[1],[1],[1],[0],[1],[0],[1]],"seed":"0"}
{"response":200,"result":[[1],[0],[0],[1],[1],[1],[0],[1],[0],[1]],"seed":"1"}
{"response":200,"result":[[1],[0],[0],[1],[1],[1],[0],[1],[0],[1]],"seed":"4294967297"} # 2^32+1
{"response":200,"result":[[1],[0],[0],[1],[1],[1],[0],[1],[0],[1]],"seed":"18446744073709551617"} # 2^64+1
---- These have the same results (but different from above) ----
{"response":200,"result":[[1],[1],[1],[1],[0],[1],[0],[1],[0],[1]],"seed":"1204605758"}
{"response":200,"result":[[1],[1],[1],[1],[0],[1],[0],[1],[0],[1]],"seed":"5499573054"} # 2^32+1204605758
{"response":200,"result":[[1],[1],[1],[1],[0],[1],[0],[1],[0],[1]],"seed":"18446744074914157374"} # 2^64+1204605758
---- These all fail ----
{"response":500,"result":"-1 fell through ETYPECASE expression.\nWanted one of (NULL (UNSIGNED-BYTE 32) UNSIGNED-BYTE).","seed":"-1"}
{"response":500,"result":"-2 fell through ETYPECASE expression.\nWanted one of (NULL (UNSIGNED-BYTE 32) UNSIGNED-BYTE).","seed":"-2"}
The origin of both issues is how rng-seed is processed by get-random-state. If the value were null, then it calls (qvm:seeded-random-state nil), but all unsigned-bytes of any bitlength are passed as-is to the same function. seeded-random-state expect an integer, an array (unsigned 32) (*), or nil, so when the rng-seed exceeds what fits in a u32, it is gets passed along to MT19937, which seemingly processes only the LSB: when that's all zeros, we see the failure cases above (because 0 is not an accepted value for the seed), but provided some of the low 32 bits are set, it's effectively truncated.
Larger values should be converted to array before passing them along. If we do so, we can get results like these:
---- These have the same results ----
{"response":200,"result":[[1],[0],[0],[1],[1],[1],[0],[1],[0],[1]],"seed":"0"}
{"response":200,"result":[[1],[0],[0],[1],[1],[1],[0],[1],[0],[1]],"seed":"1"}
---- These all have different results ----
{"response":200,"result":[[1],[1],[1],[1],[0],[1],[0],[1],[0],[1]],"seed":"1204605758"}
{"response":200,"result":[[1],[1],[1],[0],[0],[1],[0],[0],[1],[1]],"seed":"4294967296"}
{"response":200,"result":[[1],[0],[0],[1],[0],[0],[0],[1],[1],[0]],"seed":"4294967297"}
{"response":200,"result":[[0],[1],[1],[1],[0],[1],[0],[1],[0],[0]],"seed":"5499573054"}
{"response":200,"result":[[0],[1],[1],[1],[1],[1],[0],[0],[0],[1]],"seed":"8589934592"}
{"response":200,"result":[[1],[1],[0],[0],[1],[1],[0],[1],[0],[0]],"seed":"9223372036854775808"}
{"response":200,"result":[[0],[1],[1],[1],[0],[0],[0],[1],[1],[0]],"seed":"18446744073709551616"}
{"response":200,"result":[[1],[0],[0],[0],[0],[0],[1],[0],[0],[1]],"seed":"18446744073709551617"}
{"response":200,"result":[[1],[0],[0],[1],[0],[0],[0],[0],[0],[0]],"seed":"18446744074914157374"}
{"response":200,"result":[[0],[1],[1],[0],[1],[0],[0],[1],[1],[0]],"seed":"340282366920938463463374607431768211456"}
---- These all fail ----
{"response":500,"result":"-1 fell through ETYPECASE expression.\nWanted one of (NULL (UNSIGNED-BYTE 32) UNSIGNED-BYTE).","seed":"-1"}
{"response":500,"result":"-2 fell through ETYPECASE expression.\nWanted one of (NULL (UNSIGNED-BYTE 32) UNSIGNED-BYTE).","seed":"-2"}
It also wouldn't hurt for seeded-random-state to check for that case. Likewise, get-random-state should probably check for negative values: they fail at the same etypecase within, but this seems mostly harmless, especially since the documentation says a non-negative value is required. Arguably, it's inappropriate to accept 0/treat it as 1, but again, probably not a big deal.
Here's the program that I used to generate the results above:
#!/bin/bash
set -euo pipefail
verbose="not-verbose"
host="localhost"
port="5000"
pgm_text="
DECLARE ro BIT[1]
H 0
MEASURE 0 ro[0]
"
while [[ "$#" -gt 0 ]]; do case $1 in
-h|--host) host="$2"; shift ;;
-p|--port) port="$2"; shift ;;
-v|--verbose) verbose="verbose" ;;
*) echo "Unknown argument: $1"; exit 2 ;;
esac; shift; done
send_pgm() {
seed="$1"
data="$(jq -nc --argjson seed "${seed}" --arg pgm "${pgm_text}" '
{
"type": "multishot",
"trials": 10,
"addresses": {"ro": true},
"rng-seed": $seed,
"quil-instructions": $pgm
}
')"
if [[ "${verbose}" == "verbose" ]]; then
printf " %s\n" "${data}"
fi
curl "${host}:${port}" --json "${data}" --silent -w '%{json}' | \
jq -csS --arg seed "${seed}" '{seed: $seed, response: .[1].response_code, result: (.[0]?.ro // .[0].status)}'
}
printf -- "---- These have the same results ----\n"
send_pgm 0
send_pgm 1
send_pgm $(python3 -c "print(2**32+1)")
send_pgm $(python3 -c "print(2**64+1)")
printf "\n---- These have the same results (but different from above) ----\n"
send_pgm 1204605758
send_pgm $(python3 -c "print(2**32+1204605758)")
send_pgm $(python3 -c "print(2**64+1204605758)")
printf "\n---- These all fail ----\n"
send_pgm $(python3 -c "print(2**32)")
send_pgm $(python3 -c "print(2**33)")
send_pgm $(python3 -c "print(2**63)")
send_pgm $(python3 -c "print(2**64)")
send_pgm -1
send_pgm -2