|
| 1 | +# Authorizing Proof Data Provider Operations |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +Node providers on the Storacha network need to perform operations on proof sets - adding piece roots, deleting roots, and other state changes. |
| 6 | +These operations require signatures from Storacha to be valid on-chain. |
| 7 | + |
| 8 | +Currently, we don't have a clean way to authorize these operations. Node providers need to: |
| 9 | +1. Prove they're authorized to request signatures from Storacha |
| 10 | +2. Get Storacha's signature for their operation payload |
| 11 | +3. Use that signature to submit blockchain transactions |
| 12 | + |
| 13 | +We need a system that: |
| 14 | +- Only allows authorized operators to request signatures |
| 15 | +- Prevents unauthorized access to the signing service |
| 16 | +- Maintains a verifiable chain of authority back to Storacha |
| 17 | +- Doesn't require manual management of operator allow-lists |
| 18 | +- Uses short-lived delegations instead of relying on revocations |
| 19 | + |
| 20 | +## Background |
| 21 | + |
| 22 | +### EIP-712: Typed Structured Data Signing |
| 23 | + |
| 24 | +The operations we're authorizing need to be submitted as blockchain transactions. |
| 25 | +Specifically, we're calling smart contract methods like `addPieces()` and `deletePieces()` on Filecoin. |
| 26 | + |
| 27 | +These contract methods require signatures that prove the operations were authorized by Storacha. |
| 28 | +We use [EIP-712](https://eips.ethereum.org/EIPS/eip-712) for this signing. |
| 29 | + |
| 30 | +EIP-712 is an Ethereum standard for signing typed, structured data. |
| 31 | +Instead of signing raw bytes (which are hard for users to verify), EIP-712 lets you sign structured data with clear types and fields. |
| 32 | +The signature proves: |
| 33 | +1. The exact data being signed (operation type, parameters, values) |
| 34 | +2. Which contract the data is intended for (domain separation) |
| 35 | +3. Who signed it (via signature verification on-chain) |
| 36 | + |
| 37 | +For example, an `AddPieces` operation might have a structure like: |
| 38 | + |
| 39 | +```solidity |
| 40 | +struct AddPieces { |
| 41 | + uint256 clientDataSetId; |
| 42 | + uint256 firstAdded; |
| 43 | + bytes32[] pieceData; |
| 44 | + bytes[] metadata; |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +The EIP-712 signature of this structure can be verified on-chain by the smart contract, which checks that it was signed by Storacha's authorized key. |
| 49 | + |
| 50 | +This is why we need a signing service. |
| 51 | +The private key that creates these EIP-712 signatures must be controlled by Storacha, but node providers need to be able to request signatures for their operations. |
| 52 | +The signing service is the bridge - it holds Storacha's key and issues signatures to authorized operators. |
| 53 | + |
| 54 | +## Proposal |
| 55 | + |
| 56 | +### UCAN-Authorized Signing Service |
| 57 | + |
| 58 | +We'll create a signing service that uses UCAN delegations to authorize signature requests. |
| 59 | +The flow works like this: |
| 60 | + |
| 61 | +1. Storacha issues delegations to authorized operators granting them the `pdp/sign` capability |
| 62 | +2. Operators create UCAN invocations requesting signatures, proving their authority via the delegation chain |
| 63 | +3. The signing service validates the delegation chain and signs the operation payload |
| 64 | +4. Operators use the returned signature for blockchain transactions |
| 65 | + |
| 66 | +The key insight: the delegation chain IS the authorization. |
| 67 | +If an operator has a valid delegation from Storacha, they can request signatures. |
| 68 | +No separate allow-list needed. |
| 69 | + |
| 70 | +### The `pdp/sign` Capability |
| 71 | + |
| 72 | +We'll introduce a new capability `pdp/sign` that represents the ability to request signatures for proof data provider operations. |
| 73 | + |
| 74 | +Storacha has the natural authority to grant this capability, as it controls the signing keys used for on-chain operations. |
| 75 | + |
| 76 | +### Flow |
| 77 | + |
| 78 | +#### Step 1: Operator Creates UCAN Invocation |
| 79 | + |
| 80 | +When a node provider needs to perform an operation (like adding pieces), they create a UCAN invocation: |
| 81 | + |
| 82 | +``` |
| 83 | +┌─────────────────────────────────────────┐ |
| 84 | +│ Issuer: did:key:PiriNode... │ ← Operator's identity |
| 85 | +│ Audience: did:key:SigningService │ ← Signing service identity |
| 86 | +│ Capability: │ |
| 87 | +│ with: "did:key:SigningService" │ ← Resource (signing service) |
| 88 | +│ can: "pdp/sign" │ ← Ability |
| 89 | +│ nb: { │ ← Caveats |
| 90 | +│ operation: "AddPieces", │ |
| 91 | +│ payload: { │ |
| 92 | +│ clientDataSetId: "123", │ |
| 93 | +│ firstAdded: "0", │ |
| 94 | +│ pieceData: ["0xabc...", ...], │ |
| 95 | +│ metadata: [...] │ |
| 96 | +│ } │ |
| 97 | +│ } │ |
| 98 | +│ Proofs: [ │ ← Delegation chain |
| 99 | +│ <delegation from Storacha> │ (proves operator can pdp/sign) |
| 100 | +│ ] │ |
| 101 | +│ Signature: <signed by operator key> │ |
| 102 | +└─────────────────────────────────────────┘ |
| 103 | +``` |
| 104 | + |
| 105 | +The `proofs` field contains a delegation from Storacha that looks like: |
| 106 | + |
| 107 | +``` |
| 108 | +┌─────────────────────────────────────────┐ |
| 109 | +│ Issuer: did:web:storacha.network │ ← Storacha's identity |
| 110 | +│ Audience: did:key:PiriNode... │ ← Operator's identity |
| 111 | +│ Capability: │ |
| 112 | +│ with: "did:key:SigningService" │ |
| 113 | +│ can: "pdp/sign" │ |
| 114 | +│ nb: { │ |
| 115 | +│ operations: [ │ |
| 116 | +│ "AddPieces", │ |
| 117 | +│ "DeletePieces", │ |
| 118 | +│ ... │ |
| 119 | +│ ] │ |
| 120 | +│ } │ |
| 121 | +│ Expiration: <short lived, e.g. 1 day> │ |
| 122 | +│ Signature: <signed by Storacha> │ |
| 123 | +└─────────────────────────────────────────┘ |
| 124 | +``` |
| 125 | + |
| 126 | +#### Step 2: Send to Signing Service |
| 127 | + |
| 128 | +``` |
| 129 | +Piri Node |
| 130 | + ↓ |
| 131 | + HTTP POST to signing-service.storacha.network |
| 132 | + Content-Type: application/car |
| 133 | + Body: <CAR file containing UCAN invocation> |
| 134 | + ↓ |
| 135 | +Signing Service |
| 136 | +``` |
| 137 | + |
| 138 | +#### Step 3: Signing Service Validates UCAN |
| 139 | + |
| 140 | +The signing service validates the invocation: |
| 141 | + |
| 142 | +1. **Verify UCAN signature** |
| 143 | + - Is the signature valid for the issuer DID? |
| 144 | + |
| 145 | +2. **Check delegation chain** |
| 146 | + - Do the proofs link back to Storacha's root authority? |
| 147 | + - Is each delegation in the chain valid and not expired? |
| 148 | + - Does the chain grant the `pdp/sign` capability? |
| 149 | + |
| 150 | +3. **Verify audience** |
| 151 | + - Is the audience the signing service's DID? |
| 152 | + |
| 153 | +4. **Validate caveats** |
| 154 | + - Is the operation valid (e.g., "AddPieces", "DeletePieces")? |
| 155 | + - Does the payload have all required fields? |
| 156 | + |
| 157 | +If validation fails, return a UCAN receipt with an error. |
| 158 | +If it succeeds, proceed to signing. |
| 159 | + |
| 160 | +#### Step 4: Signing Service Signs Payload |
| 161 | + |
| 162 | +The signing service extracts the operation and payload from the caveats and calls the appropriate EIP-712 signer: |
| 163 | + |
| 164 | +```go |
| 165 | +operation := invocation.Capability.Nb["operation"] |
| 166 | +payload := invocation.Capability.Nb["payload"] |
| 167 | + |
| 168 | +// For AddPieces 'operation': |
| 169 | +signature := signer.SignAddPieces( |
| 170 | + payload.ClientDataSetId, |
| 171 | + payload.FirstAdded, |
| 172 | + payload.PieceData, |
| 173 | + payload.Metadata, |
| 174 | +) |
| 175 | + |
| 176 | +// Returns: {v: 28, r: 0x..., s: 0x..., signature: 0x...} |
| 177 | +``` |
| 178 | + |
| 179 | +#### Step 5: Signing Service Returns UCAN Receipt |
| 180 | + |
| 181 | +The signing service creates a receipt linking back to the invocation: |
| 182 | + |
| 183 | +``` |
| 184 | +┌─────────────────────────────────────────┐ |
| 185 | +│ Issuer: did:key:SigningService │ ← Signing service identity |
| 186 | +│ Ran: <CID of invocation> │ ← Links to the request |
| 187 | +│ Out: │ |
| 188 | +│ ok: { │ |
| 189 | +│ v: 28, │ |
| 190 | +│ r: "0x1234...", │ |
| 191 | +│ s: "0x5678...", │ |
| 192 | +│ signature: "0xabcd...", │ |
| 193 | +│ signedData: "0xef01...", │ |
| 194 | +│ signer: "0x742d35..." │ |
| 195 | +│ } │ |
| 196 | +│ Signature: <signed by signing service> │ |
| 197 | +└─────────────────────────────────────────┘ |
| 198 | +``` |
| 199 | + |
| 200 | +For errors: |
| 201 | + |
| 202 | +``` |
| 203 | +┌─────────────────────────────────────────┐ |
| 204 | +│ Issuer: did:key:SigningService │ |
| 205 | +│ Ran: <CID of invocation> │ |
| 206 | +│ Out: │ |
| 207 | +│ error: { │ |
| 208 | +│ name: "UnauthorizedError", │ |
| 209 | +│ message: "Invalid delegation chain" │ |
| 210 | +│ } │ |
| 211 | +│ Signature: <signed by signing service> │ |
| 212 | +└─────────────────────────────────────────┘ |
| 213 | +``` |
| 214 | + |
| 215 | +#### Step 6: Operator Receives Response |
| 216 | + |
| 217 | +``` |
| 218 | +Signing Service |
| 219 | + ↓ |
| 220 | + HTTP 200 OK |
| 221 | + Content-Type: application/car |
| 222 | + Body: <CAR file containing UCAN receipt> |
| 223 | + ↓ |
| 224 | +Piri Node |
| 225 | +``` |
| 226 | + |
| 227 | +The operator validates the receipt and extracts the signature: |
| 228 | + |
| 229 | +```go |
| 230 | +// Validate receipt |
| 231 | +if receipt.Ran != invocationCID { |
| 232 | + return errors.New("receipt doesn't match invocation") |
| 233 | +} |
| 234 | + |
| 235 | +// Verify receipt signature |
| 236 | +if !verifySignature(receipt, signingServiceDID) { |
| 237 | + return errors.New("invalid receipt signature") |
| 238 | +} |
| 239 | + |
| 240 | +// Extract signature components |
| 241 | +result := receipt.Out.Ok |
| 242 | +v := result.V |
| 243 | +r := result.R |
| 244 | +s := result.S |
| 245 | + |
| 246 | +// Submit blockchain transaction |
| 247 | +contract.AddPieces(v, r, s, clientDataSetId, firstAdded, pieceData, metadata) |
| 248 | +``` |
| 249 | + |
| 250 | +### Benefits |
| 251 | + |
| 252 | +**No manual operator management**: The delegation chain proves authorization. No need to maintain allow-lists in the signing service. |
| 253 | + |
| 254 | +**Short-lived delegations**: Storacha can issue delegations with short expiration times (e.g., 1 day). Revocations are rarely needed. |
| 255 | + |
| 256 | +**Auditable**: Every signature request is a UCAN invocation with a full delegation chain. We can trace who requested what and when. |
| 257 | + |
| 258 | +**Flexible**: Easy to add new operations or adjust scopes in delegations without redeploying services. |
| 259 | + |
| 260 | +**Standard**: Uses UCAN patterns consistent with the rest of the network. |
| 261 | + |
| 262 | +### Implementation Notes |
| 263 | + |
| 264 | +**Delegation distribution**: Storacha needs a mechanism to issue delegations to new operators. This could be: |
| 265 | +- Automated when operators join the network |
| 266 | +- Requested via an `access/authorize` flow (see https://github.com/storacha/RFC/pull/68) |
| 267 | +- Issued through an admin interface |
| 268 | + |
| 269 | +**Delegation caching**: Operators should cache delegations until they expire to avoid requesting new ones for every operation. |
| 270 | + |
| 271 | +**Replay protection**: The blockchain transaction layer handles replay protection via nonces. |
| 272 | +The signing service doesn't need to track used invocations, though it could for monitoring purposes. |
| 273 | + |
| 274 | +**Operation types**: Initial operations will be `CreateDataSet`, `AddPieces` and `DeletePieces`. |
| 275 | +The design supports adding more operation types as needed. |
| 276 | + |
0 commit comments