diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6ef218 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea + diff --git a/bindings.go b/bindings.go new file mode 100644 index 0000000..5b5fc0f --- /dev/null +++ b/bindings.go @@ -0,0 +1,306 @@ +package silkworm_go + +// #cgo CFLAGS: -I${SRCDIR}/include +// #include "silkworm.h" +import "C" + +/* + +#include +#include + +static bool go_string_copy(_GoString_ s, char *dest, size_t size) { + size_t len = _GoStringLen(s); + if (len >= size) return false; + const char *src = _GoStringPtr(s); + strncpy(dest, src, len); + dest[len] = '\0'; + return true; +} + +*/ +import "C" + +import ( + "errors" + "fmt" + "math/big" + "runtime" + "unsafe" +) + +const ( + SILKWORM_OK = C.SILKWORM_OK + SILKWORM_INTERNAL_ERROR = C.SILKWORM_INTERNAL_ERROR + SILKWORM_UNKNOWN_ERROR = C.SILKWORM_UNKNOWN_ERROR + SILKWORM_INVALID_HANDLE = C.SILKWORM_INVALID_HANDLE + SILKWORM_INVALID_PATH = C.SILKWORM_INVALID_PATH + SILKWORM_INVALID_SNAPSHOT = C.SILKWORM_INVALID_SNAPSHOT + SILKWORM_INVALID_MDBX_TXN = C.SILKWORM_INVALID_MDBX_TXN + SILKWORM_INVALID_BLOCK_RANGE = C.SILKWORM_INVALID_BLOCK_RANGE + SILKWORM_BLOCK_NOT_FOUND = C.SILKWORM_BLOCK_NOT_FOUND + SILKWORM_UNKNOWN_CHAIN_ID = C.SILKWORM_UNKNOWN_CHAIN_ID + SILKWORM_MDBX_ERROR = C.SILKWORM_MDBX_ERROR + SILKWORM_INVALID_BLOCK = C.SILKWORM_INVALID_BLOCK + SILKWORM_DECODING_ERROR = C.SILKWORM_DECODING_ERROR + SILKWORM_TOO_MANY_INSTANCES = C.SILKWORM_TOO_MANY_INSTANCES + SILKWORM_INVALID_SETTINGS = C.SILKWORM_INVALID_SETTINGS + SILKWORM_TERMINATION_SIGNAL = C.SILKWORM_TERMINATION_SIGNAL + SILKWORM_SERVICE_ALREADY_STARTED = C.SILKWORM_SERVICE_ALREADY_STARTED +) + +// ErrInterrupted is the error returned by Silkworm APIs when stopped by any termination signal. +var ErrInterrupted = errors.New("interrupted") +var ErrInvalidBlock = errors.New("invalid block") + +type Silkworm struct { + handle C.SilkwormHandle +} + +func New(dataDirPath string) (*Silkworm, error) { + silkworm := &Silkworm{ + handle: nil, + } + + settings := &C.struct_SilkwormSettings{} + + if !C.go_string_copy(dataDirPath, &settings.data_dir_path[0], C.SILKWORM_PATH_SIZE) { + return nil, errors.New("silkworm.New failed to copy dataDirPath") + } + + status := C.silkworm_init(&silkworm.handle, settings) //nolint:gocritic + if status == SILKWORM_OK { + return silkworm, nil + } + return nil, fmt.Errorf("silkworm_init error %d", status) +} + +func (s *Silkworm) Close() error { + status := C.silkworm_fini(s.handle) + s.handle = nil + if status == SILKWORM_OK { + return nil + } + return fmt.Errorf("silkworm_fini error %d", status) +} + +func (s *Silkworm) AddSnapshot(snapshot *MappedChainSnapshot) error { + cHeadersSegmentFilePath := C.CString(snapshot.Headers.Segment.FilePath) + defer C.free(unsafe.Pointer(cHeadersSegmentFilePath)) + cHeadersIdxHeaderHashFilePath := C.CString(snapshot.Headers.IdxHeaderHash.FilePath) + defer C.free(unsafe.Pointer(cHeadersIdxHeaderHashFilePath)) + cHeadersSnapshot := C.struct_SilkwormHeadersSnapshot{ + segment: C.struct_SilkwormMemoryMappedFile{ + file_path: cHeadersSegmentFilePath, + memory_address: (*C.uchar)(snapshot.Headers.Segment.DataHandle), + memory_length: C.uint64_t(snapshot.Headers.Segment.Size), + }, + header_hash_index: C.struct_SilkwormMemoryMappedFile{ + file_path: cHeadersIdxHeaderHashFilePath, + memory_address: (*C.uchar)(snapshot.Headers.IdxHeaderHash.DataHandle), + memory_length: C.uint64_t(snapshot.Headers.IdxHeaderHash.Size), + }, + } + + cBodiesSegmentFilePath := C.CString(snapshot.Bodies.Segment.FilePath) + defer C.free(unsafe.Pointer(cBodiesSegmentFilePath)) + cBodiesIdxBodyNumberFilePath := C.CString(snapshot.Bodies.IdxBodyNumber.FilePath) + defer C.free(unsafe.Pointer(cBodiesIdxBodyNumberFilePath)) + cBodiesSnapshot := C.struct_SilkwormBodiesSnapshot{ + segment: C.struct_SilkwormMemoryMappedFile{ + file_path: cBodiesSegmentFilePath, + memory_address: (*C.uchar)(snapshot.Bodies.Segment.DataHandle), + memory_length: C.uint64_t(snapshot.Bodies.Segment.Size), + }, + block_num_index: C.struct_SilkwormMemoryMappedFile{ + file_path: cBodiesIdxBodyNumberFilePath, + memory_address: (*C.uchar)(snapshot.Bodies.IdxBodyNumber.DataHandle), + memory_length: C.uint64_t(snapshot.Bodies.IdxBodyNumber.Size), + }, + } + + cTxsSegmentFilePath := C.CString(snapshot.Txs.Segment.FilePath) + defer C.free(unsafe.Pointer(cTxsSegmentFilePath)) + cTxsIdxTxnHashFilePath := C.CString(snapshot.Txs.IdxTxnHash.FilePath) + defer C.free(unsafe.Pointer(cTxsIdxTxnHashFilePath)) + cTxsIdxTxnHash2BlockFilePath := C.CString(snapshot.Txs.IdxTxnHash2BlockNum.FilePath) + defer C.free(unsafe.Pointer(cTxsIdxTxnHash2BlockFilePath)) + cTxsSnapshot := C.struct_SilkwormTransactionsSnapshot{ + segment: C.struct_SilkwormMemoryMappedFile{ + file_path: cTxsSegmentFilePath, + memory_address: (*C.uchar)(snapshot.Txs.Segment.DataHandle), + memory_length: C.uint64_t(snapshot.Txs.Segment.Size), + }, + tx_hash_index: C.struct_SilkwormMemoryMappedFile{ + file_path: cTxsIdxTxnHashFilePath, + memory_address: (*C.uchar)(snapshot.Txs.IdxTxnHash.DataHandle), + memory_length: C.uint64_t(snapshot.Txs.IdxTxnHash.Size), + }, + tx_hash_2_block_index: C.struct_SilkwormMemoryMappedFile{ + file_path: cTxsIdxTxnHash2BlockFilePath, + memory_address: (*C.uchar)(snapshot.Txs.IdxTxnHash2BlockNum.DataHandle), + memory_length: C.uint64_t(snapshot.Txs.IdxTxnHash2BlockNum.Size), + }, + } + + cChainSnapshot := C.struct_SilkwormChainSnapshot{ + headers: cHeadersSnapshot, + bodies: cBodiesSnapshot, + transactions: cTxsSnapshot, + } + + status := C.silkworm_add_snapshot(s.handle, &cChainSnapshot) //nolint:gocritic + if status == SILKWORM_OK { + return nil + } + return fmt.Errorf("silkworm_add_snapshot error %d", status) +} + +func (s *Silkworm) StartRpcDaemon(dbEnvCHandle unsafe.Pointer) error { + cEnv := (*C.MDBX_env)(dbEnvCHandle) + status := C.silkworm_start_rpcdaemon(s.handle, cEnv) + // Handle successful execution + if status == SILKWORM_OK { + return nil + } + return fmt.Errorf("silkworm_start_rpcdaemon error %d", status) +} + +func (s *Silkworm) StopRpcDaemon() error { + status := C.silkworm_stop_rpcdaemon(s.handle) + // Handle successful execution + if status == SILKWORM_OK { + return nil + } + return fmt.Errorf("silkworm_stop_rpcdaemon error %d", status) +} + +type SentrySettings struct { + ClientId string + ApiPort int + Port int + Nat string + NetworkId uint64 + NodeKey []byte + StaticPeers []string + Bootnodes []string + NoDiscover bool + MaxPeers int +} + +func copyPeerURLs(list []string, cList *[C.SILKWORM_SENTRY_SETTINGS_PEERS_MAX][C.SILKWORM_SENTRY_SETTINGS_PEER_URL_SIZE]C.char) error { + listLen := len(list) + if listLen > C.SILKWORM_SENTRY_SETTINGS_PEERS_MAX { + return errors.New("copyPeerURLs: peers URL list has too many items") + } + // mark the list end with an empty string + if listLen < C.SILKWORM_SENTRY_SETTINGS_PEERS_MAX { + cList[listLen][0] = 0 + } + for i, url := range list { + if !C.go_string_copy(url, &cList[i][0], C.SILKWORM_SENTRY_SETTINGS_PEER_URL_SIZE) { + return fmt.Errorf("copyPeerURLs: failed to copy peer URL %d", i) + } + } + return nil +} + +func makeCSentrySettings(settings SentrySettings) (*C.struct_SilkwormSentrySettings, error) { + cSettings := &C.struct_SilkwormSentrySettings{ + api_port: C.uint16_t(settings.ApiPort), + port: C.uint16_t(settings.Port), + network_id: C.uint64_t(settings.NetworkId), + no_discover: C.bool(settings.NoDiscover), + max_peers: C.size_t(settings.MaxPeers), + } + if !C.go_string_copy(settings.ClientId, &cSettings.client_id[0], C.SILKWORM_SENTRY_SETTINGS_CLIENT_ID_SIZE) { + return nil, errors.New("makeCSentrySettings failed to copy ClientId") + } + if !C.go_string_copy(settings.Nat, &cSettings.nat[0], C.SILKWORM_SENTRY_SETTINGS_NAT_SIZE) { + return nil, errors.New("makeCSentrySettings failed to copy Nat") + } + if len(settings.NodeKey) == C.SILKWORM_SENTRY_SETTINGS_NODE_KEY_SIZE { + C.memcpy(unsafe.Pointer(&cSettings.node_key[0]), unsafe.Pointer(&settings.NodeKey[0]), C.SILKWORM_SENTRY_SETTINGS_NODE_KEY_SIZE) //nolint:gocritic + } else { + return nil, errors.New("makeCSentrySettings failed to copy NodeKey") + } + if err := copyPeerURLs(settings.StaticPeers, &cSettings.static_peers); err != nil { + return nil, fmt.Errorf("copyPeerURLs failed to copy StaticPeers: %w", err) + } + if err := copyPeerURLs(settings.Bootnodes, &cSettings.bootnodes); err != nil { + return nil, fmt.Errorf("copyPeerURLs failed to copy Bootnodes: %w", err) + } + return cSettings, nil +} + +func (s *Silkworm) SentryStart(settings SentrySettings) error { + cSettings, err := makeCSentrySettings(settings) + if err != nil { + return err + } + status := C.silkworm_sentry_start(s.handle, cSettings) + if status == SILKWORM_OK { + return nil + } + return fmt.Errorf("silkworm_sentry_start error %d", status) +} + +func (s *Silkworm) SentryStop() error { + status := C.silkworm_sentry_stop(s.handle) + if status == SILKWORM_OK { + return nil + } + return fmt.Errorf("silkworm_sentry_stop error %d", status) +} + +func (s *Silkworm) ExecuteBlocks( + txnCHandle unsafe.Pointer, + chainID *big.Int, + startBlock uint64, + maxBlock uint64, + batchSize uint64, + writeChangeSets, + writeReceipts, + writeCallTraces bool, +) (lastExecutedBlock uint64, err error) { + if runtime.GOOS == "darwin" { + return 0, errors.New("silkworm execution is incompatible with Go runtime on macOS due to stack size mismatch (see https://github.com/golang/go/issues/28024)") + } + + cTxn := (*C.MDBX_txn)(txnCHandle) + cChainId := C.uint64_t(chainID.Uint64()) + cStartBlock := C.uint64_t(startBlock) + cMaxBlock := C.uint64_t(maxBlock) + cBatchSize := C.uint64_t(batchSize) + cWriteChangeSets := C._Bool(writeChangeSets) + cWriteReceipts := C._Bool(writeReceipts) + cWriteCallTraces := C._Bool(writeCallTraces) + cLastExecutedBlock := C.uint64_t(startBlock - 1) + cMdbxErrorCode := C.int(0) + status := C.silkworm_execute_blocks( + s.handle, + cTxn, + cChainId, + cStartBlock, + cMaxBlock, + cBatchSize, + cWriteChangeSets, + cWriteReceipts, + cWriteCallTraces, + &cLastExecutedBlock, + &cMdbxErrorCode, + ) + lastExecutedBlock = uint64(cLastExecutedBlock) + // Handle successful execution + if status == SILKWORM_OK { + return lastExecutedBlock, nil + } + // Handle special errors + if status == SILKWORM_INVALID_BLOCK { + return lastExecutedBlock, ErrInvalidBlock + } + if status == SILKWORM_TERMINATION_SIGNAL { + return lastExecutedBlock, ErrInterrupted + } + return lastExecutedBlock, fmt.Errorf("silkworm_execute_blocks error %d, MDBX error %d", status, cMdbxErrorCode) +} diff --git a/bindings_test.go b/bindings_test.go new file mode 100644 index 0000000..db62712 --- /dev/null +++ b/bindings_test.go @@ -0,0 +1,16 @@ +package silkworm_go + +import ( + "testing" +) + +func TestInit(t *testing.T) { + silkworm, err := New(t.TempDir()) + if err != nil { + t.Error(err) + } + err = silkworm.Close() + if err != nil { + t.Error(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..206903f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/erigontech/silkworm-go + +go 1.20 diff --git a/link_linux_arm64.go b/link_linux_arm64.go new file mode 100644 index 0000000..73f3c45 --- /dev/null +++ b/link_linux_arm64.go @@ -0,0 +1,8 @@ +//go:build linux && arm64 + +package silkworm_go + +// #cgo LDFLAGS: -lsilkworm_capi +// #cgo LDFLAGS: -L${SRCDIR}/lib/linux_arm64 +// #cgo LDFLAGS: -Wl,-rpath ${SRCDIR}/lib/linux_arm64 +import "C" diff --git a/link_linux_x64.go b/link_linux_x64.go new file mode 100644 index 0000000..a1f2728 --- /dev/null +++ b/link_linux_x64.go @@ -0,0 +1,8 @@ +//go:build linux && amd64 + +package silkworm_go + +// #cgo LDFLAGS: -lsilkworm_capi +// #cgo LDFLAGS: -L${SRCDIR}/lib/linux_x64 +// #cgo LDFLAGS: -Wl,-rpath ${SRCDIR}/lib/linux_x64 +import "C" diff --git a/link_macos_arm64.go b/link_macos_arm64.go new file mode 100644 index 0000000..f4f7680 --- /dev/null +++ b/link_macos_arm64.go @@ -0,0 +1,9 @@ +//go:build darwin && arm64 + +package silkworm_go + +// #cgo LDFLAGS: -lsilkworm_capi +// #cgo LDFLAGS: -L${SRCDIR}/lib/macos_arm64 +// #cgo LDFLAGS: -Wl,-rpath ${SRCDIR}/lib/macos_arm64 +// #cgo LDFLAGS: -mmacosx-version-min=13.3 +import "C" diff --git a/snapshot_types.go b/snapshot_types.go new file mode 100644 index 0000000..f5e3ba4 --- /dev/null +++ b/snapshot_types.go @@ -0,0 +1,65 @@ +package silkworm_go + +import "unsafe" + +type MemoryMappedRegion struct { + FilePath string + DataHandle unsafe.Pointer + Size int64 +} + +type MappedHeaderSnapshot struct { + Segment *MemoryMappedRegion + IdxHeaderHash *MemoryMappedRegion +} + +type MappedBodySnapshot struct { + Segment *MemoryMappedRegion + IdxBodyNumber *MemoryMappedRegion +} + +type MappedTxnSnapshot struct { + Segment *MemoryMappedRegion + IdxTxnHash *MemoryMappedRegion + IdxTxnHash2BlockNum *MemoryMappedRegion +} + +type MappedChainSnapshot struct { + Headers *MappedHeaderSnapshot + Bodies *MappedBodySnapshot + Txs *MappedTxnSnapshot +} + +func NewMemoryMappedRegion(filePath string, dataHandle unsafe.Pointer, size int64) *MemoryMappedRegion { + region := &MemoryMappedRegion{ + FilePath: filePath, + DataHandle: dataHandle, + Size: size, + } + return region +} + +func NewMappedHeaderSnapshot(segment, idxHeaderHash *MemoryMappedRegion) *MappedHeaderSnapshot { + snapshot := &MappedHeaderSnapshot{ + Segment: segment, + IdxHeaderHash: idxHeaderHash, + } + return snapshot +} + +func NewMappedBodySnapshot(segment, idxBodyNumber *MemoryMappedRegion) *MappedBodySnapshot { + snapshot := &MappedBodySnapshot{ + Segment: segment, + IdxBodyNumber: idxBodyNumber, + } + return snapshot +} + +func NewMappedTxnSnapshot(segment, idxTxnHash, idxTxnHash2BlockNum *MemoryMappedRegion) *MappedTxnSnapshot { + snapshot := &MappedTxnSnapshot{ + Segment: segment, + IdxTxnHash: idxTxnHash, + IdxTxnHash2BlockNum: idxTxnHash2BlockNum, + } + return snapshot +}