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
17 changes: 9 additions & 8 deletions curio/start_scripts/curio-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ echo "All ready. Lets go"
myip=$(getent hosts curio | awk '{print $1}')

# Start a temporary Curio node, wait for its API, then run a callback and stop it.
# Usage: with_temporary_curio <callback_function>
# Usage: with_temporary_curio <callback_function> [layers]
with_temporary_curio() {
local callback="$1"
local layers="${2:-seal,post,gui}"

echo "Starting temporary Curio node..."
CURIO_FAKE_CPU=5 curio run --nosync --layers seal,post,pdp-only,gui &
echo "Starting temporary Curio node (layers: $layers)..."
CURIO_FAKE_CPU=5 curio run --nosync --layers "$layers" &
local curio_pid=$!
sleep 20
sleep 40

echo "Waiting for Curio API to be ready..."
until curio cli --machine "$myip:12300" wait-api; do
Expand Down Expand Up @@ -80,7 +81,7 @@ if [ ! -f "$CURIO_REPO_PATH/.init.curio" ]; then
fi

if [ ! -f "$CURIO_REPO_PATH/.init.config" ]; then
newminer=$(lotus state list-miners | grep -E -v 't01000|t01001' | head -1)
newminer=$(lotus state list-miners | grep -v -E 't01000|t01001' | tail -1)
echo "New Miner is $newminer"

echo "Initiating a new Curio cluster..."
Expand Down Expand Up @@ -132,7 +133,7 @@ LAYER_EOF
curio --version
curio cli --machine "$myip:12300" storage attach --init --seal --store "$CURIO_REPO_PATH"
}
with_temporary_curio attach_storage
with_temporary_curio attach_storage "seal,post,gui"

touch "$CURIO_REPO_PATH/.init.curio"
fi
Expand Down Expand Up @@ -170,9 +171,9 @@ if [ ! -f "$CURIO_REPO_PATH/.init.pdp" ]; then
pdptool create-jwt-token pdp | grep -v "JWT Token:" > jwt_token.txt

echo "Testing PDP connectivity..."
pdptool ping --service-url http://curio:80 --service-name pdp
pdptool ping --service-url http://curio:80 --service-name pdp || echo "PDP ping skipped (PDP HTTP not running in setup phase, will work after final start)"
}
with_temporary_curio setup_pdp
with_temporary_curio setup_pdp "seal,post,gui"

touch "$CURIO_REPO_PATH/.init.pdp"
echo "PDP service setup complete"
Expand Down
37 changes: 12 additions & 25 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ services:
container_name: workload
environment:
- STRESS_NODES=lotus0,lotus1,lotus2,lotus3
- FORK_POLL_INTERVAL_SECS=30
- STRESS_RPC_PORT=1234
- STRESS_FOREST_RPC_PORT=3456
- STRESS_KEYSTORE_PATH=/shared/configs/stress_keystore.json
Expand All @@ -151,51 +152,34 @@ services:
- STRESS_WEIGHT_STATE_AUDIT=3 # full state tree audit
- STRESS_WEIGHT_F3_MONITOR=2 # passive F3 health monitor
- STRESS_WEIGHT_F3_AGREEMENT=3 # cross-node F3 certificate consistency
- STRESS_WEIGHT_DRAND_BEACON_AUDIT=3 # cross-node drand beacon entry consistency
- STRESS_WEIGHT_REORG=0 # power-aware reorg testing (disabled — consensus lifecycle handles partitions)
- STRESS_WEIGHT_POWER_SLASH=4 # power-aware miner slashing
# - STRESS_WEIGHT_QUORUM_STALL=0 # deliberate F3 stall (opt-in, destructive)
- FUZZER_ENABLED=0 # protocol fuzzer off for consensus threshold testing
- STRESS_WEIGHT_POWER_SLASH=0 # disabled for FOC — causes miner disruption
- FUZZER_ENABLED=0 # protocol fuzzer off
- STRESS_CONSENSUS_TEST=0 # n-split disabled for FOC — disrupts Curio
#
# --- Consensus integration test (background lifecycle, not deck) ---
- STRESS_CONSENSUS_TEST=1 # enable structured EC/F3 safety proof lifecycle
#
# --- Non-FOC stress vectors ---
#
# EVM contract stress
# --- Non-FOC stress vectors (skipped when FOC active, kept for reference) ---
- STRESS_WEIGHT_DEPLOY=1 # Init actor & state tree growth via EAM.CreateExternal
- STRESS_WEIGHT_CONTRACT_CALL=1 # deep recursion, delegatecall, external recursive calls
- STRESS_WEIGHT_SELFDESTRUCT=0 # actor destruction state consistency across nodes
- STRESS_WEIGHT_CONTRACT_RACE=2 # conflicting txs to diff nodes — state divergence during forks
# Background chain activity (deck — runs between test cycles)
- STRESS_WEIGHT_TRANSFER=2 # FIL transfers (state changes for forks to reconcile)
- STRESS_WEIGHT_GAS_WAR=1 # mempool replacement across forks
- STRESS_WEIGHT_NONCE_RACE=1 # gas-premium race across different nodes
- STRESS_WEIGHT_HEAVY_COMPUTE=1 # intra-node state recomputation verification
# Resource stress (disabled)
- STRESS_WEIGHT_MAX_BLOCK_GAS=0 # maxes out block gas
- STRESS_WEIGHT_LOG_BLASTER=0 # receipt storage, bloom filters, event indexing
- STRESS_WEIGHT_MEMORY_BOMB=0 # FVM memory accounting
- STRESS_WEIGHT_STORAGE_SPAM=0 # state trie (HAMT), SplitStore
# Nonce/ordering chaos
- STRESS_WEIGHT_MSG_ORDERING=1 # cross-node message replacement/mempool ordering
- STRESS_WEIGHT_NONCE_BOMBARD=0 # N+x gap handling & out-of-order execution
- STRESS_WEIGHT_ADVERSARIAL=0 # double-spends (handled by consensus lifecycle)
# Gas pressure (disabled)
- STRESS_WEIGHT_GAS_EXHAUST=0 # high-gas call competing with small msgs
# Cross-node consistency
- STRESS_WEIGHT_RECEIPT_AUDIT=4 # asserts receipt fields match across every node
- STRESS_WEIGHT_ACTOR_MIGRATION=1 # burst-creates & deletes actors, stresses HAMT during forks
- STRESS_WEIGHT_ACTOR_LIFECYCLE=1 # full actor lifecycle
# --- FOC (Filecoin On-Chain Cloud) vectors ---
# All FOC vectors require the `foc` compose profile to be active.
# The lifecycle state machine must reach "Ready" before steady-state
# vectors will fire. Higher weight = picked more often from the deck.
#
# SETUP: drives the sequential state machine one step per pick
# Init → Approved → Deposited → OperatorApproved → DataSetCreated → Ready
- STRESS_WEIGHT_FOC_LIFECYCLE=6
#
# STEADY-STATE: only execute once lifecycle reaches Ready
- STRESS_WEIGHT_FOC_UPLOAD=4 # upload random data to Curio PDP API
- STRESS_WEIGHT_FOC_ADD_PIECES=3 # add uploaded pieces to on-chain proofset
Expand All @@ -204,13 +188,16 @@ services:
- STRESS_WEIGHT_FOC_TRANSFER=2 # ERC-20 USDFC transfer (client → deployer)
- STRESS_WEIGHT_FOC_SETTLE=2 # settle active payment rail
- STRESS_WEIGHT_FOC_WITHDRAW=2 # withdraw USDFC from FilecoinPay
#
# DESTRUCTIVE: weight 0 = disabled by default, set >0 to opt-in
# DESTRUCTIVE
- STRESS_WEIGHT_FOC_DELETE_PIECE=1 # schedule piece deletion from proofset
- STRESS_WEIGHT_FOC_DELETE_DS=0 # delete entire dataset + reset lifecycle
#
# ADVERSARIAL: economic security + griefing probes
- STRESS_WEIGHT_REORG_CHAOS=0 # disable — partitions lotus0 which stalls all FOC ops
# ADVERSARIAL: griefing probes (cooldown after first dataset creation)
- STRESS_WEIGHT_PDP_GRIEFING=4 # fee extraction, insolvency, replay, burst attacks
# SECURITY: piece lifecycle, payment rail, resilience
- STRESS_WEIGHT_FOC_PIECE_SECURITY=2 # piece lifecycle + attack probes
- STRESS_WEIGHT_FOC_PAYMENT_SECURITY=2 # rail settlement + audit L01/L04/L06/#288
- STRESS_WEIGHT_FOC_RESILIENCE=1 # Curio HTTP stress + orphan rail
#
- CURIO_PDP_URL=http://curio:80

Expand Down
126 changes: 97 additions & 29 deletions workload/FOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,30 @@ Contract logic executes deterministically inside FVM's WASM sandbox — unit tes
```
workload/
├── cmd/
│ ├── stress-engine/ # Main fuzz driver
│ │ ├── main.go # Init, deck building, main loop
│ │ ├── foc_vectors.go # FOC lifecycle + steady-state vectors
│ │ ├── actions.go # Non-FOC stress vectors (transfers, contracts, etc.)
│ │ └── contracts.go # Embedded EVM bytecodes
│ ├── foc-sidecar/ # Independent safety monitor
│ │ ├── main.go # Polling loop
│ │ ├── assertions.go # 5 safety assertions (assert.Always)
│ │ ├── events.go # Event log parsing (DataSetCreated, RailCreated, etc.)
│ │ └── state.go # Thread-safe state tracking
│ └── genesis-prep/ # Wallet generation (runs before stress-engine)
│ ├── stress-engine/ # Main fuzz driver
│ │ ├── main.go # Init, deck building, main loop
│ │ ├── foc_vectors.go # FOC lifecycle + steady-state vectors
│ │ ├── griefing_vectors.go # Payment griefing probes (fee extraction, insolvency, replay)
│ │ ├── foc_piece_security.go # Piece lifecycle security scenario (8 phases)
│ │ ├── foc_payment_security.go # Rail/payment security scenario (7 phases)
│ │ ├── foc_resilience.go # Curio resilience + orphan rail scenario (3 phases)
│ │ ├── actions.go # Non-FOC stress vectors (transfers, contracts, etc.)
│ │ └── contracts.go # Embedded EVM bytecodes
│ ├── foc-sidecar/ # Independent safety monitor
│ │ ├── main.go # Polling loop
│ │ ├── assertions.go # Safety assertions (assert.Always + assert.Sometimes)
│ │ ├── events.go # Event log parsing (DataSetCreated, RailCreated, etc.)
│ │ └── state.go # Thread-safe state tracking
│ └── genesis-prep/ # Wallet generation (runs before stress-engine)
│ └── main.go
└── internal/
└── foc/ # Shared FOC library
├── config.go # Parse /shared/environment.env + SP key
├── eth.go # EVM tx submission (SendEthTx, SendEthTxConfirmed, BuildCalldata)
├── eip712.go # EIP-712 typed data signing for FWSS
├── curio.go # Curio PDP HTTP API client (upload, create dataset, add pieces)
├── commp.go # PieceCIDv2 calculation (CommP)
└── selectors.go # ABI function selectors for all contracts
└── foc/ # Shared FOC library
├── config.go # Parse /shared/environment.env + SP key
├── eth.go # EVM tx submission + read helpers
├── eip712.go # EIP-712 typed data signing for FWSS
├── curio.go # Curio PDP HTTP API client
├── commp.go # PieceCIDv2 calculation (CommP)
└── selectors.go # ABI function selectors for all contracts
```

### Smart Contracts
Expand Down Expand Up @@ -164,6 +168,65 @@ Resets the lifecycle to `Init` on success. **Destructive** — disabled by defau

---

## Security Scenarios

Three scenario state machines test the full connected lifecycle with security edge cases. Each is a single deck entry that advances one phase per invocation. They use a dedicated secondary client wallet (set up by the griefing runtime) to avoid interfering with the primary FOC lifecycle.

### Scenario 1: Piece Lifecycle Security (`foc_piece_security.go`, weight: 2)

Tests the full piece add/delete/retrieve lifecycle with attack probes at each step.

```
Init → Added → Verified → DeleteScheduled → DeleteVerified → AttackPhase → Terminated → Cleanup
```

| Phase | What It Tests | Key Assertion |
|-------|--------------|---------------|
| **Init→Added** | Upload piece, add to dataset, verify `activePieceCount` increases | `Sometimes(countIncreased)` |
| **Added→Verified** | Download piece, recompute CID, verify integrity | `Sometimes(cidMatch)` |
| **Verified→DeleteScheduled** | Schedule deletion, immediately re-retrieve (**curio#1039** "prove deleted data" edge) | `Sometimes(retrievalClean)` |
| **DeleteScheduled→DeleteVerified** | Verify piece count decreased, proving still advances | `Sometimes(countDecreased)`, `Sometimes(provingAdvances)` |
| **DeleteVerified→AttackPhase** | Random attack (one per cycle): | |
| | — **Nonce replay**: reuse addPieces nonce | `Sometimes(replayRejected)` |
| | — **Cross-dataset injection**: sign for DS A, submit to DS B | `Sometimes(crossDSRejected)` |
| | — **Double deletion**: delete same pieceID twice | `Sometimes(doubleFails)` |
| | — **Nonexistent delete**: delete pieceID=999999 | `Sometimes(nonexistentFails)` |
| **AttackPhase→Terminated** | Call `terminateService`, then immediately try `addPieces` (**post-termination race**) | `Sometimes(postTermAddRejected)` |
| **Terminated→Cleanup** | Delete dataset, reset for next cycle | `Sometimes(cycleCompletes)` |

### Scenario 2: Payment Rail Security (`foc_payment_security.go`, weight: 2)

Tests the full payment rail lifecycle targeting audit findings.

```
Init → Settled → DoubleSettled → RailChecked → RateModified → Withdrawn → Refunded
```

| Phase | What It Tests | Audit Finding | Key Assertion |
|-------|--------------|---------------|---------------|
| **Init→Settled** | Settle rail, verify lockup ≤ before | **L01**: lockup after settlement | `Sometimes(lockupNoIncrease)` |
| **Settled→DoubleSettled** | Settle same rail+epoch again | Double-settle idempotency | `Sometimes(noExtraDeduction)` |
| **DoubleSettled→RailChecked** | Read all 3 rail IDs, verify cacheMiss+cdn rates=0 (no FILCDN/IPNI) | Rail config sanity | Logged for observability |
| **RailChecked→RateModified** | `modifyRailPayment` twice, verify latest persists | **L06**: rate queue clearing | `Sometimes(latestRatePersists)` |
| **RateModified→Withdrawn** | Withdraw all `available = funds - lockup` | **#288**: locked funds | `Sometimes(withdrawOK)` |
| **Withdrawn→Refunded** | Attacker deposits to victim's account + refund | **L04**: unauthorized deposit | `Always(!primaryInflated)` |

### Scenario 3: Curio Resilience (`foc_resilience.go`, weight: 1)

Tests Curio HTTP API resilience and orphan rail economics.

```
Init → OrphanCreated → OrphanChecked → (back to Init)
```

| Phase | What It Tests | Risks DB Item | Key Assertion |
|-------|--------------|---------------|---------------|
| **Init** | Send 7 malformed HTTP requests, verify Curio survives | Network-wide Curio crash (Sev2) | `Always(curioPingOK)` |
| **OrphanCreated** | Create empty dataset (no pieces), snapshot funds | Upload failures + orphan rails | — |
| **OrphanChecked** | Verify empty dataset doesn't accumulate charges, cleanup | Orphan rail billing | `Sometimes(noChargeForEmpty)` |

---

## Assertions

The Antithesis SDK provides three assertion types:
Expand Down Expand Up @@ -193,20 +256,21 @@ All stress-engine assertions use `assert.Sometimes` because individual transacti

### Sidecar Assertions (`assertions.go`)

Sidecar assertions use `assert.Always` for safety invariants that must hold on every poll cycle. These run independently of the stress-engine against finalized chain state (30-epoch finality window).
Sidecar assertions run independently against finalized chain state (30-epoch finality window).

| Assertion Message | Type | Function | What It Validates |
|-------------------|------|----------|-------------------|
| `"Rail-to-dataset reverse mapping is consistent"` | Always | checkRailToDataset | `railToDataSet(pdpRailId)` returns the expected `dataSetId` for every tracked dataset. Detects rail/dataset mapping corruption. |
| `"FilecoinPay holds sufficient USDFC (solvency)"` | Always | checkFilecoinPaySolvency | `balanceOf(FilecoinPay)` >= sum of all tracked `accounts.funds + accounts.lockup`. Detects insolvency / phantom balance creation. |
| `"Provider ID matches registry for dataset"` | Always | checkProviderIDConsistency | `addressToProviderId(sp)` matches the `providerId` from the `DataSetCreated` event. Detects registry corruption or SP impersonation. |
| `"Active proofset is live on-chain"` | Always | checkProofSetLiveness | Every non-deleted dataset has `dataSetLive() == true`. Detects unexpected dataset termination or proof failure. |
| `"Deleted proofset is not live"` | Always | checkDeletedDataSetNotLive | Every deleted dataset has `dataSetLive() == false`. Detects zombie datasets that survive deletion. |

| `"Proving period advances (challenge epoch changed)"` | Sometimes | checkProvingAdvancement | `getNextChallengeEpoch` changes over time for active datasets. Confirms proving pipeline is running. |
| `"Dataset proof submitted (proven epoch advanced)"` | Sometimes | checkProvingAdvancement | `getDataSetLastProvenEpoch` advances. Confirms Curio is submitting proofs. |
| `"Active piece count does not exceed leaf count"` | Always | checkPieceAccountingConsistency | `getActivePieceCount <= getDataSetLeafCount`. Detects piece accounting corruption. |
| `"Active dataset rail has non-zero payment rate"` | Always | checkRateConsistency | Datasets with pieces must have `paymentRate > 0` on their PDP rail. Detects rate miscalculation. |
| `"Rail-to-dataset reverse mapping is consistent"` | Always | checkRailToDataset | `railToDataSet(pdpRailId)` returns expected `dataSetId`. Detects mapping corruption. |
| `"FilecoinPay holds sufficient USDFC (solvency)"` | Always | checkFilecoinPaySolvency | `balanceOf(FilecoinPay)` >= sum of all `accounts.funds`. Detects insolvency. |
| `"Provider ID matches registry for dataset"` | Always | checkProviderIDConsistency | `addressToProviderId(sp)` matches `DataSetCreated` event. |
| `"Active proofset is live on-chain"` | Always | checkProofSetLiveness | Non-deleted datasets have `dataSetLive() == true`. |
| `"Deleted proofset is not live"` | Always | checkDeletedDataSetNotLive | Deleted datasets have `dataSetLive() == false`. |
| `"Proving period advances"` | Sometimes | checkProvingAdvancement | `getNextChallengeEpoch` changes over time. |
| `"Dataset proof submitted"` | Sometimes | checkProvingAdvancement | `getDataSetLastProvenEpoch` advances. |
| `"Active piece count ≤ leaf count"` | Always | checkPieceAccountingConsistency | Detects piece accounting corruption. |
| `"Active dataset rail has non-zero payment rate"` | Always | checkRateConsistency | Datasets with pieces must have `paymentRate > 0`. |
| `"Lockup never exceeds funds for any payer"` | Always | checkLockupNeverExceedsFunds | **Audit L01**: `lockup ≤ funds` for every tracked payer. Fundamental accounting invariant. |
| `"Deleted dataset rail has endEpoch set"` | Sometimes | checkDeletedDatasetRailTerminated | **#288**: Deleted dataset rails must be terminated. Detects zombie rails. |

### Event Tracking

Expand Down Expand Up @@ -320,6 +384,10 @@ When the FOC profile is active, non-FOC stress vectors (EVM contracts, nonce cha
| `STRESS_WEIGHT_FOC_WITHDRAW` | `2` | Steady-state | Withdraw USDFC from FilecoinPay |
| `STRESS_WEIGHT_FOC_DELETE_PIECE` | `1` | Destructive | Schedule piece deletion from proofset |
| `STRESS_WEIGHT_FOC_DELETE_DS` | `0` | Destructive | Delete entire dataset + reset lifecycle |
| `STRESS_WEIGHT_PDP_GRIEFING` | `8` | Adversarial | Payment griefing: fee extraction, insolvency, cross-payer replay, burst |
| `STRESS_WEIGHT_FOC_PIECE_SECURITY` | `2` | Security | Piece lifecycle: add/delete/retrieve + nonce replay, cross-DS, double-delete |
| `STRESS_WEIGHT_FOC_PAYMENT_SECURITY` | `2` | Security | Rail lifecycle: settlement lockup (L01), rate change (L06), unauthorized deposit (L04), withdrawal (#288) |
| `STRESS_WEIGHT_FOC_RESILIENCE` | `1` | Security | Curio HTTP resilience + orphan rail billing |

---

Expand Down
Loading
Loading