diff --git a/native/osx-x64/libbitcoinkernel.dylib b/native/osx-x64/libbitcoinkernel.dylib index 0d82f60..6fcfd84 100755 Binary files a/native/osx-x64/libbitcoinkernel.dylib and b/native/osx-x64/libbitcoinkernel.dylib differ diff --git a/src/BitcoinKernel.Core/Abstractions/Transaction.cs b/src/BitcoinKernel.Core/Abstractions/Transaction.cs index ffe3702..1bfbcbb 100644 --- a/src/BitcoinKernel.Core/Abstractions/Transaction.cs +++ b/src/BitcoinKernel.Core/Abstractions/Transaction.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using BitcoinKernel.Core.Exceptions; +using BitcoinKernel.Core.TransactionValidation; using BitcoinKernel.Interop; namespace BitcoinKernel.Core.Abstractions; @@ -158,6 +159,24 @@ public Transaction Copy() return new Transaction(copyHandle); } + /// + /// Validates this transaction against consensus rules. + /// + /// A TxValidationState containing the validation result. + public TxValidationState Validate() + { + return TransactionValidator.CheckTransaction(this); + } + + /// + /// Checks if this transaction is valid according to consensus rules. + /// + /// True if the transaction is valid, false otherwise. + public bool IsValid() + { + return TransactionValidator.IsValid(this); + } + internal IntPtr Handle => _handle; public void Dispose() diff --git a/src/BitcoinKernel.Core/TransactionValidation/TransactionValidator.cs b/src/BitcoinKernel.Core/TransactionValidation/TransactionValidator.cs new file mode 100644 index 0000000..6fb98f6 --- /dev/null +++ b/src/BitcoinKernel.Core/TransactionValidation/TransactionValidator.cs @@ -0,0 +1,46 @@ +using BitcoinKernel.Core.Abstractions; +using BitcoinKernel.Core.Exceptions; +using BitcoinKernel.Interop; + +namespace BitcoinKernel.Core.TransactionValidation; + +/// +/// Provides transaction validation functionality for consensus checks. +/// +public static class TransactionValidator +{ + /// + /// Checks if a transaction is valid according to consensus rules. + /// This is more efficient than CheckTransaction() when you only need a boolean result. + /// + /// The transaction to validate. + /// True if the transaction is valid, false otherwise. + /// Thrown when transaction is null. + public static bool IsValid(Transaction transaction) + { + ArgumentNullException.ThrowIfNull(transaction); + return NativeMethods.CheckTransaction(transaction.Handle, out nint statePtr) == 1; + } + + /// + /// Performs context-free consensus checks on a transaction and returns detailed validation state. + /// Use this when you need to know why a transaction is invalid. + /// For simple validation, use IsValid() instead. + /// + /// The transaction to validate. + /// A TxValidationState containing the validation result. + /// Thrown when transaction is null. + /// Thrown when validation fails to execute. + public static TxValidationState CheckTransaction(Transaction transaction) + { + ArgumentNullException.ThrowIfNull(transaction); + + IntPtr statePtr; + int result = NativeMethods.CheckTransaction(transaction.Handle, out statePtr); + + if (statePtr == IntPtr.Zero) + throw new TransactionException("Failed to create validation state"); + + return new TxValidationState(statePtr); + } +} diff --git a/src/BitcoinKernel.Core/TransactionValidation/TxValidationState.cs b/src/BitcoinKernel.Core/TransactionValidation/TxValidationState.cs new file mode 100644 index 0000000..d6933a2 --- /dev/null +++ b/src/BitcoinKernel.Core/TransactionValidation/TxValidationState.cs @@ -0,0 +1,75 @@ +using BitcoinKernel.Interop; +using BitcoinKernel.Interop.Enums; + +namespace BitcoinKernel.Core.TransactionValidation; + +/// +/// Represents the validation state of a transaction after consensus checks. +/// +public sealed class TxValidationState : IDisposable +{ + private IntPtr _handle; + private bool _disposed; + + internal TxValidationState(IntPtr handle) + { + _handle = handle != IntPtr.Zero + ? handle + : throw new ArgumentException("Invalid validation state handle", nameof(handle)); + } + + /// + /// Gets the validation mode (VALID, INVALID, or INTERNAL_ERROR). + /// + public ValidationMode Mode + { + get + { + ThrowIfDisposed(); + return NativeMethods.TxValidationStateGetValidationMode(_handle); + } + } + + /// + /// Gets the detailed validation result indicating why the transaction was invalid. + /// + public TxValidationResult Result + { + get + { + ThrowIfDisposed(); + return NativeMethods.TxValidationStateGetTxValidationResult(_handle); + } + } + + /// + /// Returns true if the transaction is valid. + /// + public bool IsValid => Mode == ValidationMode.VALID; + + /// + /// Returns true if the transaction is invalid. + /// + public bool IsInvalid => Mode == ValidationMode.INVALID; + + /// + /// Returns true if an internal error occurred during validation. + /// + public bool IsError => Mode == ValidationMode.INTERNAL_ERROR; + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(TxValidationState)); + } + + public void Dispose() + { + if (!_disposed && _handle != IntPtr.Zero) + { + NativeMethods.TxValidationStateDestroy(_handle); + _handle = IntPtr.Zero; + _disposed = true; + } + } +} diff --git a/src/BitcoinKernel.Interop/Enums/TxValidationResult.cs b/src/BitcoinKernel.Interop/Enums/TxValidationResult.cs new file mode 100644 index 0000000..60185a4 --- /dev/null +++ b/src/BitcoinKernel.Interop/Enums/TxValidationResult.cs @@ -0,0 +1,23 @@ +namespace BitcoinKernel.Interop.Enums +{ + /// + /// A granular "reason" why a transaction was invalid. + /// Mirrors consensus/validation.h: enum class TxValidationResult. + /// + public enum TxValidationResult : uint + { + UNSET = 0, + CONSENSUS = 1, + INPUTS_NOT_STANDARD = 2, + NOT_STANDARD = 3, + MISSING_INPUTS = 4, + PREMATURE_SPEND = 5, + WITNESS_MUTATED = 6, + WITNESS_STRIPPED = 7, + CONFLICT = 8, + MEMPOOL_POLICY = 9, + NO_MEMPOOL = 10, + RECONSIDERABLE = 11, + UNKNOWN = 12 + } +} diff --git a/src/BitcoinKernel.Interop/NativeMethods.cs b/src/BitcoinKernel.Interop/NativeMethods.cs index 6c36ba9..3549f9f 100644 --- a/src/BitcoinKernel.Interop/NativeMethods.cs +++ b/src/BitcoinKernel.Interop/NativeMethods.cs @@ -377,6 +377,13 @@ public static extern int TransactionToBytes( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_destroy")] public static extern void TransactionDestroy(IntPtr transaction); + /// + /// Run consensus/tx_check::CheckTransaction on a transaction and return the filled state. + /// Returns 1 if valid, 0 if invalid. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_check_transaction")] + public static extern int CheckTransaction(IntPtr tx, out IntPtr out_state); + /// /// Gets the script pubkey from a transaction output. /// Returns a pointer to btck_ScriptPubkey. @@ -404,6 +411,28 @@ public static extern int TransactionToBytes( #endregion + #region TxValidationState Operations + + /// + /// Gets the validation mode from a transaction validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_tx_validation_state_get_validation_mode")] + public static extern ValidationMode TxValidationStateGetValidationMode(IntPtr state); + + /// + /// Gets the transaction validation result from a transaction validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_tx_validation_state_get_tx_validation_result")] + public static extern TxValidationResult TxValidationStateGetTxValidationResult(IntPtr state); + + /// + /// Destroys a transaction validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_tx_validation_state_destroy")] + public static extern void TxValidationStateDestroy(IntPtr state); + + #endregion + #region ScriptPubkey Operations /// diff --git a/tests/BitcoinKernel.Core.Tests/TransactionValidationTests.cs b/tests/BitcoinKernel.Core.Tests/TransactionValidationTests.cs new file mode 100644 index 0000000..5f389c7 --- /dev/null +++ b/tests/BitcoinKernel.Core.Tests/TransactionValidationTests.cs @@ -0,0 +1,115 @@ +using Xunit; +using BitcoinKernel.Core.Abstractions; +using BitcoinKernel.Core.TransactionValidation; +using BitcoinKernel.Interop.Enums; + +namespace BitcoinKernel.Core.Tests; + +public class TransactionValidationTests +{ + + // Vojtěch Strnad Transaction + private readonly string validTxHex = "0100000000010a9b9653ae4536f14723f5e7fdd730af3e41536a0c57807de8d67b1d2c96246ad40000000048473044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601f78c3201e36747271635683f16a2c9574cd79fac51f34af08e3a63fb293b0204ac479bcb000000006946304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad8510221028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d4087693201e36747271635683f16a2c9574cd79fac51f34af08e3a63fb293b0204ac479bcb010000008300453042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e2033b303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481685e2d01d6ca61cbfb1bab46294999b8f5650861d71bb701c8cc101807cb9a5933cbe14500000000fdaa0100443041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea013a303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef8239303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba834ced532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae10000000e36747271635683f16a2c9574cd79fac51f34af08e3a63fb293b0204ac479bcb02000000171600149b27f072e4b972927c445d1946162a550b0914d88d000000e36747271635683f16a2c9574cd79fac51f34af08e3a63fb293b0204ac479bcb0300000023220020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75efbeaddee36747271635683f16a2c9574cd79fac51f34af08e3a63fb293b0204ac479bcb0400000000406f400101d08b572974320b3d3650d672b1e53776ee38c72df9812bb2b18f8a92b37d630000000000d9b4bef9412c98a33b0d9ae72b87c117287b5722ffeb89d1ac5baeeb625012df02db2000000000000055010000b4061297f91ef316f4ed912e45f6d48f97d317d02d8d1c4bb131c4f9ec41577900000000005601000009400200000000000023210261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32aac22020000000000001976a9140240539af6c68431e4ce9cc5ef464f12c1741b3c88ac4602000000000000255121028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae2851ae1c0200000000000017a91449ed2c96e33b6134408af8484508bcc3248c8dbd872601000000000000160014c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba4a01000000000000220020c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a4a01000000000000225120a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9f0000000000000000451024e730000000000000000356a224e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e005152535455565758595a5b5c5d5e5f600000000002433040021c23902a01d4c5cff2c33c8bdb778a5aadea78a9a0d6d4db60aaa0fba1022069237d9dbf2db8cff9c260ba71250493682d01a746f4a45c5c7ea386e56d2bc902210240187acd3e2fd3d8e1acffefa85907b6550730c24f78dfd3301c829fc4daf3cc0342303f021c65aee6696e80be6e14545cfd64b44f17b0514c150eefdb090c0f0bd9021f3fef4aa95c252a225622aba99e4d5af5a6fe40d177acd593e64cf2f8557ccc032103b55c6f0749e0f3e2caeca05f68e3699f1b3c62a550730f704985a6a9aae437a18576a914db865fd920959506111079995f1e4017b489bfe38763ac6721024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c7c820120876475527c2103443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca52ae67a91446c3747322b220fdb925c9802f0e949c1feab99988ac68680241303e021c11f60486afd0f5d6573603fb2076ef2f676455b92ada257d2f25558a021e317719c946f951d49bf4df4285a618629cd9e554fcbf787c319a0c4dd2260121032467f24cc31664f0cf34ff8d5cbb590888ddc1dcfec724a32ae3dd5338b8508e0340303d021c32f9454db85cb1a4ca63a9883d4347c5e13f3654e884ae44e9efa3c8021d62f07fe452c06b084bc3e09afd3aac4039136549a465533bc1ca6696790201014c632102fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd67012ab27521034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f68ac0140d822f203827852998cad370232e8c57294540a5da51107fa26cf466bdd2b8b0b3d161999cc80aed8de7386a2bd5d5313aea159a231cc26fa53aaa702b7fa21ed0940fe6eb715dceffefc067fdc787d250a9a9116682d216f6356ea38fc1f112bd74995faa90315e81981d2c2260b7eaca3c41a16b280362980f0d8faf4c05ebb82c541e34ad0ad33885a473831f8ba8d9339123cb19d0e642e156d8e0d6e2ab2691aedb30e55a35637a806927225e1aa72223d41e59f92c6579b819e7d331a7ada9d2e01412a4861fb4cb951c791bf6c93859ef65abccd90034f91b9b77abb918e13b6fce75d5fa3e2d2f6eeeae105315178c2cb9db2ef238fe89b282f691c06db43bc71ca0241fc97bb2be673c3bf388aaf58178ef14d354caf83c92aca8ef1831d619b8511e928f4f5fdea3962067b11e7cecfe094cd0f66a4ea9af9ec836d70d18f2b37df028141a5781a0adaa80ab7f7f164172dd1a1cb127e523daa0d6949aba074a15c589f12dfb8183182afec9230cb7947b7422a4abc1bb78173550d66274ea19f6c9dd92c820000f0205f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1ac205f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3ba205ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996ba20b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690ba20d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5ba20cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0ba20aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545ba559cfdc102c0b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f5534a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bf4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e166f7cf9580f1c2dfb3c4d5d043cdbb128c640e3f20161245aa7372e9666168516a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48dd5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb46829a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713d29c9c0e8e4d2a9790922af73f0b8d51f0bd4bb19940d9cf910ead8fbe85bc9bbb41a757f405890fb0f5856228e23b715702d714d59bf2b1feb70d8b2b4e3e089fdbcf0ef9d8d00f66e47917f67cc5d78aec1ac786e2abb8d2facb4e4790aad6cc455ae816e6cdafdb58d54e35d4f46d860047458eacf1c7405dc634631c570d8d31992805518fd62daa3bdd2a5c4fd2cd3054c9b3dca1d78055e9528cff6adc8f907925d2ebe48765103e6845c06f1f2bb77c6adc1cc002865865eb5cfd5c1cb10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d4133e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac9879903637777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8fd456524104a6674693c29946543f8a0befccce5a352bda55ec8559fc630f5f37393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef7373be398c4bdc43709db7398106609eea2a7841aaf3a4fa2000dc18184faa2a7eb5a2af5845a8d3796308ff9840e567b14cf6bb158ff26c999e6f9a1f5448f9aa29ab5f49"; + + + [Fact] + public void ValidTransaction_ShouldPassConsensusCheck() + { + using var transaction = Transaction.FromHex(validTxHex); + using var state = TransactionValidator.CheckTransaction(transaction); + + Assert.Equal(ValidationMode.VALID, state.Mode); + Assert.Equal(TxValidationResult.UNSET, state.Result); + Assert.True(state.IsValid); + Assert.False(state.IsInvalid); + Assert.False(state.IsError); + } + + [Fact] + public void ValidTransaction_ValidateMethod_ShouldPassConsensusCheck() + { + using var transaction = Transaction.FromHex(validTxHex); + using var state = transaction.Validate(); + + Assert.Equal(ValidationMode.VALID, state.Mode); + Assert.Equal(TxValidationResult.UNSET, state.Result); + Assert.True(state.IsValid); + Assert.False(state.IsInvalid); + Assert.False(state.IsError); + } + + [Fact] + public void ValidTransaction_IsValidMethod_ShouldReturnTrue() + { + + using var transaction = Transaction.FromHex(validTxHex); + + bool isValid = TransactionValidator.IsValid(transaction); + Assert.True(isValid); + } + + [Fact] + public void Transaction_IsValidMethod_ShouldReturnTrue() + { + using var transaction = Transaction.FromHex(validTxHex); + + Assert.True(transaction.IsValid()); + } + + [Fact] + public void InvalidTransaction_DuplicateInputs_ShouldFailConsensusCheck() + { + // Transaction with duplicate inputs (invalid by consensus) + string invalidTxHex = "01000000023f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff3f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff0100f2052a010000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988ac00000000"; + + using var transaction = Transaction.FromHex(invalidTxHex); + using var state = TransactionValidator.CheckTransaction(transaction); + + // Should be invalid due to duplicate inputs + Assert.Equal(ValidationMode.INVALID, state.Mode); + Assert.False(state.IsValid); + Assert.True(state.IsInvalid); + Assert.Equal(TxValidationResult.CONSENSUS, state.Result); + } + + [Fact] + public void InvalidTransaction_ValidateMethod_DuplicateInputs_ShouldFailConsensusCheck() + { + // Transaction with duplicate inputs (invalid by consensus) + string invalidTxHex = "01000000023f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff3f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff0100f2052a010000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988ac00000000"; + + using var transaction = Transaction.FromHex(invalidTxHex); + using var state = transaction.Validate(); + + // Should be invalid due to duplicate inputs + Assert.Equal(ValidationMode.INVALID, state.Mode); + Assert.False(state.IsValid); + Assert.True(state.IsInvalid); + Assert.Equal(TxValidationResult.CONSENSUS, state.Result); + } + + [Fact] + public void InvalidTransaction_IsValidMethod_ShouldReturnFalse() + { + // Transaction with duplicate inputs (invalid by consensus) + string invalidTxHex = "01000000023f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff3f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff0100f2052a010000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988ac00000000"; + + using var transaction = Transaction.FromHex(invalidTxHex); + + Assert.False(transaction.IsValid()); + } + + [Fact] + public void CheckTransaction_WithNullTransaction_ShouldThrowArgumentNullException() + { + Assert.Throws(() => + TransactionValidator.CheckTransaction(null!)); + } + + [Fact] + public void IsValid_WithNullTransaction_ShouldThrowArgumentNullException() + { + Assert.Throws(() => + TransactionValidator.IsValid(null!)); + } +}