Ecosystem metrics
- New Repos
- 14
- down 44.0%
- Commits
- 974
- up 21.4%
- Releases
- 11
- up 83.3%
- Contributors
- 60
- up 30.4%
- Merges
- 16
- down 30.4%
Activity Overview
Commits and releases over time
- Commits
- Releases
- Authors
Repository Explorer
No repositories match that filter.
10618 commits in all time
May 15, 2026 14:32 – Aug 13, 2026 14:32 UTC
docs(PINNED_BUILD): the pin is the network's release, not staleness (#7)
Corrects the note added earlier on this branch, which was misleading in three ways. All three were found by follow-up verification and adversarial review after that note was written. 1. It said the pin is "several commits behind main" without the decisive fact: go-algorand's go.mod requires github.com/algorand/falcon v0.1.0, and that tag dereferences to exactly ce15e75b. It is the repo's ONLY tag. The pin is alignment with the code defining on-chain falcon_verify behaviour - which is the point for a signer whose output must be accepted by that verifier. main (956d9bc0) is 7 ahead / 0 behind, and UNRELEASED. 2. It called the PR #16 change "a malleability gap". Wrong term. Signature malleability means distinct valid signatures for one (key, message) pair; that does not occur on either build. It is non-canonical input acceptance in a parser. The original wording would have pointed an auditor at the wrong invariant. 3. It warned that TRELYAN's off-chain accept/reject "can diverge from what the chain accepts". Misleading: falcon_det1024_convert_compressed_to_ct is not bound by this SDK at all - trelyan_pq.falcon binds only shake256_init_prng_from_system, keygen, sign_compressed and verify_compressed. The affected function is unreachable from TRELYAN. It IS reachable in go-algorand via GetFixedLengthHashableRepresentation -> ConvertToCT, making it an upstream report rather than a local divergence. Also records that bumping was tested, not assumed: both trees built with FALCON_FPEMU=1 and compared over 120 signatures (12 keypairs x 10 message shapes) plus the committed KAT - byte-identical throughout, each build accepting the other's output. A bump would be safe but would make this repo stricter than the deployed verifier for no reachable benefit. Adds the more precise description of the underflow: sig_compressed_len - 2 wraps to SIZE_MAX on a 1-byte input, so the decoder receives a pointer past the buffer with a claimed length of 2^64-1. Docs only. Pinned commit and both digests unchanged. Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
3ff4f886
main
1/29 ++ 10 --
docs(PINNED_BUILD): the pin is the network's release, not staleness
Corrects the note added earlier on this branch, which was misleading in three ways. All three were found by follow-up verification and adversarial review after that note was written. 1. It said the pin is "several commits behind main" without the decisive fact: go-algorand's go.mod requires github.com/algorand/falcon v0.1.0, and that tag dereferences to exactly ce15e75b. It is the repo's ONLY tag. The pin is alignment with the code defining on-chain falcon_verify behaviour - which is the point for a signer whose output must be accepted by that verifier. main (956d9bc0) is 7 ahead / 0 behind, and UNRELEASED. 2. It called the PR #16 change "a malleability gap". Wrong term. Signature malleability means distinct valid signatures for one (key, message) pair; that does not occur on either build. It is non-canonical input acceptance in a parser. The original wording would have pointed an auditor at the wrong invariant. 3. It warned that TRELYAN's off-chain accept/reject "can diverge from what the chain accepts". Misleading: falcon_det1024_convert_compressed_to_ct is not bound by this SDK at all - trelyan_pq.falcon binds only shake256_init_prng_from_system, keygen, sign_compressed and verify_compressed. The affected function is unreachable from TRELYAN. It IS reachable in go-algorand via GetFixedLengthHashableRepresentation -> ConvertToCT, making it an upstream report rather than a local divergence. Also records that bumping was tested, not assumed: both trees built with FALCON_FPEMU=1 and compared over 120 signatures (12 keypairs x 10 message shapes) plus the committed KAT - byte-identical throughout, each build accepting the other's output. A bump would be safe but would make this repo stricter than the deployed verifier for no reachable benefit. Adds the more precise description of the underflow: sig_compressed_len - 2 wraps to SIZE_MAX on a 1-byte input, so the decoder receives a pointer past the buffer with a claimed length of 2^64-1. Docs only. Pinned commit and both digests unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34b480da
pull/7/head
1/29 ++ 10 --
Gate the release on the tests passing (#20)
release.yml had no test job. The graph was
build -> provenance -> cosign-sign -> pypi-publish
with `build`'s only substantive step being `python -m build`, and no occurrence
of `pytest` or verify_pinned_digest anywhere in the file. A version tag
therefore produced a signed, SLSA-attested, Rekor-logged artifact without one
test having run.
The attestations were never WRONG about this. SLSA provenance attests to build
provenance and does not claim tests passed, and RELEASES.md's verify commands
say exactly that. The gap is that "signed, attested, transparency-logged" reads
as "vouched for" to essentially every downstream consumer, and here it vouched
only for where the bytes came from.
Adds a `test` job and makes `build` need it, so nothing is built, signed or
attested until the suite is green.
Cross-workflow `needs:` does not exist in GitHub Actions, so ci.yml cannot gate
this workflow and the check has to be duplicated here. That is why this is a
small fast job rather than the full 3-OS matrix: a release gate has to be cheap
enough that nobody is tempted to route around it.
The step also asserts 15+ tests actually EXECUTED (17 locally). pytest exits 5
on "no tests collected", but a suite that silently shrank still exits 0 - and a
release gate that passes because nothing ran is worse than no gate at all,
because it ships a signature saying otherwise. Same reasoning as the guards in
the CI workflow, applied to the path that publishes.
Found by an adversarial claim-vs-enforcement sweep (TCE-23).
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
8082e6b4
main
1/52 ++ 0 --
Delete an inert workflow copy and fix two dangling audit references (#19)
AUDIT_READINESS.md:172 points an auditor at `sdk/ci/ci.yml` as the repo's CI definition. That file does not exist: git ls-files sdk/ci/ -> release.yml, verify_pinned_digest.py scripts/verify_all.sh:10 makes the same reference, claiming to "mirror the sdk/ci/ci.yml signature-kat job recipe EXACTLY". Both now point at .github/workflows/ci.yml, which is the file that actually runs. An auditor following the readiness doc hits a 404 on its first CI citation. sdk/ci/release.yml is deleted. GitHub Actions only executes YAML under .github/workflows/, so it never ran, and it had drifted into being the WORSE copy: ad20b42 SHA-pinned every action in the live workflow while this one kept floating tags - actions/checkout@v5 vs @fbc6f39... # v5.0.1 actions/setup-python@v6 vs @ece7cb0... # v6.0.0 actions/upload-artifact@v4 vs @ea165f8... # v4.6.2 actions/download-artifact@v4 vs @d3f86a1... # v4.3.0 sigstore/cosign-installer@v4.1.0 vs @ba7bc0a... # v4.1.0 softprops/action-gh-release@v2.6.2 vs @3bb1273... # v2.6.2 19 differing lines, all pins. An inert file is harmless right until someone copies it back - and this is the release workflow, the one that signs artifacts and publishes them. Also gitignores .venv-contracts/, the local virtualenv the contract suite needs. It was untracked and unignored, which is how a `git add -A` in this directory sweeps 1757 site-packages files into a commit. Nothing references the deleted file; verify_all.sh still parses (bash -n). Found by an adversarial claim-vs-enforcement sweep (TCE-15). Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
d52b0570
main
4/5 ++ 182 --
Run the contract suite in CI (#18)
The contract suite has never run in CI. It is the only thing that exercises
inscription.py at all - falcon_verify on-chain, the C1 ownership conjunction,
write-once, cross-cell replay, the update_owner guards - and the only record
that it passed was a hand-run note in LOCALNET_VALIDATION_2026-06-01.md, dated
two weeks BEFORE 2ec798e changed the contract.
So the flagship's on-chain logic has had no automated coverage of any kind,
and the drift in 2ec798e went unnoticed partly for that reason.
It was never wired up because it needs three things at once:
1. a LocalNet (algod on :4001) - without it: 22x urllib.error.URLError
2. the Falcon C library - without it: 19x "Could not load Falcon
shared library at './libfalcondet1024.so'"
3. the committed typed client - already in the repo
Both failure modes above were reproduced locally before writing this job, so
the recipe is derived from what actually breaks, not from guesswork. The
pinned compile venv (puyapy) is deliberately NOT needed: these tests deploy
the committed client, they do not rebuild the contract.
The library is built from the pinned tarball behind the same digest + FPEMU
gate signature-kat uses. These tests produce REAL det1024 signatures that the
AVM's falcon_verify opcode must accept, so the signer has to be the pinned
build for exactly the reason that job states.
Two guards, both from failures seen elsewhere in this workflow:
* LocalNet health is polled before the suite runs. A half-started sandbox
fails with connection errors that read like test failures, destroying the
signal this job exists to provide.
* The run asserts 20+ tests actually EXECUTED. pytest exits 5 on "no tests
collected", but a suite that silently shrank to a handful still exits 0 -
and "green meaning nothing ran" is the precise defect this job is here to
end.
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
e4675c93
main
1/80 ++ 0 --
Run the contract suite in CI
The contract suite has never run in CI. It is the only thing that exercises
inscription.py at all - falcon_verify on-chain, the C1 ownership conjunction,
write-once, cross-cell replay, the update_owner guards - and the only record
that it passed was a hand-run note in LOCALNET_VALIDATION_2026-06-01.md, dated
two weeks BEFORE 2ec798e changed the contract.
So the flagship's on-chain logic has had no automated coverage of any kind,
and the drift in 2ec798e went unnoticed partly for that reason.
It was never wired up because it needs three things at once:
1. a LocalNet (algod on :4001) - without it: 22x urllib.error.URLError
2. the Falcon C library - without it: 19x "Could not load Falcon
shared library at './libfalcondet1024.so'"
3. the committed typed client - already in the repo
Both failure modes above were reproduced locally before writing this job, so
the recipe is derived from what actually breaks, not from guesswork. The
pinned compile venv (puyapy) is deliberately NOT needed: these tests deploy
the committed client, they do not rebuild the contract.
The library is built from the pinned tarball behind the same digest + FPEMU
gate signature-kat uses. These tests produce REAL det1024 signatures that the
AVM's falcon_verify opcode must accept, so the signer has to be the pinned
build for exactly the reason that job states.
Two guards, both from failures seen elsewhere in this workflow:
* LocalNet health is polled before the suite runs. A half-started sandbox
fails with connection errors that read like test failures, destroying the
signal this job exists to provide.
* The run asserts 20+ tests actually EXECUTED. pytest exits 5 on "no tests
collected", but a suite that silently shrank to a handful still exits 0 -
and "green meaning nothing ran" is the precise defect this job is here to
end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c52fe337
pull/18/head
1/80 ++ 0 --
Say that the algo-pqc-kit KAT has produced zero comparisons (#24)
The module docstring reads as evidence: "ANY divergence in the Gaussian
sampler, rounding, or compressed encoding surfaces here as a byte mismatch.
That divergence-detection is the entire point of this KAT." Nothing in it has
ever run.
Two independent reasons, and the second is the one that gets missed:
1. Nothing installs algo-pqc-kit. pyproject declares only `algorand` and
`dev`; both CI test jobs run pip install -e ".[dev]". All three
differential tests carry @requires_apk and skip on every run.
2. Installing the package would not be enough. _ApkAdapter is wired to a
GUESSED upstream API shape and degrades to NotImplementedError -> skip if
that guess is wrong. The upstream API still has to be confirmed.
The fixture IS populated, so this is a working test wired to a dependency no
environment provides - which is why it reads green.
Adds TRELYAN_REQUIRE_INTEROP=1, mirroring TRELYAN_REQUIRE_KAT in
test_signature_kat.py, where a skip fails the job. CI does NOT set it, because
the dependency genuinely is not available. It exists so the inertness is
testable on demand rather than taken on trust, and so that pinning upstream
later is one environment variable away from being enforced.
No behaviour change: unset, the new test skips like the other three.
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
7d513e69
main
1/59 ++ 0 --
Stop passing a signature where the SDK expects a private key (#23)
interop_algo_pqc_kit.py called
c.inscribe(cell, artifact_hash, sig, b"ipfs://demo")
but inscribe()'s third positional parameter is a PRIVATE KEY: the client
re-derives M and signs internally (inscription.py:135-136). The signature
reached falcon.py:192 and raised "privkey must be 2305 bytes, got ...".
This happened in BOTH branches, so the flagship's documented interop example
had never run as shipped. Nothing caught it because no test in this repo
touches examples/ at all -- and the file's own "NOT yet executed end-to-end"
disclaimer was doing the job a test should have been doing.
The INTEROP_RECONCILED=True branch cannot be written against today's SDK
regardless of how the encoding question lands: submitting an externally
produced signature needs an inscribe_presigned() entry point, and none
exists. That branch now stops with an explicit error naming the missing API
instead of shipping code that cannot run.
Adds tests/test_examples_call_sites.py, which binds every example's SDK call
sites to the real inspect.signature and checks provenance: a value produced
by .sign() must never land in a `privkey` parameter. Arity alone cannot see
this -- a signature and a private key are both bytes and both fit the slot.
Security impact: none at runtime. Fail-closed, no key disclosure, nothing
deployed. The defect is that an audit-bound repo shipped an examples/ file
that could not execute, and had no mechanism that would ever say so.
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
c20ba21a
main
2/238 ++ 22 --
One build_message per side, and a test that compares them (#22)
There were FOUR copies of the signed-message construction and no test
compared any two:
sdk/src/trelyan_pq/message.py the SDK's, published to PyPI
contracts/falcon_det1024.py the contracts-side signer
contracts/deploy_testnet.py a local copy - deleted here
contracts/test_inscription.py a local copy - deleted here
The deleted copies were not harmless duplication. deploy_testnet.py's omitted
the length checks entirely:
return DOMAIN_TAG + app_id.to_bytes(8,"big") + cell_id.to_bytes(8,"big") \
+ artifact_hash + genesis
so a 31- or 33-byte artifact hash produced a 101- or 103-byte M and was signed
without complaint - in the script that signs REAL TestNet inscriptions. The
ABI's StaticArray[Byte, 32] stops that reaching the chain, which is why this is
low rather than high, but "a later layer catches it" is a mitigation, not a
reason for the earlier layer to be wrong.
Both files already imported falcon_det1024, so removing the local definitions
is a deletion, not a refactor.
Two implementations remain on purpose: contracts/ has to work without the SDK
installed. Two is a decision; four was an accident. What makes two safe is that
something compares them, which is what
sdk/tests/test_build_message_differential.py now does:
- both implementations agree across six inputs, including uint64 max and the
live TestNet vector
- both reproduce the exact M the chain accepted for cell 763809098, read from
the live inscription record - an anchor that is neither implementation's
opinion but what TestNet actually took
- both reject wrong-length artifact and genesis hashes, which is the
divergence that actually existed
- the two names resolve to different function objects, so the comparison
cannot become a function compared with itself
Mutation-proved: flipping the contracts-side cell_id to little-endian turns 5
of the 15 tests red. Restored: 15 passed.
Fixes AUDIT_READINESS.md S2.3, which cited three tests as covering this; each
exercised one implementation and none compared two.
Contract suite still 22 passed on LocalNet.
Note for reviewers: the SDK suite reports 3 unrelated errors on this machine -
PermissionError on pytest's tmp_path directory, in test_seal.py and
test_isolated_signer.py, both last modified in 2ec798e and untouched here. It
is local file-locking, not this change; CI runs on Linux.
Found by an independent re-check of the risk register (TCE-26).
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
751398e3
main
3/141 ++ 12 --
Test the freeze and manager guards at register_cell (#21)
register_cell asserts three things about a Cell ASA:
:205 clawback == zero_address
:206 freeze == zero_address
:207 manager == zero_address
Only :205 had a test. test_register_rejects_clawback_cell's docstring claims
to cover "clawback (or freeze/manager)" but its fixture sets only `clawback`,
and the asserts are sequential, so it trips :205 and the other two are never
evaluated. The words freeze and manager appear in this file only in prose.
Deleting :206 and :207 turns nothing red.
Not cosmetic. A live freeze address can freeze the holder AFTER registration,
and a live manager can RE-ADD clawback and freeze afterwards - which restores
both AssetHoldingGet timing vectors the hardening closed at the source. The
manager guard is the load-bearing one: it is what makes the cell's config
immutable rather than merely clean at registration time.
Adds one fixture per guard and asserts WHICH guard fired, so a future
reordering of the three asserts cannot silently leave one uncovered again.
VERIFICATION IS INCOMPLETE, and deliberately not overstated: unlike the other
contract test added in this series, these two are NOT mutation-proved. Docker
Desktop on this machine wedged part-way through (orphaned runtime sockets that
cannot be deleted without a reboot - my own hard-kill of the process is the
likely cause), so LocalNet is unavailable and the suite cannot run here.
What HAS been checked:
- both tests collect (24 collected, up from 22)
- the asserted strings match the contract's messages exactly, one occurrence
each in inscription.py and in the test
- the fixtures mirror test_register_rejects_clawback_cell, which passes
What has NOT been checked: that they fail when :206/:207 are deleted. The CI
job in the contract-suite PR runs LocalNet successfully on a GitHub runner, so
that is where this should be confirmed.
Found by an adversarial claim-vs-enforcement sweep (TCE-19).
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
f08d0926
main
1/46 ++ 0 --
Test the untested half of C1: the recorded owner who no longer holds (#17)
C1 is a conjunction - inscribe requires that the sender (a) currently holds
the Cell ASA AND (b) is the controlling owner recorded at mint - but only (b)
had coverage.
test_flash_custody_rejected builds a holder who is not the owner. Every other
test mints with mint_cell(algorand, admin, admin), so the sender is always
both, and mint_cell's transfer branch is itself dead. The state in which
`assert exists and balance == UInt64(1)` is the DECIDING assert - recorded
owner, no longer holding - was never constructed.
It is not a hypothetical. update_owner is a separate, voluntary call, so a
seller who transfers the Cell ASA on a secondary market stays the recorded
controlling owner until someone calls it. Without (a) that seller can inscribe
the buyer's cell; C2 makes inscription write-once and update_owner is blocked
afterwards, so the buyer's cell is PERMANENTLY consumed with the seller's
artifact. That is the exact inverse of the flash-custody vector the suite does
cover.
The test also asserts WHICH conjunct fired. A bare pytest.raises(Exception) -
the pattern every other negative test here uses - would pass if the rejection
came from (b) or from anything unrelated, leaving (a) untested while looking
covered.
MUTATION PROOF, end to end rather than by inspection:
- deleted the C1(a) assert from inscription.py
- recompiled with the pinned puyapy 5.8.1 and regenerated the typed client
- confirmed the string is gone from the TEAL and the client
- ran the suite against a LocalNet deployment of the mutated contract
1 failed, 22 passed
test_recorded_owner_who_no_longer_holds_is_rejected: DID NOT RAISE
The 22 pre-existing tests stayed green, exactly as predicted, and the failure
mode is the vulnerability itself: the inscribe SUCCEEDED. Restored and
re-verified: 23 passed.
Reproducing this needs LocalNet, a built Falcon library, and the pinned
compile venv; see the PR for the build notes.
Found by an adversarial claim-vs-enforcement sweep (TCE-14).
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
7cf9097a
main
1/45 ++ 0 --
Close the cwd library-hijack in the loader that actually signs (#15)
There are two ctypes loaders for the same Falcon signing core:
sdk/src/trelyan_pq/falcon.py used by the SDK
contracts/falcon_det1024.py used by deploy_testnet.py and
test_inscription.py
Commit eb67e4a (2026-08-10, "close two local library/module load-path
hijack vectors", pre-audit findings TF-01/TF-02) removed the cwd-relative
default. `git show --stat eb67e4a` confirms it touched only two files, both
under sdk/. contracts/falcon_det1024.py has not been modified since the
initial commit a806f63 and still read:
_LIB_PATH = os.environ.get("FALCON_DET1024_LIB", "./libfalcondet1024.so")
A dlopen path containing a slash resolves against the process working
directory, so that default loads keygen, sign and verify out of whatever
./libfalcondet1024.so happens to be in cwd (CWE-426).
The unfixed copy is the one on the documented deployment path: README.md
tells the operator to run `python contracts/deploy_testnet.py` from the repo
root, and that script imports this module and calls keygen(),
sign_compressed() and verify_compressed() to mint a real inscription. So an
explicit pre-audit security fix was applied everywhere except the path that
signs. Two aggravating details: .gitignore excludes *.so/*.dylib/*.dll, so a
planted library never appears in `git status`; and until yesterday the CI
path filter excluded contracts/ entirely, so nothing here was ever executed.
Fix: default to "" and fail closed with the same message the SDK uses.
sdk/tests/test_loader_parity.py plants a decoy libfalcondet1024.{so,dylib,dll}
in a temp directory, runs each loader from there in a subprocess with
FALCON_DET1024_LIB unset, and requires refusal BEFORE the decoy is touched -
asserting the error names the missing variable and does NOT name the decoy.
Checking the constant alone would only prove a literal changed. A second test
guards the constant too, so a re-introduced relative default is obvious in
review.
Note the SDK case needs m.keygen(), not m.default_signer(): the loader is
lazy, so constructing the signer loads nothing and the test would pass
vacuously. Verified by mutation - restoring the old default turns both the
behavioural and the source test red; the fix turns them green.
The test lives in sdk/tests/ because that is what CI runs; contracts/ has no
test job, and a parity test parked where nothing executes it is the same
defect it exists to prevent.
Found by an adversarial claim-vs-enforcement sweep (TCE-03).
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
7a66d334
main
2/143 ++ 1 --
Make the Falcon "unreachable" risk acceptance enforceable (#13)
The pinned build (ce15e75b = tag v0.1.0, the release go-algorand vendors) has a confirmed size_t underflow in falcon_det1024_convert_compressed_to_ct giving an unbounded read. TRELYAN's documented position is accept-and- document, and it rests on exactly one fact: the SDK never binds that function. The pin is deliberately NOT bumped - ce15e75b is the network's release, and the untagged fix commit would make TRELYAN stricter than the deployed on-chain verifier for no reachable benefit. That makes the acceptance only as durable as the binding surface, and nothing was watching it. A later commit adding CT-format support would convert a documented non-issue into a live out-of-bounds read silently. sdk/tests/test_binding_surface.py derives the bound symbol set by parsing the AST of _bind() and compares it to a declared allowlist. Note this is NOT the tautology it resembles: the two sides come from independent sources - one parsed from the source, one the security policy - so it can fail. A constant compared against itself cannot. The correct response to a failure is to justify the binding, not to edit the allowlist green. A second test sweeps all shipped sources textually, catching dynamic access (getattr(lib, ...)) that the AST check on _bind() alone would miss. A third asserts the extraction itself works, so the guard cannot pass vacuously. Static only - no shared library is loaded, so it runs in the no-native-lib CI job. Verified by mutation, not just by passing: injecting lib.falcon_det1024_convert_compressed_to_ct into _bind() turns both guards red with the quarantine reason attached; reverting turns them green. Full SDK suite 24 passed / 18 skipped (skips are the native-lib tests). Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
40d76fb5
main
1/154 ++ 0 --
Make deployment verification able to fail (#12)
* Make deployment verification able to fail
The deployed application has not matched the committed contract since
2ec798e (2026-06-16), and nothing could observe it. Three defects combined:
1. ci.yml filtered on sdk/** only, so no change under contracts/ ever
triggered CI. inscription.py was edited, merged and released without a
single test run.
2. verify_trelyan.py pinned EXPECTED_APPROVAL_SHA512_256 - a constant copied
from the deployed program - and compared it to the chain. The contract
blocks Update/Delete (I1/I5), so the deployed bytecode is immutable and
that comparison cannot fail. It was a tautology reported as a PASS, and
the weekly schedule has been re-proving it every Monday since June.
3. The pin was recorded 2026-06-17, one day AFTER the divergence, so it
captured the superseded value.
Changes:
- contracts/verify_deployment.py (new): derives the expected bytecode by
assembling the committed TEAL artifact and compares it to the deployed
program. No stored expected hash. Exit 1 = drift, exit 2 = could not
check; an unreachable node must read as neither agreement nor drift, and
an unexpected crash exits 2 rather than masquerading as a finding.
--compile-url lets a reviewer split the trust so one endpoint does not
both assemble and serve. --recompile re-derives the TEAL from
inscription.py so the artifact is not trusted either.
- ci.yml: add contracts/** to the path filters; add the contract-drift job.
- verify_trelyan.py: separate the two claims that were conflated. The pinned
constant now asserts only "the app was not replaced", which is what it can
actually support. Source correspondence is checked properly when the
committed artifact is present, and reported as NOT CHECKED when it is not,
rather than being silently covered by the weaker check.
Verified against live TestNet: drift detected (exit 1, committed source
builds to 112fcad3 / 667 B, chain serves d24d9071 / 660 B). The MATCH path
is reachable and was proven separately by round-tripping the deployed
program through algod disassemble/assemble (exit 0, byte-identical).
contract-drift and verify-live are both expected to be RED until the
committed contract is redeployed. That red is the finding, not a broken job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Reconstruct M from the artifact hash, not from the record header
Step [5] is the reviewer's "byte-exact reconstruction of the message M a
live inscription must have signed". It read the wrong bytes.
InscriptionRecord is an arc4.Struct, so the ARC4 head packs fixed-size
fields in declaration order:
version UInt8 rec[0:1]
cell_id UInt64 rec[1:9]
artifact_hash StaticArray[Byte,32] rec[9:41] <- the signed bytes
inscribed_round UInt64 rec[41:49]
inscriber Address rec[49:81]
payload_uri DynamicBytes rec[81:83] -> tail
The code used rec[0:32], which is version || cell_id || the first 23 bytes
of the hash. Confirmed against live TestNet cell 763809098: rec[0] == 1 and
rec[1:9] == 763809098, matching the box name exactly, so the layout is not
in doubt. The script displayed
rec[0:32] = 01000000002d86cd4a5680fc4ddf9d331b585198239ab41bdc768e3192efc477
rec[9:41] = 5680fc4ddf9d331b585198239ab41bdc768e3192efc477793a9daa1445dd4574
and rebuilt M from the first. No signature has ever covered those bytes.
It went unnoticed because the sole assertion was len(M_live) == 102, and
build_message validates both inputs are 32 bytes and then concatenates
fixed-width fields - so it either raises or returns exactly 102. The check
could not observe a wrong hash. Same shape as the approval-program pin
this branch already replaced: an assertion with no reachable failure.
Replaced with checks that can fail:
- version == 1
- the cell_id INSIDE the record equals the cell_id in the box NAME. Two
independent sources for one claim, and the check that actually pins the
offsets above. Mutation-tested: reading rec[0:8] instead of rec[1:9]
yields "record 72057594040911565 vs box 763809098" and fails.
- M matches the spec layout at every field offset, so a drift in
build_message surfaces instead of being absorbed.
Note the tempting check is NOT sufficient: comparing M's embedded hash to
art32 would have passed under the old bug too, since both sides shift
together. That is why the layout is pinned by the cross-source comparison.
Also states plainly that M is reconstructed, not verified: the signature is
a call argument the AVM checks, and is not stored on-chain, so no local
signature verification is possible from box state alone.
Live: 17 passed, 1 failed (the failure is the deployment drift). Standalone,
without the committed artifact: 17 passed, 0 failed.
Found by an adversarial claim-vs-enforcement sweep (TCE-02).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Report whether the Falcon library actually loaded, not whether the import worked
The reviewer script printed
full falcon verify available locally: YES (run verify(M, sig, pk))
unconditionally. Two defects in two lines.
`verify` is re-exported from trelyan_pq/__init__.py:48, so
`from trelyan_pq import verify` succeeds whether or not the C library exists
and the `except` branch was unreachable. The script therefore told every
reviewer that full local verification was available, including on machines
with no library at all - which is most of them, since building it is a
documented manual step.
And the hint it printed had the arguments backwards. The real signature is
verify(sig, pubkey, message)
so a reviewer following `verify(M, sig, pk)` passes the message where the
signature belongs and gets a failure that looks like a bad signature.
Now forces the load via default_signer()._lib_ref() and reports what actually
happened, including the underlying error when it did not load. Verified both
ways on this machine:
without FALCON_DET1024_LIB
full local falcon verify: NOT available (RuntimeError: FALCON_DET1024_LIB
is not set. Build the deterministic Falcon-1024 shared library ...)
with a locally built library
full falcon verify available locally: YES
(signature order is verify(sig, pubkey, message) - sig FIRST, not the message)
Uses a private accessor deliberately: the public API is lazy, so probing it
answers "did the import work", which is the question that produced the wrong
answer in the first place.
Also makes the remaining printed lines ASCII. This script runs in CI and on
reviewers' terminals; an em-dash mojibakes to a replacement char on a Windows
console codepage, and a verification tool should not look corrupted.
Found by an independent re-check of the risk register (TCE-09), which reported
the mechanism accurately after the original sweep's headline claim for that
finding had been refuted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
92373cc8
main
4/302 ++ 17 --
Let the secret scan actually scan history (#16)
ci.yml claimed "Full history is scanned, not just the diff, so a secret
introduced earlier is still caught". It was scanned on no automatic
trigger.
From gitleaks-action v2 at the pinned SHA, src/gitleaks.js:103-115, only
two event types get a commit range:
push --log-opts=--no-merges --first-parent <base>^..<head>
pull_request --log-opts=--no-merges --first-parent <base>^..<head>
Every other supported event (src/index.js:176 - schedule,
workflow_dispatch) runs with no --log-opts, which is what makes gitleaks
walk the whole history.
Observed on this repo's own runner, scanning two commits:
gitleaks detect --redact -v --exit-code=2 ... \
--log-opts=--no-merges --first-parent 6d7d771^..8aea860
And the job carried `if: github.event_name != 'schedule'`, excluding the
one automatic trigger that would have scanned everything. Verified against
scheduled run 31368848691: `Secret scan (gitleaks)` does not appear in it
at all. So push and PR each covered a handful of commits, the weekly run
covered none, and a secret committed before the pushed range was caught by
nothing. fetch-depth: 0 was already correct - the checkout was never the
problem, the trigger was.
This matters here specifically because the deployment path uses
DEPLOYER_MNEMONIC, an Algorand mnemonic that controls its account outright:
a leak in history is irrevocable, and history is exactly what was unscanned.
Removing the condition lets the Monday schedule perform the full-history
scan the comment already promised.
Independently checked before enabling, so the first scheduled run is not a
surprise: 35 commits, the only .env-shaped file ever added is .env.example,
and no private-key blocks or mnemonic assignments appear anywhere in
history. That check covers the highest-risk categories for this repo, not
gitleaks' full ruleset - if the first full run does flag something, most
likely an allow-list gap around the deliberate public test vector in
sdk/tests/vectors/, that is information rather than breakage.
Found by an adversarial claim-vs-enforcement sweep (TCE-10).
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
7bec0f4d
main
1/19 ++ 2 --
Run the lib-gated security tests, which never ran in CI (#14)
* Run the lib-gated security tests, which never ran in CI
Thirteen tests behind skipif(not _lib_available()) had never executed in
CI, on any event, since the workflow was written. The two jobs each had
half of what was needed and neither had both:
wire-format runs `pytest tests` (the whole suite) but never builds
the Falcon C library, so every lib-gated test skipped.
signature-kat builds the library on 3 OSes but ran only
`pytest tests/test_signature_kat.py`.
testnet-e2e builds the library, runs no pytest, and is manual-gated.
The intersection was empty. The never-run set is not incidental - it is
where the constitution's hardest crypto rules are actually enforced:
test_seal_wipes_private_key_buffer S2.3 zeroization
test_worker_stdout_carries_only_public_values S2.3 no secrets in output
test_isolated_result_has_no_private_key
test_isolated_require_locked_fails_closed_when_mlockall_unavailable
test_bad_build_fails_closed_without_consuming_cell
test_seal_signature_verifies_and_has_0xBA_header
test_second_seal_of_same_cell_raises
test_isolated_seal_produces_verifying_signature
test_isolated_second_seal_raises
+ 3 signature fuzzers and 1 interop cross-verify
Every green CI run has reported success while the private-key
zeroization check and the secret-leak-to-stdout check did not run.
This adds one step to signature-kat, the only job that builds the
library, running the full suite there.
The grep is not belt-and-braces, it is the substance: without it a failed
library build silently returns all thirteen tests to "skipped" and the job
goes green again, reintroducing this exact defect in the exact place it
was removed from. Passing must require that they RAN, not merely that
nothing failed. Same reasoning as TRELYAN_REQUIRE_KAT one step above,
which was applied to the KAT and never extended to its neighbours.
set -o pipefail so the tee does not swallow pytest's exit code.
Found by an adversarial sweep for claims that nothing mechanically
enforces; this was the highest-severity survivor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Export the audited symbols from the Windows Falcon DLL
Adding the previous step revealed why the Windows leg was skipping: the
DLL built, linked and loaded with an EMPTY export table.
MSVC exports nothing from a DLL without __declspec(dllexport) or an
explicit /EXPORT, whereas ELF and Mach-O export every non-static symbol.
The upstream Falcon sources carry no dllexport annotations. Evidence from
the failing run: the linker never printed "Creating library", which MSVC
emits only when at least one symbol is exported.
So on Windows every ctypes lookup raised, _lib_available() caught it under
a bare `except Exception`, and the tests skipped with the reason
"FALCON_DET1024_LIB not built/set" - which was FALSE. The log shows the
variable set correctly to a real file that exists. The diagnostic named
the one thing that was not wrong.
Consequence: the Windows leg of the 3-OS byte-identity matrix has verified
nothing since it was written, while reporting green. The cross-platform
reproducibility claim it exists to support (reviewer Q11/Q7, and the
"byte-identical on Linux, macOS and Windows" language in the KAT header)
held on two platforms of the three it names.
S2.6 forbids editing the audited reference, so the exports are declared at
link time rather than by annotating the C sources. The four names are
exactly the SDK's audited binding surface, which makes the Windows export
table an enforcement of that allowlist: a binding added without a matching
export now fails loudly here.
Not reproducible locally - no MSVC on this machine - so CI is the
verification. The preceding step's grep is what will confirm it: if the
exports work, no test can report "FALCON_DET1024_LIB not" and the job
goes green having actually run the cryptography on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fetch the pinned Falcon tarball fail-fast, with retries
The windows leg of run 31626290777 failed with:
tar: Error is not recoverable: exiting now
The archive was fine; the download was not. `curl -sL ... | tar xz` has
three defects that combine to produce exactly that misleading message:
-s without -f curl exits 0 on an HTTP error and pipes the error PAGE
into tar, so a failed download presents as a corrupt
archive
no pipefail the pipeline's exit status is tar's regardless
no retry a transient network blip fails the whole build
Split download and extract into separate commands, add -f so an HTTP error
is an error, --retry 3 --retry-delay 2 --retry-all-errors for transient
failures, and set -o pipefail. A future failure now names the component
that actually failed.
Applied to both occurrences (signature-kat and testnet-e2e).
Not a security fix: the pin, the digest gate and the FPEMU assertion that
follow are unchanged, and they remain what actually proves the fetched tree
is the audited one. This only stops a network blip from being reported as a
corrupt archive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Sellam <brandon.sellam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
52c142b9
main
1/67 ++ 3 --
Merge 0472447662b9238ff40cefa165a7f451e45a9891 into c89215d7048708c6f19274cb813911144da93eaa
ad153365
pull/836/merge
40/1,605 ++ 607 --
chore: There's no reason for Falcon1024Salt() to fail
3ad3b014
extensible-signing-api
1/1 ++ 1 --
Merge 727379471f6b43248b6860f22dc72eea86414790 into 1562a6f3ee4aaf0b23028855b9fce143ce229f2d
2444bbca
pull/678/merge
2/5 ++ 5 --
Merge 1f7d85d556424af3b3b785663bb18b9cd4a273b6 into 1562a6f3ee4aaf0b23028855b9fce143ce229f2d
36595d80
pull/676/merge
2/5 ++ 5 --
Merge ac89eb01b4d6502cfa6360e937679594250b056b into 1562a6f3ee4aaf0b23028855b9fce143ce229f2d
220eb13f
pull/675/merge
2/5 ++ 5 --
Merge ff7e237899650e2faaa501aa43802701a5af1268 into 1562a6f3ee4aaf0b23028855b9fce143ce229f2d
6799db6e
pull/677/merge
2/5 ++ 5 --
Merge e3b49727caa30e0610a59ca361374e04a761ab97 into 1562a6f3ee4aaf0b23028855b9fce143ce229f2d
76ce6c8a
pull/674/merge
2/5 ++ 5 --
chore: Add a note for VerifyLogicSig
b1432b27
extensible-signing-api
1/3 ++ 0 --