diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 109b55a..a3cb1af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: platform: linux_amd64 env: - TEST_VERSION: '0.0.3-alpha.4' + TEST_VERSION: '0.0.4-alpha.pr14.5' TEST_REPO: 'stringintech/kernel-bindings-tests' TEST_DIR: '.conformance-tests' diff --git a/examples/BasicUsage/Program.cs b/examples/BasicUsage/Program.cs index d7c21db..9a0f422 100644 --- a/examples/BasicUsage/Program.cs +++ b/examples/BasicUsage/Program.cs @@ -38,16 +38,16 @@ static void FullChainstateExample() { var chain = chainstate.GetActiveChain(); Console.WriteLine($" Chain height: {chain.Height}"); - Console.WriteLine($" Genesis hash: {Convert.ToHexString(chain.GetGenesis().GetBlockHash())}"); + Console.WriteLine($" Genesis hash: {Convert.ToHexString(chain.GetGenesis().GetHash())}"); if (chain.Height > 0) { var tip = chain.GetTip(); - Console.WriteLine($" Tip hash: {Convert.ToHexString(tip.GetBlockHash())}"); + Console.WriteLine($" Tip hash: {Convert.ToHexString(tip.GetHash())}"); var genesis = chain.GetBlockByHeight(0); if (genesis != null) - Console.WriteLine($" Block 0 hash: {Convert.ToHexString(genesis.GetBlockHash())}"); + Console.WriteLine($" Block 0 hash: {Convert.ToHexString(genesis.GetHash())}"); } Console.WriteLine(" Chain queries working"); diff --git a/examples/BlockProcessing/Program.cs b/examples/BlockProcessing/Program.cs index 1962399..40683c5 100644 --- a/examples/BlockProcessing/Program.cs +++ b/examples/BlockProcessing/Program.cs @@ -59,7 +59,7 @@ static void Main(string[] args) var activeChain = chainstate.GetActiveChain(); Console.WriteLine($"Block processed! Chain height: {activeChain.Height}"); var tip = activeChain.GetTip(); - Console.WriteLine($" - Tip: {BitConverter.ToString(tip.GetBlockHash()).Replace("-", "")}"); + Console.WriteLine($" - Tip: {BitConverter.ToString(tip.GetHash()).Replace("-", "")}"); } else { diff --git a/native/linux-x64/libbitcoinkernel.so b/native/linux-x64/libbitcoinkernel.so index 3b6cb11..2527e43 100755 Binary files a/native/linux-x64/libbitcoinkernel.so and b/native/linux-x64/libbitcoinkernel.so differ diff --git a/native/osx-x64/libbitcoinkernel.dylib b/native/osx-x64/libbitcoinkernel.dylib index 8e7369f..6071804 100755 Binary files a/native/osx-x64/libbitcoinkernel.dylib and b/native/osx-x64/libbitcoinkernel.dylib differ diff --git a/src/BitcoinKernel.Interop/Enums/BlockCheckFlags.cs b/src/BitcoinKernel.Interop/Enums/BlockCheckFlags.cs new file mode 100644 index 0000000..c501ceb --- /dev/null +++ b/src/BitcoinKernel.Interop/Enums/BlockCheckFlags.cs @@ -0,0 +1,31 @@ +namespace BitcoinKernel.Interop.Enums; + +/// +/// Flags controlling optional context-free block checks performed by +/// btck_block_check. The base checks (size limits, coinbase structure, +/// transaction checks, sigop limits) always run; these flags toggle the +/// optional proof-of-work and merkle-root checks. +/// +[Flags] +public enum BlockCheckFlags : uint +{ + /// + /// Run the base context-free block checks only. + /// + Base = 0, + + /// + /// Run CheckProofOfWork via CheckBlockHeader. + /// + Pow = 1U << 0, + + /// + /// Verify merkle root (and mutation detection). + /// + Merkle = 1U << 1, + + /// + /// Enable all optional context-free block checks. + /// + All = Pow | Merkle +} diff --git a/src/BitcoinKernel.Interop/Enums/ChainType.cs b/src/BitcoinKernel.Interop/Enums/ChainType.cs index f7e9ea1..95fe543 100644 --- a/src/BitcoinKernel.Interop/Enums/ChainType.cs +++ b/src/BitcoinKernel.Interop/Enums/ChainType.cs @@ -1,6 +1,7 @@ namespace BitcoinKernel.Interop.Enums; -public enum ChainType : uint +// btck_ChainType is a uint8_t in bitcoinkernel.h. +public enum ChainType : byte { MAINNET = 0, TESTNET = 1, diff --git a/src/BitcoinKernel.Interop/Enums/LogLevel.cs b/src/BitcoinKernel.Interop/Enums/LogLevel.cs index c5b3c5d..9ae128c 100644 --- a/src/BitcoinKernel.Interop/Enums/LogLevel.cs +++ b/src/BitcoinKernel.Interop/Enums/LogLevel.cs @@ -1,6 +1,7 @@ namespace BitcoinKernel.Interop.Enums; -public enum LogLevel : uint +// btck_LogLevel is a uint8_t in bitcoinkernel.h. +public enum LogLevel : byte { TRACE = 0, DEBUG = 1, diff --git a/src/BitcoinKernel.Interop/Enums/TxValidationResult.cs b/src/BitcoinKernel.Interop/Enums/TxValidationResult.cs new file mode 100644 index 0000000..88122cd --- /dev/null +++ b/src/BitcoinKernel.Interop/Enums/TxValidationResult.cs @@ -0,0 +1,72 @@ +namespace BitcoinKernel.Interop.Enums; + +/// +/// A granular "reason" why a transaction was invalid. +/// +public enum TxValidationResult : uint +{ + /// + /// Initial value. Tx has not yet been rejected. + /// + UNSET = 0, + + /// + /// Invalid by consensus rules. + /// + CONSENSUS = 1, + + /// + /// Inputs (covered by txid) failed policy rules. + /// + INPUTS_NOT_STANDARD = 2, + + /// + /// Otherwise didn't meet local policy rules. + /// + NOT_STANDARD = 3, + + /// + /// Transaction was missing some of its inputs. + /// + MISSING_INPUTS = 4, + + /// + /// Transaction spends a coinbase too early, or violates locktime/sequence locks. + /// + PREMATURE_SPEND = 5, + + /// + /// Witness may have been malleated or is prior to SegWit activation. + /// + WITNESS_MUTATED = 6, + + /// + /// Transaction is missing a witness. + /// + WITNESS_STRIPPED = 7, + + /// + /// Tx already in mempool or conflicts with a tx in the chain. + /// + CONFLICT = 8, + + /// + /// Violated mempool's fee/size/descendant/RBF/etc limits. + /// + MEMPOOL_POLICY = 9, + + /// + /// This node does not have a mempool so can't validate the transaction. + /// + NO_MEMPOOL = 10, + + /// + /// Fails some policy, but might be acceptable if submitted in a (different) package. + /// + RECONSIDERABLE = 11, + + /// + /// Transaction was not validated because package failed. + /// + UNKNOWN = 12 +} diff --git a/src/BitcoinKernel.Interop/NativeMethods.cs b/src/BitcoinKernel.Interop/NativeMethods.cs index 1b5c770..d897c0c 100644 --- a/src/BitcoinKernel.Interop/NativeMethods.cs +++ b/src/BitcoinKernel.Interop/NativeMethods.cs @@ -96,6 +96,13 @@ static NativeMethods() [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_parameters_destroy")] public static extern void ChainParametersDestroy(IntPtr chain_params); + /// + /// Gets the consensus parameters from chain parameters. The returned pointer + /// is unowned and only valid for the lifetime of the chain parameters. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_parameters_get_consensus_params")] + public static extern IntPtr ChainParametersGetConsensusParams(IntPtr chain_parameters); + #endregion #region Chainstate Manager @@ -143,14 +150,14 @@ public static extern IntPtr ChainstateManagerGetBlockTreeEntryByHash( public static extern IntPtr ChainstateManagerGetBestEntry(IntPtr manager); /// - /// Processes and validates a block header. - /// Returns 0 on success. + /// Processes and validates a block header. Returns a newly-allocated + /// btck_BlockValidationState (owned by the caller) describing the outcome, + /// or IntPtr.Zero on failure. /// [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chainstate_manager_process_block_header")] - public static extern int ChainstateManagerProcessBlockHeader( + public static extern IntPtr ChainstateManagerProcessBlockHeader( IntPtr manager, - IntPtr header, - IntPtr block_validation_state); + IntPtr header); /// /// Imports blocks from an array of file paths. @@ -320,6 +327,26 @@ public static extern int BlockToBytes( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_get_transaction_at")] public static extern IntPtr BlockGetTransactionAt(IntPtr block, nuint index); + /// + /// Returns the ancestor of a block tree entry at the given height. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_tree_entry_get_ancestor")] + public static extern IntPtr BlockTreeEntryGetAncestor(IntPtr block_tree_entry, int height); + + /// + /// Performs context-free validation checks on a block. + /// Runs base checks (size, coinbase, tx, sigops) plus optional POW and + /// merkle-root checks controlled by . The + /// validation_state is updated in-place. + /// Returns 1 if the block passed the checks, 0 otherwise. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_check")] + public static extern int BlockCheck( + IntPtr block, + IntPtr consensus_params, + BlockCheckFlags flags, + IntPtr validation_state); + #endregion #region BlockHash Operations @@ -409,6 +436,15 @@ public static extern IntPtr BlockHeaderCreate( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_destroy")] public static extern void BlockHeaderDestroy(IntPtr header); + /// + /// Serializes a block header to 80 bytes. + /// Returns 0 on success. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_to_bytes")] + public static extern int BlockHeaderToBytes( + IntPtr header, + [MarshalAs(UnmanagedType.LPArray, SizeConst = 80)] byte[] output); + #endregion #region Chain Operations @@ -519,6 +555,20 @@ public static extern int TransactionToBytes( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_output_destroy")] public static extern void TransactionOutputDestroy(IntPtr output); + /// + /// Gets the nLockTime value of a transaction. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_get_locktime")] + public static extern uint TransactionGetLocktime(IntPtr transaction); + + /// + /// Runs context-free consensus validation on a transaction. + /// The validation_state is reset on entry and updated in-place. + /// Returns 1 if valid, 0 if invalid. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_check")] + public static extern int TransactionCheck(IntPtr tx, IntPtr validation_state); + #endregion #region PrecomputedTransactionData Operations @@ -867,6 +917,12 @@ public static extern IntPtr TransactionSpentOutputsGetCoinAt( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_destroy")] public static extern void TransactionInputDestroy(IntPtr transaction_input); + /// + /// Gets the nSequence value of a transaction input. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_sequence")] + public static extern uint TransactionInputGetSequence(IntPtr transaction_input); + #endregion #region TransactionOutPoint Operations @@ -897,4 +953,33 @@ public static extern IntPtr TransactionSpentOutputsGetCoinAt( #endregion + #region Tx Validation State + + /// + /// Creates a new transaction validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_tx_validation_state_create")] + public static extern IntPtr TxValidationStateCreate(); + + /// + /// 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 validation_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 validation_state); + + /// + /// Destroys a transaction validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_tx_validation_state_destroy")] + public static extern void TxValidationStateDestroy(IntPtr validation_state); + + #endregion + + } diff --git a/src/BitcoinKernel/Chain/ChainStateManager.cs b/src/BitcoinKernel/Chain/ChainStateManager.cs index cd2fb1c..2604c1a 100644 --- a/src/BitcoinKernel/Chain/ChainStateManager.cs +++ b/src/BitcoinKernel/Chain/ChainStateManager.cs @@ -88,14 +88,15 @@ public bool ProcessBlockHeader(BlockHeader header, out BlockValidationState vali ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(header); - using var state = new BlockValidationState(); - int result = NativeMethods.ChainstateManagerProcessBlockHeader( - _handle, - header.Handle, - state.Handle); + // The native call returns a newly-allocated validation state (owned here). + var statePtr = NativeMethods.ChainstateManagerProcessBlockHeader(_handle, header.Handle); + if (statePtr == IntPtr.Zero) + { + throw new ChainstateManagerException("Failed to process block header"); + } - validationState = state.Copy(); - return result == 0; + validationState = new BlockValidationState(statePtr); + return validationState.ValidationMode == Interop.Enums.ValidationMode.VALID; } /// diff --git a/src/BitcoinKernel/Primitives/Block.cs b/src/BitcoinKernel/Primitives/Block.cs index b856e23..c572907 100644 --- a/src/BitcoinKernel/Primitives/Block.cs +++ b/src/BitcoinKernel/Primitives/Block.cs @@ -57,9 +57,18 @@ public int TransactionCount } /// - /// Gets the block hash. + /// Gets the block hash as a 32-byte array. /// public byte[] GetHash() + { + using var blockHash = GetBlockHash(); + return blockHash.ToBytes(); + } + + /// + /// Gets the block hash as an owned object. + /// + public BlockHash GetBlockHash() { ThrowIfDisposed(); var hashPtr = NativeMethods.BlockGetHash(_handle); @@ -68,8 +77,22 @@ public byte[] GetHash() throw new BlockException("Failed to get block hash"); } - using var blockHash = new BlockHash(hashPtr); - return blockHash.ToBytes(); + return new BlockHash(hashPtr); + } + + /// + /// Creates an owned copy of this block. + /// + public Block Copy() + { + ThrowIfDisposed(); + var copy = NativeMethods.BlockCopy(_handle); + if (copy == IntPtr.Zero) + { + throw new BlockException("Failed to copy block"); + } + + return new Block(copy); } /// @@ -93,19 +116,19 @@ public BlockHeader GetHeader() public byte[] ToBytes() { ThrowIfDisposed(); - byte[]? result = null; + var result = new List(); NativeMethods.BlockToBytes(_handle, (data, size, userData) => { unsafe { var span = new ReadOnlySpan((byte*)data, (int)size); - result = span.ToArray(); + result.AddRange(span); } return 0; }, IntPtr.Zero); - return result ?? Array.Empty(); + return result.ToArray(); } /// diff --git a/src/BitcoinKernel/Primitives/BlockHash.cs b/src/BitcoinKernel/Primitives/BlockHash.cs index 6efa7b3..f7dab4f 100644 --- a/src/BitcoinKernel/Primitives/BlockHash.cs +++ b/src/BitcoinKernel/Primitives/BlockHash.cs @@ -5,7 +5,7 @@ namespace BitcoinKernel.Primitives; /// /// Represents a block hash. /// -public sealed class BlockHash : IDisposable +public sealed class BlockHash : IDisposable, IEquatable { private IntPtr _handle; private bool _disposed; @@ -60,6 +60,33 @@ public byte[] ToBytes() return bytes; } + /// + /// Creates an owned copy of this block hash. + /// + public BlockHash Copy() + { + ThrowIfDisposed(); + var copy = NativeMethods.BlockHashCopy(_handle); + if (copy == IntPtr.Zero) + throw new BlockException("Failed to copy block hash"); + + return new BlockHash(copy); + } + + /// + /// Determines whether this block hash equals another. + /// + public bool Equals(BlockHash? other) + { + if (other is null) return false; + ThrowIfDisposed(); + return NativeMethods.BlockHashEquals(_handle, other.Handle) != 0; + } + + public override bool Equals(object? obj) => obj is BlockHash other && Equals(other); + + public override int GetHashCode() => Convert.ToHexString(ToBytes()).GetHashCode(); + private void ThrowIfDisposed() { if (_disposed) diff --git a/src/BitcoinKernel/Primitives/BlockHeader.cs b/src/BitcoinKernel/Primitives/BlockHeader.cs index 1568deb..c179a5a 100644 --- a/src/BitcoinKernel/Primitives/BlockHeader.cs +++ b/src/BitcoinKernel/Primitives/BlockHeader.cs @@ -49,9 +49,27 @@ internal IntPtr Handle } /// - /// Gets the block hash of this header. + /// Gets the block hash of this header as a 32-byte array. /// public byte[] GetHash() + { + using var blockHash = GetBlockHash(); + return blockHash.ToBytes(); + } + + /// + /// Gets the previous block hash from this header as a 32-byte array. + /// + public byte[] GetPrevHash() + { + using var prevHash = GetPrevBlockHash(); + return prevHash.ToBytes(); + } + + /// + /// Gets the block hash of this header as an owned object. + /// + public BlockHash GetBlockHash() { ThrowIfDisposed(); var hashPtr = NativeMethods.BlockHeaderGetHash(_handle); @@ -60,14 +78,13 @@ public byte[] GetHash() throw new BlockException("Failed to get block hash from header"); } - using var blockHash = new BlockHash(hashPtr); - return blockHash.ToBytes(); + return new BlockHash(hashPtr); } /// - /// Gets the previous block hash from this header. + /// Gets the previous block hash of this header as an owned object. /// - public byte[] GetPrevHash() + public BlockHash GetPrevBlockHash() { ThrowIfDisposed(); var hashPtr = NativeMethods.BlockHeaderGetPrevHash(_handle); @@ -76,12 +93,48 @@ public byte[] GetPrevHash() throw new BlockException("Failed to get previous block hash from header"); } - // The hash pointer is unowned and only valid for the lifetime of the header - var bytes = new byte[32]; - NativeMethods.BlockHashToBytes(hashPtr, bytes); + // The returned pointer is unowned (tied to the header lifetime), so copy it + // into an independently owned block hash. + var copy = NativeMethods.BlockHashCopy(hashPtr); + if (copy == IntPtr.Zero) + { + throw new BlockException("Failed to copy previous block hash"); + } + + return new BlockHash(copy); + } + + /// + /// Serializes this header to its 80-byte representation. + /// + public byte[] ToBytes() + { + ThrowIfDisposed(); + var bytes = new byte[80]; + int result = NativeMethods.BlockHeaderToBytes(_handle, bytes); + if (result != 0) + { + throw new BlockException("Failed to serialize block header"); + } + return bytes; } + /// + /// Creates an owned copy of this block header. + /// + public BlockHeader Copy() + { + ThrowIfDisposed(); + var copy = NativeMethods.BlockHeaderCopy(_handle); + if (copy == IntPtr.Zero) + { + throw new BlockException("Failed to copy block header"); + } + + return new BlockHeader(copy); + } + /// /// Gets the timestamp from this header (Unix epoch seconds). /// diff --git a/src/BitcoinKernel/Primitives/BlockIndex.cs b/src/BitcoinKernel/Primitives/BlockIndex.cs index b39905c..47b3057 100644 --- a/src/BitcoinKernel/Primitives/BlockIndex.cs +++ b/src/BitcoinKernel/Primitives/BlockIndex.cs @@ -26,19 +26,30 @@ internal BlockIndex(IntPtr handle, bool ownsHandle) public int Height => NativeMethods.BlockTreeEntryGetHeight(_handle); /// - /// Gets the block hash. + /// Gets the block hash of this entry as a 32-byte array. /// - public byte[] GetBlockHash() + public byte[] GetHash() + { + using var hash = GetBlockHash(); + return hash.ToBytes(); + } + + /// + /// Gets the block hash of this entry as an owned object. + /// + public BlockHash GetBlockHash() { var hashPtr = NativeMethods.BlockTreeEntryGetBlockHash(_handle); if (hashPtr == IntPtr.Zero) throw new InvalidOperationException("Failed to get block hash"); - // The hash pointer is owned by the block tree entry, so we just read the bytes - // without wrapping it in a BlockHash that would try to destroy it - var bytes = new byte[32]; - NativeMethods.BlockHashToBytes(hashPtr, bytes); - return bytes; + // The returned pointer is owned by the block tree entry, so copy it into an + // independently owned block hash. + var copy = NativeMethods.BlockHashCopy(hashPtr); + if (copy == IntPtr.Zero) + throw new InvalidOperationException("Failed to copy block hash"); + + return new BlockHash(copy); } /// diff --git a/src/BitcoinKernel/Primitives/OutPoint.cs b/src/BitcoinKernel/Primitives/OutPoint.cs new file mode 100644 index 0000000..40df9b1 --- /dev/null +++ b/src/BitcoinKernel/Primitives/OutPoint.cs @@ -0,0 +1,96 @@ +using BitcoinKernel.Exceptions; +using BitcoinKernel.Interop; + +namespace BitcoinKernel.Primitives; + +/// +/// Represents a transaction out point (the txid and output index a transaction input spends). +/// +public sealed class OutPoint : IDisposable +{ + private IntPtr _handle; + private bool _disposed; + private readonly bool _ownsHandle; + + internal OutPoint(IntPtr handle, bool ownsHandle = true) + { + _handle = handle != IntPtr.Zero + ? handle + : throw new ArgumentException("Invalid out point handle", nameof(handle)); + _ownsHandle = ownsHandle; + } + + internal IntPtr Handle + { + get + { + ThrowIfDisposed(); + return _handle; + } + } + + /// + /// Gets the index of the referenced output. + /// + public uint Index + { + get + { + ThrowIfDisposed(); + return NativeMethods.TransactionOutPointGetIndex(_handle); + } + } + + /// + /// Gets the txid of the referenced transaction. The returned txid is a + /// non-owning view whose lifetime is tied to this out point. + /// + public Txid GetTxid() + { + ThrowIfDisposed(); + var txidPtr = NativeMethods.TransactionOutPointGetTxid(_handle); + if (txidPtr == IntPtr.Zero) + throw new TransactionException("Failed to get txid from out point"); + + return new Txid(txidPtr, ownsHandle: false); + } + + /// + /// Creates an owned copy of this out point. + /// + public OutPoint Copy() + { + ThrowIfDisposed(); + var copy = NativeMethods.TransactionOutPointCopy(_handle); + if (copy == IntPtr.Zero) + throw new TransactionException("Failed to copy out point"); + + return new OutPoint(copy); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(OutPoint)); + } + + public void Dispose() + { + if (!_disposed) + { + if (_handle != IntPtr.Zero && _ownsHandle) + { + NativeMethods.TransactionOutPointDestroy(_handle); + _handle = IntPtr.Zero; + } + _disposed = true; + } + GC.SuppressFinalize(this); + } + + ~OutPoint() + { + if (_ownsHandle) + Dispose(); + } +} diff --git a/src/BitcoinKernel/Primitives/ScriptPubKey.cs b/src/BitcoinKernel/Primitives/ScriptPubKey.cs index f54ca3e..7cdc8bc 100644 --- a/src/BitcoinKernel/Primitives/ScriptPubKey.cs +++ b/src/BitcoinKernel/Primitives/ScriptPubKey.cs @@ -56,6 +56,39 @@ public static ScriptPubKey FromHex(string hexString) internal IntPtr Handle => _handle; + /// + /// Serializes this script pubkey to bytes. + /// + public byte[] ToBytes() + { + var bytes = new List(); + NativeMethods.WriteBytes writer = (data, len, _) => + { + var buffer = new byte[len]; + System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, (int)len); + bytes.AddRange(buffer); + return 0; + }; + + int result = NativeMethods.ScriptPubkeyToBytes(_handle, writer, IntPtr.Zero); + if (result != 0) + throw new TransactionException("Failed to serialize script pubkey"); + + return bytes.ToArray(); + } + + /// + /// Creates an owned copy of this script pubkey. + /// + public ScriptPubKey Copy() + { + var copy = NativeMethods.ScriptPubkeyCopy(_handle); + if (copy == IntPtr.Zero) + throw new TransactionException("Failed to copy script pubkey"); + + return new ScriptPubKey(copy, ownsHandle: true); + } + public void Dispose() { if (!_disposed && _handle != IntPtr.Zero) diff --git a/src/BitcoinKernel/Primitives/Transaction.cs b/src/BitcoinKernel/Primitives/Transaction.cs index d560a41..dc557a0 100644 --- a/src/BitcoinKernel/Primitives/Transaction.cs +++ b/src/BitcoinKernel/Primitives/Transaction.cs @@ -90,19 +90,26 @@ internal Transaction(IntPtr handle, bool ownsHandle = true) public int OutputCount => (int)NativeMethods.TransactionCountOutputs(_handle); /// - /// Gets the transaction ID (txid) as bytes. + /// Gets the transaction ID (txid) as a non-owning object whose + /// lifetime is tied to this transaction. /// - /// The transaction ID as a byte array. - public byte[] GetTxid() + public Txid GetTxid() { IntPtr txidPtr = NativeMethods.TransactionGetTxid(_handle); if (txidPtr == IntPtr.Zero) throw new TransactionException("Failed to get transaction ID"); - const int TxidSize = 32; - byte[] txid = new byte[TxidSize]; - Marshal.Copy(txidPtr, txid, 0, TxidSize); - return txid; + return new Txid(txidPtr, ownsHandle: false); + } + + /// + /// Gets the transaction ID (txid) as a 32-byte array. + /// + /// The transaction ID as a byte array. + public byte[] GetTxidBytes() + { + using var txid = GetTxid(); + return txid.ToBytes(); } /// @@ -111,14 +118,37 @@ public byte[] GetTxid() /// The transaction ID as a hex string. public string GetTxidHex() { - byte[] txid = GetTxid(); - return Convert.ToHexString(txid).ToLowerInvariant(); + return Convert.ToHexString(GetTxidBytes()).ToLowerInvariant(); } + /// + /// Serializes this transaction to bytes. + /// + public byte[] ToBytes() + { + var bytes = new List(); + NativeMethods.WriteBytes writer = (data, len, _) => + { + var buffer = new byte[len]; + Marshal.Copy(data, buffer, 0, (int)len); + bytes.AddRange(buffer); + return 0; + }; + + int result = NativeMethods.TransactionToBytes(_handle, writer, IntPtr.Zero); + if (result != 0) + throw new TransactionException("Failed to serialize transaction"); + return bytes.ToArray(); + } + + /// + /// Gets the transaction input at the specified index as a non-owning + /// whose lifetime is tied to this transaction. + /// /// Thrown when index is out of range. /// Thrown when input retrieval fails. - public IntPtr GetInputAt(int index) + public TransactionInput GetInputAt(int index) { ArgumentOutOfRangeException.ThrowIfNegative(index); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, InputCount); @@ -127,7 +157,7 @@ public IntPtr GetInputAt(int index) if (inputPtr == IntPtr.Zero) throw new TransactionException($"Failed to get input at index {index}"); - return inputPtr; + return new TransactionInput(inputPtr, ownsHandle: false); } /// The TxOut at the specified index. diff --git a/src/BitcoinKernel/Primitives/TransactionInput.cs b/src/BitcoinKernel/Primitives/TransactionInput.cs new file mode 100644 index 0000000..2e5d929 --- /dev/null +++ b/src/BitcoinKernel/Primitives/TransactionInput.cs @@ -0,0 +1,96 @@ +using BitcoinKernel.Exceptions; +using BitcoinKernel.Interop; + +namespace BitcoinKernel.Primitives; + +/// +/// Represents a transaction input. +/// +public sealed class TransactionInput : IDisposable +{ + private IntPtr _handle; + private bool _disposed; + private readonly bool _ownsHandle; + + internal TransactionInput(IntPtr handle, bool ownsHandle = true) + { + _handle = handle != IntPtr.Zero + ? handle + : throw new ArgumentException("Invalid transaction input handle", nameof(handle)); + _ownsHandle = ownsHandle; + } + + internal IntPtr Handle + { + get + { + ThrowIfDisposed(); + return _handle; + } + } + + /// + /// Gets the nSequence value of this input. + /// + public uint Sequence + { + get + { + ThrowIfDisposed(); + return NativeMethods.TransactionInputGetSequence(_handle); + } + } + + /// + /// Gets the out point spent by this input. The returned out point is a + /// non-owning view whose lifetime is tied to this input. + /// + public OutPoint GetOutPoint() + { + ThrowIfDisposed(); + var outPointPtr = NativeMethods.TransactionInputGetOutPoint(_handle); + if (outPointPtr == IntPtr.Zero) + throw new TransactionException("Failed to get out point from transaction input"); + + return new OutPoint(outPointPtr, ownsHandle: false); + } + + /// + /// Creates an owned copy of this transaction input. + /// + public TransactionInput Copy() + { + ThrowIfDisposed(); + var copy = NativeMethods.TransactionInputCopy(_handle); + if (copy == IntPtr.Zero) + throw new TransactionException("Failed to copy transaction input"); + + return new TransactionInput(copy); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(TransactionInput)); + } + + public void Dispose() + { + if (!_disposed) + { + if (_handle != IntPtr.Zero && _ownsHandle) + { + NativeMethods.TransactionInputDestroy(_handle); + _handle = IntPtr.Zero; + } + _disposed = true; + } + GC.SuppressFinalize(this); + } + + ~TransactionInput() + { + if (_ownsHandle) + Dispose(); + } +} diff --git a/src/BitcoinKernel/Primitives/TxOut.cs b/src/BitcoinKernel/Primitives/TxOut.cs index ca89601..da07078 100644 --- a/src/BitcoinKernel/Primitives/TxOut.cs +++ b/src/BitcoinKernel/Primitives/TxOut.cs @@ -72,29 +72,22 @@ public IntPtr GetScriptPubkeyPtr() } /// - /// Gets the script pubkey as a byte array. + /// Gets the script pubkey of this output as a non-owning + /// object whose lifetime is tied to this output. /// - /// The script pubkey bytes. - public byte[] GetScriptPubkey() + public ScriptPubKey GetScriptPubkey() { - IntPtr scriptPtr = GetScriptPubkeyPtr(); - - var bytes = new List(); - NativeMethods.WriteBytes writer = (data, len, _) => - { - var buffer = new byte[len]; - Marshal.Copy(data, buffer, 0, (int)len); - bytes.AddRange(buffer); - return 0; - }; - - int result = NativeMethods.ScriptPubkeyToBytes(scriptPtr, writer, IntPtr.Zero); - if (result != 0) - { - throw new TransactionException("Failed to serialize script pubkey"); - } + return new ScriptPubKey(GetScriptPubkeyPtr(), ownsHandle: false); + } - return bytes.ToArray(); + /// + /// Gets the script pubkey of this output as a byte array. + /// + /// The script pubkey bytes. + public byte[] GetScriptPubkeyBytes() + { + using var scriptPubkey = GetScriptPubkey(); + return scriptPubkey.ToBytes(); } /// diff --git a/src/BitcoinKernel/Primitives/Txid.cs b/src/BitcoinKernel/Primitives/Txid.cs new file mode 100644 index 0000000..40c266c --- /dev/null +++ b/src/BitcoinKernel/Primitives/Txid.cs @@ -0,0 +1,96 @@ +using System.Runtime.InteropServices; +using BitcoinKernel.Exceptions; +using BitcoinKernel.Interop; + +namespace BitcoinKernel.Primitives; + +/// +/// Represents a transaction id (txid). +/// +public sealed class Txid : IDisposable, IEquatable +{ + private IntPtr _handle; + private bool _disposed; + private readonly bool _ownsHandle; + + internal Txid(IntPtr handle, bool ownsHandle = true) + { + _handle = handle != IntPtr.Zero + ? handle + : throw new ArgumentException("Invalid txid handle", nameof(handle)); + _ownsHandle = ownsHandle; + } + + internal IntPtr Handle + { + get + { + ThrowIfDisposed(); + return _handle; + } + } + + /// + /// Converts the txid to a 32-byte array. + /// + public byte[] ToBytes() + { + ThrowIfDisposed(); + var bytes = new byte[32]; + NativeMethods.TxidToBytes(_handle, bytes); + return bytes; + } + + /// + /// Creates an owned copy of this txid. + /// + public Txid Copy() + { + ThrowIfDisposed(); + var copy = NativeMethods.TxidCopy(_handle); + if (copy == IntPtr.Zero) + throw new TransactionException("Failed to copy txid"); + + return new Txid(copy); + } + + /// + /// Determines whether this txid equals another. + /// + public bool Equals(Txid? other) + { + if (other is null) return false; + ThrowIfDisposed(); + return NativeMethods.TxidEquals(_handle, other.Handle) != 0; + } + + public override bool Equals(object? obj) => obj is Txid other && Equals(other); + + public override int GetHashCode() => Convert.ToHexString(ToBytes()).GetHashCode(); + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(Txid)); + } + + public void Dispose() + { + if (!_disposed) + { + if (_handle != IntPtr.Zero && _ownsHandle) + { + NativeMethods.TxidDestroy(_handle); + _handle = IntPtr.Zero; + } + _disposed = true; + } + GC.SuppressFinalize(this); + } + + ~Txid() + { + if (_ownsHandle) + Dispose(); + } +} diff --git a/tests/BitcoinKernel.Tests/BlockHeaderTests.cs b/tests/BitcoinKernel.Tests/BlockHeaderTests.cs index 6762204..aae920b 100644 --- a/tests/BitcoinKernel.Tests/BlockHeaderTests.cs +++ b/tests/BitcoinKernel.Tests/BlockHeaderTests.cs @@ -223,7 +223,7 @@ public void BlockIndex_GetBlockHeader_HashShouldMatchIndexHash() var chain = _chainstateManager!.GetActiveChain(); var tip = chain.GetTip(); - var tipHash = tip.GetBlockHash(); + var tipHash = tip.GetHash(); using var header = tip.GetBlockHeader(); var headerHash = header.GetHash(); @@ -241,7 +241,7 @@ public void ChainstateManager_GetBestBlockIndex_ShouldReturnTip() var tip = chain.GetTip(); Assert.Equal(tip.Height, bestIndex.Height); - Assert.Equal(tip.GetBlockHash(), bestIndex.GetBlockHash()); + Assert.Equal(tip.GetHash(), bestIndex.GetHash()); } [Fact] diff --git a/tests/BitcoinKernel.Tests/BlockProcessingTests.cs b/tests/BitcoinKernel.Tests/BlockProcessingTests.cs index a4b4215..dbd6cb7 100644 --- a/tests/BitcoinKernel.Tests/BlockProcessingTests.cs +++ b/tests/BitcoinKernel.Tests/BlockProcessingTests.cs @@ -257,7 +257,7 @@ public void TestScanTransactions() Assert.NotNull(output); // Verify we can get the script pubkey from the output - var scriptPubkeyBytes = output.GetScriptPubkey(); + var scriptPubkeyBytes = output.GetScriptPubkeyBytes(); Assert.NotNull(scriptPubkeyBytes); Assert.True(scriptPubkeyBytes.Length >= 0, "Script pubkey should have valid length"); @@ -286,14 +286,14 @@ public void TestChainOperations() var genesis = chain.GetGenesis(); Assert.NotNull(genesis); Assert.Equal(0, genesis.Height); - var genesisHash = genesis.GetBlockHash(); + var genesisHash = genesis.GetHash(); Assert.NotNull(genesisHash); // Test tip block var tip = chain.GetTip(); Assert.NotNull(tip); var tipHeight = tip.Height; - var tipHash = tip.GetBlockHash(); + var tipHash = tip.GetHash(); Assert.True(tipHeight > 0); Assert.False(genesisHash.SequenceEqual(tipHash)); @@ -302,13 +302,13 @@ public void TestChainOperations() var genesisViaHeight = chain.GetBlockByHeight(0); Assert.NotNull(genesisViaHeight); Assert.Equal(0, genesisViaHeight.Height); - Assert.True(genesisHash.SequenceEqual(genesisViaHeight.GetBlockHash())); + Assert.True(genesisHash.SequenceEqual(genesisViaHeight.GetHash())); // Test accessing block by height - tip var tipViaHeight = chain.GetBlockByHeight(tipHeight); Assert.NotNull(tipViaHeight); Assert.Equal(tipHeight, tipViaHeight.Height); - Assert.True(tipHash.SequenceEqual(tipViaHeight.GetBlockHash())); + Assert.True(tipHash.SequenceEqual(tipViaHeight.GetHash())); // Test invalid height returns null var invalidEntry = chain.GetBlockByHeight(9999); diff --git a/tests/BitcoinKernel.Tests/BlockTreeEntryTests.cs b/tests/BitcoinKernel.Tests/BlockTreeEntryTests.cs index 8a2e39c..e3911fa 100644 --- a/tests/BitcoinKernel.Tests/BlockTreeEntryTests.cs +++ b/tests/BitcoinKernel.Tests/BlockTreeEntryTests.cs @@ -69,7 +69,7 @@ private static List ReadBlockData() public void Equals_SameBlock_ReturnsTrue() { SetupWithBlocks(); - var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash(); + var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetHash(); var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash); var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash); @@ -83,8 +83,8 @@ public void Equals_DifferentBlocks_ReturnsFalse() SetupWithBlocks(); var chain = _chainstateManager!.GetActiveChain(); - var tipEntry = _blockProcessor!.GetBlockTreeEntry(chain.GetTip().GetBlockHash()); - var genesisEntry = _blockProcessor.GetBlockTreeEntry(chain.GetBlockByHeight(0)!.GetBlockHash()); + var tipEntry = _blockProcessor!.GetBlockTreeEntry(chain.GetTip().GetHash()); + var genesisEntry = _blockProcessor.GetBlockTreeEntry(chain.GetBlockByHeight(0)!.GetHash()); Assert.False(tipEntry!.Equals(genesisEntry)); } @@ -93,7 +93,7 @@ public void Equals_DifferentBlocks_ReturnsFalse() public void Equals_WithNull_ReturnsFalse() { SetupWithBlocks(); - var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash(); + var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetHash(); var entry = _blockProcessor!.GetBlockTreeEntry(tipHash); @@ -104,7 +104,7 @@ public void Equals_WithNull_ReturnsFalse() public void GetHashCode_EqualEntries_ReturnsSameHashCode() { SetupWithBlocks(); - var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash(); + var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetHash(); var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash); var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash); @@ -116,7 +116,7 @@ public void GetHashCode_EqualEntries_ReturnsSameHashCode() public void OperatorEquals_SameBlock_ReturnsTrue() { SetupWithBlocks(); - var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash(); + var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetHash(); var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash); var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash); @@ -130,8 +130,8 @@ public void OperatorNotEquals_DifferentBlocks_ReturnsTrue() SetupWithBlocks(); var chain = _chainstateManager!.GetActiveChain(); - var tipEntry = _blockProcessor!.GetBlockTreeEntry(chain.GetTip().GetBlockHash()); - var genesisEntry = _blockProcessor.GetBlockTreeEntry(chain.GetBlockByHeight(0)!.GetBlockHash()); + var tipEntry = _blockProcessor!.GetBlockTreeEntry(chain.GetTip().GetHash()); + var genesisEntry = _blockProcessor.GetBlockTreeEntry(chain.GetBlockByHeight(0)!.GetHash()); Assert.True(tipEntry != genesisEntry); } @@ -140,7 +140,7 @@ public void OperatorNotEquals_DifferentBlocks_ReturnsTrue() public void GetPrevious_EqualEntries_ReturnEqualPrevious() { SetupWithBlocks(); - var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash(); + var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetHash(); var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash); var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash); diff --git a/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs b/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs index 001a7ce..e9d72ef 100644 --- a/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs +++ b/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs @@ -175,13 +175,261 @@ public Response BlockCreate(string id, string? refName, BtckBlockCreateParams p) } } - public Response BlockTreeEntryGetBlockHash(string id, BtckBlockTreeEntryGetBlockHashParams p) + public Response BlockGetHash(string id, string? refName, BtckBlockRefParams p) { + if (refName == null) return RefError(id); + if (p.Block?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetBlockHash()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockGetHeader(string id, string? refName, BtckBlockRefParams p) + { + if (refName == null) return RefError(id); + if (p.Block?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetHeader()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockCopy(string id, string? refName, BtckBlockRefParams p) + { + if (refName == null) return RefError(id); + if (p.Block?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockCountTransactions(string id, BtckBlockRefParams p) + { + if (p.Block?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).TransactionCount); + } + + public Response BlockGetTransactionAt(string id, string? refName, BtckBlockGetTransactionAtParams p) + { + if (refName == null) return RefError(id); + if (p.Block?.Ref is not { } r) return RefError(id); + + try + { + var tx = Get(r).GetTransaction(p.TransactionIndex); + if (tx == null) return Responses.EmptyError(id); + _registry.Register(refName, tx); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockToBytes(string id, BtckBlockRefParams p) + { + if (p.Block?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Hex(Get(r).ToBytes())); + } + + public Response BlockDestroy(string id, BtckBlockRefParams p) + { + if (p.Block?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + + // ── Block Hash ──────────────────────────────────────────────────────────── + + public Response BlockHashCreate(string id, string? refName, BtckBlockHashCreateParams p) + { + if (refName == null) return RefError(id); + + try + { + var hash = BlockHash.FromBytes(Convert.FromHexString(p.BlockHashHex)); + _registry.Register(refName, hash); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockHashToBytes(string id, BtckBlockHashRefParams p) + { + if (p.BlockHash?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Hex(Get(r).ToBytes())); + } + + public Response BlockHashEquals(string id, BtckBlockHashEqualsParams p) + { + if (p.Hash1?.Ref is not { } r1) return RefError(id); + if (p.Hash2?.Ref is not { } r2) return RefError(id); + return Responses.Ok(id, Get(r1).Equals(Get(r2))); + } + + public Response BlockHashCopy(string id, string? refName, BtckBlockHashRefParams p) + { + if (refName == null) return RefError(id); + if (p.BlockHash?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockHashDestroy(string id, BtckBlockHashRefParams p) + { + if (p.BlockHash?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + + // ── Block Header ────────────────────────────────────────────────────────── + + public Response BlockHeaderCreate(string id, string? refName, BtckBlockHeaderCreateParams p) + { + if (refName == null) return RefError(id); + + try + { + var header = BlockHeader.FromBytes(Convert.FromHexString(p.RawBlockHeader)); + _registry.Register(refName, header); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockHeaderToBytes(string id, BtckBlockHeaderRefParams p) + { + if (p.Header?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Hex(Get(r).ToBytes())); + } + + public Response BlockHeaderGetHash(string id, string? refName, BtckBlockHeaderRefParams p) + { + if (refName == null) return RefError(id); + if (p.Header?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetBlockHash()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockHeaderGetPrevHash(string id, string? refName, BtckBlockHeaderRefParams p) + { + if (refName == null) return RefError(id); + if (p.Header?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetPrevBlockHash()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockHeaderGetVersion(string id, BtckBlockHeaderRefParams p) + { + if (p.Header?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).Version); + } + + public Response BlockHeaderGetTimestamp(string id, BtckBlockHeaderRefParams p) + { + if (p.Header?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).Timestamp); + } + + public Response BlockHeaderGetBits(string id, BtckBlockHeaderRefParams p) + { + if (p.Header?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).Bits); + } + + public Response BlockHeaderGetNonce(string id, BtckBlockHeaderRefParams p) + { + if (p.Header?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).Nonce); + } + + public Response BlockHeaderCopy(string id, string? refName, BtckBlockHeaderRefParams p) + { + if (refName == null) return RefError(id); + if (p.Header?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response BlockHeaderDestroy(string id, BtckBlockHeaderRefParams p) + { + if (p.Header?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + + // ── Block Tree Entry ────────────────────────────────────────────────────── + + public Response BlockTreeEntryGetBlockHash(string id, string? refName, BtckBlockTreeEntryGetBlockHashParams p) + { + if (refName == null) return RefError(id); if (p.BlockTreeEntry?.Ref is not { } bteRef) return RefError(id); - var hashBytes = GetVal(bteRef).GetBlockHash(); - // Reverse bytes to get display (big-endian) order - return Responses.Ok(id, Convert.ToHexString(hashBytes.Reverse().ToArray()).ToLowerInvariant()); + try + { + _registry.Register(refName, GetVal(bteRef).GetBlockHash()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } } // ── Script Pubkey ───────────────────────────────────────────────────────── @@ -192,7 +440,8 @@ public Response ScriptPubkeyCreate(string id, string? refName, BtckScriptPubkeyC try { - var spk = ScriptPubKey.FromHex(p.ScriptPubKeyHex); + // Use FromBytes (not FromHex) so an empty script pubkey (empty hex) is accepted. + var spk = ScriptPubKey.FromBytes(Convert.FromHexString(p.ScriptPubKeyHex)); _registry.Register(refName, spk); return Responses.Ref(id, refName); } @@ -202,10 +451,26 @@ public Response ScriptPubkeyCreate(string id, string? refName, BtckScriptPubkeyC } } - public Response ScriptPubkeyDestroy(string id, BtckScriptPubkeyDestroyParams p) + public Response ScriptPubkeyCopy(string id, string? refName, BtckScriptPubkeyRefParams p) { - if (p.ScriptPubKey?.Ref is { } r) _registry.Destroy(r); - return Responses.Null(id); + if (refName == null) return RefError(id); + if (p.ScriptPubKey?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response ScriptPubkeyToBytes(string id, BtckScriptPubkeyRefParams p) + { + if (p.ScriptPubKey?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Hex(Get(r).ToBytes())); } public Response ScriptPubkeyVerify(string id, BtckScriptPubkeyVerifyParams p) @@ -244,6 +509,12 @@ public Response ScriptPubkeyVerify(string id, BtckScriptPubkeyVerifyParams p) } } + public Response ScriptPubkeyDestroy(string id, BtckScriptPubkeyDestroyParams p) + { + if (p.ScriptPubKey?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + // ── Transaction ─────────────────────────────────────────────────────────── public Response TransactionCreate(string id, string? refName, BtckTransactionCreateParams p) @@ -262,12 +533,217 @@ public Response TransactionCreate(string id, string? refName, BtckTransactionCre } } + public Response TransactionCopy(string id, string? refName, BtckTransactionRefParams p) + { + if (refName == null) return RefError(id); + if (p.Transaction?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionCountInputs(string id, BtckTransactionRefParams p) + { + if (p.Transaction?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).InputCount); + } + + public Response TransactionCountOutputs(string id, BtckTransactionRefParams p) + { + if (p.Transaction?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).OutputCount); + } + + public Response TransactionGetTxid(string id, string? refName, BtckTransactionRefParams p) + { + if (refName == null) return RefError(id); + if (p.Transaction?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetTxid()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionToBytes(string id, BtckTransactionRefParams p) + { + if (p.Transaction?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Hex(Get(r).ToBytes())); + } + + public Response TransactionGetInputAt(string id, string? refName, BtckTransactionGetInputAtParams p) + { + if (refName == null) return RefError(id); + if (p.Transaction?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetInputAt(p.InputIndex)); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionGetOutputAt(string id, string? refName, BtckTransactionGetOutputAtParams p) + { + if (refName == null) return RefError(id); + if (p.Transaction?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetOutputAt(p.OutputIndex)); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + public Response TransactionDestroy(string id, BtckTransactionDestroyParams p) { if (p.Transaction?.Ref is { } r) _registry.Destroy(r); return Responses.Null(id); } + // ── Transaction Input ───────────────────────────────────────────────────── + + public Response TransactionInputGetOutPoint(string id, string? refName, BtckTransactionInputRefParams p) + { + if (refName == null) return RefError(id); + if (p.TransactionInput?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetOutPoint()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionInputCopy(string id, string? refName, BtckTransactionInputRefParams p) + { + if (refName == null) return RefError(id); + if (p.TransactionInput?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionInputDestroy(string id, BtckTransactionInputRefParams p) + { + if (p.TransactionInput?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + + // ── Transaction Out Point ───────────────────────────────────────────────── + + public Response TransactionOutPointGetIndex(string id, BtckTransactionOutPointRefParams p) + { + if (p.TransactionOutPoint?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).Index); + } + + public Response TransactionOutPointGetTxid(string id, string? refName, BtckTransactionOutPointRefParams p) + { + if (refName == null) return RefError(id); + if (p.TransactionOutPoint?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetTxid()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionOutPointCopy(string id, string? refName, BtckTransactionOutPointRefParams p) + { + if (refName == null) return RefError(id); + if (p.TransactionOutPoint?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionOutPointDestroy(string id, BtckTransactionOutPointRefParams p) + { + if (p.TransactionOutPoint?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + + // ── Txid ────────────────────────────────────────────────────────────────── + + public Response TxidToBytes(string id, BtckTxidRefParams p) + { + if (p.Txid?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Hex(Get(r).ToBytes())); + } + + public Response TxidEquals(string id, BtckTxidEqualsParams p) + { + if (p.Txid1?.Ref is not { } r1) return RefError(id); + if (p.Txid2?.Ref is not { } r2) return RefError(id); + return Responses.Ok(id, Get(r1).Equals(Get(r2))); + } + + public Response TxidCopy(string id, string? refName, BtckTxidRefParams p) + { + if (refName == null) return RefError(id); + if (p.Txid?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TxidDestroy(string id, BtckTxidRefParams p) + { + if (p.Txid?.Ref is { } r) _registry.Destroy(r); + return Responses.Null(id); + } + // ── Transaction Output ──────────────────────────────────────────────────── public Response TransactionOutputCreate(string id, string? refName, BtckTransactionOutputCreateParams p) @@ -288,6 +764,44 @@ public Response TransactionOutputCreate(string id, string? refName, BtckTransact } } + public Response TransactionOutputCopy(string id, string? refName, BtckTransactionOutputRefParams p) + { + if (refName == null) return RefError(id); + if (p.TransactionOutput?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).Copy()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + + public Response TransactionOutputGetAmount(string id, BtckTransactionOutputRefParams p) + { + if (p.TransactionOutput?.Ref is not { } r) return RefError(id); + return Responses.Ok(id, Get(r).Amount); + } + + public Response TransactionOutputGetScriptPubkey(string id, string? refName, BtckTransactionOutputRefParams p) + { + if (refName == null) return RefError(id); + if (p.TransactionOutput?.Ref is not { } r) return RefError(id); + + try + { + _registry.Register(refName, Get(r).GetScriptPubkey()); + return Responses.Ref(id, refName); + } + catch + { + return Responses.EmptyError(id); + } + } + public Response TransactionOutputDestroy(string id, BtckTransactionOutputDestroyParams p) { if (p.TransactionOutput?.Ref is { } r) _registry.Destroy(r); @@ -331,6 +845,8 @@ public Response PrecomputedTransactionDataDestroy(string id, BtckPrecomputedTran // ── Parsing helpers ─────────────────────────────────────────────────────── + private static string Hex(byte[] bytes) => Convert.ToHexString(bytes).ToLowerInvariant(); + private static ChainType ParseChainType(string s) => s switch { "btck_ChainType_MAINNET" => ChainType.MAINNET, diff --git a/tools/kernel-bindings-test-handler/Program.cs b/tools/kernel-bindings-test-handler/Program.cs index f7afa58..97469ca 100644 --- a/tools/kernel-bindings-test-handler/Program.cs +++ b/tools/kernel-bindings-test-handler/Program.cs @@ -113,10 +113,100 @@ private static Response Dispatch(Request request, MethodDispatcher dispatcher, J dispatcher.BlockCreate(id, request.Ref, Deserialize(request.Params, opts)), + "btck_block_get_hash" => + dispatcher.BlockGetHash(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_get_header" => + dispatcher.BlockGetHeader(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_copy" => + dispatcher.BlockCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_count_transactions" => + dispatcher.BlockCountTransactions(id, + Deserialize(request.Params, opts)), + + "btck_block_get_transaction_at" => + dispatcher.BlockGetTransactionAt(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_to_bytes" => + dispatcher.BlockToBytes(id, + Deserialize(request.Params, opts)), + + "btck_block_destroy" => + dispatcher.BlockDestroy(id, + Deserialize(request.Params, opts)), + "btck_block_tree_entry_get_block_hash" => - dispatcher.BlockTreeEntryGetBlockHash(id, + dispatcher.BlockTreeEntryGetBlockHash(id, request.Ref, Deserialize(request.Params, opts)), + // ── Block Hash ──────────────────────────────────────────────── + "btck_block_hash_create" => + dispatcher.BlockHashCreate(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_hash_to_bytes" => + dispatcher.BlockHashToBytes(id, + Deserialize(request.Params, opts)), + + "btck_block_hash_equals" => + dispatcher.BlockHashEquals(id, + Deserialize(request.Params, opts)), + + "btck_block_hash_copy" => + dispatcher.BlockHashCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_hash_destroy" => + dispatcher.BlockHashDestroy(id, + Deserialize(request.Params, opts)), + + // ── Block Header ────────────────────────────────────────────── + "btck_block_header_create" => + dispatcher.BlockHeaderCreate(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_header_to_bytes" => + dispatcher.BlockHeaderToBytes(id, + Deserialize(request.Params, opts)), + + "btck_block_header_get_hash" => + dispatcher.BlockHeaderGetHash(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_header_get_prev_hash" => + dispatcher.BlockHeaderGetPrevHash(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_header_get_version" => + dispatcher.BlockHeaderGetVersion(id, + Deserialize(request.Params, opts)), + + "btck_block_header_get_timestamp" => + dispatcher.BlockHeaderGetTimestamp(id, + Deserialize(request.Params, opts)), + + "btck_block_header_get_bits" => + dispatcher.BlockHeaderGetBits(id, + Deserialize(request.Params, opts)), + + "btck_block_header_get_nonce" => + dispatcher.BlockHeaderGetNonce(id, + Deserialize(request.Params, opts)), + + "btck_block_header_copy" => + dispatcher.BlockHeaderCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_block_header_destroy" => + dispatcher.BlockHeaderDestroy(id, + Deserialize(request.Params, opts)), + // ── Script Pubkey ───────────────────────────────────────────── "btck_script_pubkey_create" => dispatcher.ScriptPubkeyCreate(id, request.Ref, @@ -126,6 +216,14 @@ private static Response Dispatch(Request request, MethodDispatcher dispatcher, J dispatcher.ScriptPubkeyDestroy(id, Deserialize(request.Params, opts)), + "btck_script_pubkey_copy" => + dispatcher.ScriptPubkeyCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_script_pubkey_to_bytes" => + dispatcher.ScriptPubkeyToBytes(id, + Deserialize(request.Params, opts)), + "btck_script_pubkey_verify" => dispatcher.ScriptPubkeyVerify(id, Deserialize(request.Params, opts)), @@ -139,6 +237,81 @@ private static Response Dispatch(Request request, MethodDispatcher dispatcher, J dispatcher.TransactionDestroy(id, Deserialize(request.Params, opts)), + "btck_transaction_copy" => + dispatcher.TransactionCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_count_inputs" => + dispatcher.TransactionCountInputs(id, + Deserialize(request.Params, opts)), + + "btck_transaction_count_outputs" => + dispatcher.TransactionCountOutputs(id, + Deserialize(request.Params, opts)), + + "btck_transaction_get_txid" => + dispatcher.TransactionGetTxid(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_to_bytes" => + dispatcher.TransactionToBytes(id, + Deserialize(request.Params, opts)), + + "btck_transaction_get_input_at" => + dispatcher.TransactionGetInputAt(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_get_output_at" => + dispatcher.TransactionGetOutputAt(id, request.Ref, + Deserialize(request.Params, opts)), + + // ── Transaction Input ───────────────────────────────────────── + "btck_transaction_input_get_out_point" => + dispatcher.TransactionInputGetOutPoint(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_input_copy" => + dispatcher.TransactionInputCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_input_destroy" => + dispatcher.TransactionInputDestroy(id, + Deserialize(request.Params, opts)), + + // ── Transaction Out Point ───────────────────────────────────── + "btck_transaction_out_point_get_index" => + dispatcher.TransactionOutPointGetIndex(id, + Deserialize(request.Params, opts)), + + "btck_transaction_out_point_get_txid" => + dispatcher.TransactionOutPointGetTxid(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_out_point_copy" => + dispatcher.TransactionOutPointCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_out_point_destroy" => + dispatcher.TransactionOutPointDestroy(id, + Deserialize(request.Params, opts)), + + // ── Txid ────────────────────────────────────────────────────── + "btck_txid_to_bytes" => + dispatcher.TxidToBytes(id, + Deserialize(request.Params, opts)), + + "btck_txid_equals" => + dispatcher.TxidEquals(id, + Deserialize(request.Params, opts)), + + "btck_txid_copy" => + dispatcher.TxidCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_txid_destroy" => + dispatcher.TxidDestroy(id, + Deserialize(request.Params, opts)), + // ── Transaction Output ──────────────────────────────────────── "btck_transaction_output_create" => dispatcher.TransactionOutputCreate(id, request.Ref, @@ -148,6 +321,18 @@ private static Response Dispatch(Request request, MethodDispatcher dispatcher, J dispatcher.TransactionOutputDestroy(id, Deserialize(request.Params, opts)), + "btck_transaction_output_copy" => + dispatcher.TransactionOutputCopy(id, request.Ref, + Deserialize(request.Params, opts)), + + "btck_transaction_output_get_amount" => + dispatcher.TransactionOutputGetAmount(id, + Deserialize(request.Params, opts)), + + "btck_transaction_output_get_script_pubkey" => + dispatcher.TransactionOutputGetScriptPubkey(id, request.Ref, + Deserialize(request.Params, opts)), + // ── Precomputed Transaction Data ────────────────────────────── "btck_precomputed_transaction_data_create" => dispatcher.PrecomputedTransactionDataCreate(id, request.Ref, diff --git a/tools/kernel-bindings-test-handler/Protocol/Request.cs b/tools/kernel-bindings-test-handler/Protocol/Request.cs index 46d37c1..388d40d 100644 --- a/tools/kernel-bindings-test-handler/Protocol/Request.cs +++ b/tools/kernel-bindings-test-handler/Protocol/Request.cs @@ -117,6 +117,63 @@ public class BtckBlockCreateParams public string RawBlock { get; set; } = string.Empty; } +/// A single block reference parameter. +public class BtckBlockRefParams +{ + [JsonPropertyName("block")] + public RefType? Block { get; set; } +} + +public class BtckBlockGetTransactionAtParams +{ + [JsonPropertyName("block")] + public RefType? Block { get; set; } + + [JsonPropertyName("transaction_index")] + public int TransactionIndex { get; set; } +} + +// ── Block Hash ──────────────────────────────────────────────────────────────── + +public class BtckBlockHashCreateParams +{ + [JsonPropertyName("block_hash")] + public string BlockHashHex { get; set; } = string.Empty; +} + +/// A single block_hash reference parameter. +public class BtckBlockHashRefParams +{ + [JsonPropertyName("block_hash")] + public RefType? BlockHash { get; set; } +} + +public class BtckBlockHashEqualsParams +{ + [JsonPropertyName("hash1")] + public RefType? Hash1 { get; set; } + + [JsonPropertyName("hash2")] + public RefType? Hash2 { get; set; } +} + +// ── Block Header ────────────────────────────────────────────────────────────── + +public class BtckBlockHeaderCreateParams +{ + [JsonPropertyName("raw_block_header")] + public string RawBlockHeader { get; set; } = string.Empty; +} + +/// A single header reference parameter. +public class BtckBlockHeaderRefParams +{ + [JsonPropertyName("header")] + public RefType? Header { get; set; } +} + +// ── Block Tree Entry ────────────────────────────────────────────────────────── + public class BtckBlockTreeEntryGetBlockHashParams { [JsonPropertyName("block_tree_entry")] @@ -131,7 +188,8 @@ public class BtckScriptPubkeyCreateParams public string ScriptPubKeyHex { get; set; } = string.Empty; } -public class BtckScriptPubkeyDestroyParams +/// A single script_pubkey reference parameter. +public class BtckScriptPubkeyRefParams { [JsonPropertyName("script_pubkey")] public RefType? ScriptPubKey { get; set; } @@ -158,6 +216,12 @@ public class BtckScriptPubkeyVerifyParams public JsonElement? Flags { get; set; } } +public class BtckScriptPubkeyDestroyParams +{ + [JsonPropertyName("script_pubkey")] + public RefType? ScriptPubKey { get; set; } +} + // ── Transaction ─────────────────────────────────────────────────────────────── public class BtckTransactionCreateParams @@ -166,12 +230,73 @@ public class BtckTransactionCreateParams public string RawTransaction { get; set; } = string.Empty; } +/// A single transaction reference parameter. +public class BtckTransactionRefParams +{ + [JsonPropertyName("transaction")] + public RefType? Transaction { get; set; } +} + +public class BtckTransactionGetInputAtParams +{ + [JsonPropertyName("transaction")] + public RefType? Transaction { get; set; } + + [JsonPropertyName("input_index")] + public int InputIndex { get; set; } +} + +public class BtckTransactionGetOutputAtParams +{ + [JsonPropertyName("transaction")] + public RefType? Transaction { get; set; } + + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } +} + public class BtckTransactionDestroyParams { [JsonPropertyName("transaction")] public RefType? Transaction { get; set; } } +// ── Transaction Input ───────────────────────────────────────────────────────── + +/// A single transaction_input reference parameter. +public class BtckTransactionInputRefParams +{ + [JsonPropertyName("transaction_input")] + public RefType? TransactionInput { get; set; } +} + +// ── Transaction Out Point ───────────────────────────────────────────────────── + +/// A single transaction_out_point reference parameter. +public class BtckTransactionOutPointRefParams +{ + [JsonPropertyName("transaction_out_point")] + public RefType? TransactionOutPoint { get; set; } +} + +// ── Txid ────────────────────────────────────────────────────────────────────── + +/// A single txid reference parameter. +public class BtckTxidRefParams +{ + [JsonPropertyName("txid")] + public RefType? Txid { get; set; } +} + +public class BtckTxidEqualsParams +{ + [JsonPropertyName("txid1")] + public RefType? Txid1 { get; set; } + + [JsonPropertyName("txid2")] + public RefType? Txid2 { get; set; } +} + // ── Transaction Output ──────────────────────────────────────────────────────── public class BtckTransactionOutputCreateParams @@ -183,6 +308,13 @@ public class BtckTransactionOutputCreateParams public long Amount { get; set; } } +/// A single transaction_output reference parameter. +public class BtckTransactionOutputRefParams +{ + [JsonPropertyName("transaction_output")] + public RefType? TransactionOutput { get; set; } +} + public class BtckTransactionOutputDestroyParams { [JsonPropertyName("transaction_output")] diff --git a/tools/kernel-bindings-test-handler/Protocol/Response.cs b/tools/kernel-bindings-test-handler/Protocol/Response.cs index 358d641..768373c 100644 --- a/tools/kernel-bindings-test-handler/Protocol/Response.cs +++ b/tools/kernel-bindings-test-handler/Protocol/Response.cs @@ -8,7 +8,7 @@ namespace BitcoinKernel.TestHandler.Protocol; /// public class Response { - [JsonPropertyName("id")] + [JsonIgnore] public string Id { get; set; } = string.Empty; [JsonPropertyName("result")]