Summary
contractpolicyenforcer's PolicyCache is keyed by policy URL alone, with no record
version, digest, or content hash in the key or the entry. Where a DISCOM rotates a policy by
publishing a new version of an existing DeDi record — one of the two options the DISCOM
policy guide explicitly offers — the lookup URL does not change, so participants keep
enforcing the superseded policy until their own independent TTLs expire. The shipped devkit
sets cacheTTL: 172800 (48 h).
Because a cache hit short-circuits the fetch entirely, verifyChecksum never runs during that
window — the integrity mechanism is bypassed precisely in the case where the policy changed.
I want to be precise about scope: if a DISCOM rotates by publishing a new record
(version in record_name, as …-policy-v1 suggests), the URL changes, the cache key changes,
and everything behaves correctly. So this is a robustness gap that the guide's own wording
leaves open, not a guaranteed break. It seemed worth reporting because the failure is silent
and the fail-closed enforcement path makes the consequences asymmetric.
Evidence
plugins/contractpolicyenforcer/cache.go — the entry carries no version:
type cacheEntry struct {
pq rego.PreparedEvalQuery
query string
fetchedAt time.Time
}
and the hit test is freshness-by-clock only:
if ok && entry.query == query && time.Since(entry.fetchedAt) < c.ttl {
return entry.pq, nil
}
config.go enforces a floor of MinCacheTTL = 24 * time.Hour; the shipped
devkits/p2p-trading-ies-wave2/config/local-p2p-trading-sellerapp.yaml uses
cacheTTL: "172800" (48 h) together with violationActions: "select,init,confirm".
Meanwhile DeDi advertises a much shorter freshness expectation and records are mutable in
place. From the live registry today:
$ curl -s https://api.dedi.global/dedi/lookup/indiaenergystack.in | jq '{ttl, version_count, updated_at}'
{
"ttl": 600,
"version_count": 2,
"updated_at": "2026-07-22T03:19:09.375Z"
}
ttl: 600 vs. the adapter's 172800 is a 288× difference, and version_count: 2 on a
stable URL is the in-place rotation this issue is about.
The guide (specification/policies/discom-policy-guide/README.md §7.2) offers both branches
in one sentence:
To change the policy, publish a new tag and a new DeDi record version (or a new
record) — never rewrite a tag.
The first branch keeps the lookup URL constant. The second changes it. Only the second is safe
under the current cache key, and the guide does not say so.
Why it matters
Enforcement is per-participant and fail-closed on violationActions. Two adapters that warmed
their caches at different times can therefore evaluate different versions of the same policy
URL at the same moment, and nothing in the protocol surfaces the divergence — the NACK an
operator sees carries no indication that the counterparty was judging against different rules.
Since the policy is authored by a commercial counterparty and sets wheeling charges and
penalties, a settlement disagreement here is a billing disagreement.
Suggested fixes (in rough order of cost)
- Honour DeDi's
ttl. The registry already tells you how long its answer is good for;
the resolver could clamp cacheTTL to it, or at minimum warn when config exceeds it by an
order of magnitude.
- Include the resolved identity in the cache key — the record
version / digest, or a
hash of the fetched rego. Cheap to add to cacheEntry, and it makes staleness detectable
rather than silent.
- Revalidate rather than re-fetch. A conditional request (ETag /
If-None-Match) against
the DeDi lookup keeps registry load near the current level while bounding skew to minutes.
- Tighten the guide. If keying by URL is the intended contract, §7.2 should say plainly
that rotation requires a new record_name, and that updating a record version in place is
unsupported. That alone closes the gap at zero code cost.
Happy to send a PR for (2) if you'd like it — it is a small change to cache.go and the
existing tests in enforce_test.go cover the surrounding behaviour.
Environment
Read at main (plugins/contractpolicyenforcer/cache.go, 136 lines) on 2026-07-29; live DeDi
values fetched the same day.
Unrelated, noticed while verifying the above — say the word and I'll open it separately:
devkits/p2p-trading-ies-wave2/README.md (line 57) documents the policy record as
https://api.dedi.global/dedi/lookup/indiaenergystack.in/ies-policies/ies-p2p-network-settlement-rego-policy-v1,
which returns 404 {"message":"Record not found"} (3/3 attempts today), and the
ies-policies registry reports record_count: 0. With violationActions: "select,init,confirm" and fail-closed resolution, a devkit run pointed at the live registry
would NACK on select.
Summary
contractpolicyenforcer'sPolicyCacheis keyed by policy URL alone, with no recordversion, digest, or content hash in the key or the entry. Where a DISCOM rotates a policy by
publishing a new version of an existing DeDi record — one of the two options the DISCOM
policy guide explicitly offers — the lookup URL does not change, so participants keep
enforcing the superseded policy until their own independent TTLs expire. The shipped devkit
sets
cacheTTL: 172800(48 h).Because a cache hit short-circuits the fetch entirely,
verifyChecksumnever runs during thatwindow — the integrity mechanism is bypassed precisely in the case where the policy changed.
I want to be precise about scope: if a DISCOM rotates by publishing a new record
(version in
record_name, as…-policy-v1suggests), the URL changes, the cache key changes,and everything behaves correctly. So this is a robustness gap that the guide's own wording
leaves open, not a guaranteed break. It seemed worth reporting because the failure is silent
and the fail-closed enforcement path makes the consequences asymmetric.
Evidence
plugins/contractpolicyenforcer/cache.go— the entry carries no version:and the hit test is freshness-by-clock only:
config.goenforces a floor ofMinCacheTTL = 24 * time.Hour; the shippeddevkits/p2p-trading-ies-wave2/config/local-p2p-trading-sellerapp.yamlusescacheTTL: "172800"(48 h) together withviolationActions: "select,init,confirm".Meanwhile DeDi advertises a much shorter freshness expectation and records are mutable in
place. From the live registry today:
ttl: 600vs. the adapter's 172800 is a 288× difference, andversion_count: 2on astable URL is the in-place rotation this issue is about.
The guide (
specification/policies/discom-policy-guide/README.md§7.2) offers both branchesin one sentence:
The first branch keeps the lookup URL constant. The second changes it. Only the second is safe
under the current cache key, and the guide does not say so.
Why it matters
Enforcement is per-participant and fail-closed on
violationActions. Two adapters that warmedtheir caches at different times can therefore evaluate different versions of the same policy
URL at the same moment, and nothing in the protocol surfaces the divergence — the NACK an
operator sees carries no indication that the counterparty was judging against different rules.
Since the policy is authored by a commercial counterparty and sets wheeling charges and
penalties, a settlement disagreement here is a billing disagreement.
Suggested fixes (in rough order of cost)
ttl. The registry already tells you how long its answer is good for;the resolver could clamp
cacheTTLto it, or at minimum warn when config exceeds it by anorder of magnitude.
version/digest, or ahash of the fetched rego. Cheap to add to
cacheEntry, and it makes staleness detectablerather than silent.
If-None-Match) againstthe DeDi lookup keeps registry load near the current level while bounding skew to minutes.
that rotation requires a new
record_name, and that updating a record version in place isunsupported. That alone closes the gap at zero code cost.
Happy to send a PR for (2) if you'd like it — it is a small change to
cache.goand theexisting tests in
enforce_test.gocover the surrounding behaviour.Environment
Read at
main(plugins/contractpolicyenforcer/cache.go, 136 lines) on 2026-07-29; live DeDivalues fetched the same day.
Unrelated, noticed while verifying the above — say the word and I'll open it separately:
devkits/p2p-trading-ies-wave2/README.md(line 57) documents the policy record ashttps://api.dedi.global/dedi/lookup/indiaenergystack.in/ies-policies/ies-p2p-network-settlement-rego-policy-v1,which returns
404 {"message":"Record not found"}(3/3 attempts today), and theies-policiesregistry reportsrecord_count: 0. WithviolationActions: "select,init,confirm"and fail-closed resolution, a devkit run pointed at the live registrywould NACK on
select.