diff --git a/Makefile b/Makefile index 7cdf9dd1..c4fa05a3 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ build: test: go test -v ./... + go test -race ./kernel -run TestTransactionHandlePtrRaceDetector clean: rm -rf depend/bitcoin/build @@ -58,4 +59,4 @@ help: @echo " lint - Lint Go code" @echo " deps - Install development dependencies" @echo " update-kernel - Update Bitcoin dependency using git subtree" - @echo " help - Show this help message" \ No newline at end of file + @echo " help - Show this help message" diff --git a/kernel/block_hash.go b/kernel/block_hash.go index feed9d4b..69cd46fa 100644 --- a/kernel/block_hash.go +++ b/kernel/block_hash.go @@ -27,35 +27,51 @@ type BlockHash struct { func newBlockHash(ptr *C.btck_BlockHash, fromOwned bool) *BlockHash { h := newHandle(unsafe.Pointer(ptr), blockHashCFuncs{}, fromOwned) - return &BlockHash{handle: h, blockHashApi: blockHashApi{(*C.btck_BlockHash)(h.ptr)}} + return &BlockHash{ + handle: h, + blockHashApi: blockHashApi{ + ptr: func() *C.btck_BlockHash { + return (*C.btck_BlockHash)(h.ptr) + }, + }, + } } // BlockHashView is a type-safe identifier for a block. type BlockHashView struct { blockHashApi - ptr *C.btck_BlockHash } func newBlockHashView(ptr *C.btck_BlockHash) *BlockHashView { return &BlockHashView{ - blockHashApi: blockHashApi{ptr}, - ptr: ptr, + blockHashApi: blockHashApi{ + ptr: func() *C.btck_BlockHash { + return ptr + }, + }, } } type blockHashApi struct { - ptr *C.btck_BlockHash + ptr func() *C.btck_BlockHash } -func (bh *blockHashApi) blockHashPtr() *C.btck_BlockHash { - return bh.ptr +func (bh *blockHashApi) cPtr() *C.btck_BlockHash { + return bh.ptr() } -// BlockHashLike is an interface for types that can provide a block hash pointer. +// BlockHashLike is implemented by *BlockHash and *BlockHashView. type BlockHashLike interface { - blockHashPtr() *C.btck_BlockHash + cPtr() *C.btck_BlockHash + Bytes() [32]byte + Copy() *BlockHash + Equals(BlockHashLike) bool + String() string } +var _ BlockHashLike = (*BlockHash)(nil) +var _ BlockHashLike = (*BlockHashView)(nil) + // NewBlockHash creates a new BlockHash from a 32-byte hash value. // // Parameters: @@ -68,13 +84,13 @@ func NewBlockHash(hashBytes [32]byte) *BlockHash { // Bytes returns the 32-byte representation of the block hash. func (bh *blockHashApi) Bytes() [32]byte { var output [32]C.uchar - C.btck_block_hash_to_bytes(bh.ptr, &output[0]) + C.btck_block_hash_to_bytes(bh.ptr(), &output[0]) return *(*[32]byte)(unsafe.Pointer(&output[0])) } // Copy creates a copy of the block hash. func (bh *blockHashApi) Copy() *BlockHash { - return newBlockHash(bh.ptr, false) + return newBlockHash(bh.ptr(), false) } // Equals checks if two block hashes are equal. @@ -84,7 +100,7 @@ func (bh *blockHashApi) Copy() *BlockHash { // // Returns true if the block hashes are equal. func (bh *blockHashApi) Equals(other BlockHashLike) bool { - return C.btck_block_hash_equals(bh.ptr, other.blockHashPtr()) != 0 + return C.btck_block_hash_equals(bh.ptr(), other.cPtr()) != 0 } // String returns the block hash as a hex string in display order (reversed). diff --git a/kernel/block_validation_state.go b/kernel/block_validation_state.go index 6a55fa4a..6e5be662 100644 --- a/kernel/block_validation_state.go +++ b/kernel/block_validation_state.go @@ -27,7 +27,14 @@ type BlockValidationState struct { func newBlockValidationState(ptr *C.btck_BlockValidationState, fromOwned bool) *BlockValidationState { h := newHandle(unsafe.Pointer(ptr), blockValidationStateCFuncs{}, fromOwned) - return &BlockValidationState{handle: h, blockValidationStateApi: blockValidationStateApi{(*C.btck_BlockValidationState)(h.ptr)}} + return &BlockValidationState{ + handle: h, + blockValidationStateApi: blockValidationStateApi{ + ptr: func() *C.btck_BlockValidationState { + return (*C.btck_BlockValidationState)(h.ptr) + }, + }, + } } // NewBlockValidationState creates a new owned BlockValidationState. @@ -41,23 +48,40 @@ func NewBlockValidationState() *BlockValidationState { // It provides read-only access to validation state without managing the underlying memory. type BlockValidationStateView struct { blockValidationStateApi - ptr *C.btck_BlockValidationState } func newBlockValidationStateView(ptr *C.btck_BlockValidationState) *BlockValidationStateView { return &BlockValidationStateView{ - blockValidationStateApi: blockValidationStateApi{ptr}, - ptr: ptr, + blockValidationStateApi: blockValidationStateApi{ + ptr: func() *C.btck_BlockValidationState { + return ptr + }, + }, } } type blockValidationStateApi struct { - ptr *C.btck_BlockValidationState + ptr func() *C.btck_BlockValidationState +} + +func (s *blockValidationStateApi) cPtr() *C.btck_BlockValidationState { + return s.ptr() } +// BlockValidationStateLike is implemented by *BlockValidationState and *BlockValidationStateView. +type BlockValidationStateLike interface { + cPtr() *C.btck_BlockValidationState + Copy() *BlockValidationState + ValidationMode() ValidationMode + ValidationResult() BlockValidationResult +} + +var _ BlockValidationStateLike = (*BlockValidationState)(nil) +var _ BlockValidationStateLike = (*BlockValidationStateView)(nil) + // Copy creates a copy of the block validation state. func (s *blockValidationStateApi) Copy() *BlockValidationState { - return newBlockValidationState(s.ptr, false) + return newBlockValidationState(s.ptr(), false) } // ValidationMode returns whether the block is valid, invalid, or encountered an error. @@ -67,7 +91,7 @@ func (s *blockValidationStateApi) Copy() *BlockValidationState { // - ValidationStateInvalid: Block failed validation // - ValidationStateError: Internal error during validation func (s *blockValidationStateApi) ValidationMode() ValidationMode { - mode := C.btck_block_validation_state_get_validation_mode(s.ptr) + mode := C.btck_block_validation_state_get_validation_mode(s.ptr()) return ValidationMode(mode) } @@ -76,7 +100,7 @@ func (s *blockValidationStateApi) ValidationMode() ValidationMode { // This provides detailed information about the specific validation failure, such as // consensus violations, invalid headers, or missing previous blocks. func (s *blockValidationStateApi) ValidationResult() BlockValidationResult { - result := C.btck_block_validation_state_get_block_validation_result(s.ptr) + result := C.btck_block_validation_state_get_block_validation_result(s.ptr()) return BlockValidationResult(result) } diff --git a/kernel/chainstate_manager.go b/kernel/chainstate_manager.go index 87065b00..889ec478 100644 --- a/kernel/chainstate_manager.go +++ b/kernel/chainstate_manager.go @@ -185,7 +185,7 @@ func (cm *ChainstateManager) GetActiveChain() *Chain { // BlockTreeEntry is a non-owned pointer valid for the lifetime of this chainstate // manager. func (cm *ChainstateManager) GetBlockTreeEntryByHash(blockHash BlockHashLike) *BlockTreeEntry { - ptr := C.btck_chainstate_manager_get_block_tree_entry_by_hash((*C.btck_ChainstateManager)(cm.ptr), blockHash.blockHashPtr()) + ptr := C.btck_chainstate_manager_get_block_tree_entry_by_hash((*C.btck_ChainstateManager)(cm.ptr), blockHash.cPtr()) if ptr == nil { return nil } diff --git a/kernel/coin.go b/kernel/coin.go index 7be70eb3..1bbc2500 100644 --- a/kernel/coin.go +++ b/kernel/coin.go @@ -27,30 +27,55 @@ type Coin struct { func newCoin(ptr *C.btck_Coin, fromOwned bool) *Coin { h := newHandle(unsafe.Pointer(ptr), coinCFuncs{}, fromOwned) - return &Coin{handle: h, coinApi: coinApi{(*C.btck_Coin)(h.ptr)}} + return &Coin{ + handle: h, + coinApi: coinApi{ + ptr: func() *C.btck_Coin { + return (*C.btck_Coin)(h.ptr) + }, + }, + } } // CoinView holds information on a transaction output, including the height it was // spent at and whether it is a coinbase output. type CoinView struct { coinApi - ptr *C.btck_Coin } func newCoinView(ptr *C.btck_Coin) *CoinView { return &CoinView{ - coinApi: coinApi{ptr}, - ptr: ptr, + coinApi: coinApi{ + ptr: func() *C.btck_Coin { + return ptr + }, + }, } } type coinApi struct { - ptr *C.btck_Coin + ptr func() *C.btck_Coin +} + +func (c *coinApi) cPtr() *C.btck_Coin { + return c.ptr() } +// CoinLike is implemented by *Coin and *CoinView. +type CoinLike interface { + cPtr() *C.btck_Coin + Copy() *Coin + GetOutput() *TransactionOutputView + ConfirmationHeight() uint32 + IsCoinbase() bool +} + +var _ CoinLike = (*Coin)(nil) +var _ CoinLike = (*CoinView)(nil) + // Copy creates a copy of the coin. func (c *coinApi) Copy() *Coin { - return newCoin(c.ptr, false) + return newCoin(c.ptr(), false) } // GetOutput returns the transaction output contained in this coin. @@ -58,16 +83,16 @@ func (c *coinApi) Copy() *Coin { // The returned TransactionOutputView is a non-owned pointer valid for the // lifetime of this coin. func (c *coinApi) GetOutput() *TransactionOutputView { - ptr := C.btck_coin_get_output(c.ptr) + ptr := C.btck_coin_get_output(c.ptr()) return newTransactionOutputView(check(ptr)) } // ConfirmationHeight returns the block height where the transaction that created this coin was included in. func (c *coinApi) ConfirmationHeight() uint32 { - return uint32(C.btck_coin_confirmation_height(c.ptr)) + return uint32(C.btck_coin_confirmation_height(c.ptr())) } // IsCoinbase returns true if this coin originates from a coinbase transaction. func (c *coinApi) IsCoinbase() bool { - return int(C.btck_coin_is_coinbase(c.ptr)) != 0 + return int(C.btck_coin_is_coinbase(c.ptr())) != 0 } diff --git a/kernel/kernel.go b/kernel/kernel.go index 36bf53aa..7563e762 100644 --- a/kernel/kernel.go +++ b/kernel/kernel.go @@ -15,4 +15,18 @@ // the resources automatically via finalizers. However, relying on finalizers may delay // resource cleanup and is not recommended for long-running programs or when working // with many objects. +// +// # Owned and View Types +// +// Some kernel objects have both an owned type and a view type, such as Transaction +// and TransactionView. Owned types hold a reference to an underlying C resource and +// provide Destroy. View types are non-owned pointers returned from another object or +// callback, and remain valid only while the object that produced them remains valid. +// +// Methods shared by an owned type and its view type are exposed through sealed +// interfaces named with the Like suffix, such as TransactionLike and TxidLike. These +// interfaces are implemented by the package's owned and view types, but cannot be +// implemented by external packages because they include an unexported pointer getter. +// This keeps APIs type-safe around kernel C pointers while still allowing callers to +// accept either owned or view values. package kernel diff --git a/kernel/precomputed_transaction_data.go b/kernel/precomputed_transaction_data.go index ff6fb10b..fcbad5ea 100644 --- a/kernel/precomputed_transaction_data.go +++ b/kernel/precomputed_transaction_data.go @@ -42,18 +42,18 @@ func newPrecomputedTransactionData(ptr *C.btck_PrecomputedTransactionData, fromO // - spentOutputs: Outputs spent by the transaction. May be nil for non-taproot verification. // // Returns an error if the precomputation fails. -func NewPrecomputedTransactionData(txTo *Transaction, spentOutputs []*TransactionOutput) (*PrecomputedTransactionData, error) { +func NewPrecomputedTransactionData(txTo TransactionLike, spentOutputs []TransactionOutputLike) (*PrecomputedTransactionData, error) { var cSpentOutputsPtr **C.btck_TransactionOutput if len(spentOutputs) > 0 { cSpentOutputs := make([]*C.btck_TransactionOutput, len(spentOutputs)) for i, output := range spentOutputs { - cSpentOutputs[i] = (*C.btck_TransactionOutput)(output.handle.ptr) + cSpentOutputs[i] = output.cPtr() } cSpentOutputsPtr = (**C.btck_TransactionOutput)(unsafe.Pointer(&cSpentOutputs[0])) } ptr := C.btck_precomputed_transaction_data_create( - (*C.btck_Transaction)(txTo.handle.ptr), + txTo.cPtr(), cSpentOutputsPtr, C.size_t(len(spentOutputs)), ) diff --git a/kernel/script_pubkey.go b/kernel/script_pubkey.go index 6c22e87e..0aae82fb 100644 --- a/kernel/script_pubkey.go +++ b/kernel/script_pubkey.go @@ -25,7 +25,14 @@ type ScriptPubkey struct { func newScriptPubkey(ptr *C.btck_ScriptPubkey, fromOwned bool) *ScriptPubkey { h := newHandle(unsafe.Pointer(ptr), scriptPubkeyCFuncs{}, fromOwned) - return &ScriptPubkey{handle: h, scriptPubkeyApi: scriptPubkeyApi{(*C.btck_ScriptPubkey)(h.ptr)}} + return &ScriptPubkey{ + handle: h, + scriptPubkeyApi: scriptPubkeyApi{ + ptr: func() *C.btck_ScriptPubkey { + return (*C.btck_ScriptPubkey)(h.ptr) + }, + }, + } } // NewScriptPubkey creates a new script pubkey from raw serialized script data. @@ -41,23 +48,40 @@ func NewScriptPubkey(rawScriptPubkey []byte) *ScriptPubkey { type ScriptPubkeyView struct { scriptPubkeyApi - ptr *C.btck_ScriptPubkey } func newScriptPubkeyView(ptr *C.btck_ScriptPubkey) *ScriptPubkeyView { return &ScriptPubkeyView{ - scriptPubkeyApi: scriptPubkeyApi{ptr}, - ptr: ptr, + scriptPubkeyApi: scriptPubkeyApi{ + ptr: func() *C.btck_ScriptPubkey { + return ptr + }, + }, } } type scriptPubkeyApi struct { - ptr *C.btck_ScriptPubkey + ptr func() *C.btck_ScriptPubkey +} + +func (s *scriptPubkeyApi) cPtr() *C.btck_ScriptPubkey { + return s.ptr() } +// ScriptPubkeyLike is implemented by *ScriptPubkey and *ScriptPubkeyView. +type ScriptPubkeyLike interface { + cPtr() *C.btck_ScriptPubkey + Copy() *ScriptPubkey + Bytes() ([]byte, error) + Verify(int64, TransactionLike, *PrecomputedTransactionData, uint, ScriptFlags) (bool, error) +} + +var _ ScriptPubkeyLike = (*ScriptPubkey)(nil) +var _ ScriptPubkeyLike = (*ScriptPubkeyView)(nil) + // Copy creates a copy of the script pubkey. func (s *scriptPubkeyApi) Copy() *ScriptPubkey { - return newScriptPubkey(s.ptr, false) + return newScriptPubkey(s.ptr(), false) } // Bytes returns the serialized representation of the script pubkey. @@ -65,7 +89,7 @@ func (s *scriptPubkeyApi) Copy() *ScriptPubkey { // Returns an error if the serialization fails. func (s *scriptPubkeyApi) Bytes() ([]byte, error) { bytes, ok := writeToBytes(func(writer C.btck_WriteBytes, user_data unsafe.Pointer) C.int { - return C.btck_script_pubkey_to_bytes(s.ptr, writer, user_data) + return C.btck_script_pubkey_to_bytes(s.ptr(), writer, user_data) }) if !ok { return nil, &SerializationError{"Failed to serialize script pubkey"} @@ -89,7 +113,7 @@ func (s *scriptPubkeyApi) Bytes() ([]byte, error) { // - bool: true if the script is valid, false if invalid (only meaningful when error is nil) // - error: non-nil if verification could not be performed due to malformed input; // nil if verification completed successfully (check bool for validity result) -func (s *scriptPubkeyApi) Verify(amount int64, txTo *Transaction, precomputedTxData *PrecomputedTransactionData, inputIndex uint, flags ScriptFlags) (bool, error) { +func (s *scriptPubkeyApi) Verify(amount int64, txTo TransactionLike, precomputedTxData *PrecomputedTransactionData, inputIndex uint, flags ScriptFlags) (bool, error) { inputCount := txTo.CountInputs() if inputIndex >= uint(inputCount) { return false, ErrVerifyScriptVerifyTxInputIndex @@ -107,9 +131,9 @@ func (s *scriptPubkeyApi) Verify(amount int64, txTo *Transaction, precomputedTxD var cStatus C.btck_ScriptVerifyStatus result := C.btck_script_pubkey_verify( - s.ptr, + s.ptr(), C.int64_t(amount), - (*C.btck_Transaction)(txTo.handle.ptr), + txTo.cPtr(), cPrecomputedTxData, C.uint(inputIndex), C.btck_ScriptVerificationFlags(flags), diff --git a/kernel/script_pubkey_test.go b/kernel/script_pubkey_test.go index 95a9e511..d5903241 100644 --- a/kernel/script_pubkey_test.go +++ b/kernel/script_pubkey_test.go @@ -207,7 +207,7 @@ func TestValidTaprootMultiInput(t *testing.T) { {scriptHex: "5120ab78e077d062e7b8acd7063668b4db5355a1b5d5fd2a46a8e98e62e5e63fab77", amount: 135125}, } - outputs := make([]*TransactionOutput, len(spentOutputs)) + outputs := make([]TransactionOutputLike, len(spentOutputs)) for i, spent := range spentOutputs { spentScriptBytes, err := hex.DecodeString(spent.scriptHex) if err != nil { @@ -216,8 +216,9 @@ func TestValidTaprootMultiInput(t *testing.T) { spentScript := NewScriptPubkey(spentScriptBytes) defer spentScript.Destroy() - outputs[i] = NewTransactionOutput(spentScript, spent.amount) - defer outputs[i].Destroy() + output := NewTransactionOutput(spentScript, spent.amount) + defer output.Destroy() + outputs[i] = output } precomputed, err := NewPrecomputedTransactionData(txTo, outputs) @@ -456,9 +457,9 @@ func testVerifyScript( defer txTo.Destroy() // Create spent outputs if provided - var outputs []*TransactionOutput + var outputs []TransactionOutputLike if len(spentOutputs) > 0 { - outputs = make([]*TransactionOutput, len(spentOutputs)) + outputs = make([]TransactionOutputLike, len(spentOutputs)) for i, spent := range spentOutputs { spentScriptBytes, err := hex.DecodeString(spent.scriptHex) if err != nil { @@ -467,8 +468,9 @@ func testVerifyScript( spentScript := NewScriptPubkey(spentScriptBytes) defer spentScript.Destroy() - outputs[i] = NewTransactionOutput(spentScript, spent.amount) - defer outputs[i].Destroy() + output := NewTransactionOutput(spentScript, spent.amount) + defer output.Destroy() + outputs[i] = output } } diff --git a/kernel/transaction.go b/kernel/transaction.go index 4468a295..dba72608 100644 --- a/kernel/transaction.go +++ b/kernel/transaction.go @@ -26,7 +26,14 @@ type Transaction struct { func newTransaction(ptr *C.btck_Transaction, fromOwned bool) *Transaction { h := newHandle(unsafe.Pointer(ptr), transactionCFuncs{}, fromOwned) - return &Transaction{handle: h, transactionApi: transactionApi{(*C.btck_Transaction)(h.ptr)}} + return &Transaction{ + handle: h, + transactionApi: transactionApi{ + ptr: func() *C.btck_Transaction { + return (*C.btck_Transaction)(h.ptr) + }, + }, + } } // NewTransaction creates a new transaction from raw serialized transaction data. @@ -45,26 +52,54 @@ func NewTransaction(rawTransaction []byte) (*Transaction, error) { type TransactionView struct { transactionApi - ptr *C.btck_Transaction } func newTransactionView(ptr *C.btck_Transaction) *TransactionView { return &TransactionView{ - transactionApi: transactionApi{ptr}, - ptr: ptr, + transactionApi: transactionApi{ + ptr: func() *C.btck_Transaction { + return ptr + }, + }, } } type transactionApi struct { - ptr *C.btck_Transaction + ptr func() *C.btck_Transaction +} + +func (t *transactionApi) cPtr() *C.btck_Transaction { + return t.ptr() } +// TransactionLike is implemented by *Transaction and *TransactionView. +type TransactionLike interface { + cPtr() *C.btck_Transaction + Copy() *Transaction + Bytes() ([]byte, error) + GetLockTime() uint32 + GetTxid() *TxidView + CountInputs() uint64 + GetInput(uint64) (*TransactionInputView, error) + Inputs() iter.Seq[*TransactionInputView] + InputsRange(uint64, uint64) iter.Seq[*TransactionInputView] + InputsFrom(uint64) iter.Seq[*TransactionInputView] + CountOutputs() uint64 + GetOutput(uint64) (*TransactionOutputView, error) + Outputs() iter.Seq[*TransactionOutputView] + OutputsRange(uint64, uint64) iter.Seq[*TransactionOutputView] + OutputsFrom(uint64) iter.Seq[*TransactionOutputView] +} + +var _ TransactionLike = (*Transaction)(nil) +var _ TransactionLike = (*TransactionView)(nil) + // Copy creates a shallow copy of the transaction by incrementing its reference count. // // Transactions are reference-counted internally, so this operation is efficient and does // not duplicate the underlying data. func (t *transactionApi) Copy() *Transaction { - return newTransaction(t.ptr, false) + return newTransaction(t.ptr(), false) } // Bytes returns the consensus serialized representation of the transaction. @@ -72,7 +107,7 @@ func (t *transactionApi) Copy() *Transaction { // Returns an error if the serialization fails. func (t *transactionApi) Bytes() ([]byte, error) { bytes, ok := writeToBytes(func(writer C.btck_WriteBytes, userData unsafe.Pointer) C.int { - return C.btck_transaction_to_bytes(t.ptr, writer, userData) + return C.btck_transaction_to_bytes(t.ptr(), writer, userData) }) if !ok { return nil, &SerializationError{"Failed to serialize transaction"} @@ -82,18 +117,18 @@ func (t *transactionApi) Bytes() ([]byte, error) { // GetLockTime returns the transaction's nLockTime value. func (t *transactionApi) GetLockTime() uint32 { - return uint32(C.btck_transaction_get_locktime(t.ptr)) + return uint32(C.btck_transaction_get_locktime(t.ptr())) } // GetTxid returns the txid for this transaction. func (t *transactionApi) GetTxid() *TxidView { - ptr := C.btck_transaction_get_txid(t.ptr) + ptr := C.btck_transaction_get_txid(t.ptr()) return newTxidView(check(ptr)) } // CountInputs returns the number of inputs in the transaction. func (t *transactionApi) CountInputs() uint64 { - return uint64(C.btck_transaction_count_inputs(t.ptr)) + return uint64(C.btck_transaction_count_inputs(t.ptr())) } // GetInput retrieves the input at the specified index. @@ -108,7 +143,7 @@ func (t *transactionApi) GetInput(index uint64) (*TransactionInputView, error) { if index >= t.CountInputs() { return nil, ErrKernelIndexOutOfBounds } - ptr := C.btck_transaction_get_input_at(t.ptr, C.size_t(index)) + ptr := C.btck_transaction_get_input_at(t.ptr(), C.size_t(index)) return newTransactionInputView(check(ptr)), nil } @@ -185,7 +220,7 @@ func (t *transactionApi) iterInputs(from, to uint64, yield func(*TransactionInpu // CountOutputs returns the number of outputs in the transaction. func (t *transactionApi) CountOutputs() uint64 { - return uint64(C.btck_transaction_count_outputs(t.ptr)) + return uint64(C.btck_transaction_count_outputs(t.ptr())) } // GetOutput retrieves the output at the specified index. @@ -200,7 +235,7 @@ func (t *transactionApi) GetOutput(index uint64) (*TransactionOutputView, error) if index >= t.CountOutputs() { return nil, ErrKernelIndexOutOfBounds } - ptr := C.btck_transaction_get_output_at(t.ptr, C.size_t(index)) + ptr := C.btck_transaction_get_output_at(t.ptr(), C.size_t(index)) return newTransactionOutputView(check(ptr)), nil } diff --git a/kernel/transaction_input.go b/kernel/transaction_input.go index fd5849fb..55d3d3d3 100644 --- a/kernel/transaction_input.go +++ b/kernel/transaction_input.go @@ -26,38 +26,62 @@ type TransactionInput struct { func newTransactionInput(ptr *C.btck_TransactionInput, fromOwned bool) *TransactionInput { h := newHandle(unsafe.Pointer(ptr), transactionInputCFuncs{}, fromOwned) - return &TransactionInput{handle: h, transactionInputApi: transactionInputApi{(*C.btck_TransactionInput)(h.ptr)}} + return &TransactionInput{ + handle: h, + transactionInputApi: transactionInputApi{ + ptr: func() *C.btck_TransactionInput { + return (*C.btck_TransactionInput)(h.ptr) + }, + }, + } } // TransactionInputView holds information on the TransactionOutPoint held within. type TransactionInputView struct { transactionInputApi - ptr *C.btck_TransactionInput } func newTransactionInputView(ptr *C.btck_TransactionInput) *TransactionInputView { return &TransactionInputView{ - transactionInputApi: transactionInputApi{ptr}, - ptr: ptr, + transactionInputApi: transactionInputApi{ + ptr: func() *C.btck_TransactionInput { + return ptr + }, + }, } } type transactionInputApi struct { - ptr *C.btck_TransactionInput + ptr func() *C.btck_TransactionInput +} + +func (t *transactionInputApi) cPtr() *C.btck_TransactionInput { + return t.ptr() } +// TransactionInputLike is implemented by *TransactionInput and *TransactionInputView. +type TransactionInputLike interface { + cPtr() *C.btck_TransactionInput + Copy() *TransactionInput + GetOutPoint() *TransactionOutPointView + GetSequence() uint32 +} + +var _ TransactionInputLike = (*TransactionInput)(nil) +var _ TransactionInputLike = (*TransactionInputView)(nil) + // Copy creates a copy of the transaction input. func (t *transactionInputApi) Copy() *TransactionInput { - return newTransactionInput(t.ptr, false) + return newTransactionInput(t.ptr(), false) } // GetOutPoint returns the transaction out point. func (t *transactionInputApi) GetOutPoint() *TransactionOutPointView { - ptr := C.btck_transaction_input_get_out_point(t.ptr) + ptr := C.btck_transaction_input_get_out_point(t.ptr()) return newTransactionOutPointView(check(ptr)) } // GetSequence returns the transaction input's nSequence value. func (t *transactionInputApi) GetSequence() uint32 { - return uint32(C.btck_transaction_input_get_sequence(t.ptr)) + return uint32(C.btck_transaction_input_get_sequence(t.ptr())) } diff --git a/kernel/transaction_out_point.go b/kernel/transaction_out_point.go index 9728f4f9..5b5af7a5 100644 --- a/kernel/transaction_out_point.go +++ b/kernel/transaction_out_point.go @@ -26,38 +26,62 @@ type TransactionOutPoint struct { func newTransactionOutPoint(ptr *C.btck_TransactionOutPoint, fromOwned bool) *TransactionOutPoint { h := newHandle(unsafe.Pointer(ptr), transactionOutPointCFuncs{}, fromOwned) - return &TransactionOutPoint{handle: h, transactionOutPointApi: transactionOutPointApi{(*C.btck_TransactionOutPoint)(h.ptr)}} + return &TransactionOutPoint{ + handle: h, + transactionOutPointApi: transactionOutPointApi{ + ptr: func() *C.btck_TransactionOutPoint { + return (*C.btck_TransactionOutPoint)(h.ptr) + }, + }, + } } // TransactionOutPointView holds the txid and output index it is pointing to. type TransactionOutPointView struct { transactionOutPointApi - ptr *C.btck_TransactionOutPoint } func newTransactionOutPointView(ptr *C.btck_TransactionOutPoint) *TransactionOutPointView { return &TransactionOutPointView{ - transactionOutPointApi: transactionOutPointApi{ptr}, - ptr: ptr, + transactionOutPointApi: transactionOutPointApi{ + ptr: func() *C.btck_TransactionOutPoint { + return ptr + }, + }, } } type transactionOutPointApi struct { - ptr *C.btck_TransactionOutPoint + ptr func() *C.btck_TransactionOutPoint +} + +func (t *transactionOutPointApi) cPtr() *C.btck_TransactionOutPoint { + return t.ptr() } +// TransactionOutPointLike is implemented by *TransactionOutPoint and *TransactionOutPointView. +type TransactionOutPointLike interface { + cPtr() *C.btck_TransactionOutPoint + Copy() *TransactionOutPoint + GetIndex() uint32 + GetTxid() *TxidView +} + +var _ TransactionOutPointLike = (*TransactionOutPoint)(nil) +var _ TransactionOutPointLike = (*TransactionOutPointView)(nil) + // Copy creates a copy of the transaction out point. func (t *transactionOutPointApi) Copy() *TransactionOutPoint { - return newTransactionOutPoint(t.ptr, false) + return newTransactionOutPoint(t.ptr(), false) } // GetIndex returns the output position from the transaction out point. func (t *transactionOutPointApi) GetIndex() uint32 { - return uint32(C.btck_transaction_out_point_get_index(t.ptr)) + return uint32(C.btck_transaction_out_point_get_index(t.ptr())) } // GetTxid returns the txid from the out point. func (t *transactionOutPointApi) GetTxid() *TxidView { - ptr := C.btck_transaction_out_point_get_txid(t.ptr) + ptr := C.btck_transaction_out_point_get_txid(t.ptr()) return newTxidView(check(ptr)) } diff --git a/kernel/transaction_output.go b/kernel/transaction_output.go index 6ddbc220..a369b8a2 100644 --- a/kernel/transaction_output.go +++ b/kernel/transaction_output.go @@ -25,7 +25,14 @@ type TransactionOutput struct { func newTransactionOutput(ptr *C.btck_TransactionOutput, fromOwned bool) *TransactionOutput { h := newHandle(unsafe.Pointer(ptr), transactionOutputCFuncs{}, fromOwned) - return &TransactionOutput{handle: h, transactionOutputApi: transactionOutputApi{(*C.btck_TransactionOutput)(h.ptr)}} + return &TransactionOutput{ + handle: h, + transactionOutputApi: transactionOutputApi{ + ptr: func() *C.btck_TransactionOutput { + return (*C.btck_TransactionOutput)(h.ptr) + }, + }, + } } // NewTransactionOutput creates a transaction output from a script pubkey and an amount. @@ -33,30 +40,47 @@ func newTransactionOutput(ptr *C.btck_TransactionOutput, fromOwned bool) *Transa // Parameters: // - scriptPubkey: ScriptPubkey defining the conditions to spend this output // - amount: The amount associated with the script pubkey for this output -func NewTransactionOutput(scriptPubkey *ScriptPubkey, amount int64) *TransactionOutput { - ptr := C.btck_transaction_output_create((*C.btck_ScriptPubkey)(scriptPubkey.handle.ptr), C.int64_t(amount)) +func NewTransactionOutput(scriptPubkey ScriptPubkeyLike, amount int64) *TransactionOutput { + ptr := C.btck_transaction_output_create(scriptPubkey.cPtr(), C.int64_t(amount)) return newTransactionOutput(check(ptr), true) } type TransactionOutputView struct { transactionOutputApi - ptr *C.btck_TransactionOutput } func newTransactionOutputView(ptr *C.btck_TransactionOutput) *TransactionOutputView { return &TransactionOutputView{ - transactionOutputApi: transactionOutputApi{ptr}, - ptr: ptr, + transactionOutputApi: transactionOutputApi{ + ptr: func() *C.btck_TransactionOutput { + return ptr + }, + }, } } type transactionOutputApi struct { - ptr *C.btck_TransactionOutput + ptr func() *C.btck_TransactionOutput +} + +func (t *transactionOutputApi) cPtr() *C.btck_TransactionOutput { + return t.ptr() } +// TransactionOutputLike is implemented by *TransactionOutput and *TransactionOutputView. +type TransactionOutputLike interface { + cPtr() *C.btck_TransactionOutput + Copy() *TransactionOutput + ScriptPubkey() *ScriptPubkeyView + Amount() int64 +} + +var _ TransactionOutputLike = (*TransactionOutput)(nil) +var _ TransactionOutputLike = (*TransactionOutputView)(nil) + // Copy creates a copy of the transaction output. func (t *transactionOutputApi) Copy() *TransactionOutput { - return newTransactionOutput(t.ptr, false) + return newTransactionOutput(t.ptr(), false) } // ScriptPubkey returns the script pubkey of this output. @@ -64,11 +88,11 @@ func (t *transactionOutputApi) Copy() *TransactionOutput { // The returned ScriptPubkeyView is a non-owned pointer valid for the lifetime of // this transaction output. func (t *transactionOutputApi) ScriptPubkey() *ScriptPubkeyView { - ptr := C.btck_transaction_output_get_script_pubkey(t.ptr) + ptr := C.btck_transaction_output_get_script_pubkey(t.ptr()) return newScriptPubkeyView(check(ptr)) } // Amount returns the amount in the output func (t *transactionOutputApi) Amount() int64 { - return int64(C.btck_transaction_output_get_amount(t.ptr)) + return int64(C.btck_transaction_output_get_amount(t.ptr())) } diff --git a/kernel/transaction_race_test.go b/kernel/transaction_race_test.go new file mode 100644 index 00000000..4c4527d9 --- /dev/null +++ b/kernel/transaction_race_test.go @@ -0,0 +1,109 @@ +//go:build race + +// This race-only test shows why owned API structs must not cache their own C +// pointer. When transactionApi.Copy calls the pointer getter, it reads +// handle.ptr, so the race detector can catch Copy running concurrently with +// Destroy, which writes to handle.ptr. If transactionApi cached the C pointer +// instead, Copy would read that cached field and the race detector would not see +// the unsafe shared access to handle.ptr. +package kernel + +import ( + "bytes" + "encoding/hex" + "os" + "os/exec" + "regexp" + "runtime" + "strings" + "testing" +) + +func TestTransactionHandlePtrRaceDetector(t *testing.T) { + if os.Getenv("KERNEL_RACE_HELPER") == "1" { + runTransactionHandlePtrRaceHelper(t) + return + } + + // Run the helper in a subprocess because it intentionally triggers a race. + // GORACE makes the helper exit successfully after printing the first report, + // so this outer test can assert that the expected report was produced. + cmd := exec.Command(os.Args[0], "-test.run=TestTransactionHandlePtrRaceDetector", "-test.v") + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GORACE=") { + continue + } + if strings.HasPrefix(env, "GOMAXPROCS=") { + continue + } + cmd.Env = append(cmd.Env, env) + } + cmd.Env = append(cmd.Env, + "KERNEL_RACE_HELPER=1", + "GOMAXPROCS=1", + "GORACE=halt_on_error=1 exitcode=0", + ) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("race helper exited unexpectedly: %v\noutput:\n%s", err, output) + } + report := string(output) + if !bytes.Contains(output, []byte("WARNING: DATA RACE")) { + t.Fatalf("expected race detector output, got:\n%s", output) + } + if !regexp.MustCompile(`(?s)[Rr]ead at .*?(\(\*transactionApi\)\.Copy\(\))`).MatchString(report) { + t.Fatalf("expected race output to mention transactionApi.Copy, got:\n%s", output) + } + if !regexp.MustCompile(`(?s)[Ww]rite at .*?(\(\*handle\)\.Destroy\(\))`).MatchString(report) { + t.Fatalf("expected race output to mention handle.Destroy, got:\n%s", output) + } +} + +func runTransactionHandlePtrRaceHelper(t *testing.T) { + rawTransaction, err := hex.DecodeString(coinbaseTxHex) + if err != nil { + t.Fatalf("DecodeString() error = %v", err) + } + + tx, err := NewTransaction(rawTransaction) + if err != nil { + t.Fatalf("NewTransaction() error = %v", err) + } + + // Race repeated public API reads against destroying the same owned value. + // Copy() is enough to prove the point because it reads handle.ptr before + // creating another owned Transaction. + ready := make(chan struct{}) + done := make(chan struct{}, 2) + + go func() { + // Warm up the copy loop so it is already active before Destroy(). + for i := 0; i < 50; i++ { + _ = tx.Copy() + runtime.Gosched() + } + close(ready) + // Keep copying after Destroy() starts to widen the race window. + for i := 0; i < 1000; i++ { + func() { + defer func() { + _ = recover() + }() + _ = tx.Copy() + }() + runtime.Gosched() + } + done <- struct{}{} + }() + + go func() { + <-ready + // Give the copy loop one more scheduling opportunity, then destroy. + runtime.Gosched() + tx.Destroy() + done <- struct{}{} + }() + + <-done + <-done +} diff --git a/kernel/transaction_spent_outputs.go b/kernel/transaction_spent_outputs.go index e14eec83..d86cf6bf 100644 --- a/kernel/transaction_spent_outputs.go +++ b/kernel/transaction_spent_outputs.go @@ -30,33 +30,60 @@ type TransactionSpentOutputs struct { func newTransactionSpentOutputs(ptr *C.btck_TransactionSpentOutputs, fromOwned bool) *TransactionSpentOutputs { h := newHandle(unsafe.Pointer(ptr), transactionSpentOutputsCFuncs{}, fromOwned) - return &TransactionSpentOutputs{handle: h, transactionSpentOutputsApi: transactionSpentOutputsApi{(*C.btck_TransactionSpentOutputs)(h.ptr)}} + return &TransactionSpentOutputs{ + handle: h, + transactionSpentOutputsApi: transactionSpentOutputsApi{ + ptr: func() *C.btck_TransactionSpentOutputs { + return (*C.btck_TransactionSpentOutputs)(h.ptr) + }, + }, + } } type TransactionSpentOutputsView struct { transactionSpentOutputsApi - ptr *C.btck_TransactionSpentOutputs } func newTransactionSpentOutputsView(ptr *C.btck_TransactionSpentOutputs) *TransactionSpentOutputsView { return &TransactionSpentOutputsView{ - transactionSpentOutputsApi: transactionSpentOutputsApi{ptr}, - ptr: ptr, + transactionSpentOutputsApi: transactionSpentOutputsApi{ + ptr: func() *C.btck_TransactionSpentOutputs { + return ptr + }, + }, } } type transactionSpentOutputsApi struct { - ptr *C.btck_TransactionSpentOutputs + ptr func() *C.btck_TransactionSpentOutputs +} + +func (t *transactionSpentOutputsApi) cPtr() *C.btck_TransactionSpentOutputs { + return t.ptr() } +// TransactionSpentOutputsLike is implemented by *TransactionSpentOutputs and *TransactionSpentOutputsView. +type TransactionSpentOutputsLike interface { + cPtr() *C.btck_TransactionSpentOutputs + Copy() *TransactionSpentOutputs + Count() uint64 + GetCoinAt(uint64) (*CoinView, error) + Coins() iter.Seq[*CoinView] + CoinsRange(uint64, uint64) iter.Seq[*CoinView] + CoinsFrom(uint64) iter.Seq[*CoinView] +} + +var _ TransactionSpentOutputsLike = (*TransactionSpentOutputs)(nil) +var _ TransactionSpentOutputsLike = (*TransactionSpentOutputsView)(nil) + // Copy creates a copy of the transaction spent outputs. func (t *transactionSpentOutputsApi) Copy() *TransactionSpentOutputs { - return newTransactionSpentOutputs(t.ptr, false) + return newTransactionSpentOutputs(t.ptr(), false) } // Count returns the number of previous transaction outputs contained in the transaction spent outputs data. func (t *transactionSpentOutputsApi) Count() uint64 { - return uint64(C.btck_transaction_spent_outputs_count(t.ptr)) + return uint64(C.btck_transaction_spent_outputs_count(t.ptr())) } // GetCoinAt returns a coin contained in the transaction spent outputs at a @@ -71,7 +98,7 @@ func (t *transactionSpentOutputsApi) GetCoinAt(index uint64) (*CoinView, error) if index >= t.Count() { return nil, ErrKernelIndexOutOfBounds } - ptr := C.btck_transaction_spent_outputs_get_coin_at(t.ptr, C.size_t(index)) + ptr := C.btck_transaction_spent_outputs_get_coin_at(t.ptr(), C.size_t(index)) return newCoinView(check(ptr)), nil } diff --git a/kernel/txid.go b/kernel/txid.go index 16570d77..1189cc10 100644 --- a/kernel/txid.go +++ b/kernel/txid.go @@ -26,39 +26,64 @@ type Txid struct { func newTxid(ptr *C.btck_Txid, fromOwned bool) *Txid { h := newHandle(unsafe.Pointer(ptr), txidCFuncs{}, fromOwned) - return &Txid{handle: h, txidApi: txidApi{(*C.btck_Txid)(h.ptr)}} + return &Txid{ + handle: h, + txidApi: txidApi{ + ptr: func() *C.btck_Txid { + return (*C.btck_Txid)(h.ptr) + }, + }, + } } type TxidView struct { txidApi - ptr *C.btck_Txid } func newTxidView(ptr *C.btck_Txid) *TxidView { return &TxidView{ - txidApi: txidApi{ptr}, - ptr: ptr, + txidApi: txidApi{ + ptr: func() *C.btck_Txid { + return ptr + }, + }, } } type txidApi struct { - ptr *C.btck_Txid + ptr func() *C.btck_Txid +} + +func (t *txidApi) cPtr() *C.btck_Txid { + return t.ptr() } +// TxidLike is implemented by *Txid and *TxidView. +type TxidLike interface { + cPtr() *C.btck_Txid + Copy() *Txid + Equals(TxidLike) bool + Bytes() [32]byte + String() string +} + +var _ TxidLike = (*Txid)(nil) +var _ TxidLike = (*TxidView)(nil) + // Copy creates a copy of the txid. func (t *txidApi) Copy() *Txid { - return newTxid(t.ptr, false) + return newTxid(t.ptr(), false) } // Equals checks if two txids are equal. -func (t *txidApi) Equals(other *Txid) bool { - return C.btck_txid_equals(t.ptr, other.txidApi.ptr) != 0 +func (t *txidApi) Equals(other TxidLike) bool { + return C.btck_txid_equals(t.ptr(), other.cPtr()) != 0 } // Bytes returns the 32-byte representation of the txid. func (t *txidApi) Bytes() [32]byte { var output [32]C.uchar - C.btck_txid_to_bytes(t.ptr, &output[0]) + C.btck_txid_to_bytes(t.ptr(), &output[0]) return *(*[32]byte)(unsafe.Pointer(&output[0])) } diff --git a/kernel/txid_test.go b/kernel/txid_test.go index 990aa760..2c5ea9f9 100644 --- a/kernel/txid_test.go +++ b/kernel/txid_test.go @@ -39,6 +39,9 @@ func TestTxid(t *testing.T) { if !txid.Equals(copiedTxid) { t.Error("txid.Equals(copiedTxid) = false, want true") } + if !copiedTxid.Equals(txid) { + t.Error("copiedTxid.Equals(txid) = false, want true") + } // Test String() expected := hex.EncodeToString(ReverseBytes(txidBytes[:]))