Skip to content

Commit 73cc1d4

Browse files
committed
Add NotificationCallbacks
1 parent c39f277 commit 73cc1d4

File tree

5 files changed

+241
-7
lines changed

5 files changed

+241
-7
lines changed

kernel/chainstate_manager_test.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import (
99
)
1010

1111
func TestChainstateManager(t *testing.T) {
12-
suite := SetupChainstateManagerTestSuite(t)
12+
suite := ChainstateManagerTestSuite{
13+
MaxBlockHeightToImport: 0, // load all blocks from data/regtest/block.txt
14+
NotificationCallbacks: nil, // no notification callbacks
15+
}
16+
suite.Setup(t)
1317

1418
t.Run("genesis validation", suite.TestGenesis)
1519
t.Run("tip validation", suite.TestTip)
@@ -107,11 +111,14 @@ func (s *ChainstateManagerTestSuite) TestBlockUndo(t *testing.T) {
107111
}
108112

109113
type ChainstateManagerTestSuite struct {
114+
MaxBlockHeightToImport int32 // leave zero to load all blocks
115+
NotificationCallbacks *NotificationCallbacks
116+
110117
Manager *ChainstateManager
111118
ImportedBlocksCount int32
112119
}
113120

114-
func SetupChainstateManagerTestSuite(t *testing.T) *ChainstateManagerTestSuite {
121+
func (s *ChainstateManagerTestSuite) Setup(t *testing.T) {
115122
tempDir, err := os.MkdirTemp("", "bitcoin_kernel_test")
116123
if err != nil {
117124
t.Fatalf("Failed to create temp dir: %v", err)
@@ -135,6 +142,13 @@ func SetupChainstateManagerTestSuite(t *testing.T) *ChainstateManagerTestSuite {
135142

136143
contextOpts.SetChainParams(chainParams)
137144

145+
if s.NotificationCallbacks != nil {
146+
err = contextOpts.SetNotifications(s.NotificationCallbacks)
147+
if err != nil {
148+
t.Fatalf("SetNotifications() error = %v", err)
149+
}
150+
}
151+
138152
ctx, err := NewContext(contextOpts)
139153
if err != nil {
140154
t.Fatalf("NewContext() error = %v", err)
@@ -183,6 +197,9 @@ func SetupChainstateManagerTestSuite(t *testing.T) *ChainstateManagerTestSuite {
183197
if line != "" {
184198
blockLines = append(blockLines, line)
185199
}
200+
if s.MaxBlockHeightToImport != 0 && len(blockLines) >= int(s.MaxBlockHeightToImport) {
201+
break
202+
}
186203
}
187204
if len(blockLines) == 0 {
188205
t.Fatal("No block data found in blocks.txt")
@@ -212,8 +229,6 @@ func SetupChainstateManagerTestSuite(t *testing.T) *ChainstateManagerTestSuite {
212229
}
213230
}
214231

215-
return &ChainstateManagerTestSuite{
216-
Manager: manager,
217-
ImportedBlocksCount: int32(len(blockLines)),
218-
}
232+
s.Manager = manager
233+
s.ImportedBlocksCount = int32(len(blockLines))
219234
}

kernel/context_options.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,47 @@ package kernel
22

33
/*
44
#include "kernel/bitcoinkernel.h"
5+
#include <stdlib.h>
6+
#include <stdint.h>
7+
8+
// Bridge functions: exported Go functions that C library can call
9+
// user_data contains the cgo.Handle ID as void* for callback identification
10+
extern void go_notify_block_tip_bridge(void* user_data, kernel_SynchronizationState state, kernel_BlockIndex* index, double verification_progress);
11+
extern void go_notify_header_tip_bridge(void* user_data, kernel_SynchronizationState state, int64_t height, int64_t timestamp, bool presync);
12+
extern void go_notify_progress_bridge(void* user_data, char* title, size_t title_len, int progress_percent, bool resume_possible);
13+
extern void go_notify_warning_set_bridge(void* user_data, kernel_Warning warning, char* message, size_t message_len);
14+
extern void go_notify_warning_unset_bridge(void* user_data, kernel_Warning warning);
15+
extern void go_notify_flush_error_bridge(void* user_data, char* message, size_t message_len);
16+
extern void go_notify_fatal_error_bridge(void* user_data, char* message, size_t message_len);
17+
18+
// Wrapper function: C helper to set notifications with Go callbacks
19+
// Converts Handle ID to void* and passes to C library
20+
static inline void set_notifications_wrapper(kernel_ContextOptions* opts, uintptr_t handle) {
21+
kernel_NotificationInterfaceCallbacks callbacks = {
22+
.user_data = (void*)handle,
23+
.block_tip = (kernel_NotifyBlockTip)go_notify_block_tip_bridge,
24+
.header_tip = (kernel_NotifyHeaderTip)go_notify_header_tip_bridge,
25+
.progress = (kernel_NotifyProgress)go_notify_progress_bridge,
26+
.warning_set = (kernel_NotifyWarningSet)go_notify_warning_set_bridge,
27+
.warning_unset = (kernel_NotifyWarningUnset)go_notify_warning_unset_bridge,
28+
.flush_error = (kernel_NotifyFlushError)go_notify_flush_error_bridge,
29+
.fatal_error = (kernel_NotifyFatalError)go_notify_fatal_error_bridge,
30+
};
31+
kernel_context_options_set_notifications(opts, callbacks);
32+
}
533
*/
634
import "C"
735
import (
836
"runtime"
37+
"runtime/cgo"
938
)
1039

1140
var _ cManagedResource = &ContextOptions{}
1241

1342
// ContextOptions wraps the C kernel_ContextOptions
1443
type ContextOptions struct {
15-
ptr *C.kernel_ContextOptions
44+
ptr *C.kernel_ContextOptions
45+
notificationHandle cgo.Handle // Prevents notification callbacks GC until Destroy() called
1646
}
1747

1848
func NewContextOptions() (*ContextOptions, error) {
@@ -37,11 +67,36 @@ func (opts *ContextOptions) SetChainParams(chainParams *ChainParameters) {
3767
C.kernel_context_options_set_chainparams(opts.ptr, chainParams.ptr)
3868
}
3969

70+
// SetNotifications sets the notification callbacks for these context options.
71+
// The context created with these options will be configured with these notifications.
72+
func (opts *ContextOptions) SetNotifications(callbacks *NotificationCallbacks) error {
73+
checkReady(opts)
74+
if callbacks == nil {
75+
return ErrNilNotificationCallbacks
76+
}
77+
78+
// Create a handle for the callbacks - this prevents garbage collection
79+
// and provides a stable ID that can be passed through C code safely
80+
handle := cgo.NewHandle(callbacks)
81+
82+
// Call the C wrapper function to set all notification callbacks
83+
C.set_notifications_wrapper(opts.ptr, C.uintptr_t(handle))
84+
85+
// Store the handle to prevent GC and allow cleanup
86+
opts.notificationHandle = handle
87+
return nil
88+
}
89+
4090
func (opts *ContextOptions) destroy() {
4191
if opts.ptr != nil {
4292
C.kernel_context_options_destroy(opts.ptr)
4393
opts.ptr = nil
4494
}
95+
if opts.notificationHandle != 0 {
96+
// Delete exposes notification callbacks to garbage collection
97+
opts.notificationHandle.Delete()
98+
opts.notificationHandle = 0
99+
}
45100
}
46101

47102
func (opts *ContextOptions) Destroy() {

kernel/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ var (
5555
ErrEmptyBlockData = errors.New("empty block data")
5656
ErrEmptyScriptPubkeyData = errors.New("empty script pubkey data")
5757
ErrEmptyTransactionData = errors.New("empty transaction data")
58+
ErrNilNotificationCallbacks = errors.New("nil notification callbacks")
5859
)
5960

6061
// UninitializedError is returned when an operation is attempted on a

kernel/notification_callbacks.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package kernel
2+
3+
/*
4+
#include "kernel/bitcoinkernel.h"
5+
*/
6+
import "C"
7+
import (
8+
"runtime/cgo"
9+
"unsafe"
10+
)
11+
12+
// NotificationCallbacks contains all the Go callback function types for notifications.
13+
type NotificationCallbacks struct {
14+
OnBlockTip func(state SynchronizationState, index *BlockIndex, progress float64)
15+
OnHeaderTip func(state SynchronizationState, height int64, timestamp int64, presync bool)
16+
OnProgress func(title string, percent int, resumable bool)
17+
OnWarningSet func(warning Warning, message string)
18+
OnWarningUnset func(warning Warning)
19+
OnFlushError func(message string)
20+
OnFatalError func(message string)
21+
}
22+
23+
//export go_notify_block_tip_bridge
24+
func go_notify_block_tip_bridge(user_data unsafe.Pointer, state C.kernel_SynchronizationState, index *C.kernel_BlockIndex, verification_progress C.double) {
25+
// Convert void* back to Handle - user_data contains Handle ID
26+
handle := cgo.Handle(user_data)
27+
// Retrieve original Go callback struct
28+
callbacks := handle.Value().(*NotificationCallbacks)
29+
30+
if callbacks.OnBlockTip != nil {
31+
goState := SynchronizationState(state)
32+
// Note: BlockIndex from notification is const and owned by kernel library
33+
// We create a wrapper but don't set finalizer since we don't own it
34+
goIndex := &BlockIndex{ptr: (*C.kernel_BlockIndex)(unsafe.Pointer(index))}
35+
callbacks.OnBlockTip(goState, goIndex, float64(verification_progress))
36+
}
37+
}
38+
39+
//export go_notify_header_tip_bridge
40+
func go_notify_header_tip_bridge(user_data unsafe.Pointer, state C.kernel_SynchronizationState, height C.int64_t, timestamp C.int64_t, presync C.bool) {
41+
handle := cgo.Handle(user_data)
42+
callbacks := handle.Value().(*NotificationCallbacks)
43+
44+
if callbacks.OnHeaderTip != nil {
45+
goState := SynchronizationState(state)
46+
callbacks.OnHeaderTip(goState, int64(height), int64(timestamp), bool(presync))
47+
}
48+
}
49+
50+
//export go_notify_progress_bridge
51+
func go_notify_progress_bridge(user_data unsafe.Pointer, title *C.char, title_len C.size_t, progress_percent C.int, resume_possible C.bool) {
52+
handle := cgo.Handle(user_data)
53+
callbacks := handle.Value().(*NotificationCallbacks)
54+
55+
if callbacks.OnProgress != nil {
56+
goTitle := C.GoStringN(title, C.int(title_len))
57+
callbacks.OnProgress(goTitle, int(progress_percent), bool(resume_possible))
58+
}
59+
}
60+
61+
//export go_notify_warning_set_bridge
62+
func go_notify_warning_set_bridge(user_data unsafe.Pointer, warning C.kernel_Warning, message *C.char, message_len C.size_t) {
63+
handle := cgo.Handle(user_data)
64+
callbacks := handle.Value().(*NotificationCallbacks)
65+
66+
if callbacks.OnWarningSet != nil {
67+
goWarning := Warning(warning)
68+
goMessage := C.GoStringN(message, C.int(message_len))
69+
callbacks.OnWarningSet(goWarning, goMessage)
70+
}
71+
}
72+
73+
//export go_notify_warning_unset_bridge
74+
func go_notify_warning_unset_bridge(user_data unsafe.Pointer, warning C.kernel_Warning) {
75+
handle := cgo.Handle(user_data)
76+
callbacks := handle.Value().(*NotificationCallbacks)
77+
78+
if callbacks.OnWarningUnset != nil {
79+
goWarning := Warning(warning)
80+
callbacks.OnWarningUnset(goWarning)
81+
}
82+
}
83+
84+
//export go_notify_flush_error_bridge
85+
func go_notify_flush_error_bridge(user_data unsafe.Pointer, message *C.char, message_len C.size_t) {
86+
handle := cgo.Handle(user_data)
87+
callbacks := handle.Value().(*NotificationCallbacks)
88+
89+
if callbacks.OnFlushError != nil {
90+
goMessage := C.GoStringN(message, C.int(message_len))
91+
callbacks.OnFlushError(goMessage)
92+
}
93+
}
94+
95+
//export go_notify_fatal_error_bridge
96+
func go_notify_fatal_error_bridge(user_data unsafe.Pointer, message *C.char, message_len C.size_t) {
97+
handle := cgo.Handle(user_data)
98+
callbacks := handle.Value().(*NotificationCallbacks)
99+
100+
if callbacks.OnFatalError != nil {
101+
goMessage := C.GoStringN(message, C.int(message_len))
102+
callbacks.OnFatalError(goMessage)
103+
}
104+
}
105+
106+
// SynchronizationState represents the current sync state passed to tip changed callbacks
107+
type SynchronizationState int
108+
109+
const (
110+
SyncStateInitReindex SynchronizationState = iota
111+
SyncStateInitDownload
112+
SyncStatePostInit
113+
)
114+
115+
// Warning represents possible warning types issued by validation
116+
type Warning int
117+
118+
const (
119+
WarningUnknownNewRulesActivated Warning = iota
120+
WarningLargeWorkInvalidChain
121+
)

kernel/notification_callbacks_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package kernel
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNotificationCallbacks(t *testing.T) {
8+
var blockTipCalled bool
9+
var headerTipCalled bool
10+
var lastBlockHeight int64
11+
var lastHeaderHeight int64
12+
13+
callbacks := &NotificationCallbacks{
14+
OnBlockTip: func(state SynchronizationState, index *BlockIndex, _ float64) {
15+
blockTipCalled = true
16+
lastBlockHeight = int64(index.Height())
17+
},
18+
OnHeaderTip: func(state SynchronizationState, height int64, timestamp int64, presync bool) {
19+
headerTipCalled = true
20+
lastHeaderHeight = height
21+
},
22+
}
23+
suite := ChainstateManagerTestSuite{
24+
MaxBlockHeightToImport: 5,
25+
NotificationCallbacks: callbacks,
26+
}
27+
suite.Setup(t)
28+
29+
if !blockTipCalled {
30+
t.Error("OnBlockTip callback was not called")
31+
}
32+
if lastBlockHeight != 5 {
33+
t.Errorf("Expected last block height 5, got %d", lastBlockHeight)
34+
}
35+
36+
if !headerTipCalled {
37+
t.Error("OnHeaderTip callback was not called")
38+
}
39+
if lastHeaderHeight != 5 {
40+
t.Errorf("Expected last header height 5, got %d", lastHeaderHeight)
41+
}
42+
}

0 commit comments

Comments
 (0)