Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/curio/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func main() {
calcCmd,
toolboxCmd,
batchCmd,
pdpCmd,
}

for _, cmd := range local {
Expand Down
13 changes: 7 additions & 6 deletions cuhttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ func attachRouters(ctx context.Context, r *chi.Mux, d *deps.Deps, sd *ServiceDep

if sd.EthSender != nil {
if err := pdp.MountRoutes(ctx, r, pdp.MountDeps{
DB: d.DB,
LocalStore: d.LocalStore,
EthClient: must.One(d.EthClient.Get()),
Chain: d.Chain,
EthSender: sd.EthSender,
AlertTask: sd.AlertTask,
DB: d.DB,
LocalStore: d.LocalStore,
EthClient: must.One(d.EthClient.Get()),
Chain: d.Chain,
EthSender: sd.EthSender,
AlertTask: sd.AlertTask,
AuthorizerConfig: d.Cfg.Subsystems.PDPAuthorizers,
}, ipp); err != nil {
return nil, err
}
Expand Down
82 changes: 82 additions & 0 deletions deps/config/doc_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions deps/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ func DefaultCurioConfig() *CurioConfig {
RemoteProofMaxUploads: 15,
ParkPieceMinFreeStoragePercent: 5,
EnableDBAnalyze: true,
// Fail closed on the gas-spending path: require an allowlisted authorizer by default,
// but ship with an empty list so a fresh SP only relays the un-authorized default path
// until the operator opts specific authorizer code in.
PDPAuthorizers: PDPAuthorizerConfig{
RequireAllowlistedAuthorizer: true,
MaxAuthorizerGas: 150_000_000,
},
},
Fees: CurioFees{
MaxPreCommitBatchGasFee: BatchFeeConfig{
Expand Down Expand Up @@ -478,6 +485,70 @@ type CurioSubsystemsConfig struct {
// by 10% since the last analyze.
// Disable this if you manage table statistics outside Curio. (Default: true)
EnableDBAnalyze bool

// PDPAuthorizers governs which IDataSetAuthorizer contracts this SP is willing to spend gas on when
// relaying authorizer-gated operations (FWSS PR #536). It applies to the shared PDP service and is
// therefore version-agnostic (PDPv0 and PDPv1/MK20 alike). See PDPAuthorizerConfig.
PDPAuthorizers PDPAuthorizerConfig
}

// PDPAuthorizerConfig is the SP-side policy for relaying authorizer-gated PDP operations.
//
// filecoin-services PR #536 lets a data set's payer attach an IDataSetAuthorizer contract that gates
// add-pieces / schedule-removals / immediate-terminate. When the SP relays one of those operations, the
// SP's transaction pays for the authorizer's isAuthorized call - up to the 150M-gas AUTHORIZER_GAS_LIMIT
// (a full P256/passkey verification is ~110-123M). A hostile or buggy authorizer can burn that gas on
// every relay, so the SP keeps an off-chain policy of which authorizer CODE it trusts.
type PDPAuthorizerConfig struct {
// RequireAllowlistedAuthorizer means an authorizer-gated operation is relayed only when the data
// set's authorizer CODE matches an ApprovedAuthorizers entry. Matching is by code identity, not
// address, so each client can run their own isolated instance of approved logic.
//
// The default is true and is deliberately fail-closed on the gas-spending path: a fresh SP ships
// with an EMPTY ApprovedAuthorizers list, so it relays only the un-authorized default payer/session
// path (where it fronts no isAuthorized gas) until the operator explicitly opts specific code in.
// Setting this to false relays for ANY authorizer and is NOT recommended, because the SP then fronts
// up to the 150M-gas isAuthorized call for authorizer code it has never reviewed. (Default: true)
RequireAllowlistedAuthorizer bool

// ApprovedAuthorizers lists the authorizer code identities this SP will spend gas on. Each entry is
// matched by CODE (see ApprovedAuthorizer.Kind), never by the authorizer's own address, so a client
// can deploy their own isolated instance (own owner, own registry) of approved logic. Empty by
// default. Only consulted when RequireAllowlistedAuthorizer is true.
ApprovedAuthorizers []ApprovedAuthorizer

// MaxAuthorizerGas is the eth_call gas limit applied to authorizer-gated relay preflights
// (addPieces / schedulePieceDeletions / terminateService). 0 means the 150M on-chain
// AUTHORIZER_GAS_LIMIT; values above 150M are clamped to it. (Default: 150000000)
MaxAuthorizerGas uint64
}

// ApprovedAuthorizer is one entry in the SP's authorizer allowlist. It identifies trusted authorizer
// logic by CODE so a client can run their own instance of it.
type ApprovedAuthorizer struct {
// Label is a human-readable name shown in logs and metrics.
Label string

// Kind selects how the on-chain authorizer is matched:
// "implementation" - accept any EIP-1167 minimal-proxy clone whose hardcoded implementation
// equals Address. This is the preferred deployment shape: audited logic is
// deployed once, and each client's clone has its own storage and cannot be
// upgraded (the implementation is baked into the 45-byte clone bytecode).
// "codehash" - accept a fixed-logic (non-proxy) deploy whose normalized runtime-code hash
// equals CodeHash. Upgradeable proxies are always refused regardless of Kind,
// because their logic can change after approval.
Kind string

// Address is the audited implementation address, 0x-prefixed. Used when Kind = "implementation".
Address string

// CodeHash is the normalized runtime-code hash, a 0x-prefixed keccak256. Used when Kind = "codehash".
// The hash is taken over the runtime bytecode after stripping the trailing Solidity CBOR metadata
// blob. Compute it with "curio pdp authorizer-id".
CodeHash string

// Notes is free-form: audit reference, gas profile, etc.
Notes string
}
type CurioFees struct {
// maxBatchFee = maxBase + maxPerSector * nSectors
Expand Down
20 changes: 18 additions & 2 deletions pdp/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ type PDPService struct {
ipp *ipni_provider.Provider

ipOffenseThrottle *IPOffenseThrottle

// ethValidator is the shared eth_call helper, including the authorizer allowlist check.
ethValidator *EthCallValidator
}

type PDPServiceNodeApi interface {
Expand All @@ -102,10 +105,11 @@ func NewPDPService(
fc PDPServiceNodeApi,
sn ETHTxSender,
alertTask *alertmanager.AlertTask,
ipp *ipni_provider.Provider) *PDPService {
ipp *ipni_provider.Provider,
authPolicy *AuthorizerAllowlist) *PDPService {
auth := &NullAuth{}
pullStore := NewDBPullStore(db)
pullValidator := NewEthCallValidator(ec, db)
pullValidator := NewEthCallValidator(ec, db, authPolicy)

p := &PDPService{
Auth: auth,
Expand All @@ -123,6 +127,8 @@ func NewPDPService(
ipp: ipp,

ipOffenseThrottle: NewIPOffenseThrottle(defaultIPOffensePolicies()),

ethValidator: pullValidator,
}

go p.ipOffenseThrottle.RunCleanup(ctx)
Expand Down Expand Up @@ -1060,6 +1066,11 @@ func (p *PDPService) handleDeleteDataSetPiece(w http.ResponseWriter, r *http.Req
http.Error(w, "Data set not found", http.StatusNotFound)
return
}

if p.refuseUnallowlistedAuthorizer(w, ctx, dataSetId) {
return
}

type DeletePiecePayload struct {
ExtraData *string `json:"extraData"`
PieceIDs []uint64 `json:"pieceIds"`
Expand Down Expand Up @@ -1162,6 +1173,11 @@ func (p *PDPService) handleDeleteDataSetPiece(w http.ResponseWriter, r *http.Req
return
}

if err := p.preflightAuthorizerCall(ctx, fromAddress, contract.ContractAddresses().PDPVerifier, data, nil); err != nil {
httpServerError(w, http.StatusBadRequest, "schedulePieceDeletions validation failed: "+err.Error(), err)
return
}

// Prepare the transaction
ethTx := types.NewTransaction(
0, // nonce will be set by SenderETH
Expand Down
9 changes: 9 additions & 0 deletions pdp/handlers_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ func (p *PDPService) handleAddPieceToDataSet(w http.ResponseWriter, r *http.Requ
return
}

if p.refuseUnallowlistedAuthorizer(w, ctx, dataSetIdUint64) {
return
}

// Convert dataSetId to *big.Int
dataSetId := new(big.Int).SetUint64(dataSetIdUint64)

Expand Down Expand Up @@ -395,6 +399,11 @@ func (p *PDPService) handleAddPieceToDataSet(w http.ResponseWriter, r *http.Requ
return
}

if err := p.preflightAuthorizerCall(ctx, fromAddress, contract.ContractAddresses().PDPVerifier, data, nil); err != nil {
httpServerError(w, http.StatusBadRequest, "addPieces validation failed: "+err.Error(), err)
return
}

// Prepare the transaction (nonce will be set to 0, SenderETH will assign it)
txEth := types.NewTransaction(
0,
Expand Down
39 changes: 25 additions & 14 deletions pdp/handlers_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"math/big"
"net/http"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"

Expand Down Expand Up @@ -59,14 +58,29 @@ type EthCallValidator struct {
ethClient ethchain.EthClient
db *harmonydb.DB
senderAddr common.Address // cached, lazily loaded

// authorizerPolicy gates which IDataSetAuthorizer contracts the SP will spend gas relaying for.
// May be nil (treated as opt-out).
authorizerPolicy *AuthorizerAllowlist
}

// NewEthCallValidator creates a validator that uses eth_call
func NewEthCallValidator(ethClient ethchain.EthClient, db *harmonydb.DB) *EthCallValidator {
return &EthCallValidator{ethClient: ethClient, db: db}
// NewEthCallValidator creates a validator that uses eth_call. authorizerPolicy may be nil, which
// disables the authorizer allowlist check (opt-out).
func NewEthCallValidator(ethClient ethchain.EthClient, db *harmonydb.DB, authorizerPolicy *AuthorizerAllowlist) *EthCallValidator {
return &EthCallValidator{ethClient: ethClient, db: db, authorizerPolicy: authorizerPolicy}
}

func (v *EthCallValidator) ValidateAddPieces(ctx context.Context, params *AddPiecesValidatorParams) error {
if params.DataSetId == nil {
return fmt.Errorf("dataSetId is required")
}

// Allowlist before the addPieces eth_call: that simulation executes isAuthorized once #536 is
// live, so an unapproved authorizer must not reach it.
if err := v.checkAuthorizerAllowed(ctx, params.DataSetId.Uint64(), params.RecordKeeper); err != nil {
return err
}

// Lazily load sender address if not cached
if v.senderAddr == (common.Address{}) && v.db != nil {
addr, err := getPDPSenderAddress(ctx, v.db)
Expand Down Expand Up @@ -95,23 +109,16 @@ func (v *EthCallValidator) ValidateAddPieces(ctx context.Context, params *AddPie
return fmt.Errorf("failed to pack addPieces call: %w", err)
}

// eth_call to validate — match tx value used for dataset creation
// eth_call to validate — match tx value used for dataset creation. Gas-capped so isAuthorized
// cannot spin past MaxAuthorizerGas (default 150M).
value := big.NewInt(0)
if isCreateNew {
value, err = contract.FilCleanupDeposit(ctx, v.ethClient)
if err != nil {
return fmt.Errorf("reading FIL cleanup deposit: %w", err)
}
}
msg := ethereum.CallMsg{
From: v.senderAddr,
To: new(contract.ContractAddresses().PDPVerifier),
Data: data,
Value: value,
}

_, err = v.ethClient.CallContract(ctx, msg, nil)
if err != nil {
if err := v.callWithAuthorizerGas(ctx, v.senderAddr, contract.ContractAddresses().PDPVerifier, data, value); err != nil {
return fmt.Errorf("addPieces validation failed: %w", err)
}

Expand Down Expand Up @@ -404,6 +411,10 @@ func (h *PullHandler) HandlePull(w http.ResponseWriter, r *http.Request) {
ExtraData: extraDataBytes,
}
if err := h.validator.ValidateAddPieces(ctx, validatorParams); err != nil {
if errors.Is(err, ErrAuthorizerNotAllowlisted) {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
httpServerError(w, http.StatusBadRequest, "extraData validation failed: "+err.Error(), err)
return
}
Expand Down
Loading
Loading