diff --git a/docs/references/kafkaMessageFormat.md b/docs/references/kafkaMessageFormat.md index 667ca219b4..260c9eaefa 100644 --- a/docs/references/kafkaMessageFormat.md +++ b/docs/references/kafkaMessageFormat.md @@ -593,7 +593,7 @@ message KafkaTxValidationTopicMessage { } message KafkaTxValidationOptions { - bool skipUtxoCreation = 1; // Skip UTXO creation if true + bool SkipUtxoCreate = 1; // Skip UTXO creation if true bool addTXToBlockAssembly = 2; // Add transaction to block assembly if true bool skipPolicyChecks = 3; // Skip policy checks if true bool createConflicting = 4; // Allow conflicting transactions if true @@ -622,7 +622,7 @@ message KafkaTxValidationOptions { ##### KafkaTxValidationOptions -###### skipUtxoCreation +###### SkipUtxoCreate - Type: bool - Description: When true, the validator will not create UTXO entries for this transaction @@ -655,7 +655,7 @@ Here's a JSON representation of the message content (for illustration purposes o "tx": "", "height": 12345, "options": { - "skipUtxoCreation": false, + "SkipUtxoCreate": false, "addTXToBlockAssembly": true, "skipPolicyChecks": false, "createConflicting": false @@ -674,7 +674,7 @@ currentHeight := uint32(12345) // current blockchain height // Create options (using defaults in this example) options := &kafkamessage.KafkaTxValidationOptions{ - SkipUtxoCreation: false, + SkipUtxoCreate: false, AddTXToBlockAssembly: true, SkipPolicyChecks: false, CreateConflicting: false, @@ -726,13 +726,13 @@ func handleTxValidationMessage(msg *kafka.Message) error { } // Process the transaction with the provided options... - skipUtxoCreation := options.SkipUtxoCreation + SkipUtxoCreate := options.SkipUtxoCreate addToBlockAssembly := options.AddTXToBlockAssembly skipPolicyChecks := options.SkipPolicyChecks createConflicting := options.CreateConflicting // Perform validation based on options... - return validateTransaction(tx, height, skipUtxoCreation, addToBlockAssembly, skipPolicyChecks, createConflicting) + return validateTransaction(tx, height, SkipUtxoCreate, addToBlockAssembly, skipPolicyChecks, createConflicting) } ``` diff --git a/services/blockvalidation/BlockValidation_test.go b/services/blockvalidation/BlockValidation_test.go index 2c6164c9d1..b4e2247a12 100644 --- a/services/blockvalidation/BlockValidation_test.go +++ b/services/blockvalidation/BlockValidation_test.go @@ -337,7 +337,7 @@ func TestBlockValidationValidateBlockSmall(t *testing.T) { require.NoError(t, err) coinbase.Outputs = nil - _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000+300) + _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000) subtreeHashes := make([]*chainhash.Hash, 0) subtreeHashes = append(subtreeHashes, subtree.RootHash()) @@ -376,7 +376,7 @@ func TestBlockValidationValidateBlockSmall(t *testing.T) { subtreeHashes, // should be the subtree with placeholder uint64(subtree.Length()), // nolint:gosec 123123, - 0, 0) + 1, 0) require.NoError(t, err) blockChainStore, err := blockchain_store.NewStore(ulogger.TestLogger{}, &url.URL{Scheme: "sqlitememory"}, tSettings) @@ -553,7 +553,7 @@ func TestBlockValidationShouldNotAllowDuplicateCoinbasePlaceholder(t *testing.T) require.NoError(t, err) coinbase.Outputs = nil - _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000+300) + _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000) require.True(t, coinbase.IsCoinbase()) @@ -641,7 +641,7 @@ func TestBlockValidationShouldNotAllowDuplicateCoinbaseTx(t *testing.T) { require.NoError(t, err) coinbase.Outputs = nil - _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000+300) + _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000) require.True(t, coinbase.IsCoinbase()) @@ -777,7 +777,7 @@ func TestInvalidBlockWithoutGenesisBlock(t *testing.T) { coinbase.Outputs = nil - _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000+300) + _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000) subtreeBytes, err := subtree.Serialize() require.NoError(t, err) @@ -898,7 +898,7 @@ func TestInvalidChainWithoutGenesisBlock(t *testing.T) { coinbase.Outputs = nil - _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000+300) + _ = coinbase.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000) subtreeBytes, err := subtree.Serialize() require.NoError(t, err) @@ -1194,7 +1194,7 @@ func TestBlockValidationRequestMissingTransaction(t *testing.T) { } // Create block header - nBits, _ := model.NewNBitFromString("2000ffff") + nBits, _ := model.NewNBitFromString("207fffff") hashPrevBlock := chaincfg.RegressionNetParams.GenesisBlock.BlockHash() // Calculate merkle root using coinbase and subtree @@ -1245,7 +1245,7 @@ func TestBlockValidationRequestMissingTransaction(t *testing.T) { return uint64(totalSize) }(), - 0, 0) + 1, 0) require.NoError(t, err) @@ -1831,6 +1831,7 @@ func TestBlockValidation_DoubleSpendInBlock(t *testing.T) { err = blockValidation.ValidateBlock(context.Background(), block, "http://localhost") require.Error(t, err) + // With validator-based storage, double spend is caught during validation phase require.ErrorContains(t, err, "BLOCK_INVALID") require.ErrorContains(t, err, "has duplicate inputs") } diff --git a/services/legacy/netsync/handle_block.go b/services/legacy/netsync/handle_block.go index a2cde58967..a632566e7d 100644 --- a/services/legacy/netsync/handle_block.go +++ b/services/legacy/netsync/handle_block.go @@ -631,7 +631,7 @@ func (sm *SyncManager) PreValidateTransactions(ctx context.Context, txMap *txmap _, err = sm.validationClient.Validate(gCtx, txWrapper.Tx, blockHeight, - validator.WithSkipUtxoCreation(true), + validator.WithSkipUtxoCreate(true), validator.WithAddTXToBlockAssembly(false), validator.WithSkipPolicyChecks(true), ) diff --git a/services/propagation/Server.go b/services/propagation/Server.go index 2fbfcd29a6..566fdd60ea 100644 --- a/services/propagation/Server.go +++ b/services/propagation/Server.go @@ -1057,7 +1057,7 @@ func (ps *PropagationServer) validateTransactionViaKafka(btTx *bt.Tx) error { Tx: btTx.SerializeBytes(), Height: 0, Options: &kafkamessage.KafkaTxValidationOptions{ - SkipUtxoCreation: validationOptions.SkipUtxoCreation, + SkipUtxoCreate: validationOptions.SkipUtxoCreate, AddTXToBlockAssembly: validationOptions.AddTXToBlockAssembly, SkipPolicyChecks: validationOptions.SkipPolicyChecks, CreateConflicting: validationOptions.CreateConflicting, diff --git a/services/subtreevalidation/check_block_subtrees.go b/services/subtreevalidation/check_block_subtrees.go index 168bc5b590..ac16f53f02 100644 --- a/services/subtreevalidation/check_block_subtrees.go +++ b/services/subtreevalidation/check_block_subtrees.go @@ -691,6 +691,9 @@ func (u *Server) processTransactionsInLevels(ctx context.Context, allTransaction // Only transactions that successfully validate should be included in parent metadata successfulTxsByLevel := make(map[uint32]map[chainhash.Hash]bool) + // Pipeline: Store level N-1 (background) while processing level N + var storageGroup *errgroup.Group + // Process each level in series, but all transactions within a level in parallel for level := uint32(0); level <= maxLevel; level++ { levelTxs := txsPerLevel[level] @@ -737,9 +740,25 @@ func (u *Server) processTransactionsInLevels(ctx context.Context, allTransaction } } - // Process all transactions at this level in parallel - g, gCtx := errgroup.WithContext(ctx) - util.SafeSetLimit(g, u.settings.SubtreeValidation.SpendBatcherSize*2) + // Step 3: Validate concurrently (CPU-intensive, can overlap with previous level storage!) + // Use CPU-based worker limit for CPU-bound validation work + validationGroup, vCtx := errgroup.WithContext(ctx) + // CPU-bound: Use existing CheckBlockSubtreesConcurrency setting + // This controls CPU-intensive script execution and signature validation + validationWorkers := u.settings.SubtreeValidation.CheckBlockSubtreesConcurrency + util.SafeSetLimit(validationGroup, validationWorkers) + + // Build validation-only options (skip UTXO spending for validation phase) + validationOnlyOptions := []validator.Option{ + validator.WithSkipPolicyChecks(true), + validator.WithCreateConflicting(true), + validator.WithIgnoreLocked(true), + validator.WithSkipUtxoSpend(true), // Skip UTXO operations during validation phase + } + if len(processedValidatorOptions.ParentMetadata) > 0 { + validationOnlyOptions = append(validationOnlyOptions, validator.WithParentMetadata(processedValidatorOptions.ParentMetadata)) + } + processedValidationOptions := validator.ProcessOptions(validationOnlyOptions...) for _, mTx := range levelTxs { tx := mTx.tx @@ -750,12 +769,12 @@ func (u *Server) processTransactionsInLevels(ctx context.Context, allTransaction // Skip transactions that were already validated (found in cache or UTXO store) if txMetaSlice[mTx.idx].isSet { u.logger.Debugf("[processTransactionsInLevels] Transaction %s already validated (pre-check), skipping", tx.TxIDChainHash().String()) - return nil + continue } - g.Go(func() error { + validationGroup.Go(func() error { // Use existing blessMissingTransaction logic for validation - txMeta, err := u.blessMissingTransaction(gCtx, blockHash, subtreeHash, tx, blockHeight, blockIds, processedValidatorOptions) + _, err := u.blessMissingTransaction(vCtx, blockHash, subtreeHash, tx, blockHeight, blockIds, processedValidationOptions) if err != nil { u.logger.Debugf("[processTransactionsInLevels] Failed to validate transaction %s: %v", tx.TxIDChainHash().String(), err) @@ -774,7 +793,7 @@ func (u *Server) processTransactionsInLevels(ctx context.Context, allTransaction // Handle missing parent transactions by adding to orphanage if errors.Is(err, errors.ErrTxMissingParent) { - isRunning, runningErr := u.blockchainClient.IsFSMCurrentState(gCtx, blockchain.FSMStateRUNNING) + isRunning, runningErr := u.blockchainClient.IsFSMCurrentState(vCtx, blockchain.FSMStateRUNNING) if runningErr == nil && isRunning { u.logger.Debugf("[processTransactionsInLevels] Transaction %s missing parent, adding to orphanage", tx.TxIDChainHash().String()) if u.orphanage.Set(*tx.TxIDChainHash(), tx) { @@ -804,22 +823,101 @@ func (u *Server) processTransactionsInLevels(ctx context.Context, allTransaction successfulTxsByLevel[level][*tx.TxIDChainHash()] = true successfulTxsMutex.Unlock() - if txMeta == nil { - u.logger.Debugf("[processTransactionsInLevels] Transaction metadata is nil for %s", tx.TxIDChainHash().String()) - } else { - u.logger.Debugf("[processTransactionsInLevels] Successfully validated transaction %s", tx.TxIDChainHash().String()) + u.logger.Debugf("[processTransactionsInLevels] Successfully validated transaction %s", tx.TxIDChainHash().String()) + return nil + }) + } + + // Wait for validation to complete - fail fast on any error + if err = validationGroup.Wait(); err != nil { + return errors.NewProcessingError("[processTransactionsInLevels] Validation failed at level %d", level+1, err) + } + + u.logger.Debugf("[processTransactionsInLevels] Level %d validation complete (%d transactions)", level, len(levelTxs)) + + // Step 4: Wait for previous level storage (dependency constraint) + // Level N storage cannot start until Level N-1 storage completes (UTXO dependencies) + // However, Level N+1 can extend+validate while Level N is storing (thanks to parent metadata!) + if storageGroup != nil { + u.logger.Debugf("[processTransactionsInLevels] Waiting for level %d storage to complete before starting level %d storage", level-1, level) + if err = storageGroup.Wait(); err != nil { + return errors.NewProcessingError("[processTransactionsInLevels] Previous level storage failed", err) + } + u.logger.Debugf("[processTransactionsInLevels] Level %d storage complete, starting level %d storage", level-1, level) + } + + // Step 5: Start storage for current level (runs in background) + // Use higher worker limit for I/O-bound storage work (Aerospike network calls) + newStorageGroup, sCtx := errgroup.WithContext(ctx) + // I/O-bound: Use higher multiplier for network latency tolerance + // SpendBatcherSize controls batch size; multiply by 6 for I/O concurrency + storageWorkers := u.settings.SubtreeValidation.SpendBatcherSize * 6 + util.SafeSetLimit(newStorageGroup, storageWorkers) + + currentState, err := u.blockchainClient.GetFSMCurrentState(ctx) + if err != nil { + return errors.NewProcessingError("[processTransactionsInLevels] Failed to get FSM current state", err) + } + + skipBlockAssembly := *currentState == blockchain.FSMStateLEGACYSYNCING || *currentState == blockchain.FSMStateCATCHINGBLOCKS + + // Build storage-phase validator options + // Skip validation (already done in validation phase), but perform UTXO operations + storageOptions := &validator.Options{ + SkipValidation: true, // Skip validation work (already done) + SkipUtxoCreate: false, // DO create UTXOs + SkipUtxoSpend: false, // DO spend parent UTXOs + SkipPolicyChecks: true, + CreateConflicting: true, + IgnoreLocked: true, + AddTXToBlockAssembly: !skipBlockAssembly, + ParentMetadata: processedValidationOptions.ParentMetadata, // Reuse parent metadata from validation phase + } + + // Only process successfully validated transactions in storage phase + for _, mTx := range levelTxs { + tx := mTx.tx + if tx == nil { + continue + } + + // Skip transactions that failed validation + successfulTxsMutex.Lock() + wasSuccessful := successfulTxsByLevel[level][*tx.TxIDChainHash()] + successfulTxsMutex.Unlock() + + if !wasSuccessful { + u.logger.Debugf("[processTransactionsInLevels] Skipping storage for transaction %s (validation failed)", tx.TxIDChainHash().String()) + continue + } + + newStorageGroup.Go(func() error { + // Store: Call validator with SkipValidation=true to perform UTXO operations only + _, storeErr := u.validatorClient.ValidateWithOptions(sCtx, tx, blockHeight, storageOptions) + if storeErr != nil { + // These are success cases - transaction was already stored + if errors.Is(storeErr, errors.ErrTxExists) { + u.logger.Debugf("[processTransactionsInLevels] Transaction %s already exists, skipping", tx.TxIDChainHash().String()) + return nil + } + + if errors.Is(storeErr, errors.ErrTxConflicting) { + u.logger.Debugf("[processTransactionsInLevels] Transaction %s is conflicting, skipping", tx.TxIDChainHash().String()) + return nil + } + + // FAIL FAST: Return storage error immediately + return errors.NewProcessingError("[processTransactionsInLevels] Storage failed for tx %s at level %d", tx.TxIDChainHash().String(), level+1, storeErr) } return nil }) } - // Fail early if we get an actual tx error thrown - if err = g.Wait(); err != nil { - return errors.NewProcessingError("[processTransactionsInLevels] Failed to process level %d", level+1, err) - } + // Store for next iteration - next level will wait for this to complete + storageGroup = newStorageGroup - u.logger.Debugf("[processTransactionsInLevels] Processing level %d/%d with %d transactions DONE", level+1, maxLevel+1, len(levelTxs)) + u.logger.Debugf("[processTransactionsInLevels] Level %d storage started (%d transactions)", level, len(levelTxs)) // PHASE 2 OPTIMIZATION: Release grandparent level (level-2) after current level succeeds // Keep current level (being processed) and parent level (level-1) for safety @@ -831,6 +929,15 @@ func (u *Server) processTransactionsInLevels(ctx context.Context, allTransaction } } + // Wait for final level's storage to complete + if storageGroup != nil { + u.logger.Debugf("[processTransactionsInLevels] Waiting for final level storage to complete") + if err = storageGroup.Wait(); err != nil { + return errors.NewProcessingError("[processTransactionsInLevels] Final level storage failed", err) + } + u.logger.Debugf("[processTransactionsInLevels] Final level storage complete") + } + if errorsFound.Load() > 0 { return errors.NewProcessingError("[processTransactionsInLevels] Completed processing with %d errors, %d transactions added to orphanage", errorsFound.Load(), addedToOrphanage.Load()) } diff --git a/services/subtreevalidation/check_block_subtrees_test.go b/services/subtreevalidation/check_block_subtrees_test.go index cb52ccc460..01b1e44b99 100644 --- a/services/subtreevalidation/check_block_subtrees_test.go +++ b/services/subtreevalidation/check_block_subtrees_test.go @@ -1429,6 +1429,19 @@ func setupTestServer(t *testing.T) (*Server, func()) { mockUtxoStore.On("GetMeta", mock.Anything, mock.Anything). Return(&utxometa.Data{}, nil).Maybe() + // Set up default mock for Get method (used by mock validator to extend transactions) + // Return a transaction with multiple outputs to support any output index + parentTxForExtension := bt.NewTx() + for i := 0; i < 20; i++ { // Create 20 outputs to handle any test case + _ = parentTxForExtension.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 5000000000) + } + mockUtxoStore.On("Get", + mock.Anything, mock.Anything, mock.Anything). + Return(&utxometa.Data{Tx: parentTxForExtension, BlockHeights: []uint32{100}}, nil).Maybe() + // Set up default mock for Spend method + mockUtxoStore.On("Spend", + mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*utxo.Spend{}, nil).Maybe() // Mock validator client mockValidatorClient := &validator.MockValidatorClient{} diff --git a/services/validator/Client.go b/services/validator/Client.go index 044b6ca57b..f63a8f5067 100644 --- a/services/validator/Client.go +++ b/services/validator/Client.go @@ -203,7 +203,7 @@ func (c *Client) ValidateWithOptions(ctx context.Context, tx *bt.Tx, blockHeight response, err := c.client.ValidateTransaction(ctx, &validator_api.ValidateTransactionRequest{ TransactionData: tx.SerializeBytes(), BlockHeight: blockHeight, - SkipUtxoCreation: &validationOptions.SkipUtxoCreation, + SkipUtxoCreate: &validationOptions.SkipUtxoCreate, AddTxToBlockAssembly: &validationOptions.AddTXToBlockAssembly, SkipPolicyChecks: &validationOptions.SkipPolicyChecks, CreateConflicting: &validationOptions.CreateConflicting, @@ -229,7 +229,7 @@ func (c *Client) ValidateWithOptions(ctx context.Context, tx *bt.Tx, blockHeight req: &validator_api.ValidateTransactionRequest{ TransactionData: tx.SerializeBytes(), BlockHeight: blockHeight, - SkipUtxoCreation: &validationOptions.SkipUtxoCreation, + SkipUtxoCreate: &validationOptions.SkipUtxoCreate, AddTxToBlockAssembly: &validationOptions.AddTXToBlockAssembly, SkipPolicyChecks: &validationOptions.SkipPolicyChecks, CreateConflicting: &validationOptions.CreateConflicting, @@ -336,7 +336,7 @@ func (c *Client) handleBatchHTTPFallback(ctx context.Context, batch []*batchItem // Create options from the request options := &Options{ - SkipUtxoCreation: *txReq.SkipUtxoCreation, + SkipUtxoCreate: *txReq.SkipUtxoCreate, AddTXToBlockAssembly: *txReq.AddTxToBlockAssembly, SkipPolicyChecks: *txReq.SkipPolicyChecks, CreateConflicting: *txReq.CreateConflicting, @@ -393,8 +393,8 @@ func (c *Client) validateTransactionViaHTTP(ctx context.Context, tx *bt.Tx, bloc // Add validation options as query parameters queryParams := url.Values{} - if validationOptions.SkipUtxoCreation { - queryParams.Add("skipUtxoCreation", "true") + if validationOptions.SkipUtxoCreate { + queryParams.Add("SkipUtxoCreate", "true") } if validationOptions.AddTXToBlockAssembly { diff --git a/services/validator/Client_test.go b/services/validator/Client_test.go index 61a19ab64b..c5244fea3e 100644 --- a/services/validator/Client_test.go +++ b/services/validator/Client_test.go @@ -360,7 +360,7 @@ func TestBatchValidation(t *testing.T) { req: &validator_api.ValidateTransactionRequest{ TransactionData: txBytes, BlockHeight: 100, - SkipUtxoCreation: boolPtr(false), + SkipUtxoCreate: boolPtr(false), AddTxToBlockAssembly: boolPtr(true), SkipPolicyChecks: boolPtr(false), CreateConflicting: boolPtr(false), @@ -371,7 +371,7 @@ func TestBatchValidation(t *testing.T) { req: &validator_api.ValidateTransactionRequest{ TransactionData: txBytes, BlockHeight: 100, - SkipUtxoCreation: boolPtr(false), + SkipUtxoCreate: boolPtr(false), AddTxToBlockAssembly: boolPtr(true), SkipPolicyChecks: boolPtr(false), CreateConflicting: boolPtr(false), @@ -418,7 +418,7 @@ func TestBatchValidation_ResourceExhausted(t *testing.T) { req: &validator_api.ValidateTransactionRequest{ TransactionData: txBytes, BlockHeight: 100, - SkipUtxoCreation: boolPtr(false), + SkipUtxoCreate: boolPtr(false), AddTxToBlockAssembly: boolPtr(true), SkipPolicyChecks: boolPtr(false), CreateConflicting: boolPtr(false), diff --git a/services/validator/Mock.go b/services/validator/Mock.go index a5dc1145b9..e152e10c51 100644 --- a/services/validator/Mock.go +++ b/services/validator/Mock.go @@ -110,7 +110,27 @@ func (m *MockValidatorClient) ValidateWithOptions(ctx context.Context, tx *bt.Tx return nil, err } - return m.UtxoStore.Create(context.Background(), tx, 0) + // If SkipUtxoCreate is true (validation-only phase), return empty metadata without storing + if validationOptions.SkipUtxoCreate { + return &meta.Data{}, nil + } + + // Extend transaction if needed (real validator does this automatically) + // Only extend if transaction isn't already extended + if len(tx.Inputs) > 0 && tx.Inputs[0].PreviousTxScript == nil { + for _, input := range tx.Inputs { + parentHash := input.PreviousTxIDChainHash() + if parentHash != nil { + parentMeta, err := m.UtxoStore.Get(ctx, parentHash, utxo.MetaFieldsWithTx...) + if err == nil && parentMeta.Tx != nil && len(parentMeta.Tx.Outputs) > int(input.PreviousTxOutIndex) { + input.PreviousTxSatoshis = parentMeta.Tx.Outputs[input.PreviousTxOutIndex].Satoshis + input.PreviousTxScript = parentMeta.Tx.Outputs[input.PreviousTxOutIndex].LockingScript + } + } + } + } + + return m.UtxoStore.Create(context.Background(), tx, blockHeight) } // TriggerBatcher implements the batcher trigger interface for testing. diff --git a/services/validator/Server.go b/services/validator/Server.go index ecc8532604..67a3abab40 100644 --- a/services/validator/Server.go +++ b/services/validator/Server.go @@ -349,7 +349,7 @@ func (v *Server) Start(ctx context.Context, readyCh chan<- struct{}) error { height := kafkaMsg.Height options := &Options{ - SkipUtxoCreation: kafkaMsg.Options.SkipUtxoCreation, + SkipUtxoCreate: kafkaMsg.Options.SkipUtxoCreate, AddTXToBlockAssembly: kafkaMsg.Options.AddTXToBlockAssembly, SkipPolicyChecks: kafkaMsg.Options.SkipPolicyChecks, CreateConflicting: kafkaMsg.Options.CreateConflicting, @@ -467,8 +467,8 @@ func (v *Server) validateTransaction(ctx context.Context, req *validator_api.Val tx.SetTxHash(tx.TxIDChainHash()) validationOptions := NewDefaultOptions() - if req.SkipUtxoCreation != nil { - validationOptions.SkipUtxoCreation = *req.SkipUtxoCreation + if req.SkipUtxoCreate != nil { + validationOptions.SkipUtxoCreate = *req.SkipUtxoCreate } if req.AddTxToBlockAssembly != nil { @@ -636,7 +636,7 @@ func (v *Server) GetMedianBlockTime(ctx context.Context, _ *validator_api.EmptyM // extractValidationParams extracts validation parameters from HTTP query string parameters. // This utility function parses and converts various query parameters into validation options // for transaction processing. It handles both numeric parameters (like blockHeight) and -// boolean flags (like skipUtxoCreation, addTxToBlockAssembly) that control validation behavior. +// boolean flags (like SkipUtxoCreate, addTxToBlockAssembly) that control validation behavior. // // The function recognizes boolean values as either 'true' or '1' strings in the query parameters. // If parameters are not provided or cannot be parsed, default values are used. @@ -665,9 +665,9 @@ func extractValidationParams(c echo.Context) (uint32, *Options) { } // Extract boolean parameters - if skipUtxoCreationStr := c.QueryParam("skipUtxoCreation"); skipUtxoCreationStr != "" { - boolVal := skipUtxoCreationStr == trueString || skipUtxoCreationStr == "1" - options.SkipUtxoCreation = boolVal + if SkipUtxoCreateStr := c.QueryParam("SkipUtxoCreate"); SkipUtxoCreateStr != "" { + boolVal := SkipUtxoCreateStr == trueString || SkipUtxoCreateStr == "1" + options.SkipUtxoCreate = boolVal } if addTxToBlockAssemblyStr := c.QueryParam("addTxToBlockAssembly"); addTxToBlockAssemblyStr != "" { @@ -696,7 +696,7 @@ func extractValidationParams(c echo.Context) (uint32, *Options) { // // The handler supports several validation options through query parameters: // - blockHeight: The blockchain height to validate against -// - skipUtxoCreation: Whether to skip UTXO creation (useful for testing/dry-runs) +// - SkipUtxoCreate: Whether to skip UTXO creation (useful for testing/dry-runs) // - addTxToBlockAssembly: Whether to include the transaction in block templates // - skipPolicyChecks: Whether to skip non-consensus policy validation checks // - createConflicting: Whether to allow creating conflicting UTXOs @@ -724,7 +724,7 @@ func (v *Server) handleSingleTx(ctx context.Context) echo.HandlerFunc { req := &validator_api.ValidateTransactionRequest{ TransactionData: body, BlockHeight: blockHeight, - SkipUtxoCreation: &options.SkipUtxoCreation, + SkipUtxoCreate: &options.SkipUtxoCreate, AddTxToBlockAssembly: &options.AddTXToBlockAssembly, SkipPolicyChecks: &options.SkipPolicyChecks, CreateConflicting: &options.CreateConflicting, @@ -792,7 +792,7 @@ func (v *Server) handleMultipleTx(ctx context.Context) echo.HandlerFunc { req := &validator_api.ValidateTransactionRequest{ TransactionData: tx.SerializeBytes(), BlockHeight: blockHeight, - SkipUtxoCreation: &options.SkipUtxoCreation, + SkipUtxoCreate: &options.SkipUtxoCreate, AddTxToBlockAssembly: &options.AddTXToBlockAssembly, SkipPolicyChecks: &options.SkipPolicyChecks, CreateConflicting: &options.CreateConflicting, diff --git a/services/validator/Server_coverage_test.go b/services/validator/Server_coverage_test.go index 70417d9f94..ebf2b6a887 100644 --- a/services/validator/Server_coverage_test.go +++ b/services/validator/Server_coverage_test.go @@ -279,7 +279,7 @@ func TestServerValidateTransaction(t *testing.T) { req := &validator_api.ValidateTransactionRequest{ TransactionData: sampleTx, BlockHeight: 100, - SkipUtxoCreation: &skipUtxo, + SkipUtxoCreate: &skipUtxo, AddTxToBlockAssembly: &addToBlock, SkipPolicyChecks: &skipPolicy, CreateConflicting: &createConflict, @@ -482,32 +482,32 @@ func TestExtractValidationParams(t *testing.T) { height, options := extractValidationParams(c) require.Equal(t, uint32(0), height) - require.False(t, options.SkipUtxoCreation) + require.False(t, options.SkipUtxoCreate) require.True(t, options.AddTXToBlockAssembly) // Default is true require.False(t, options.SkipPolicyChecks) require.False(t, options.CreateConflicting) }) t.Run("all parameters true", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/tx?blockHeight=100&skipUtxoCreation=true&addTxToBlockAssembly=true&skipPolicyChecks=true&createConflicting=true", nil) + req := httptest.NewRequest(http.MethodPost, "/tx?blockHeight=100&SkipUtxoCreate=true&addTxToBlockAssembly=true&skipPolicyChecks=true&createConflicting=true", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) height, options := extractValidationParams(c) require.Equal(t, uint32(100), height) - require.True(t, options.SkipUtxoCreation) + require.True(t, options.SkipUtxoCreate) require.True(t, options.AddTXToBlockAssembly) require.True(t, options.SkipPolicyChecks) require.True(t, options.CreateConflicting) }) t.Run("parameters with 1", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/tx?skipUtxoCreation=1&addTxToBlockAssembly=1&skipPolicyChecks=1&createConflicting=1", nil) + req := httptest.NewRequest(http.MethodPost, "/tx?SkipUtxoCreate=1&addTxToBlockAssembly=1&skipPolicyChecks=1&createConflicting=1", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) _, options := extractValidationParams(c) - require.True(t, options.SkipUtxoCreation) + require.True(t, options.SkipUtxoCreate) require.True(t, options.AddTXToBlockAssembly) require.True(t, options.SkipPolicyChecks) require.True(t, options.CreateConflicting) diff --git a/services/validator/Validator.go b/services/validator/Validator.go index 543390fafc..f0be2020b1 100644 --- a/services/validator/Validator.go +++ b/services/validator/Validator.go @@ -413,87 +413,88 @@ func (v *Validator) validateInternal(ctx context.Context, tx *bt.Tx, blockHeight }() } - var spentUtxos []*utxo.Spend + // Skip all validation when SkipValidation=true (already validated in previous phase) + if !validationOptions.SkipValidation { + // Get atomic block state to prevent race conditions between height and median time reads + blockState := v.GetBlockState() - // Get atomic block state to prevent race conditions between height and median time reads - blockState := v.GetBlockState() + if blockHeight == 0 { + blockHeight = blockState.Height + 1 + } - if blockHeight == 0 { - blockHeight = blockState.Height + 1 - } + // We do not check IsFinal for transactions before BIP113 change (block height 419328) + // This is an exception for transactions before the media block time was used + if blockHeight > v.settings.ChainCfgParams.CSVHeight { - // We do not check IsFinal for transactions before BIP113 change (block height 419328) - // This is an exception for transactions before the media block time was used - if blockHeight > v.settings.ChainCfgParams.CSVHeight { + utxoStoreMedianBlockTime := blockState.MedianTime + if utxoStoreMedianBlockTime == 0 { + err = errors.NewProcessingError("utxo store not ready, block height: %d, median block time: %d", blockHeight, utxoStoreMedianBlockTime) + span.RecordError(err) - utxoStoreMedianBlockTime := blockState.MedianTime - if utxoStoreMedianBlockTime == 0 { - err = errors.NewProcessingError("utxo store not ready, block height: %d, median block time: %d", blockHeight, utxoStoreMedianBlockTime) - span.RecordError(err) + return nil, err + } - return nil, err + // this function should be moved into go-bt + if err = util.IsTransactionFinal(tx, blockHeight, utxoStoreMedianBlockTime); err != nil { + err = errors.NewUtxoNonFinalError("[Validate][%s] transaction is not final", txID, err) + span.RecordError(err) + + return nil, err + } } - // this function should be moved into go-bt - if err = util.IsTransactionFinal(tx, blockHeight, utxoStoreMedianBlockTime); err != nil { - err = errors.NewUtxoNonFinalError("[Validate][%s] transaction is not final", txID, err) + if tx.IsCoinbase() { + err = errors.NewProcessingError("[Validate][%s] coinbase transactions are not supported", txID) span.RecordError(err) return nil, err } - } - if tx.IsCoinbase() { - err = errors.NewProcessingError("[Validate][%s] coinbase transactions are not supported", txID) - span.RecordError(err) + var utxoHeights []uint32 - return nil, err - } + // check whether the transaction is extended, extend it if not + // we also get the block heights of the inputs of the transaction since we are doing a DB lookup + if !tx.IsExtended() { + // get the block heights of all inputs of the transaction and extend the inputs of not extended transaction. + // utxoHeights is a slice of block heights for each input + // txInpoints is a struct containing the parent tx hashes and the vout indexes of each input + if utxoHeights, err = v.getTransactionInputBlockHeightsAndExtendTx(ctx, tx, txID, validationOptions); err != nil { + err = errors.NewProcessingError("[Validate][%s] error getting transaction input block heights", txID, err) + span.RecordError(err) - var utxoHeights []uint32 + return nil, err + } + } - // check whether the transaction is extended, extend it if not - // we also get the block heights of the inputs of the transaction since we are doing a DB lookup - if !tx.IsExtended() { - // get the block heights of all inputs of the transaction and extend the inputs of not extended transaction. - // utxoHeights is a slice of block heights for each input - // txInpoints is a struct containing the parent tx hashes and the vout indexes of each input - if utxoHeights, err = v.getTransactionInputBlockHeightsAndExtendTx(ctx, tx, txID, validationOptions); err != nil { - err = errors.NewProcessingError("[Validate][%s] error getting transaction input block heights", txID, err) + // validate the transaction format, consensus rules etc. + // this does not validate the signatures in the transaction yet + if err = v.validateTransaction(ctx, tx, blockHeight, utxoHeights, validationOptions); err != nil { + err = errors.NewProcessingError("[Validate][%s] error validating transaction", txID, err) span.RecordError(err) return nil, err } - } - // validate the transaction format, consensus rules etc. - // this does not validate the signatures in the transaction yet - if err = v.validateTransaction(ctx, tx, blockHeight, utxoHeights, validationOptions); err != nil { - err = errors.NewProcessingError("[Validate][%s] error validating transaction", txID, err) - span.RecordError(err) + // if the transaction was extended, we still need to get the block heights of the inputs + // since that processing did not happen before the validateTransaction step + if len(utxoHeights) == 0 { + if utxoHeights, err = v.getTransactionInputBlockHeightsAndExtendTx(ctx, tx, txID, validationOptions); err != nil { + err = errors.NewProcessingError("[Validate][%s] error getting transaction input block heights", txID, err) + span.RecordError(err) - return nil, err - } + return nil, err + } + } - // if the transaction was extended, we still need to get the block heights of the inputs - // since that processing did not happen before the validateTransaction step - if len(utxoHeights) == 0 { - if utxoHeights, err = v.getTransactionInputBlockHeightsAndExtendTx(ctx, tx, txID, validationOptions); err != nil { - err = errors.NewProcessingError("[Validate][%s] error getting transaction input block heights", txID, err) + // validate the transaction scripts and signatures + if err = v.validateTransactionScripts(ctx, tx, blockHeight, utxoHeights, validationOptions); err != nil { + err = errors.NewProcessingError("[Validate][%s] error validating transaction scripts", txID, err) span.RecordError(err) return nil, err } } - // validate the transaction scripts and signatures - if err = v.validateTransactionScripts(ctx, tx, blockHeight, utxoHeights, validationOptions); err != nil { - err = errors.NewProcessingError("[Validate][%s] error validating transaction scripts", txID, err) - span.RecordError(err) - - return nil, err - } - // decouple the tracing context to not cancel the context when finalize the block assembly decoupledCtx, _, deferFn := tracing.DecoupleTracingSpan(ctx, "validator", "decoupledSpan") defer deferFn() @@ -509,64 +510,81 @@ func (v *Validator) validateInternal(ctx context.Context, tx *bt.Tx, blockHeight */ var ( + spentUtxos []*utxo.Spend tErr *errors.Error utxoMapErr error ) // this will reverse the spends if there is an error - if spentUtxos, err = v.spendUtxos(decoupledCtx, tx, blockHeight, validationOptions.IgnoreLocked); err != nil { - if errors.Is(err, errors.ErrUtxoError) { - saveAsConflicting := false + if !validationOptions.SkipUtxoSpend { + if spentUtxos, err = v.spendUtxos(decoupledCtx, tx, blockHeight, validationOptions.IgnoreLocked); err != nil { + if errors.Is(err, errors.ErrUtxoError) { + saveAsConflicting := false - var spendErrs *errors.Error + var spendErrs *errors.Error - for _, spend := range spentUtxos { - if spend.Err != nil { - if validationOptions.CreateConflicting && (errors.Is(spend.Err, errors.ErrSpent) || errors.Is(spend.Err, errors.ErrTxConflicting)) { - saveAsConflicting = true - } + for _, spend := range spentUtxos { + if spend.Err != nil { + if validationOptions.CreateConflicting && (errors.Is(spend.Err, errors.ErrSpent) || errors.Is(spend.Err, errors.ErrTxConflicting)) { + saveAsConflicting = true + } - var spendErr *errors.Error - if errors.As(spend.Err, &spendErr) { - if spendErrs == nil { - spendErrs = errors.New(spendErr.Code(), spendErr.Message(), spendErr) - } else { - spendErrs = errors.New(spendErrs.Code(), spendErrs.Message(), spendErr) + var spendErr *errors.Error + if errors.As(spend.Err, &spendErr) { + if spendErrs == nil { + spendErrs = errors.New(spendErr.Code(), spendErr.Message(), spendErr) + } else { + spendErrs = errors.New(spendErrs.Code(), spendErrs.Message(), spendErr) + } } } } - } - if spendErrs != nil { - if errors.As(err, &tErr) { - tErr.SetWrappedErr(spendErrs) + if spendErrs != nil { + if errors.As(err, &tErr) { + tErr.SetWrappedErr(spendErrs) + } } - } - if saveAsConflicting { - if txMetaData, utxoMapErr = v.CreateInUtxoStore(decoupledCtx, tx, blockHeight, true, false); utxoMapErr != nil { - if errors.Is(utxoMapErr, errors.ErrTxExists) { - txMetaData = &meta.Data{} - if err = v.utxoStore.GetMeta(decoupledCtx, tx.TxIDChainHash(), txMetaData); err != nil { - err = errors.NewProcessingError("[Validate][%s] CreateInUtxoStore failed - tx exists but unable to get meta data", txID, utxoMapErr) - span.RecordError(err) + if saveAsConflicting { + if txMetaData, utxoMapErr = v.CreateInUtxoStore(decoupledCtx, tx, blockHeight, true, false); utxoMapErr != nil { + if errors.Is(utxoMapErr, errors.ErrTxExists) { + txMetaData = &meta.Data{} + if err = v.utxoStore.GetMeta(decoupledCtx, tx.TxIDChainHash(), txMetaData); err != nil { + err = errors.NewProcessingError("[Validate][%s] CreateInUtxoStore failed - tx exists but unable to get meta data", txID, utxoMapErr) + span.RecordError(err) - return nil, err + return nil, err + } + + return txMetaData, nil } + + err = errors.NewProcessingError("[Validate][%s] CreateInUtxoStore failed", txID, utxoMapErr) + span.RecordError(err) + + return nil, err } - err = errors.NewProcessingError("[Validate][%s] CreateInUtxoStore failed - tx exists but unable to get meta data", txID, utxoMapErr) + // We successfully added the tx to the utxo store as a conflicting tx, + // so we can return a conflicting error + err = errors.NewTxConflictingError("[Validate][%s] tx is conflicting", txID, err) span.RecordError(err) return txMetaData, err } + } - // We successfully added the tx to the utxo store as a conflicting tx, - // so we can return a conflicting error - err = errors.NewTxConflictingError("[Validate][%s] tx is conflicting", txID, err) - span.RecordError(err) + if errors.Is(err, errors.ErrTxNotFound) { + // the parent transaction was not found, this can happen when the parent tx has been DAH'd and removed from + // the utxo store. We can check whether the tx already exists, which means it has been validated and + // blessed. In this case we can just return early. + txMetaData = &meta.Data{} + if err = v.utxoStore.GetMeta(decoupledCtx, tx.TxIDChainHash(), txMetaData); err == nil { + v.logger.Warnf("[Validate][%s] parent tx not found, but tx already exists in store, assuming already blessed", txID) - return txMetaData, err + return txMetaData, nil + } } } else if errors.Is(err, errors.ErrTxNotFound) { // the parent transaction was not found, this can happen when the parent tx has been DAH'd and removed from @@ -590,7 +608,7 @@ func (v *Validator) validateInternal(ctx context.Context, tx *bt.Tx, blockHeight blockAssemblyEnabled := !v.settings.BlockAssembly.Disabled addToBlockAssembly := blockAssemblyEnabled && validationOptions.AddTXToBlockAssembly - if !validationOptions.SkipUtxoCreation { + if !validationOptions.SkipUtxoCreate { // store the transaction in the UTXO store, marking it as locked if we are going to add it to the block assembly txMetaData, err = v.CreateInUtxoStore(decoupledCtx, tx, blockHeight, false, addToBlockAssembly) if err != nil { @@ -753,19 +771,16 @@ func (v *Validator) getUtxoBlockHeightAndExtendForParentTx(gCtx context.Context, utxoHeights []uint32, tx *bt.Tx, extend bool, validationOptions *Options) error { // OPTIMIZATION: Check if parent metadata is provided in options (for in-block parents) - // This allows validation without UTXO store lookups for in-block parent transactions - // SAFETY: Parent metadata only includes transactions that successfully validated AND created UTXOs - // (see check_block_subtrees.go:buildParentMetadata which filters by successful validations) - if validationOptions != nil && validationOptions.ParentMetadata != nil { + // This allows validation of Level N while Level N-1 is still storing + if validationOptions.ParentMetadata != nil { if parentMeta, found := validationOptions.ParentMetadata[parentTxHash]; found { // Use pre-fetched metadata instead of UTXO store lookup - // Safe because metadata only includes transactions that completed full validation+storage for _, idx := range idxs { utxoHeights[idx] = parentMeta.BlockHeight } - // If transaction is already extended, we have all the data we need - // The parent metadata optimization works best with pre-extended transactions + // If transaction is already extended (which it should be in optimized pipeline), + // we have all the metadata we need from ParentMetadata - return early if !extend { return nil } @@ -773,6 +788,7 @@ func (v *Validator) getUtxoBlockHeightAndExtendForParentTx(gCtx context.Context, } } + // Normal path: Fetch from UTXO store f := []fields.FieldName{fields.BlockIDs, fields.BlockHeights} if extend { diff --git a/services/validator/http_server_test.go b/services/validator/http_server_test.go index 3c44ce4f2b..6374a40ab7 100644 --- a/services/validator/http_server_test.go +++ b/services/validator/http_server_test.go @@ -98,7 +98,7 @@ func TestHTTPEndpoints(t *testing.T) { utxoMock.On("GetBlockState").Return(utxo.BlockState{Height: 1000, MedianTime: 1625097600}) utxoMock.On("PreviousOutputsDecorate", mock.Anything, mock.Anything).Return(nil) - // Add expectation for the Get method which may be called regardless of skipUtxoCreation + // Add expectation for the Get method which may be called regardless of SkipUtxoCreate metaData := &meta.Data{ Fee: 32279815860, SizeInBytes: 245, @@ -119,7 +119,7 @@ func TestHTTPEndpoints(t *testing.T) { e := echo.New() // Build URL with parameters to bypass validation issues - queryParams := "skipUtxoCreation=true&addTxToBlockAssembly=false&skipPolicyChecks=true&createConflicting=false" + queryParams := "SkipUtxoCreate=true&addTxToBlockAssembly=false&skipPolicyChecks=true&createConflicting=false" req := httptest.NewRequest(http.MethodPost, "/tx?"+queryParams, bytes.NewReader(txBytes)) rec := httptest.NewRecorder() @@ -175,7 +175,7 @@ func TestHTTPEndpoints(t *testing.T) { buf.Write(txBytes) // Build URL with parameters to bypass validation issues - queryParams := "skipUtxoCreation=true&addTxToBlockAssembly=false&skipPolicyChecks=true&createConflicting=false" + queryParams := "SkipUtxoCreate=true&addTxToBlockAssembly=false&skipPolicyChecks=true&createConflicting=false" req := httptest.NewRequest(http.MethodPost, "/txs?"+queryParams, &buf) rec := httptest.NewRecorder() diff --git a/services/validator/options.go b/services/validator/options.go index c710896561..b669006bec 100644 --- a/services/validator/options.go +++ b/services/validator/options.go @@ -19,9 +19,9 @@ type ParentTxMetadata struct { // Options defines the configuration options for validation operations type Options struct { - // SkipUtxoCreation determines whether UTXO creation should be skipped + // SkipUtxoCreate determines whether UTXO creation should be skipped // When true, the validator won't create new UTXOs for transaction outputs - SkipUtxoCreation bool + SkipUtxoCreate bool // AddTXToBlockAssembly determines whether transactions should be added to block assembly // When true, validated transactions are forwarded to the block assembly process @@ -41,9 +41,19 @@ type Options struct { // IgnoreLocked determines whether to ignore transactions marked as locked when spending IgnoreLocked bool + // SkipUtxoSpend skips spending parent UTXOs in UTXO store + // Used for validate-only mode (CPU-intensive validation without I/O) + SkipUtxoSpend bool + + // SkipValidation skips script execution and signature verification + // Used for store-only mode (I/O operations without CPU-intensive validation) + SkipValidation bool + // ParentMetadata provides pre-fetched metadata for parent transactions // When provided, the validator will check this map before calling utxoStore.Get() - // This enables validation to proceed without UTXO store lookups for in-block parents + // This allows validation of Level N while Level N-1 is still storing + // SAFETY: Only includes transactions that successfully validated AND created UTXOs + // This prevents validation bypass when child references a failed parent transaction // Key: parent transaction hash, Value: metadata (block height) ParentMetadata map[chainhash.Hash]*ParentTxMetadata } @@ -54,14 +64,14 @@ type Option func(*Options) // NewDefaultOptions creates a new Options instance with default settings // Default configuration: -// - skipUtxoCreation: false (UTXOs will be created) +// - SkipUtxoCreate: false (UTXOs will be created) // - addTXToBlockAssembly: true (transactions will be added to block assembly) // // Returns: // - *Options: New options instance with default settings func NewDefaultOptions() *Options { return &Options{ - SkipUtxoCreation: false, + SkipUtxoCreate: false, AddTXToBlockAssembly: true, SkipPolicyChecks: false, CreateConflicting: false, @@ -83,15 +93,15 @@ func ProcessOptions(opts ...Option) *Options { return options } -// WithSkipUtxoCreation creates an option to control UTXO creation +// WithSkipUtxoCreate creates an option to control UTXO creation // Parameters: // - skip: When true, UTXO creation will be skipped // // Returns: -// - Option: Function that sets the skipUtxoCreation option -func WithSkipUtxoCreation(skip bool) Option { +// - Option: Function that sets the skipUtxoCreate option +func WithSkipUtxoCreate(skip bool) Option { return func(o *Options) { - o.SkipUtxoCreation = skip + o.SkipUtxoCreate = skip } } @@ -155,12 +165,42 @@ func WithIgnoreLocked(ignoreLocked bool) Option { } } +// WithSkipUtxoSpend creates an option to skip UTXO store spending +// Used for validate-only mode (CPU-intensive validation without I/O operations) +// Parameters: +// - skip: When true, utxoStore.Spend() will be skipped +// +// Returns: +// - Option: Function that sets the SkipUtxoSpend option +func WithSkipUtxoSpend(skip bool) Option { + return func(o *Options) { + o.SkipUtxoSpend = skip + } +} + +// WithSkipValidation creates an option to skip script validation +// Used for store-only mode (I/O operations without CPU-intensive validation) +// Parameters: +// - skip: When true, script execution and signature verification will be skipped +// +// Returns: +// - Option: Function that sets the SkipValidation option +func WithSkipValidation(skip bool) Option { + return func(o *Options) { + o.SkipValidation = skip + } +} + // WithParentMetadata creates an option to provide pre-fetched parent transaction metadata +// This allows the validator to skip UTXO store lookups for in-block parents, enabling +// validation of Level N transactions while Level N-1 is still storing to the UTXO store. +// SAFETY: Only provide metadata for transactions that successfully validated +// // Parameters: -// - metadata: Map of parent transaction hashes to their metadata (block height, etc.) +// - metadata: Map of parent transaction hashes to their metadata (block height) // // Returns: -// - Option: Function that sets the parentMetadata option +// - Option: Function that sets the ParentMetadata option func WithParentMetadata(metadata map[chainhash.Hash]*ParentTxMetadata) Option { return func(o *Options) { o.ParentMetadata = metadata diff --git a/services/validator/options_test.go b/services/validator/options_test.go index 82e6fa79a3..41ff276d25 100644 --- a/services/validator/options_test.go +++ b/services/validator/options_test.go @@ -7,8 +7,8 @@ import ( func TestNewDefaultOptions(t *testing.T) { opts := NewDefaultOptions() - if opts.SkipUtxoCreation { - t.Error("Default SkipUtxoCreation should be false") + if opts.SkipUtxoCreate { + t.Error("Default SkipUtxoCreate should be false") } if !opts.AddTXToBlockAssembly { @@ -34,7 +34,7 @@ func TestProcessOptions(t *testing.T) { name: "No options", opts: []Option{}, expected: Options{ - SkipUtxoCreation: false, + SkipUtxoCreate: false, AddTXToBlockAssembly: true, SkipPolicyChecks: false, CreateConflicting: false, @@ -43,10 +43,10 @@ func TestProcessOptions(t *testing.T) { { name: "Single option", opts: []Option{ - WithSkipUtxoCreation(true), + WithSkipUtxoCreate(true), }, expected: Options{ - SkipUtxoCreation: true, + SkipUtxoCreate: true, AddTXToBlockAssembly: true, SkipPolicyChecks: false, CreateConflicting: false, @@ -55,13 +55,13 @@ func TestProcessOptions(t *testing.T) { { name: "Multiple options", opts: []Option{ - WithSkipUtxoCreation(true), + WithSkipUtxoCreate(true), WithAddTXToBlockAssembly(false), WithSkipPolicyChecks(true), WithCreateConflicting(true), }, expected: Options{ - SkipUtxoCreation: true, + SkipUtxoCreate: true, AddTXToBlockAssembly: false, SkipPolicyChecks: true, CreateConflicting: true, @@ -72,8 +72,8 @@ func TestProcessOptions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ProcessOptions(tt.opts...) - if result.SkipUtxoCreation != tt.expected.SkipUtxoCreation { - t.Errorf("SkipUtxoCreation = %v, want %v", result.SkipUtxoCreation, tt.expected.SkipUtxoCreation) + if result.SkipUtxoCreate != tt.expected.SkipUtxoCreate { + t.Errorf("SkipUtxoCreate = %v, want %v", result.SkipUtxoCreate, tt.expected.SkipUtxoCreate) } if result.AddTXToBlockAssembly != tt.expected.AddTXToBlockAssembly { @@ -91,7 +91,7 @@ func TestProcessOptions(t *testing.T) { } } -func TestWithSkipUtxoCreation(t *testing.T) { +func TestWithSkipUtxoCreate(t *testing.T) { tests := []struct { name string skip bool @@ -103,9 +103,9 @@ func TestWithSkipUtxoCreation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - opts := ProcessOptions(WithSkipUtxoCreation(tt.skip)) - if opts.SkipUtxoCreation != tt.expected { - t.Errorf("SkipUtxoCreation = %v, want %v", opts.SkipUtxoCreation, tt.expected) + opts := ProcessOptions(WithSkipUtxoCreate(tt.skip)) + if opts.SkipUtxoCreate != tt.expected { + t.Errorf("SkipUtxoCreate = %v, want %v", opts.SkipUtxoCreate, tt.expected) } }) } diff --git a/services/validator/validator_api/validator_api.pb.go b/services/validator/validator_api/validator_api.pb.go index 2300e60ce0..5c23dd22bb 100644 --- a/services/validator/validator_api/validator_api.pb.go +++ b/services/validator/validator_api/validator_api.pb.go @@ -136,7 +136,7 @@ type ValidateTransactionRequest struct { TransactionData []byte `protobuf:"bytes,1,opt,name=transaction_data,json=transactionData,proto3" json:"transaction_data,omitempty"` // Raw transaction data to validate BlockHeight uint32 `protobuf:"varint,2,opt,name=block_height,json=blockHeight,proto3" json:"block_height,omitempty"` // Block height for validation context // validation options - SkipUtxoCreation *bool `protobuf:"varint,3,opt,name=skip_utxo_creation,json=skipUtxoCreation,proto3,oneof" json:"skip_utxo_creation,omitempty"` // Skip UTXO creation for validation + SkipUtxoCreate *bool `protobuf:"varint,3,opt,name=skip_utxo_create,json=skipUtxoCreate,proto3,oneof" json:"skip_utxo_create,omitempty"` // Skip UTXO creation for validation AddTxToBlockAssembly *bool `protobuf:"varint,4,opt,name=add_tx_to_block_assembly,json=addTxToBlockAssembly,proto3,oneof" json:"add_tx_to_block_assembly,omitempty"` // Add transaction to block assembly SkipPolicyChecks *bool `protobuf:"varint,5,opt,name=skip_policy_checks,json=skipPolicyChecks,proto3,oneof" json:"skip_policy_checks,omitempty"` // Skip policy checks CreateConflicting *bool `protobuf:"varint,6,opt,name=create_conflicting,json=createConflicting,proto3,oneof" json:"create_conflicting,omitempty"` // Create conflicting transaction @@ -188,9 +188,9 @@ func (x *ValidateTransactionRequest) GetBlockHeight() uint32 { return 0 } -func (x *ValidateTransactionRequest) GetSkipUtxoCreation() bool { - if x != nil && x.SkipUtxoCreation != nil { - return *x.SkipUtxoCreation +func (x *ValidateTransactionRequest) GetSkipUtxoCreate() bool { + if x != nil && x.SkipUtxoCreate != nil { + return *x.SkipUtxoCreate } return false } @@ -495,15 +495,15 @@ const file_services_validator_validator_api_validator_api_proto_rawDesc = "" + "\x0eHealthResponse\x12\x0e\n" + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x18\n" + "\adetails\x18\x02 \x01(\tR\adetails\x128\n" + - "\ttimestamp\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"\xa3\x03\n" + + "\ttimestamp\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\"\x9d\x03\n" + "\x1aValidateTransactionRequest\x12)\n" + "\x10transaction_data\x18\x01 \x01(\fR\x0ftransactionData\x12!\n" + - "\fblock_height\x18\x02 \x01(\rR\vblockHeight\x121\n" + - "\x12skip_utxo_creation\x18\x03 \x01(\bH\x00R\x10skipUtxoCreation\x88\x01\x01\x12;\n" + + "\fblock_height\x18\x02 \x01(\rR\vblockHeight\x12-\n" + + "\x10skip_utxo_create\x18\x03 \x01(\bH\x00R\x0eskipUtxoCreate\x88\x01\x01\x12;\n" + "\x18add_tx_to_block_assembly\x18\x04 \x01(\bH\x01R\x14addTxToBlockAssembly\x88\x01\x01\x121\n" + "\x12skip_policy_checks\x18\x05 \x01(\bH\x02R\x10skipPolicyChecks\x88\x01\x01\x122\n" + - "\x12create_conflicting\x18\x06 \x01(\bH\x03R\x11createConflicting\x88\x01\x01B\x15\n" + - "\x13_skip_utxo_creationB\x1b\n" + + "\x12create_conflicting\x18\x06 \x01(\bH\x03R\x11createConflicting\x88\x01\x01B\x13\n" + + "\x11_skip_utxo_createB\x1b\n" + "\x19_add_tx_to_block_assemblyB\x15\n" + "\x13_skip_policy_checksB\x15\n" + "\x13_create_conflicting\"{\n" + diff --git a/services/validator/validator_api/validator_api.proto b/services/validator/validator_api/validator_api.proto index 93e50f08b8..3aa5872445 100644 --- a/services/validator/validator_api/validator_api.proto +++ b/services/validator/validator_api/validator_api.proto @@ -57,7 +57,7 @@ message ValidateTransactionRequest { bytes transaction_data = 1; // Raw transaction data to validate uint32 block_height = 2; // Block height for validation context // validation options - optional bool skip_utxo_creation = 3; // Skip UTXO creation for validation + optional bool skip_utxo_create = 3; // Skip UTXO creation for validation optional bool add_tx_to_block_assembly = 4; // Add transaction to block assembly optional bool skip_policy_checks = 5; // Skip policy checks optional bool create_conflicting = 6; // Create conflicting transaction @@ -96,4 +96,4 @@ message GetBlockHeightResponse { // swagger:model GetMedianBlockTimeResponse message GetMedianBlockTimeResponse { uint32 median_time = 1; // Median time of recent blocks -} \ No newline at end of file +} diff --git a/util/kafka/kafka_message/kafka_messages.pb.go b/util/kafka/kafka_message/kafka_messages.pb.go index e7842d998c..fc91588d98 100644 --- a/util/kafka/kafka_message/kafka_messages.pb.go +++ b/util/kafka/kafka_message/kafka_messages.pb.go @@ -413,7 +413,7 @@ func (x *KafkaTxValidationTopicMessage) GetOptions() *KafkaTxValidationOptions { type KafkaTxValidationOptions struct { state protoimpl.MessageState `protogen:"open.v1"` - SkipUtxoCreation bool `protobuf:"varint,1,opt,name=skipUtxoCreation,proto3" json:"skipUtxoCreation,omitempty"` + SkipUtxoCreate bool `protobuf:"varint,1,opt,name=skipUtxoCreate,proto3" json:"skipUtxoCreate,omitempty"` AddTXToBlockAssembly bool `protobuf:"varint,2,opt,name=addTXToBlockAssembly,proto3" json:"addTXToBlockAssembly,omitempty"` SkipPolicyChecks bool `protobuf:"varint,3,opt,name=skipPolicyChecks,proto3" json:"skipPolicyChecks,omitempty"` CreateConflicting bool `protobuf:"varint,4,opt,name=createConflicting,proto3" json:"createConflicting,omitempty"` @@ -451,9 +451,9 @@ func (*KafkaTxValidationOptions) Descriptor() ([]byte, []int) { return file_util_kafka_kafka_message_kafka_messages_proto_rawDescGZIP(), []int{5} } -func (x *KafkaTxValidationOptions) GetSkipUtxoCreation() bool { +func (x *KafkaTxValidationOptions) GetSkipUtxoCreate() bool { if x != nil { - return x.SkipUtxoCreation + return x.SkipUtxoCreate } return false } @@ -810,9 +810,9 @@ const file_util_kafka_kafka_message_kafka_messages_proto_rawDesc = "" + "\x1dKafkaTxValidationTopicMessage\x12\x0e\n" + "\x02tx\x18\x01 \x01(\fR\x02tx\x12\x16\n" + "\x06height\x18\x02 \x01(\rR\x06height\x12@\n" + - "\aoptions\x18\x03 \x01(\v2&.kafkamessage.KafkaTxValidationOptionsR\aoptions\"\xd4\x01\n" + - "\x18KafkaTxValidationOptions\x12*\n" + - "\x10skipUtxoCreation\x18\x01 \x01(\bR\x10skipUtxoCreation\x122\n" + + "\aoptions\x18\x03 \x01(\v2&.kafkamessage.KafkaTxValidationOptionsR\aoptions\"\xd0\x01\n" + + "\x18KafkaTxValidationOptions\x12&\n" + + "\x0eskipUtxoCreate\x18\x01 \x01(\bR\x0eskipUtxoCreate\x122\n" + "\x14addTXToBlockAssembly\x18\x02 \x01(\bR\x14addTXToBlockAssembly\x12*\n" + "\x10skipPolicyChecks\x18\x03 \x01(\bR\x10skipPolicyChecks\x12,\n" + "\x11createConflicting\x18\x04 \x01(\bR\x11createConflicting\"f\n" + diff --git a/util/kafka/kafka_message/kafka_messages.proto b/util/kafka/kafka_message/kafka_messages.proto index 6a71791ce2..47b8e383f4 100644 --- a/util/kafka/kafka_message/kafka_messages.proto +++ b/util/kafka/kafka_message/kafka_messages.proto @@ -34,7 +34,7 @@ message KafkaTxValidationTopicMessage { } message KafkaTxValidationOptions { - bool skipUtxoCreation = 1; + bool skipUtxoCreate = 1; bool addTXToBlockAssembly = 2; bool skipPolicyChecks = 3; bool createConflicting = 4;