Skip to content

Simplify the certified-transaction wire format, and test #146 against a real aggregator - #147

Merged
MastaP merged 8 commits into
service-timefrom
review/146-followups
Aug 26, 2026
Merged

Simplify the certified-transaction wire format, and test #146 against a real aggregator#147
MastaP merged 8 commits into
service-timefrom
review/146-followups

Conversation

@MastaP

@MastaP MastaP commented Aug 25, 2026

Copy link
Copy Markdown
Member

Follow-up to #146, targeting service-time so it can be folded in before that PR merges.

Two things prompted this: a review of #146 cross-checked against the aggregator source at 0c7f70b0ccbc, and the observation that a change to four wire formats plus a new leaf derivation had no coverage against a real aggregator. The only aggregator #146 exercises is the in-repo fake, which derives leaf values with the very code under test.

Read the first section before reviewing. It corrects a security claim the first version of this PR made incorrectly.

BREAKING CHANGES — releases as 3.0.0

package.json moves 2.1.0 → 3.0.0. The formats this branch changes are shared with the Unicity Service and move in both directions at once, so ^2.1.0 must not resolve to it.

Existing tokens stop loading

Token.VERSION is now 2. Token.fromCBOR rejects a token minted by 2.x with Unsupported Token version: 1. There is no migration path — affected tokens have to be re-minted. Four of the five wire versions move:

Structure 2.x 3.0
Token 1 2
MintTransaction 1 2
TransferTransaction 1 2
CertificationData 1 2
InclusionProof 1 1 — unchanged, matches the Go implementation

Aggregator compatibility

A 3.0 client requires ghcr.io/unicitynetwork/aggregator-go:sha-ae08165 or later. The certified leaf value is now SHA-256(CBOR([transactionHash, referenceTime])) rather than the transaction hash alone, so a 3.0 client cannot verify proofs from a 2.x-era service, and a 2.x client cannot verify proofs from a current one.

Compile-time breaks

Change Effect
InclusionProofVerificationRule.verify drops its referenceTime parameter read from the inclusion proof instead
InclusionProofVerificationStatus.REFERENCE_TIME_MISMATCH removed replaced by REFERENCE_TIME_AFTER_ROUND and INCOMPLETE_INCLUSION_PROOF; a downstream switch breaks
Certified mint/transfer CBOR: 3 → 2 elements the reference time is no longer stored beside the proof that already carries it
Worker verification format: 4 → 3 elements expiresAt recovered from the transfer bytes it is committed to
expiresAt validated at the factories a negative, zero or over-wide deadline throws at create instead of failing later inside CBOR encoding

Additive

expiresAt on MintTransaction.create, TransferTransaction.create and TokenSplit.split; TransferTransaction.expiresAtFromCBOR; validateExpiresAt. version getters restored on MintTransaction, TransferTransaction and CertificationData#146 had removed them while nine other wire types kept theirs.

Documentation

README gains an "Upgrading to 3.0" section and a "Request deadlines" section — the feature this release exists for had no user-facing documentation. unicity-token-protocol-spec.md gains §2.7 on deadlines. Note the rest of that spec is stale independently of this branch: it still describes "Request IDs" and "Authenticators", which the code has not called that since 2.0. Refreshing it is its own piece of work and is not attempted here.

Request deadlines: what verification can and cannot establish

This section was wrong in the first version of this PR, and is corrected here. The original review of #146 proposed a signed upper bound on the leaf reference time as a fix for back-dating, and I implemented it repeating that claim. Codex flagged it as a P1 and was right.

InclusionProofVerificationRule now rejects a leaf claiming to postdate the round that certified it (REFERENCE_TIME_AFTER_ROUND). Consensus signs the round timestamp, which is that round's own reference time, so the bound is free and exact: buildCertificationInputRecord in the aggregator sets InputRecord.Timestamp = referenceTime, and against a live service the two are equal on first fetch and diverge only as later rounds certify.

It does not stop back-dating, which is the direction that matters. A service that receives a request after its deadline T can insert the leaf now and write referenceTime = T - 1 into it: the expiry check passes because T - 1 < T, the new bound passes because the certifying round's timestamp is later still, and the SMT path authenticates the value the service chose rather than the moment it chose it. Verified concretely — deadline 1000, leaf back-dated to 999, round clock 5000 → the rule returns OK. There is a test pinning exactly this, so the gap stays visible and fails loudly if it is ever closed.

The check stays because a leaf postdating its own round is an impossible pairing worth rejecting, and it already caught a fixture that paired a leaf with a round timestamp of zero. Only the claim about it changed.

Closing the real gap needs signed evidence of the creation round, which an inclusion proof does not carry — an aggregator-side protocol change, not something this SDK can do alone. In the meantime expiresAt is an instruction to an honest service (a late request is dropped rather than executed), not something a verifier can prove after the fact; that is now stated in the README, and it is worth deciding at protocol level whether it should be more.

Dropping the duplicated reference time

The rule now reads the reference time off the proof instead of being handed it separately. The aggregator records the leaf's creation time on the record and serves that same value for every proof of that leaf, so the copy the certified transactions carried could never legitimately differ — it cost a wire element and three consistency checks that could only ever agree.

  • Certified mint and transfer arrays: 3 elements back to 2.
  • Token.VERSION 1 → 2, since the element counts underneath it changed. Without it a token written by an older SDK passes the version check and then dies on a CBOR array-length error that never mentions versioning.
  • The worker wire format loses its separate expiresAt element and recovers the deadline from the transfer bytes the transaction hash commits to, via a new TransferTransaction.expiresAtFromCBOR. The old copy travelled outside those bytes and was never cross-checked against them.
  • InclusionProofVerificationStatus.REFERENCE_TIME_MISMATCH is gone — there is no caller-supplied value left to disagree with the proof.

Other fixes from the review

  • Partial proofs. A proof that is present but structurally impossible read as "keep polling", so the wait ran to its own deadline and reported a timeout naming neither the missing field nor the service that dropped it. The rule now separates all three leaf fields absent (pending, poll through) from any partial combination, each reporting what is missing. New status INCOMPLETE_INCLUSION_PROOF.
  • Pending status. Binding a transaction to a proof for an uncertified state reports INCLUSION_CERTIFICATE_MISSING again. A guard in both certified-transaction factories was intercepting it and reporting a missing reference time, which no retry path recognises.
  • Deadline validation. expiresAt was unvalidated at the factory boundary: -1n and 2n ** 70n were accepted and only failed later inside the transaction hash with a bare CborError, and 0n was accepted outright and produced a request expired by construction. A shared validateExpiresAt now runs in MintTransaction.create, TransferTransaction.create and TokenSplit.split. Its upper bound matches the encoder exactly — 2^64-1 encodes, 2^64 throws.
  • Version getters restored on MintTransaction, TransferTransaction and CertificationData. Nine other wire types still expose theirs; these three lost theirs in the same change that altered their shape.

Integration suite against a real aggregator

Testcontainers starts the stack in tests/integration/docker — a BFT root node, mongodb, redis and a pinned aggregator-go build (sha-ae08165) — waits for consensus to certify a round, and tears it down when the run ends:

npm run test:integration

No environment, no separate up/down. The aggregator is published on an ephemeral port, so concurrent runs and CI jobs cannot collide, and a failed run does not leave containers behind.

That costs a cold start of about a minute. scripts/integration-aggregator.sh stays for iteration: start the stack once, eval "$(./scripts/integration-aggregator.sh env)", and runs drop to ~20s. A stack the run did not start is one it does not stop.

Waiting for the container healthcheck is not sufficient to run against — until consensus hands the aggregator a reference time it answers every certification request with SERVICE_NOT_READY — so setup blocks on a certified block instead.

These are integration tests, not e2e: they own the service they talk to and depend on nothing outside docker. tests/e2e keeps its existing meaning, a suite pointed at a deployed network, and is unchanged apart from the transition-flow helper it shares. The integration suite runs in CI (new integration job); the e2e suite does not, since it needs a live network to be pointed at.

This is where the wire formats get checked. Certification data, the transaction encodings, the inclusion proof and the reference-time-bound leaf value are all shared with the service, and the fake aggregator in tests/functional derives them with the very code under test.

Why that matters, concretely: revert calculateLeafValue to its pre-#146 form — a change that breaks every real submission — and the fake-aggregator suite passes 25/25 while the integration suite fails.

tests/integration/RequestDeadlineTest.ts covers the deadline semantics on real data: explicit deadline through mint → transfer → verify, REQUEST_EXPIRED for a past deadline and for one equal to an observed reference time (exclusivity), the service-assigned branch binding a deadline without recording it, the signed round bound in both directions, and the reference time staying pinned while the certifying round moves on. The transition flow runs against the stack too, under both deadline modes.

Three upstream compose bugs had to be worked around: the x-bft anchor materialises a stray container, the mongo healthcheck reports ready before a primary is elected, and the image's own healthcheck spiders /health with HEAD against a GET-only route so it never passes.

On the aggregator image

Pinned to sha-ae08165 (2026-08-26), up from sha-0c7f70b. The one commit between them — unicitynetwork/aggregator-go#182 — adds request/state-ID, certified-shard and network bindings to the Go reference verifier and returns referenceTime on block records. The CBOR wire format is unchanged, and the SDK already had all three bindings: network at UnicityCertificateVerifier.ts:72, shard in ShardIdMatchesStateIdRule, and the state-ID binding by deriving it from the certification data rather than trusting the requested key. Nothing to add; 11/11 pass against it.

Its docs/inclusion-proof-wire.md also states "Do not recover τ from UC.IR.t. Use the referenceTime element. They coincide only for the proof issued in the leaf's own round" — which is exactly the relation this PR relies on.

Other test changes

  • The shared transition flow runs under both an explicit and a service-assigned deadline, in the functional, integration and e2e suites alike. The expiresAt helper parameter added in Bind service time and enforce request timeouts #146 had no caller, so the explicit path was constructed but never exercised end to end.
  • The fake aggregator models the service-assigned deadline (referenceTime + RequestTTL) and SERVICE_NOT_READY. The null-deadline branch is what every caller in this repo actually uses, and it was tested against a model that could not reject it.
  • UnicityCertificateFixture defaulted its round timestamp to 0n, pairing leaves with rounds that could not have certified them — an impossible combination the rule now rejects. It defaults to the leaf's reference time.
  • CertificationDataTest's expect(hex).toContain('f6') passed regardless of how the absent deadline was encoded; it decodes the actual element now.
  • New: CertifiedTransactionWireTest (element counts, version rejection, pending-status regression), unit/transaction/ExpiresAtTest, partial-proof cases in InclusionProofUtilsTest, worker payload shape, split deadline validation.

Verification

build, build:check and lint clean. 192/192 unit + functional. 11/11 integration on sha-ae08165 — verified both ways, Testcontainers starting the stack from cold (~84s, nothing left behind afterwards) and reusing an externally started one (~22s, left running). 7/7 e2e when pointed at that same stack.

Negative controls: removing the new round-timestamp bound fails both the unit and the integration test for it; reverting the leaf derivation fails the integration suite while the functional suite passes whole; dropping expiresAt from the worker payload fails exactly the one worker test that covers it.

Bind the leaf reference time to the signed round it was certified in, and
drop the copy of it that travelled beside the proof.

The service alone picks a leaf's reference time and the SMT path proves
only that it committed to whatever it picked, so on its own that lets a
service back-date a leaf and admit a request its deadline should have
killed. Consensus signs the round's timestamp, which is that round's
reference time, and a leaf cannot predate the round certifying it, so
InclusionProofVerificationRule now enforces that as an upper bound.

The rule reads the reference time off the proof instead of being handed
it. The service records the leaf's creation time on the record and serves
that same value for every proof of the leaf, so the copy the certified
transactions carried could never legitimately differ from it; it cost a
wire element and three consistency checks. Certified mint and transfer
arrays are back to two elements and Token.VERSION goes to 2, so a token
written by an older SDK is rejected by the version check rather than by a
CBOR array-length error further down. The worker wire format loses its
separate expiresAt element too and recovers the deadline from the
transfer bytes the transaction hash commits to.

Also:

- Report what a partially present proof is missing rather than folding it
  into "not certified yet", which left the caller polling to its own
  deadline and blaming the timeout. Binding a transaction to a proof for
  an uncertified state reports INCLUSION_CERTIFICATE_MISSING again, the
  status retry paths branch on.
- Validate expiresAt where it is accepted. Negative, zero and oversized
  deadlines were taken and surfaced much later as a bare CborError from
  inside the transaction hash.
- Restore the version getters on MintTransaction, TransferTransaction and
  CertificationData; nine other wire types still expose theirs.

Tests:

- An e2e suite against a real aggregator, with the stack to run it:
  tests/e2e/docker brings up a BFT root node, mongodb, redis and a pinned
  aggregator build, driven by scripts/e2e-aggregator.sh. It is not wired
  into CI. Nothing but a real service can tell whether the SDK and the
  aggregator still agree on these formats: with the leaf derivation
  reverted to its pre-change form, the fake-aggregator suite passes
  whole and the e2e suite fails.
- The transition flow now runs under both an explicit and a
  service-assigned deadline, so the explicit path is exercised end to
  end rather than only constructed.
- The fake aggregator models the service-assigned deadline and
  SERVICE_NOT_READY, the branches every caller in the repo actually uses.
- Fixture certificates now certify a round whose clock matches the leaf,
  instead of pairing a leaf with a round timestamp of zero.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 242d7a1fe4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// this is a signed upper bound available for free. The tree is append-only,
// so a proof re-fetched later is certified by a later round and the bound
// only loosens.
if (referenceTime > inclusionProof.unicityCertificate.inputRecord.timestamp) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the reference time to the leaf's creation round

When a malicious or misbehaving service receives a request after deadline T, it can commit a new leaf with referenceTime = T - 1; both the expiration check and this comparison pass because the current certifying round's timestamp is later than T - 1. The SMT path authenticates the service-chosen value but not when the leaf was created, so requiring the value to be no later than a current or later proof's round does not prevent the backdating attack described by this change. Verification needs signed evidence of the leaf's creation-round time (or an equivalent admission guarantee), rather than this one-sided upper bound.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and fixed in 3fb2a4b. You're right: the bound is one-sided and the useful direction is the other one.

I verified it concretely rather than reasoning about it — deadline 1000, leaf back-dated to 999, certifying round clock 5000:

deadline=1000 backdated_leaf=999 round_clock=5000 -> OK

The expiry check passes because 999 < 1000, this bound passes because 999 < 5000, and the SMT path authenticates the value the service chose rather than the moment it chose it. The attack survives exactly as you describe.

The origin of the error is worth recording: the review of #146 that prompted this PR proposed referenceTime <= inputRecord.timestamp as the fix for back-dating, and I implemented it while repeating that claim in the code comment, the commit message and the PR description. All three are now corrected.

I kept the check, because a leaf postdating the round that certified it is an impossible pairing worth rejecting and the bound costs nothing — it already caught a test fixture pairing a leaf with a round timestamp of zero. Only what it claims has changed. There is now a test pinning the back-dating gap so it stays visible and fails loudly if the protocol ever closes it, and README.md states that expiresAt is enforced by an honest service at admission and is not something a verifier can prove after the fact.

On your last point — "verification needs signed evidence of the leaf's creation-round time (or an equivalent admission guarantee)" — I agree, and that is an aggregator-side protocol change rather than something this SDK can do on its own: the proof would have to carry the certificate of the round that created the leaf, or bind a round number verifiable against the certificate chain. Worth raising against aggregator-go separately; this PR does not attempt it.

@MastaP
MastaP requested review from martti007 and ristik August 25, 2026 15:58
The bound added in the previous commit was described as preventing a
service from back-dating a leaf to slip a request past its deadline. It
does not, and Codex was right to flag it: it bounds the reference time
from above, and the attack needs a bound from below.

A service that receives a request after its deadline T can insert the
leaf now and write referenceTime = T - 1 into it. The expiry check passes
because T - 1 is below the deadline, the new bound passes because the
certifying round's timestamp is later still, and the SMT path
authenticates the value the service chose rather than the moment it chose
it. Verified: with a deadline of 1000, a leaf back-dated to 999 and a
round clock of 5000, the rule returns OK.

The check stays, because a leaf postdating the round that certified it is
an impossible pairing worth rejecting and the bound is free — it already
caught a fixture that paired a leaf with a round timestamp of zero. Only
the claim about it changes.

- Rewrite the rule's comment to say what it does establish, and to state
  plainly that it does not bound back-dating and why.
- Add a test pinning the gap, so it stays visible in the codebase and
  fails loudly if the protocol ever closes it.
- Correct the same claim in the unit and e2e test comments.
- Document in README.md that expiresAt is enforced by an honest service
  at admission and is not something a verifier can prove after the fact.

Closing this properly needs signed evidence of the creation round, which
an inclusion proof does not carry — an aggregator-side protocol change,
not something this SDK can do on its own.
@MastaP MastaP changed the title Bound the leaf reference time to its signed round, and test against a real aggregator Simplify the certified-transaction wire format, and test #146 against a real aggregator Aug 25, 2026
MastaP added 2 commits August 26, 2026 15:33
These tests are not end-to-end. They bring up their own aggregator in
docker and talk to nothing outside it, which is what an integration suite
is; e2e here means the deployed testnet, and that suite already exists
and stays as it is.

- tests/e2e/docker -> tests/integration/docker, and the request-deadline
  suite with it. scripts/e2e-aggregator.sh -> integration-aggregator.sh,
  and the npm scripts follow: integration:up, integration:down,
  test:integration. test:ci ignores the new directory.
- The integration suite finds its own stack: it defaults to localhost and
  to the genesis the root node generates on first start, so it needs no
  environment setup, and it says so rather than failing obscurely when
  the stack was never started. The e2e suite keeps taking an endpoint and
  a trust base, since it has no service of its own.
- Run the transition flow against the local stack too, under both
  deadline modes, so the flow is covered without a deployed network.

Aggregator image moves to sha-ae08165, which adds request/state-ID,
certified-shard and network bindings to its own reference verifier. The
CBOR wire format is unchanged, and the SDK already had all three: the
network binding at UnicityCertificateVerifier.ts:72, the shard binding in
ShardIdMatchesStateIdRule, and the state-ID binding by deriving it from
the certification data. Nothing to add; 11/11 pass against it.

The image also surfaced a cold-start race: the aggregator gives up
creating its indexes while the fresh replica set is still electing, then
restarts into a working state, which `compose up --wait` reports as a
failure. Its mongo timeouts are raised to ride out the election, and `up`
treats certification, not `--wait`, as the gate.
The stack was started by a shell script and stopped by another, which
works on a laptop and not much else: the port was fixed, so two runs
collided, and a failed run left containers behind. Testcontainers starts
it from the same compose file, publishes the aggregator on an ephemeral
port, and tears it down when the run ends.

  npm run test:integration

is now the whole thing — no environment, no separate up and down. It
costs a cold start of about a minute, so the script stays for the case it
was actually good at: start the stack once, point the suite at it with
`eval "$(./scripts/integration-aggregator.sh env)"`, and iterate at ~20s
a run. A stack the run did not start is also one it does not stop.

Waiting for the container to be healthy is not enough to run against:
until consensus hands the aggregator a reference time it answers every
certification request with SERVICE_NOT_READY, so setup blocks on a
certified block rather than on the healthcheck.

The support files are .mjs because the babel transform would otherwise
rewrite globalSetup to CJS and then fail to require its own ESM imports;
that also puts them outside the TS project, so they join tests/utils/*.mjs
in the lint ignores.

CI gets an integration job. It has no external dependency now — the
ephemeral port means concurrent jobs cannot collide, and nothing is left
running for the next one to trip over. The e2e suite stays out, since it
needs a deployed network to be pointed at.
@MastaP

MastaP commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 683d32c6c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/npm.yml
Comment on lines +67 to +68
integration:
name: Integration against a real aggregator

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate publishing on the integration job

When the real-aggregator integration suite fails but the unit/build job succeeds, this sibling job does not prevent the build job's main-branch or release steps from publishing to npm; GitHub Actions runs jobs without needs independently, so an incompatible package can be published before the workflow ultimately turns red. Make publication depend on successful completion of this integration job, or move the publish steps into a downstream job.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 50e0c16. You're right, and this one is on me for making it worse: before this PR there was no integration job at all, so adding one that looks like a gate without being one is arguably worse than not having it — a reviewer reads the workflow and assumes releases are covered.

Publishing now lives in its own job:

publish:
  needs: [build, integration]
  if: github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main')

needs is the gate; the event conditions only choose which kind of release it is. I took the second of your two options — a downstream job rather than needs: integration on build — so that PR feedback stays parallel: making build wait on integration would serialise every PR for a constraint that only matters on main and on release.

Two consequences of the split worth noting for review:

  • id-token: write is only needed to publish, so build and integration no longer request it.
  • The publish job re-runs npm ci and npm run build, since jobs don't share a filesystem and files in package.json ships lib/. That's ~45s of duplicated work, which seemed the right trade against publishing an unbuilt package.

One thing I checked while making the change: I had initially renamed the build job to "Build and test", which would have changed the reported check name. There is no branch protection on this repo today, so nothing would have broken — but a future required check configured as build would have silently never matched. Reverted; the job keeps its bare name, and only the job body changed.

MastaP added 4 commits August 26, 2026 16:50
The publish steps lived in the `build` job, and `integration` was a
sibling with no dependency between them. GitHub runs jobs without `needs`
independently, so on a push to main `build` published as soon as its own
steps went green — whether or not the integration suite had finished, or
passed. Adding that job made the workflow look like it gated releases
while it did not, and an npm publish cannot be taken back.

Publishing moves into its own job with `needs: [build, integration]`. The
event conditions stay where they were and now only choose which kind of
release this is; `needs` is what gates it.

Two things fall out of the split. `id-token: write` is needed only to
publish, so the two test jobs no longer request it. And the publish job
re-runs `npm ci` and `npm run build`, because jobs do not share a
filesystem and `files` in package.json ships `lib/`.

The `build` job keeps its bare name, so the check name PRs have always
reported does not change under anyone's branch protection.

Reported by Codex on #147.
The formats this branch changes are shared with the Unicity Service, and
they move in both directions at once: a token minted by 2.x no longer
loads, and a 3.0 client and a 2.x-era aggregator cannot verify each
other's proofs. Token, MintTransaction, TransferTransaction and
CertificationData all go to wire version 2, and the certified leaf value
becomes SHA-256(CBOR([transactionHash, referenceTime])) rather than the
transaction hash alone. Under semver that is a major, and the package is
published with real consumers on 2.1.0, so `^2.1.0` must not resolve to
it.

README gains an "Upgrading to 3.0" section covering what stops working —
existing tokens, aggregator compatibility, and the compile-time breaks
for anyone building on the verification internals — and a section on
request deadlines, which had no user-facing documentation at all despite
being the feature this release exists for.

The protocol spec gains a section on deadlines in its own conceptual
register: what a round's reference time is, why admission is exclusive,
how an explicit deadline differs from a service-assigned one, and what a
deadline does not guarantee against a dishonest service.

Note the rest of that spec is stale independently of this change — it
still describes Request IDs and Authenticators, which the code has not
called that since 2.0. Refreshing it is its own piece of work.
The option interfaces a caller writes to said "Unix seconds", but the
getters a caller reads did not, and neither did the verification rule,
the predicate verifier, the leaf value helper or the protocol spec. Both
values are wall-clock instants, and both could plausibly have been round
numbers or block heights to someone reading only the types — they are
bigints named after time, sitting next to an InputRecord that really does
carry a round number.

Units added to the expiresAt getters on CertifiedMintTransaction,
CertifiedTransferTransaction and TransferTransaction, and to the six
places reference time is documented.

The comparison itself now records something that was written down
nowhere: both sides are consensus time, not the caller's. A round's
reference time is the timestamp of the BFT seal, so a deadline derived
from a local clock is compared against the root chain's, and the two
differ by seconds. README says the same and adds the practical
consequence — leave margin for skew and queue time; hour-scale deadlines
do not care, second-scale ones do. The spec section says it too, and now
states that neither value is a round number or block height.
The three support files are the Testcontainers integration, not leftovers
from before it: aggregatorStack.mjs is what constructs the
DockerComposeEnvironment, and globalSetup/globalTeardown are the Jest
hooks that start and stop it. What was left over was the shell path
beside them.

scripts/integration-aggregator.sh and the integration:up and
integration:down scripts are gone, and with them the branch that reused a
stack the run did not start. That branch existed to make the script's
workflow fast, and without the script its only remaining effect would be
to let a stray AGGREGATOR_URL silently redirect the suite — a green run
that proved nothing about the compose file it exists to exercise.

IntegrationConfig follows: it now requires the endpoint and trust base the
setup hook publishes rather than defaulting to localhost and a guessed
path, and says how to run the suite when they are missing. Pointing the
SDK at a service someone else runs is what the e2e suite is for, and that
distinction is now enforced rather than merely described.

Verified from cold with nothing pre-started: 11/11, no containers and no
genesis directory left behind.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant