Skip to content

Optional dataset-level programmable acls - #536

Open
essamhassan wants to merge 12 commits into
FilOzone:mainfrom
essamhassan:feat/dataset-level-acl
Open

Optional dataset-level programmable acls#536
essamhassan wants to merge 12 commits into
FilOzone:mainfrom
essamhassan:feat/dataset-level-acl

Conversation

@essamhassan

@essamhassan essamhassan commented Jun 23, 2026

Copy link
Copy Markdown

Summary

Optional per-dataset IDataSetAuthorizer write-authorization hook. No authorizer → existing payer/session-key behavior unchanged. Authorizer set → it is the sole gate for add-pieces, schedule-removals, and signed termination.

Behavior

  • Payer sets/rotates/clears the authorizer; state view exposes it; dataset deletion clears it.
  • Receives dataSetId, payer, operation typehash, EIP-712 digest, raw signature, and ABI-encoded operationData.
  • isAuthorized is a state-mutating CALL (authorizers may keep nonces/rate-limits): falseErrors.Unauthorized; a revert bubbles.
  • Griefing/reentrancy: 150M gas cap (enforcement verified live on FEVM) + an EIP-1153 transient reentrancy latch scoped to the authorization path.

Tests

Backward compat; delegated add/removal/termination; session keys via owner; operationData forwarding; real vm.sign recovery; false-vs-revert; state mutation during authorization; reentrancy latch (blocks + clears); cleanup on deletion.

@FilOzzy FilOzzy added this to FOC Jun 23, 2026
@github-project-automation github-project-automation Bot moved this to 📌 Triage in FOC Jun 23, 2026
@BigLep BigLep moved this from 📌 Triage to ⌨️ In Progress in FOC Jun 24, 2026
Comment thread service_contracts/src/interfaces/IDataSetAuthorizer.sol Outdated
Comment thread service_contracts/src/FilecoinWarmStorageService.sol Outdated
Comment thread service_contracts/src/interfaces/IDataSetAuthorizer.sol Outdated
Comment thread service_contracts/src/FilecoinWarmStorageService.sol Outdated
@essamhassan essamhassan changed the title DRAFT: dataset level acls Dataset-level programmable acls Jun 25, 2026
@essamhassan
essamhassan force-pushed the feat/dataset-level-acl branch from c674c26 to 6ca36f4 Compare June 25, 2026 20:20
@essamhassan essamhassan changed the title Dataset-level programmable acls Dataset-level optional programmable acls Jun 26, 2026
@essamhassan essamhassan changed the title Dataset-level optional programmable acls Optional dataset-level programmable acls Jun 26, 2026
@essamhassan
essamhassan requested a review from wjmelements June 26, 2026 11:01
Comment thread service_contracts/src/FilecoinWarmStorageService.sol Outdated
Comment thread service_contracts/src/lib/SignatureVerificationLib.sol Outdated
Comment thread service_contracts/src/lib/SignatureVerificationLib.sol Outdated
Comment thread service_contracts/src/lib/SignatureVerificationLib.sol Outdated
Comment thread service_contracts/src/interfaces/IDataSetAuthorizer.sol Outdated
Comment thread service_contracts/src/lib/SignatureVerificationLib.sol Outdated
Comment thread service_contracts/tools/generate_storage_layout.sh
Comment thread service_contracts/Makefile Outdated
Comment thread service_contracts/Makefile
Comment thread service_contracts/Makefile Outdated
Comment thread service_contracts/src/interfaces/IDataSetAuthorizer.sol
@essamhassan
essamhassan requested a review from wjmelements June 29, 2026 15:01
@wjmelements wjmelements added the enhancement New feature or request label Jun 29, 2026
@wjmelements

Copy link
Copy Markdown
Contributor

One kind of griefing that would be hard to detect is a randomized authorizer. Suppose the validator approved 1% of signatures (regardless of what they were). There would be a 1% chance they would pass eth_estimateGas (and the griefer would only submit ones that succeed), but then have a 99% chance to fail when the transaction is submitted, thereby wasting a lot of resources. In fact you could construct a signature that would succeed in one block but fail in all subsequent ones. I don't know a good way to detect or mitigate this griefing (because it is indistinguishable from an SP submitting with fake signatures), so we would probably have to have a whitelist for authorizers, on or off chain.

Comment thread service_contracts/src/FilecoinWarmStorageService.sol Outdated
Comment thread service_contracts/src/FilecoinWarmStorageService.sol
@wjmelements

Copy link
Copy Markdown
Contributor

A bit too much history in here for me to catch up on

Use Claude.

The other way is that you could lock up a data set with this if the client can control this authoriser contract. So the SP needs a new failure mode where it stops proving and needs to terminate.

No. An authorizer that always rejects is equivalent to a client who walks away. SPs can already terminate the data set unilaterally.

A bit too much history in here for me to catch up on but I see that there's no gas cap applied to this any more, why not?

Because that doesn't solve the problem at all. Operation fees are not denominated in gas units. To ensure the operation fee pays for the operation, you have to know the gas of the entire operation, not just this subcall. You also have to know the gas price and the price ratio of the payment token to the gas token. This information is all available to the SP when they sign the transaction. I already suggested (on Github, on Slack) that Curio should reject operations where the operation fee does not cover the cost, or keep them until the gas price comes down (like a mempool).

most obvious griefing angle

I describe a much worse griefing strategy. It is worse because the SP does not get paid (which cannot be fixed without also allowing the SP to grief the client). I also described two ways to solve it. Please review that discussion. I think static analysis is possible and I have done something similar in the past (for ERC20).

@rvagg

rvagg commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Because that doesn't solve the problem at all. Operation fees are not denominated in gas units.

It's not about "solving", actually solving for the disconnect between fees paid and costs by the SP isn't something we're attempting to do, we're doing it directionally and adjusting. Over time we'll do an analysis of what it actually cost on average for SPs and adjust pricing accordingly. What I'm suggesting is that a cap is appropriate here to limit the damage and make it so we don't end up adjusting our pricing based on what clients are foisting on SPs. If we want something more unbounded here, then I'd suggest we levy an additional fee for just enabling this feature. Client wants complexity and gas? Fine, pay for it.

@essamhassan

Copy link
Copy Markdown
Author

we're doing it directionally and adjusting. Over time we'll do an analysis of what it actually cost on

@rvagg earlier version had an authorizer gas limit, I don't have particularly strong opinion on this as long as it's generous enough for a sane auth call. early discussions with @wjmelements noted that there is already a hard limit on the cost posed on the SP in the per op cost.

@wjmelements

Copy link
Copy Markdown
Contributor

@rvagg

  1. The DoS vector I raised is more significant and it cannot be fixed with a gas limit. Please read it.
  2. The solution I propose also solves the issue you raised. Please read it.
  3. Therefore, you should catch up on the discussion in this topic before blocking this PR. Please read it.

@BigLep

BigLep commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@wjmelements : I'm not up on the specifics of this PR, and I understand there is discussion getting resolved, but while I'm thinking about it, is there any issue with it rolling out in a FWSS contract upgrade? Does it require special attention, user notification, documentation, Synapse support, etc?

@wjmelements

wjmelements commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

I also described two ways to solve it.

I had some discussion with @jennijuju earlier and we agreed there were some other benefits to the whitelist solution, particularly that we could document the availability of approved authorization registries and how to use them. This whitelist could live on or off-chain: worth discussing.

is there any issue with it rolling out in a FWSS contract upgrade? Does it require special attention, user notification, documentation, Synapse support, etc?

@BigLep It requires some curio changes (reject or delay over-cost actions, the client-auth whitelist if we do that off-chain), and we should document it and add support to synapse once we have some ACLs whitelisted (probably after deployment), but otherwise doesn't require any immediate special care.

@wjmelements

Copy link
Copy Markdown
Contributor

This whitelist could live on or off-chain: worth discussing.

The three kinds of authenticator whitelist we could do:

  1. Hard: enforced in the contract with require (checked with eth_estimateGas)
  2. Soft: managed on-chain (checked with eth_call) but not enforced in the contract
  3. Purely in Curio: new auth supported with new curio releases

I don't have a strong preference.

@BigLep

BigLep commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

First, genuine thanks to @essamhassan for the effort here, and to everyone who's put review cycles into it. It sees the iteration has been thorough and the IDataSetAuthorizer interface is clearly a thoughtful piece of work. Nothing below is a knock on the quality of the contribution.

I'm admittedly no expert here, and there's a lot about our contracts and system I'm still learning. But as I've been thinking about this effort and digging into it more, I have some thoughts I want to share. I realize I may be off base on some of this, and I'm very happy to be corrected.

My instinct is that we should pause this PR rather than merge it right now, for a few reasons:

  1. I don't think it has gone through a design phase. From what I can tell, this is a new and effectively permanent extension point on FWSS, adding contract surface and on-chain state that I understand we can't cleanly migrate away from later. For a change with that kind of lock-in, I'd have expected a short design doc up front laying out the motivation, the alternatives considered, and why this shape is the right one. Right now a lot of the rationale seems to live in resolved review threads, which makes it hard for me (and probably others later) to reconstruct why the design is what it is. If I'm missing where that design thinking happened, please point me to it.

  2. I don't think it's currently a prioritized product need. As far as I know this wasn't something we set out to build, and it isn't on our roadmap. I know this is a contribution from @essamhassan (thank you), but before we take on the ongoing cost of what looks to me like a new plugin system, I'd want a clearer product signal that this fills a real, prioritized gap. If that signal does exist, I'd love for someone to surface it here so we can justify and plan the work properly.

  3. I think it overlaps with other work we haven't reconciled it against. In particular the session key registry (to my eye an authorizer looks a bit like a second, differently-shaped session-key mechanism) and the ACL / encryption work @Kubuxu has been scoping. I know there have also been discussions about secp256r1 / browser-encryption. My worry is that we could end up with several isolated pieces of functionality that we then can't pull together without a larger FWSS revision. I'd feel better looking at these together, possibly including revisiting the session key registry itself, rather than adding another one-off. I could be wrong about how much these actually overlap, so I'd welcome input from people closer to it.

To be clear, I'm suggesting a pause, not a rejection, and I don't want to leave this contribution in limbo. What I think would help is: (a) confirming the product priority, and if it's there, (b) writing up the design so we can evaluate the shape and how it interacts with session keys, ACLs/encryption, and pricing more holistically.

@BigLep

BigLep commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

For visibility, we're using slack for quicker/tighter coordination on next steps between Lightshift and FOC working group in a private slack channel: https://filecoinproject.slack.com/archives/C0BE621FD98/p1783511763289569 . We'll report back here with decisions.

@wjmelements

Copy link
Copy Markdown
Contributor

I met with @rvagg. We agreed on the whitelisting approach for DoS. We can develop a static analysis approach if these truly proliferate enough to justify it (but I don't think they will).

@jennijuju

Copy link
Copy Markdown
Collaborator

I also described two ways to solve it.

I had some discussion with @jennijuju earlier and we agreed there were some other benefits to the whitelist solution, particularly that we could document the availability of approved authorization registries and how to use them. This whitelist could live on or off-chain: worth discussing.

I was lack of context and after reading through the full PR/design doc, I don’t think the whitelist mitigation is sufficient here.

It might address protocol security gap but I think the biggest blocker is from the product side: I don’t see how SP gas cost for isAuthorized gets covered.

For context, we went through this exact problem for the base write path #469. The whole reason per-op pricing exists (ADD_PIECES_BASE_FEE, ADD_PIECES_PER_PIECE_FEE, etc.) is so SPs aren’t stuck absorbing unpredictable gas cost — fees are calibrated against known, bounded gas per op and reflected in what the customer actually pays.

The current authorizer design breaks that assumption. isAuthorized can be arbitrarily expensive — P256 verification, UCAN-style path parsing, recursive permission checks, whatever a given authorizer implements, but the op fee for e.g. addPieces stays fixed and authorizer-agnostic. So either:

  1. SPs eat the extra cost on authorizer-enabled datasets, or
  2. they build their own per-authorizer cost estimation with no protocol support, or
  3. they just decline to service datasets with expensive authorizers.

None of these are good outcomes: (1)/(2) push SP-facing operational cost modeling onto SPs at a stage where that’s not the work they signed up for — they’re here to provide storage service, and from talking to many SPs, understanding chain cost is already a known pain point for biz planning and potential walk-away cause. (3) undercuts adoption for exactly the platforms this feature is meant to serve.

A whitelist might address malicious or nondeterministic cost, but it doesn’t solve the underlying gap: predictable pricing isn’t embedded into the operation/deal pricing itself for a legitimate, well-behaved authorizer.

Context on why this matters: Filecoin SPs are effectively DC operators — they know their infra and ops costs and price accordingly. Onchain gas is outside that comfort zone; it’s an extra cost of supporting on-chain primitives on top of the storage service they actually signed up to provide. That’s exactly why we benchmark gas cost as platform builders and embed it into op pricing in the first place — SPs shouldn’t have to re-derive that per integration, per authorizer.

Is there a plan for authorizer-tiered or authorizer-declared cost (e.g. authorizer publishes expected gas, op fee scales to match)?

@jennijuju

Copy link
Copy Markdown
Collaborator

One potential mitigation could be pairing the whitelist with a declared max gas cost for operations like isAuthorized (anything SPs need to pay for), binding it into pricing:

  • Declare a max gas cost at whitelist time. When an authorizer gets white listed, require it to declare a benchmarked worst-case gas cost for isAuthorized.
  • Priced as a surcharge. That declared max determines an authorizer-specific fee surcharge on top of the base op fee — same mechanism Per-operation pricing for FilecoinWarmStorageService #469 uses for ADD_PIECES_BASE_FEE/ADD_PIECES_PER_PIECE_FEE — so SPs get paid for it.
  • Enforced as a hard gas cap. If the call exceeds the declared max, revert.

That said, I’m not actually a fan of this…. It adds real ongoing complexity: every authorizer needs to define its gas cost and pricing correctly, and as maintainers we’d be on the hook for validating those declarations aren’t lowballed — indefinitely, per authorizer, forever. That’s not a one-time review, it’s a standing operational burden.

So stepping back — I think the more useful question isn’t “how do we price arbitrary authorizers,” it’s: do we actually need full programmability at this stage, or is there one generic authorizer FWSS can offer that unblocks the need?

so the requirements I’m reading

  • path-scoped permissions: is this a must?onchain?
  • fast/ephemeral delegation, revocation: sounds reasonable and the default ACL should be meant to cover.

can we build that one authorized for now, benchmark the gas and define the ops fee; skip whitelist/per authorizer pricing; revisiting the programmability later if a 2nd use case shows up with genuinely incompatible auth needs?

Feedback welcomed!

/// @param operationData The ABI-encoded raw operation payload forwarded by FWSS, carrying all
/// operation-specific data (e.g. piece metadata) so the authorizer can gate on its
/// contents — for example enforcing metadata- or path-based ACLs. Empty for operations
/// whose data is fully described by the other parameters (e.g. terminate).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

raw operation payload forwarded and carrying all operation-specific data could be at conflict here.
The raw operation payload suggests that the operationData should only be the information covered by the digest/signature but carrying all operation-specific data might be more than that.

Are there any such cases? Do we want to separate the data covered by the signature from any other supplemental data that FWSS (or maybe the uploader) might provide in the future?

@Kubuxu

Kubuxu commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Because of how generic the interface is, no concerns about limiting ourselves in the scope of authorisation schemes. Although it also means that the authorizer becomes tied to the FWSS version. If the typed hash of the operation changes, it will require redeployments or upgrades to the authorizer.
Is that the tradeoff we want to make?

Admitting arbitrary authorizers exposes us to the griefing attack discussed above, and if we don't resolve that issue, providers will do so by whitelisting which authorizers they are willing to utilise during upload. I'm not saying that a whitelist is a good solution, but it is the easiest one to reach for.

Assuming some kind of whitelisting, can we keep the authorizers under clients' control while allowing for whitelisting based on code and upgrades?

Even if we were to deploy a single more constrained authorizer, I would suggest keeping the rough interface shape as long as we agree to make the first trade-off.

@wjmelements

Copy link
Copy Markdown
Contributor

Following discussion with @jennijuju, we will start with a gas cap on that call, in addition to the whitelist, in order to reduce our operational burden. The reason is that we will need to quantify the possible gas use anyway, as part of the approval process. Another benefit might be that the constraint will push developers to use less gas. This doesn't solve the pricing problem but it does prevent operational gas usage from becoming too excessive.

A complexity with this cap is that storage gas costs increase over time due to HAMT traversal, so we would still need to ensure these plugins are well below the limit. If they begin just under it, storage growth can push them over, and then transactions would start reverting. This rejection behavior is identical to Curio rejecting the requests due to high gas cost, which is the behavior I want anyway.

I still think we have to worry about network congestion and gas price in Curio. If SPs can't reject or delay messages during periods of high gas price, then they can be drained to DoS the network relatively cheaply.

I recommend 100 million gas for the limit. A single addPiece uses about 500 million gas right now. That should be more than enough headroom.

We also still need to agree on where the whitelist will live.

I don't have a strong preference.

@jennijuju

Copy link
Copy Markdown
Collaborator

Assuming some kind of whitelisting, can we keep the authorizers under clients' control while allowing for whitelisting based on code and upgrades?

ah @Kubuxu — do you mean we only care about the gas cap; but making sure the authorizer meets the protocol constraint is then integrators’s concern?

@Kubuxu

Kubuxu commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

I mean the following:

  1. I want us/others to be able to upgrade contracts that are fulfilling IDataSetAuthorizer as the method fingerprints can change.
  2. But clients probably don't want the contracts to be upgraded from under them. They might want to vet and then upgrade. (product hypothesis).
  3. If we implement whitelisting, we need to identify what code is being run for the authorisation contract in the face of upgrades and client-based ownership.

@BigLep

BigLep commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@jennijuju

Copy link
Copy Markdown
Collaborator

I understand the use case better now and understand where the programmable requirement is coming from; and I can also see how the design here is extensible, in a good way.

adding a bit more on my takeaway after discussing w/ Will on the gas cap; I'm now comfortable with the cap-only approach for the time being — i.e. we don't need to add an authorizer ops-fee immediately, as long as we size the gas cap so the uncompensated cost stays negligible against existing ops fees.

isAuthorized is called at each Addpiece and there already exists a add-piece fee (0.0005 + 0.0003×N), so any gas isAuthorized adds comes straight out of the SP's fee from addpiece. Assuming my source is right lolol from 30-day data, a typical addPieces call burns ~3.45B gas at ~22 pieces/call, and a single-piece call is ~384M gas. So William's proposed 100M cap's impact is

batch size 100M cap = SP fee reduction
1 piece (worst case) ~26%
22 pieces (current avg) ~3%
60 pieces (full batch) ~1%

At batch sizes we actually see, 100M is ~3%, and at a full batch ~1% — negligible, agreed. My unease is on the worst case: a client doing single-piece adds gives up ~26% of the SP's fee, and i prefer size to the worst case, not the average. I'd like the worst case constrained to ≤5% if we can, which points to a cap around ~20M gas (that's ~0.6% on an average batch).

However, the blocker on picking the number: the first authorizer I actually want to make sure we fully unblock — and the common case I'd like supported first — is P256 / passkey verification, and we don't yet know w
hat P256 costs on FEVM. (Im not sure if this is also Refound's priority, if not we also needs to unblock them ideally). FIP-0113 adds the P256VERIFY precompile but deliberately assigns no gas number (FEVM meters by Wasm execution) and explicitly lists benchmarking it as an open TODO. Until someone benchmarks a representative P256VERIFY on calibnet, we can't say whether a passkey authorizer lives under a 5%-ceiling cap.

So two ways forward to unblock this for now:

  1. Land conservative now, raise later. Ship with ~20M (≤5% worst case), and bump it via the normal upgrade path once we have the P256 benchmark and know what the real authorizers need. This is my preference — it keeps SP downside bounded while we learn, and loosening a cap later is low-risk.
  2. Loosen now, accept more downside. Cap at 50M and accept a ~13% worst-case single-piece haircut. Simpler, gives P256 more headroom immediately, but I'm less comfortable committing SPs to that before we know we need it.

Can we get that from the FIP-0113 implementers, or measure it on calibnet? That single figure tells us whether the passkey case fits a conservative cap or genuinely needs the higher-cap-plus-surcharge tier .

To be clear on the ops-fee question: I'm fine deferring it. The cap alone could be sufficient for now precisely because at these sizes the uncompensated gas is a rounding error on batched calls. We still need to revisit a surcharge if/when an authorizer genuinely requires a cap high enough to move SP margin.

An attached authorizer is untrusted code: it is the sole gate for every
write on its data set, so FWSS must bound what it can burn on the
caller's behalf. Cap the subcall at 150M gas.

verifyAuthorizer is the single funnel for all three authorizer paths
(addPieces, schedulePieceRemovals, terminate), so one ceiling covers
them all. Per EIP-150 the call already forwards at most 63/64 of the
remaining gas, so the cap only binds when the caller supplies more and
can never starve a call that would otherwise succeed.

Revert semantics are unchanged: a high-level call bubbles an inner
revert rather than returning false, so Errors.Unauthorized still fires
only on a clean false, and an authorizer that exhausts its allotment
reverts the operation, as IDataSetAuthorizer documents.
…reentrancy

Change IDataSetAuthorizer.isAuthorized from view to a state-mutating CALL so
authorizers can maintain state while deciding (nonces, rate-limits). Wrap the
single untrusted call in a transient reentrancy latch (EIP-1153, confirmed on
FEVM) scoped to the authorization path only; the 150M gas cap is preserved.
@essamhassan

Copy link
Copy Markdown
Author

As discussed moved this the authorizer from external view to mutating call to leverage caching and reduce cost, this also unlock a number of interesting authorizer use-cases for other builders.

Comment thread service_contracts/src/lib/SignatureVerificationLib.sol Outdated
Addresses review feedback (r3660109390): use a modifier for the
set-before / clear-after latch instead of inline bracketing. Pure
readability change — the single-use modifier inlines to identical
bytecode (gas unchanged), and the latch still clears after `_;` and
on revert.
@wjmelements

wjmelements commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Merge origin/main, which bumps forge to 1.7.1. Locally, do:

foundryup --update
foundryup --install v1.7.1

Resolve merge conflicts and rerun make gen, forge fmt, forge lint with v1.7.1, since those have changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: ✔️ Approved by reviewer

Development

Successfully merging this pull request may close these issues.

10 participants