beckn / beckn/DEG

contractpolicyenforcer: policy cache keyed by URL only — in-place DeDi record rotation serves a stale policy for up to cacheTTL (48h in devkit), bypassing checksum verification

Open
#477 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
61
Forks
55
Avg merge
1d 19h
Merged PRs (30d)
6

Description

## 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:

```go
type cacheEntry struct {
pq rego.PreparedEvalQuery
query string
fetchedAt time.Time
}
```

and the hit test is freshness-by-clock only:

```go
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:

```console
$ 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)

1. **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.
2. **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.
3. **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.
4. **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`.

Contributor guide

Open the contributing guide

Research direction

Start in plugins/contractpolicyenforcer/cache.go, then read the surrounding cache tests in enforce_test.go and the cacheTTL handling in config.go. Confirm the intended freshness or record-identity behavior with maintainers, add regression coverage for in-place DeDi rotation, and consider the work done when rotated policies cannot remain silently stale while existing enforcement tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.