🗑️ Pruner Service
Index
- Description
- Functionality
- Data Model
- Technology
- Directory Structure and Main Files
- How to Run
- Configuration Options (Settings Flags)
- Other Resources
1. Description
The Pruner service is a standalone microservice responsible for managing UTXO data pruning operations in Teranode. The Pruner operates as an event-driven overlay service that continuously monitors blockchain events and removes stale UTXO data to prevent unbounded database growth.
The Pruner service:
- Event-Driven: Responds to
BlockPersistednotifications instead of polling - Standalone: Runs as an independent gRPC service (port 8096)
- Safety-First: Implements a critical two-phase process to prevent data loss
- Coordinated: Works with Block Persister to protect transaction data during catchup
The Pruner service ensures that:
- Parent transactions of old unmined transactions remain available for resubmission
- UTXO records marked for deletion are removed at the appropriate block height
- Transaction data remains accessible until Block Persister creates
.subtree_datafiles - External transaction blobs are cleaned up from blob storage (S3/filesystem)
Note: For information about how the Pruner service is initialized during daemon startup and how it interacts with other services, see the Teranode Daemon Reference.

The Pruner service subscribes to blockchain events and coordinates with Block Persister to ensure safe pruning operations.

The Pruner service consists of:
- Server Component: Manages gRPC server, health checks, and service lifecycle
- Worker Component: Handles event subscriptions, channel management, and two-phase processing
- Job Manager: LIFO queue for pruning jobs with worker pool and status tracking
- Store Implementations: Aerospike and SQL-specific pruning logic
Detailed Component View
The following diagram provides a deeper level of detail into the Pruner Service's internal components and their interactions:
2. Functionality
2.1 Service Initialization
The Pruner service initializes through the following sequence:
-
Load Configuration Settings
- Reads pruner-specific settings from
settings.conf - Configures job timeout, gRPC ports, worker counts
- Reads pruner-specific settings from
-
Initialize Store Pruner
- Retrieves store-specific pruner implementation from UTXO Store
- Aerospike: Initializes secondary index waiter, 4-worker pool
- SQL: Initializes 2-worker pool with simple DELETE queries
-
Initialize Service Clients
- Blockchain Client: For event subscriptions and state queries
- Block Assembly Client: For state checks before pruning
-
Start gRPC Server
- Listens on port 8096 (default)
- Exposes health check API
- Registers with Service Manager
-
Subscribe to Events
- Primary:
BlockPersistednotifications from Block Persister - Fallback:
Blocknotifications when Block Persister not running
- Primary:
-
Ready State
- Event-driven pruning active
- Waiting for BlockPersisted events
2.2 Event-Driven Trigger Mechanism
The Pruner service uses an event-driven architecture with two trigger mechanisms:
Primary Trigger: BlockPersisted Notifications
When Block Persister is Active:
-
Block Persister completes block persistence
- Creates
.block,.subtree,.utxo-additions,.utxo-deletionsfiles - All transaction data is safely stored in
.subtree_datafiles
- Creates
-
Block Persister updates blockchain state
- Sets
BlockPersisterHeight = N - Sends
BlockPersistednotification with height N
- Sets
-
Pruner receives notification
- Updates
lastPersistedHeight = N - Knows that blocks up to height N have
.subtree_datafiles
- Updates
-
Pruner sends pruning request to buffered channel
- Channel size: 1 (non-blocking)
- If channel full: Request dropped (deduplication)
- If channel available: Request queued
-
Pruning workflow triggered for height N
Channel Deduplication Logic:
The buffered channel (size 1) ensures that:
- Only one pruning operation runs at a time
- During catchup, intermediate heights are skipped
- Latest height is always processed
- No blocking or queue buildup
Fallback Trigger: Block Notifications
When Block Persister is NOT Running:
-
Block Validation completes block validation
- Block is fully validated and added to blockchain
-
Blockchain Service sends
Blocknotification- Includes
mined_set = trueflag
- Includes
-
Pruner checks
lastPersistedHeight- If
lastPersistedHeight == 0: Block Persister not running - If
lastPersistedHeight > 0: Ignore (handled by BlockPersisted)
- If
-
Pruner verifies
mined_set == true- Ensures block validation completed
-
Pruning triggered for block height
- No coordination needed (no
.subtree_datafiles to protect)
- No coordination needed (no
2.3 Two-Phase Pruning Process
The Pruner implements a critical two-phase safety mechanism to prevent data loss:
Safety Check: Block Assembly State
Before pruning begins, Pruner checks Block Assembly state:
- State RUNNING: Proceed with pruning
- State NOT RUNNING: Abort (reorg or reset in progress)
This prevents pruning during blockchain reorganizations when transaction states may be changing.
Phase 1: Preserve Parents (CRITICAL)
Purpose: Protect parent transactions of old unmined transactions from deletion
Why This is Critical:
When a transaction remains unmined for a long time, its parent transactions (UTXOs it spends) might be marked for deletion. If the unmined transaction is later resubmitted, it needs those parent transactions to be valid. Without parent preservation, resubmitted transactions would fail validation due to missing inputs.
Process:
-
Calculate cutoff height
cutoffHeight = currentHeight - UnminedTxRetention- Default:
UnminedTxRetention = BlockHeightRetention / 2
-
Query for old unmined transactions
WHERE unmined_since < cutoffHeight- Find transactions older than retention period
-
For each old unmined transaction:
- Get transaction metadata (includes inpoints)
- Extract parent transaction IDs from inpoints
-
For each parent TxID:
- Update parent:
SET PreserveUntil = currentHeight + ParentPreservationBlocks - Default:
ParentPreservationBlocks = blocksInADayOnAverage * 10(≈1440 blocks)
- Update parent:
-
Critical Error Handling:
- If ANY parent update fails: ABORT ENTIRE PRUNING
- Do NOT proceed to Phase 2
- Prevents orphaning of resubmitted transactions
-
Success: All parents preserved
- Safe to proceed to Phase 2
Store Implementation:
- Aerospike: Batch operations for efficiency
- SQL: Individual UPDATE statements
- Common Logic:
PreserveParentsOfOldUnminedTransactions()in/stores/utxo/pruner_unmined.go
Phase 2: DAH (Delete-At-Height) Pruning
Purpose: Remove UTXO records marked for deletion at specific block heights
Process:
-
Job Manager receives
UpdateBlockHeight(height)request -
Calculate safe height for deletion
- Get
persistedHeightfrom Block Persister coordination safeHeight = min(currentHeight, persistedHeight)- Ensures transaction data is in
.subtree_datafiles before deletion
- Get
-
Create pruning job
- Job includes
safeHeightfor deletion - Added to job queue with LIFO prioritization
- Job includes
-
Cancel superseded jobs
- Only newest pending job is processed
- Older jobs are marked as
Cancelled
-
Worker pool executes pruning
- Aerospike: Query with filter
deleteAtHeight <= safeHeightusing secondary index - SQL:
DELETE FROM utxos WHERE delete_at_height <= safeHeight
- Aerospike: Query with filter
-
For each record to delete:
-
If external transaction data exists:
- Delete
.txfile from Blob Store (S3/filesystem)- Delete UTXO record from database
- Update metrics:
utxo_cleanup_batch_duration_seconds
- Delete
-
-
Job completion
- Timeout (10 minutes): Non-error, job continues in background
- Success: Job marked as
Completed
-
Pruner updates metrics
pruner_duration_seconds{operation="dah_pruner"}pruner_processed_total
Worker Pool Configuration:
- Aerospike: 4 workers (default), concurrent batch operations
- SQL: 2 workers (default), simpler DELETE queries
2.4 Coordination with Block Persister
Critical coordination mechanism to prevent premature deletion of transaction data:
The Problem:
Block Persister creates .subtree_data files containing transaction data needed for catchup nodes to replay blocks. If Pruner deletes transactions before these files are created, catchup nodes cannot recover the data.
The Solution:
-
Block Persister Signals Completion:
-
After creating all files for block N:
- Updates:
BlockPersisterHeight = N - Sends:
BlockPersistednotification
- Updates:
-
-
Pruner Tracks Persisted Height:
- Receives
BlockPersistednotification - Updates:
lastPersistedHeight = N
- Receives
-
Store-Level Safe Height Calculation:
- Store Pruner gets
persistedHeightfrom Pruner service - Calculates:
safeHeight = min(currentHeight, persistedHeight)
- Store Pruner gets
-
Example Scenario:
- Current blockchain height: 100
- Block Persister at height: 95 (creating files for blocks 96-100)
safeHeight = min(100, 95) = 95- Result: Only prune records with
deleteAtHeight <= 95 - Blocks 96-100 protected until
.subtree_datafiles created
-
Without Block Persister:
persistedHeight = 0(Block Persister not running)safeHeight = min(100, 0) = 0→ UsescurrentHeight- No
.subtree_datafiles to protect - Safe to prune at current height
Benefits:
- Data Integrity: Transaction data available until safely persisted
- Catchup Support: Nodes can replay blocks from
.subtree_datafiles - Graceful Degradation: Works with or without Block Persister
2.5 Job Queue Management
LIFO (Last In, First Out) Pattern:
The Pruner uses a LIFO queue to efficiently handle pruning during catchup:
Why LIFO?
During blockchain catchup, blocks arrive rapidly. Processing every intermediate height is wasteful. LIFO ensures:
- Only the newest pending job is processed
- Intermediate heights are skipped automatically
- Efficient pruning during catchup
Job States:
- Pending: Waiting for worker
- Running: Currently being processed
- Completed: Successfully finished
- Failed: Error occurred
- Cancelled: Superseded by newer job
Job Lifecycle:
-
New job arrives (height 100)
- Added to queue with status
Pending - All workers busy
- Added to queue with status
-
Another job arrives (height 101)
- Added to queue with status
Pending - Job Manager finds superseded jobs
- Job(100) status changed to
Cancelled
- Added to queue with status
-
Worker becomes available
- Gets newest pending job: Job(101)
- Job(101) status →
Running - Worker executes pruning for height 101
-
Job completes
- Job(101) status →
Completed - Metrics updated
- Job(101) status →
Job History Retention:
- Aerospike: Last 1000 jobs
- SQL: Last 10 jobs
- Older jobs automatically removed
Timeout Handling:
- Default timeout: 10 minutes
-
On timeout:
- Coordinator moves on (non-error)
- Job continues in background
- Will be re-queued if needed
3. Data Model
The Pruner service operates on the UTXO data model. Please refer to the UTXO Data Model documentation for detailed information.
Key Fields for Pruning
Delete-At-Height (DAH)
- Field:
DeleteAtHeight(uint32) - Purpose: Marks when a UTXO record should be deleted
- Set By: UTXO Store during transaction spending or coinbase maturity
- Queried By: Pruner during Phase 2 (DAH Pruning)
- Index: Aerospike secondary index on
deleteAtHeightfield
Example:
type UTXO struct {
TxID *chainhash.Hash
Index uint32
Value uint64
Height uint32
Script []byte
Coinbase bool
DeleteAtHeight uint32 // Pruner queries this field
// ... other fields
}
PreserveUntil
- Field:
PreserveUntil(uint32) - Purpose: Protects parent transactions from deletion
- Set By: Pruner during Phase 1 (Preserve Parents)
- Value:
currentHeight + ParentPreservationBlocks - Effect: Prevents deletion even if
DeleteAtHeightreached
UnminedSince
- Field:
UnminedSince(uint32) - Purpose: Tracks how long a transaction has been unmined
- Set By: UTXO Store when transaction added
- Queried By: Pruner to find old unmined transactions
- Used In: Phase 1 to identify transactions needing parent preservation
External Transaction Data
For large transactions stored externally:
- Location: Blob Store (S3 or filesystem)
- File Extension:
.tx - Naming:
<txid>.tx - Deletion: Pruner deletes external file during DAH pruning
- Store: Aerospike-specific (SQL stores inline)
4. Technology
- Language: Go 1.25+
- Communication: gRPC (port 8096), Protocol Buffers
- Storage: Store-agnostic (Aerospike or SQL via interface)
- Metrics: Prometheus
- Concurrency: Goroutines, channels, worker pools
- Event System: Blockchain service notifications
Dependencies
- UTXO Store (Aerospike or SQL)
- Blockchain Service (event subscriptions)
- Block Assembly Service (state checks)
- Block Persister (optional, for coordination)
- Blob Store (S3/filesystem, for external tx cleanup)
Store Implementations
Aerospike Implementation
- Location:
/stores/utxo/aerospike/pruner/ - Secondary Index: Required on
DeleteAtHeightfield - Query: Filter expression
deleteAtHeight <= safeHeight - Workers: 4 goroutines (configurable)
- Batch Operations: Efficient parent updates
- External Storage: Deletes
.txfiles from blob store - Max Job History: 1000 jobs
SQL Implementation
- Location:
/stores/utxo/sql/pruner/ - Query: Simple
DELETE WHERE delete_at_height <= ? - Workers: 2 goroutines (configurable)
- No External Dependencies: All data inline
- Max Job History: 10 jobs
5. Directory Structure and Main Files
/services/pruner/ # Standalone microservice
├── server.go # Service initialization, gRPC, health checks
├── worker.go # Pruning processor, two-phase logic, event handler
├── metrics.go # Prometheus metrics
└── pruner_api/
├── pruner_api.proto # gRPC API definition
├── pruner_api.pb.go # Generated protobuf code
└── pruner_api_grpc.pb.go # Generated gRPC code
/stores/pruner/ # Generic store-agnostic components
├── interfaces.go # Service and provider interfaces
├── job.go # Job definition and states
├── job_processor.go # Job queue and worker management
├── job_processor_test.go
└── example/
└── example_service.go # Example implementation
/stores/utxo/aerospike/pruner/ # Aerospike-specific implementation
├── pruner_service.go # Aerospike pruner service (900+ lines)
├── pruner_service_test.go
├── index_waiter.go # Index readiness checker
├── mock_index_waiter_test.go
└── README.md # Aerospike pruner documentation
/stores/utxo/sql/pruner/ # SQL-specific implementation
├── pruner_service.go # SQL pruner service
├── pruner_service_test.go
└── mock.go
/stores/utxo/ # Store-agnostic utility
└── pruner_unmined.go # PreserveParentsOfOldUnminedTransactions()
Key Files
server.go: Service lifecycle, gRPC server, health checksworker.go: Event handling, channel management, two-phase processingmetrics.go: Prometheus metric definitionsinterfaces.go: Store-agnostic interfaces for pruner implementationsjob_processor.go: Generic job queue with LIFO pattern and worker poolaerospike/pruner/pruner_service.go(900+ lines): Complete Aerospike implementationsql/pruner/pruner_service.go: Simplified SQL implementation
6. How to Run
To run the Pruner Service locally, you can execute the following command:
SETTINGS_CONTEXT=dev.[YOUR_CONTEXT] go run . -pruner=1
Please refer to the Locally Running Services Documentation document for more information on running the Pruner Service locally.
7. Configuration Options (Settings Flags)
For complete settings reference, see Pruner Settings Reference.
Core Settings
| Setting | Type | Default | Description |
|---|---|---|---|
startPruner |
bool | true |
Enable/disable Pruner service |
pruner_grpcPort |
int | 8096 |
gRPC server port |
pruner_jobTimeout |
duration | 10m |
Timeout for pruning job completion |
UTXO Store Settings
| Setting | Type | Default | Description |
|---|---|---|---|
utxostore_unminedTxRetention |
uint32 | globalBlockHeightRetention/2 |
Blocks to retain unmined transactions |
utxostore_parentPreservationBlocks |
uint32 | blocksInADayOnAverage*10 |
Blocks to preserve parent transactions (≈14400) |
utxostore_prunerMaxConcurrentOperations |
int | Connection pool size | Max concurrent Aerospike operations |
utxostore_disableDAHCleaner |
bool | false |
Disable DAH pruning (testing only) |
Context-Specific Settings
# Development
pruner_grpcAddress.dev = localhost:8096
# Docker
pruner_grpcAddress.docker.m = pruner:8096
pruner_grpcAddress.docker = ${clientName}:8096
# Kubernetes/Operator
pruner_grpcAddress.operator = k8s:///pruner.${clientName}.svc.cluster.local:8096
# Disable for specific nodes
startPruner.docker.host.teranode1 = false
startPruner.docker.host.teranode2 = false
8. Other Resources
Related Documentation
- UTXO Store Documentation
- UTXO Data Model
- Block Persister Service
- Block Assembly Service
- Teranode Daemon Reference
API Reference
Code Reference
- GitHub: /services/pruner/
- Store-Level: /stores/pruner/
- Aerospike: /stores/utxo/aerospike/pruner/
- SQL: /stores/utxo/sql/pruner/
Metrics Documentation
For Prometheus metrics details, see Prometheus Metrics Reference.
Call Graph Visualization
For visual representation of the Pruner service's function call patterns, see the Call Graphs documentation.