Skip to content

Commit c39f277

Browse files
committed
Add VerifyScript function
1 parent 2a5ff0e commit c39f277

File tree

5 files changed

+252
-16
lines changed

5 files changed

+252
-16
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A Go wrapper for Bitcoin Core's [`libbitcoinkernel`](https://github.com/bitcoin/
44

55
## ⚠️ Work in Progress
66

7-
This library is currently under active development. Check [TODO.md](./TODO.md) for current status and missing wrappers.
7+
This library is currently under active development. Check [TODO.md](./TODO.md) for missing wrappers.
88

99
## Overview
1010

TODO.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22

33
This document lists the remaining C API functions and data structures from [`bitcoinkernel.h`](./depend/bitcoin/src/kernel/bitcoinkernel.h) that haven't been wrapped yet in the Go kernel package.
44

5-
## Missing Data Structures
6-
7-
- **`kernel_BlockPointer`** - Non-owned block pointers (from callbacks)
8-
9-
## Missing Functions by Category
10-
11-
### Script Operations
12-
- [ ] `kernel_verify_script()` - **Script verification (IMPORTANT!)**
13-
145
### Block Operations (Additional)
156
- [ ] `kernel_block_pointer_get_hash()` - Get hash from block pointer
167
- [ ] `kernel_copy_block_pointer_data()` - Copy data from block pointer

kernel/errors.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,20 @@ var (
4141
ErrKernelChainParametersCreate = &KernelError{Operation: "kernel_chain_parameters_create"}
4242
ErrKernelContextOptionsCreate = &KernelError{Operation: "kernel_context_options_create"}
4343

44-
ErrInvalidChainType = errors.New("invalid chain type")
45-
ErrInvalidLogLevel = errors.New("invalid log level")
46-
ErrInvalidLogCategory = errors.New("invalid log category")
47-
ErrEmptyBlockData = errors.New("empty block data")
48-
ErrEmptyScriptPubkeyData = errors.New("empty script pubkey data")
49-
ErrEmptyTransactionData = errors.New("empty transaction data")
44+
ErrScriptVerify = &KernelError{Operation: "kernel_verify_script"}
45+
ErrScriptVerifyTxInputIndex = &KernelError{Operation: "kernel_verify_script", Detail: "the provided input index is out of range of the actual number of inputs of the transaction"}
46+
ErrScriptVerifyInvalidFlags = &KernelError{Operation: "kernel_verify_script", Detail: "the provided bitfield for the flags was invalid"}
47+
ErrScriptVerifyInvalidFlagsCombination = &KernelError{Operation: "kernel_verify_script", Detail: "the flags were combined in an invalid way"}
48+
ErrScriptVerifySpentOutputsRequired = &KernelError{Operation: "kernel_verify_script", Detail: "the taproot flag was set, so valid spent_outputs have to be provided"}
49+
ErrScriptVerifySpentOutputsMismatch = &KernelError{Operation: "kernel_verify_script", Detail: "the number of spent outputs does not match the number of inputs of the transaction"}
50+
51+
ErrInvalidChainType = errors.New("invalid chain type")
52+
ErrInvalidLogLevel = errors.New("invalid log level")
53+
ErrInvalidLogCategory = errors.New("invalid log category")
54+
ErrInvalidScriptVerifyStatus = errors.New("invalid script verify status")
55+
ErrEmptyBlockData = errors.New("empty block data")
56+
ErrEmptyScriptPubkeyData = errors.New("empty script pubkey data")
57+
ErrEmptyTransactionData = errors.New("empty transaction data")
5058
)
5159

5260
// UninitializedError is returned when an operation is attempted on a
@@ -65,8 +73,12 @@ func (e *UninitializedError) Error() string {
6573
type KernelError struct {
6674
// Operation describes the C-level action that failed
6775
Operation string
76+
Detail string
6877
}
6978

7079
func (e *KernelError) Error() string {
80+
if e.Detail != "" {
81+
return fmt.Sprintf("kernel error during '%s': %s", e.Operation, e.Detail)
82+
}
7183
return fmt.Sprintf("kernel error during '%s'", e.Operation)
7284
}

kernel/script.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package kernel
2+
3+
/*
4+
#include "kernel/bitcoinkernel.h"
5+
*/
6+
import "C"
7+
import (
8+
"fmt"
9+
)
10+
11+
// VerifyScript verifies a script pubkey against a transaction input
12+
func VerifyScript(scriptPubkey *ScriptPubkey, amount int64, txTo *Transaction,
13+
spentOutputs []*TransactionOutput, inputIndex uint, flags ScriptFlags) error {
14+
if err := validateReady(scriptPubkey); err != nil {
15+
return err
16+
}
17+
if err := validateReady(txTo); err != nil {
18+
return err
19+
}
20+
21+
var cSpentOutputs **C.kernel_TransactionOutput
22+
spentOutputsLen := C.size_t(len(spentOutputs))
23+
if len(spentOutputs) > 0 {
24+
for i, output := range spentOutputs {
25+
if err := validateReady(output); err != nil {
26+
return fmt.Errorf("invalid transaction output at index %d: %w", i, err)
27+
}
28+
}
29+
cPtrs := make([]*C.kernel_TransactionOutput, len(spentOutputs))
30+
for i, output := range spentOutputs {
31+
cPtrs[i] = output.ptr
32+
}
33+
cSpentOutputs = &cPtrs[0]
34+
}
35+
36+
var cStatus C.kernel_ScriptVerifyStatus
37+
success := C.kernel_verify_script(
38+
scriptPubkey.ptr,
39+
C.int64_t(amount),
40+
txTo.ptr,
41+
cSpentOutputs,
42+
spentOutputsLen,
43+
C.uint(inputIndex),
44+
flags.c(),
45+
&cStatus,
46+
)
47+
if !success {
48+
status := scriptVerifyStatusFromC(cStatus)
49+
return status.err()
50+
}
51+
return nil
52+
}
53+
54+
// ScriptFlags represents script verification flags
55+
type ScriptFlags uint
56+
57+
const (
58+
ScriptFlagsVerifyNone = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_NONE)
59+
ScriptFlagsVerifyP2SH = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_P2SH)
60+
ScriptFlagsVerifyDERSig = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_DERSIG)
61+
ScriptFlagsVerifyNullDummy = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_NULLDUMMY)
62+
ScriptFlagsVerifyCheckLockTimeVerify = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_CHECKLOCKTIMEVERIFY)
63+
ScriptFlagsVerifyCheckSequenceVerify = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_CHECKSEQUENCEVERIFY)
64+
ScriptFlagsVerifyWitness = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_WITNESS)
65+
ScriptFlagsVerifyTaproot = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_TAPROOT)
66+
ScriptFlagsVerifyAll = ScriptFlags(C.kernel_SCRIPT_FLAGS_VERIFY_ALL)
67+
)
68+
69+
func (s ScriptFlags) c() C.uint {
70+
return C.uint(s)
71+
}
72+
73+
// ScriptVerifyStatus represents the status of script verification
74+
type ScriptVerifyStatus int
75+
76+
const (
77+
ScriptVerifyOK ScriptVerifyStatus = iota
78+
ScriptVerifyErrorTxInputIndex
79+
ScriptVerifyErrorInvalidFlags
80+
ScriptVerifyErrorInvalidFlagsCombination
81+
ScriptVerifyErrorSpentOutputsRequired
82+
ScriptVerifyErrorSpentOutputsMismatch
83+
)
84+
85+
func (s ScriptVerifyStatus) err() error {
86+
switch s {
87+
case ScriptVerifyErrorTxInputIndex:
88+
return ErrScriptVerifyTxInputIndex
89+
case ScriptVerifyErrorInvalidFlags:
90+
return ErrScriptVerifyInvalidFlags
91+
case ScriptVerifyErrorInvalidFlagsCombination:
92+
return ErrScriptVerifyInvalidFlagsCombination
93+
case ScriptVerifyErrorSpentOutputsRequired:
94+
return ErrScriptVerifySpentOutputsRequired
95+
case ScriptVerifyErrorSpentOutputsMismatch:
96+
return ErrScriptVerifySpentOutputsMismatch
97+
default:
98+
return ErrScriptVerify
99+
}
100+
}
101+
102+
func scriptVerifyStatusFromC(status C.kernel_ScriptVerifyStatus) ScriptVerifyStatus {
103+
s := ScriptVerifyStatus(status)
104+
if s < ScriptVerifyOK || s > ScriptVerifyErrorSpentOutputsMismatch {
105+
panic(ErrInvalidScriptVerifyStatus)
106+
}
107+
return s
108+
}

kernel/script_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package kernel
2+
3+
import (
4+
"encoding/hex"
5+
"errors"
6+
"testing"
7+
)
8+
9+
func TestValidScripts(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
scriptPubkeyHex string
13+
amount int64
14+
txToHex string
15+
inputIndex uint
16+
description string
17+
}{
18+
{
19+
name: "old_style_transaction",
20+
scriptPubkeyHex: "76a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988ac",
21+
amount: 0,
22+
txToHex: "02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700",
23+
inputIndex: 0,
24+
description: "a random old-style transaction from the blockchain",
25+
},
26+
{
27+
name: "segwit_p2sh_transaction",
28+
scriptPubkeyHex: "a91434c06f8c87e355e123bdc6dda4ffabc64b6989ef87",
29+
amount: 1900000,
30+
txToHex: "01000000000101d9fd94d0ff0026d307c994d0003180a5f248146efb6371d040c5973f5f66d9df0400000017160014b31b31a6cb654cfab3c50567bcf124f48a0beaecffffffff012cbd1c000000000017a914233b74bf0823fa58bbbd26dfc3bb4ae715547167870247304402206f60569cac136c114a58aedd80f6fa1c51b49093e7af883e605c212bdafcd8d202200e91a55f408a021ad2631bc29a67bd6915b2d7e9ef0265627eabd7f7234455f6012103e7e802f50344303c76d12c089c8724c1b230e3b745693bbe16aad536293d15e300000000",
31+
inputIndex: 0,
32+
description: "a random segwit transaction from the blockchain using P2SH",
33+
},
34+
{
35+
name: "native_segwit_transaction",
36+
scriptPubkeyHex: "0020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d",
37+
amount: 18393430,
38+
txToHex: "010000000001011f97548fbbe7a0db7588a66e18d803d0089315aa7d4cc28360b6ec50ef36718a0100000000ffffffff02df1776000000000017a9146c002a686959067f4866b8fb493ad7970290ab728757d29f0000000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004730440220565d170eed95ff95027a69b313758450ba84a01224e1f7f130dda46e94d13f8602207bdd20e307f062594022f12ed5017bbf4a055a06aea91c10110a0e3bb23117fc014730440220647d2dc5b15f60bc37dc42618a370b2a1490293f9e5c8464f53ec4fe1dfe067302203598773895b4b16d37485cbe21b337f4e4b650739880098c592553add7dd4355016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000",
39+
inputIndex: 0,
40+
description: "a random segwit transaction from the blockchain using native segwit",
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
err := testVerifyScript(t, tt.scriptPubkeyHex, tt.amount, tt.txToHex, tt.inputIndex)
47+
if err != nil {
48+
t.Errorf("testVerifyScript() error = %v", err)
49+
}
50+
})
51+
}
52+
}
53+
54+
func TestInvalidScripts(t *testing.T) {
55+
tests := []struct {
56+
name string
57+
scriptPubkeyHex string
58+
amount int64
59+
txToHex string
60+
inputIndex uint
61+
description string
62+
}{
63+
{
64+
name: "old_style_wrong_signature",
65+
scriptPubkeyHex: "76a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988ff", // Modified last byte from 'ac' to 'ff'
66+
amount: 0,
67+
txToHex: "02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700",
68+
inputIndex: 0,
69+
description: "a random old-style transaction from the blockchain - WITH WRONG SIGNATURE for the address",
70+
},
71+
{
72+
name: "segwit_p2sh_wrong_amount",
73+
scriptPubkeyHex: "a91434c06f8c87e355e123bdc6dda4ffabc64b6989ef87",
74+
amount: 900000, // Wrong amount, should be 1900000
75+
txToHex: "01000000000101d9fd94d0ff0026d307c994d0003180a5f248146efb6371d040c5973f5f66d9df0400000017160014b31b31a6cb654cfab3c50567bcf124f48a0beaecffffffff012cbd1c000000000017a914233b74bf0823fa58bbbd26dfc3bb4ae715547167870247304402206f60569cac136c114a58aedd80f6fa1c51b49093e7af883e605c212bdafcd8d202200e91a55f408a021ad2631bc29a67bd6915b2d7e9ef0265627eabd7f7234455f6012103e7e802f50344303c76d12c089c8724c1b230e3b745693bbe16aad536293d15e300000000",
76+
inputIndex: 0,
77+
description: "a random segwit transaction from the blockchain using P2SH - WITH WRONG AMOUNT",
78+
},
79+
{
80+
name: "native_segwit_wrong_segwit",
81+
scriptPubkeyHex: "0020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58f", // Modified last byte from 'd' to 'f'
82+
amount: 18393430,
83+
txToHex: "010000000001011f97548fbbe7a0db7588a66e18d803d0089315aa7d4cc28360b6ec50ef36718a0100000000ffffffff02df1776000000000017a9146c002a686959067f4866b8fb493ad7970290ab728757d29f0000000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004730440220565d170eed95ff95027a69b313758450ba84a01224e1f7f130dda46e94d13f8602207bdd20e307f062594022f12ed5017bbf4a055a06aea91c10110a0e3bb23117fc014730440220647d2dc5b15f60bc37dc42618a370b2a1490293f9e5c8464f53ec4fe1dfe067302203598773895b4b16d37485cbe21b337f4e4b650739880098c592553add7dd4355016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000",
84+
inputIndex: 0,
85+
description: "a random segwit transaction from the blockchain using native segwit - WITH WRONG SEGWIT",
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
err := testVerifyScript(t, tt.scriptPubkeyHex, tt.amount, tt.txToHex, tt.inputIndex)
92+
if err == nil || !errors.Is(err, ErrScriptVerify) {
93+
t.Errorf("testVerifyScript() was expected to fail with error '%v'", ErrScriptVerify)
94+
}
95+
})
96+
}
97+
}
98+
99+
// testVerifyScript is a helper function that creates the necessary objects and calls VerifyScript
100+
func testVerifyScript(t *testing.T, scriptPubkeyHex string, amount int64, txToHex string, inputIndex uint) error {
101+
scriptPubkeyBytes, err := hex.DecodeString(scriptPubkeyHex)
102+
if err != nil {
103+
t.Fatalf("Failed to decode script pubkey hex: %v", err)
104+
}
105+
106+
scriptPubkey, err := NewScriptPubkeyFromRaw(scriptPubkeyBytes)
107+
if err != nil {
108+
t.Fatalf("Failed to create script pubkey: %v", err)
109+
}
110+
defer scriptPubkey.Destroy()
111+
112+
txToBytes, err := hex.DecodeString(txToHex)
113+
if err != nil {
114+
t.Fatalf("Failed to decode transaction hex: %v", err)
115+
}
116+
117+
txTo, err := NewTransactionFromRaw(txToBytes)
118+
if err != nil {
119+
t.Fatalf("Failed to create transaction: %v", err)
120+
}
121+
defer txTo.Destroy()
122+
123+
flags := ScriptFlagsVerifyAll &^ ScriptFlagsVerifyTaproot
124+
return VerifyScript(scriptPubkey, amount, txTo, nil, inputIndex, flags)
125+
}

0 commit comments

Comments
 (0)