public async Task InvalidTxDetectedAsync(InvalidTxDetectedEvent e) { if (e.Message.RejectionCode == InvalidTxRejectionCodes.TxMempoolConflict || e.Message.RejectionCode == InvalidTxRejectionCodes.TxDoubleSpendDetected) { if (e.Message.CollidedWith != null && e.Message.CollidedWith.Length > 0) { var collisionTxList = e.Message.CollidedWith.Select(t => new uint256(t.TxId).ToBytes()); var txsWithDSCheck = (await txRepository.GetTxsForDSCheckAsync(collisionTxList, true)).ToArray(); if (txsWithDSCheck.Any()) { var dsTxId = new uint256(e.Message.TxId).ToBytes(); var dsTxPayload = string.IsNullOrEmpty(e.Message.Hex) ? Array.Empty <byte>() : HelperTools.HexStringToByteArray(e.Message.Hex); foreach (var tx in txsWithDSCheck) { var inserted = await txRepository.InsertMempoolDoubleSpendAsync( tx.TxInternalId, dsTxId, dsTxPayload); if (inserted == 0) { return; } var notificationData = new NotificationData { TxExternalId = tx.TxExternalIdBytes, DoubleSpendTxId = dsTxId, CallbackUrl = tx.CallbackUrl, CallbackEncryption = tx.CallbackEncryption, CallbackToken = tx.CallbackToken, TxInternalId = tx.TxInternalId, BlockHeight = -1, BlockInternalId = -1, BlockHash = null }; var notificationEvent = new NewNotificationEvent() { CreationDate = clock.UtcNow(), NotificationType = CallbackReason.DoubleSpendAttempt, TransactionId = tx.TxExternalIdBytes, NotificationData = notificationData }; eventBus.Publish(notificationEvent); } } } } }
public async Task <ActionResult> SubmitDSAsync([FromQuery] string txId, [FromQuery] int?n, [FromQuery] string cTxId, [FromQuery] int?cn) { logger.LogInformation($"SubmitDSAsync call received for txid:'{txId}', n:'{n}', cTxId:'{cTxId}', cn:'{cn}'"); // Set response header here that we are interested in DS submit again in case of any error this.Response.Headers.Add(DSHeader, "1"); if (string.IsNullOrEmpty(txId)) { return(AddBanScoreAndReturnResult("'txid' must not be null or empty.", "")); } if (string.IsNullOrEmpty(cTxId)) { return(AddBanScoreAndReturnResult("'ctxid' must not be null or empty.", txId)); } if (!uint256.TryParse(txId, out uint256 uTxId)) { return(AddBanScoreAndReturnResult("Invalid 'txid' format.", txId)); } if (!uint256.TryParse(cTxId, out uint256 ucTxId)) { return(AddBanScoreAndReturnResult("Invalid 'ctxid' format.", txId)); } if (txId == cTxId) { return(AddBanScoreAndReturnResult("'ctxid' parameter must not be the same as 'txid'.", txId, HostBanList.WarningScore)); } if (n == null || n < 0) { return(AddBanScoreAndReturnResult("'n' must be equal or greater than 0.", txId, HostBanList.WarningScore)); } if (cn == null || cn < 0) { return(AddBanScoreAndReturnResult("'cn' must be equal or greater than 0.", txId, HostBanList.WarningScore)); } if (!transactionRequestsCheck.WasTransactionIdQueried(Request.Host.Host, uTxId)) { return(AddBanScoreAndReturnResult("Submitted transactionId was not queried before making a call to submit, or it was already submitted.", txId)); } var tx = (await txRepository.GetTxsForDSCheckAsync(new byte[][] { uTxId.ToBytes() }, true)).SingleOrDefault(); if (tx == null) { return(AddBanScoreAndReturnResult($"There is no transaction waiting for double-spend notification with given transaction id '{txId}'.", txId, HostBanList.WarningScore)); } if (n > tx.OrderderInputs.Length) { return(AddBanScoreAndReturnResult($"'n' parameter must not be greater than total number of inputs.", txId, HostBanList.WarningScore)); } transactionRequestsCheck.LogKnownTransactionId(Request.Host.Host, uTxId); byte[] dsTxBytes; using (var ms = new MemoryStream()) { await Request.Body.CopyToAsync(ms); dsTxBytes = ms.ToArray(); } if (dsTxBytes.Length == 0) { return(AddBanScoreAndReturnResult("Proof must not be empty.", txId)); } if (Hashes.DoubleSHA256(dsTxBytes) != ucTxId) { return(AddBanScoreAndReturnResult("Double-spend transaction does not match the 'ctxid' parameter.", txId)); } Transaction dsTx; try { dsTx = HelperTools.ParseBytesToTransaction(dsTxBytes); } catch (Exception) { return(AddBanScoreAndReturnResult("'dsProof' is invalid.", txId)); } if (cn > dsTx.Inputs.Count) { return(AddBanScoreAndReturnResult($"'cn' parameter must not be greater than total number of inputs.", txId)); } var dsTxIn = dsTx.Inputs[cn.Value]; var txIn = tx.OrderderInputs[n.Value]; if (!(new uint256(txIn.PrevTxId) == dsTxIn.PrevOut.Hash && txIn.PrevN == dsTxIn.PrevOut.N)) { return(AddBanScoreAndReturnResult("Transaction marked as double-spend does not spend same inputs as original transaction.", txId)); } logger.LogInformation($"Double spend checks completed successfully for '{txId}' and '{cTxId}'. Verifying script."); var scripts = new List <(string Tx, int N)>() { (dsTx.ToHex(), cn.Value) }.AsEnumerable(); // We take single result, because we will be sending only 1 tx at a time var verified = (await rpcMultiClient.VerifyScriptAsync(true, appSettings.DSScriptValidationTimeoutSec, scripts)).Single(); if (verified.Result != "ok") { return(AddBanScoreAndReturnResult($"Invalid proof script. Reason: '{verified.Description}'.", cTxId)); } logger.LogInformation($"Successfully verified script for transaction '{cTxId}'. Inserting notification data into database"); transactionRequestsCheck.RemoveQueriedTransactionId(Request.Host.Host, uTxId); var inserted = await txRepository.InsertMempoolDoubleSpendAsync( tx.TxInternalId, dsTx.GetHash(Const.NBitcoinMaxArraySize).ToBytes(), dsTxBytes); if (inserted > 0) { var notificationData = new NotificationData { TxExternalId = uTxId.ToBytes(), DoubleSpendTxId = ucTxId.ToBytes(), CallbackEncryption = tx.CallbackEncryption, CallbackToken = tx.CallbackToken, CallbackUrl = tx.CallbackUrl, TxInternalId = tx.TxInternalId, BlockHeight = -1, BlockInternalId = -1, BlockHash = null }; eventBus.Publish(new Domain.Models.Events.NewNotificationEvent { CreationDate = clock.UtcNow(), NotificationData = notificationData, NotificationType = CallbackReason.DoubleSpendAttempt, TransactionId = uTxId.ToBytes() }); logger.LogInformation($"Inserted notification push data into database for '{txId}'."); } // Submit was successfull we set the x-bsv-dsnt to 0, to signal the node we are not interested in this DS anymore this.Response.Headers[DSHeader] = "0"; return(Ok()); }