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);
                        }
                    }
                }
            }
        }
Esempio n. 2
0
        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());
        }