diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml
new file mode 100644
index 0000000..cdf121e
--- /dev/null
+++ b/.github/workflows/image.yml
@@ -0,0 +1,46 @@
+name: Build and push OCI image to Docker Hub
+
+on:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ image:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: "./go.mod"
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: bsvb/alert-key
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9057c4b..73276ce 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,14 +23,14 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: 1.19
+ go-version: 1.21
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5.0.0
with:
distribution: goreleaser
version: latest
- args: release --rm-dist --debug
+ args: release --clean --debug
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#- name: Syndicate to GoDocs
- # run: make godocs
\ No newline at end of file
+ # run: make godocs
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 8566faa..f513a82 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -51,7 +51,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Cache code
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
~/go/pkg/mod # Module download cache
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 3505765..3d3f9b9 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -18,8 +18,40 @@ changelog:
# ---------------------------
# Builder
# ---------------------------
-build:
- skip: true
+builds:
+ - env:
+ - CGO_ENABLED=0 # Required for SQLite
+ - GO111MODULE=on
+ #- CGO_CFLAGS=-Wno-error=unused-command-line-argument
+ main: ./cmd/
+ binary: "alert_system"
+ goarch:
+ - amd64
+ - arm64
+ goos:
+ - darwin
+ - linux
+ goarm:
+ - 6
+ - 7
+ goamd64:
+ - v2
+ - v3
+ mod_timestamp: "{{ .CommitTimestamp }}"
+ ldflags:
+ - -s -w -X github.com/bitcoin-sv/{{ .ProjectName }}/cmd.Version={{ .Version }}
+
+# ---------------------------
+# Archives + Checksums
+# ---------------------------
+archives:
+ - wrap_in_directory: true
+ format: tar.gz
+ name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}_{{ .Version }}'
+
+checksum:
+ name_template: "{{ .ProjectName }}_checksums.txt"
+ algorithm: sha256
# ---------------------------
# GitHub Release
@@ -68,4 +100,4 @@ announce:
# Defaults to `{{ .GitURL }}/releases/tag/{{ .Tag }}`
# url_template: 'https://github.com/bitcoin-sv/{{ .ProjectName }}/releases/tag/{{ .Tag }}'
# Defaults to `{{ .ProjectName }} {{ .Tag }} is out!`
- title_template: '{{ .ProjectName }} {{ .Tag }} is out!'
+ title_template: '{{ .ProjectName }} {{ .Tag }} is out!'
\ No newline at end of file
diff --git a/.make/common.mk b/.make/common.mk
index d0d1fdf..5d566d3 100644
--- a/.make/common.mk
+++ b/.make/common.mk
@@ -50,7 +50,7 @@ install-releaser: ## Install the GoReleaser application
release:: ## Full production release (creates release in GitHub)
@echo "releasing..."
@test $(github_token)
- @export GITHUB_TOKEN=$(github_token) && goreleaser --rm-dist
+ @export GITHUB_TOKEN=$(github_token) && goreleaser --clean
.PHONY: release-test
release-test: ## Full production test release (everything except deploy)
diff --git a/Dockerfile b/Dockerfile
index 65c7a0f..3bda224 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,6 +15,6 @@ WORKDIR /
RUN mkdir /.bitcoin
RUN touch /.bitcoin/alert_system_private_key
COPY --from=builder /opt/app-root/src/alert-system .
-COPY example_settings_local.conf settings_local.conf
USER 65534:65534
+ENV ALERT_SYSTEM_ENVIRONMENT=local
CMD ["/alert-system"]
diff --git a/LICENSE b/LICENSE
index 7a09f69..ea13950 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,28 +1,26 @@
-Open BSV License version 4
+Open BSV License Version 5 – granted by BSV Association, authorised licensee
-Copyright (c) 2023 BSV Blockchain Association ("BA")
+For the purposes of this license, the definitions below have the following meanings:
+“Bitcoin Protocol” means the protocol implementation, cryptographic rules, network protocols, and consensus mechanisms in the Bitcoin White Paper as described here https://protocol.bsvblockchain.org.
+“Bitcoin White Paper” means the paper entitled ‘Bitcoin: A Peer-to-Peer Electronic Cash System’ published by ‘Satoshi Nakamoto’ in October 2008.
+“BSV Blockchains” means:
+ (a) the Bitcoin blockchain containing block height #556767 with the hash "000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b" and that contains the longest honest persistent chain of blocks which has been produced in a manner which is consistent with the rules set forth in the Network Access Rules; and
+ (b) the test blockchains that contain the longest honest persistent chains of blocks which has been produced in a manner which is consistent with the rules set forth in the Network Access Rules.
+“Network Access Rules” or “Rules” means the set of rules regulating the relationship between BSV Association and the nodes on BSV based on the Bitcoin Protocol rules and those set out in the Bitcoin White Paper, and available here https://bsvblockchain.org/network-access-rules.
+“Software” means the software the subject of this licence, including any/all intellectual property rights therein and associated documentation files.
+BSV Association grants permission, free of charge and on a non-exclusive and revocable basis, to any person obtaining a copy of the Software to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+1 - The text “© BSV Association,” and this license shall be included in all copies or substantial portions of the Software.
+2 - The Software, and any software that is derived from the Software or parts thereof, must only be used on the BSV Blockchains.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES REGARDING ENTITLEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS THEREOF BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+Version 0.1.1 of the Bitcoin SV software, and prior versions of software upon which it was based, were licensed under the MIT License, which is included below.
+The MIT License (MIT)
+Copyright (c) 2009-2010 Satoshi Nakamoto
+Copyright (c) 2009-2015 Bitcoin Developers
+Copyright (c) 2009-2017 The Bitcoin Core developers
+Copyright (c) 2017 The Bitcoin ABC developers
+Copyright (c) 2018 Bitcoin Association for BSV
+Copyright (c) 2023 BSV Association
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-1 - The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-2 - The Software, and any software that is derived from the Software or parts thereof,
-can only be used on the Bitcoin SV blockchains. The Bitcoin SV blockchains are defined,
-for purposes of this license, as the Bitcoin blockchain containing block height #556767
-with the hash "000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b" and
-that contains the longest persistent chain of blocks accepted by this Software and which are valid under the rules set forth in the Bitcoin white paper (S. Nakamoto, Bitcoin: A Peer-to-Peer Electronic Cash System, posted online October 2008) and the latest version of this Software available in this repository or another repository designated by BA,
-as well as the test blockchains that contain the longest persistent chains of blocks accepted by this Software and which are valid under the rules set forth in the Bitcoin whitepaper (S. Nakamoto, Bitcoin: A Peer-to-Peer Electronic Cash System, posted online October 2008) and the latest version of this Software available in this repository, or another repository designated by BA.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index 22a6fca..bf84d47 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,13 @@ Configuration files can be found in the [config](app/config/envs) directory.
+## Container Environment
+**Note:** to use a custom settings file, it needs to be mounted and the appropriate environment variables set. Running it as below will run an ephemeral database but the container should sync up from the peers on the network on startup.
+### podman
+```
+$ podman run -u root -e P2P_PORT=9908 -e P2P_IP=0.0.0.0 --expose 9908 docker.io/galtbv/alert-system:0.0.2
+```
+
## Documentation
View the generated [documentation](https://pkg.go.dev/github.com/bitcoin-sv/alert-system)
diff --git a/app/api/base/alert.go b/app/api/base/alert.go
new file mode 100644
index 0000000..46ded72
--- /dev/null
+++ b/app/api/base/alert.go
@@ -0,0 +1,77 @@
+package base
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strconv"
+
+ "github.com/bitcoin-sv/alert-system/app/webhook"
+
+ "github.com/bitcoin-sv/alert-system/app"
+ "github.com/bitcoin-sv/alert-system/app/models"
+ "github.com/bitcoin-sv/alert-system/app/models/model"
+ "github.com/julienschmidt/httprouter"
+ apirouter "github.com/mrz1836/go-api-router"
+)
+
+// alerts will return the saved
+func (a *Action) alert(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
+ // Read params
+ params := apirouter.GetParams(req)
+ if params == nil {
+ apiError := apirouter.ErrorFromRequest(req, "parameters is nil", "no parameters specified", http.StatusBadRequest, http.StatusBadRequest, "")
+ apirouter.ReturnResponse(w, req, apiError.Code, apiError)
+ return
+ }
+ idStr := params.GetString("sequence")
+ if idStr == "" {
+ apiError := apirouter.ErrorFromRequest(req, "missing sequence param", "missing sequence param", http.StatusBadRequest, http.StatusBadRequest, "")
+ apirouter.ReturnResponse(w, req, apiError.Code, apiError)
+ return
+ }
+ sequenceNumber, err := strconv.Atoi(idStr)
+ if err != nil {
+ apiError := apirouter.ErrorFromRequest(req, "sequence is invalid", "sequence is invalid", http.StatusBadRequest, http.StatusBadRequest, "")
+ apirouter.ReturnResponse(w, req, apiError.Code, apiError)
+ return
+ }
+
+ // Get alert
+ alertModel, err := models.GetAlertMessageBySequenceNumber(req.Context(), uint32(sequenceNumber), model.WithAllDependencies(a.Config))
+ if err != nil {
+ app.APIErrorResponse(w, req, http.StatusInternalServerError, err)
+ return
+ } else if alertModel == nil {
+ app.APIErrorResponse(w, req, http.StatusNotFound, errors.New("alert not found"))
+ return
+ }
+ err = alertModel.ReadRaw()
+ if err != nil {
+ app.APIErrorResponse(w, req, http.StatusInternalServerError, errors.New("alert faile"))
+ return
+ }
+ am := alertModel.ProcessAlertMessage()
+ if am == nil {
+ app.APIErrorResponse(w, req, http.StatusInternalServerError, errors.New("alert not valid type"))
+ return
+ }
+ err = am.Read(alertModel.GetRawMessage())
+ if err != nil {
+ app.APIErrorResponse(w, req, http.StatusInternalServerError, err)
+ return
+ }
+ p := webhook.Payload{
+ AlertType: alertModel.GetAlertType(),
+ Sequence: alertModel.SequenceNumber,
+ Raw: hex.EncodeToString(alertModel.GetRawData()),
+ Text: am.MessageString(),
+ }
+ // Return the response
+ _ = apirouter.ReturnJSONEncode(
+ w,
+ http.StatusOK,
+ json.NewEncoder(w),
+ p, []string{"sequence", "raw", "text", "alert_type"})
+}
diff --git a/app/api/base/alerts.go b/app/api/base/alerts.go
new file mode 100644
index 0000000..75259e8
--- /dev/null
+++ b/app/api/base/alerts.go
@@ -0,0 +1,43 @@
+package base
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+
+ "github.com/bitcoin-sv/alert-system/app"
+ "github.com/bitcoin-sv/alert-system/app/models"
+ "github.com/bitcoin-sv/alert-system/app/models/model"
+ "github.com/julienschmidt/httprouter"
+ apirouter "github.com/mrz1836/go-api-router"
+)
+
+// AlertsResponse is the response for the alerts endpoint
+type AlertsResponse struct {
+ Alerts []*models.AlertMessage `json:"alerts"`
+ LatestSequence uint32 `json:"latest_sequence"`
+}
+
+// alerts will return the saved
+func (a *Action) alerts(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
+
+ // Get all alerts
+ alerts, err := models.GetAllAlerts(req.Context(), nil, model.WithAllDependencies(a.Config))
+ if err != nil {
+ app.APIErrorResponse(w, req, http.StatusBadRequest, err)
+ return
+ } else if alerts == nil {
+ app.APIErrorResponse(w, req, http.StatusNotFound, errors.New("alert not found"))
+ return
+ }
+
+ // Return the response
+ _ = apirouter.ReturnJSONEncode(
+ w,
+ http.StatusOK,
+ json.NewEncoder(w),
+ AlertsResponse{
+ Alerts: alerts,
+ LatestSequence: alerts[len(alerts)-1].SequenceNumber,
+ }, []string{"alerts", "latest_sequence"})
+}
diff --git a/app/api/base/index.go b/app/api/base/index.go
index 8567e71..e916d61 100644
--- a/app/api/base/index.go
+++ b/app/api/base/index.go
@@ -1,15 +1,67 @@
package base
import (
+ "context"
+ "embed"
+ "html/template"
+ "log"
"net/http"
+ "github.com/bitcoin-sv/alert-system/app/models/model"
+
+ "github.com/bitcoin-sv/alert-system/app/models"
+
"github.com/julienschmidt/httprouter"
- apirouter "github.com/mrz1836/go-api-router"
)
+//go:embed ui/templates/*
+var content embed.FS
+
+// PageData contains the page data
+type PageData struct {
+ Alerts []*models.AlertMessage
+}
+
+func substr(s string, start, length int) string {
+ end := start + length
+ if start < 0 || start >= len(s) || end > len(s) {
+ return s
+ }
+ return s[start:end]
+}
+
// index is the default index route of the API for testing purposes: (Hello World)
-func index(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
- apirouter.ReturnResponse(
- w, req, http.StatusOK, "Bitcoin SV Alert System",
- )
+func (a *Action) index(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
+ htmlContent, err := content.ReadFile("ui/templates/index.tmpl")
+ if err != nil {
+ log.Print(err.Error())
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ ts, err := template.New("index").Funcs(template.FuncMap{"substr": substr}).Parse(string(htmlContent))
+ if err != nil {
+ log.Print(err.Error())
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ alerts, err := models.GetAllAlerts(context.Background(), nil, model.WithAllDependencies(a.Config))
+ if err != nil {
+ log.Print(err.Error())
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ data := PageData{
+ Alerts: alerts,
+ }
+
+ // Then we use the Execute() method on the template set to write the
+ // template content as the response body. The last parameter to Execute()
+ // represents any dynamic data that we want to pass in, which for now we'll
+ // leave as nil.
+ err = ts.Execute(w, data)
+ if err != nil {
+ log.Print(err.Error())
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ }
}
diff --git a/app/api/base/index_test.go b/app/api/base/index_test.go
deleted file mode 100644
index cfd9c74..0000000
--- a/app/api/base/index_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package base
-
-import (
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-// TestIndex will test the method index()
-func (ts *TestSuite) TestIndex() {
- ts.T().Run("test index", func(t *testing.T) {
- req := httptest.NewRequest(http.MethodGet, "/", nil)
- w := httptest.NewRecorder()
-
- // Fire the request
- index(w, req, nil)
- res := w.Result()
- defer func() {
- _ = res.Body.Close()
- }()
-
- // Test the body
- data, err := io.ReadAll(res.Body)
- require.NoError(t, err)
- require.Equal(t, "\"Bitcoin SV Alert System\"\n", string(data))
-
- // Check the result
- require.Equal(t, "200 OK", res.Status)
- require.Equal(t, http.StatusOK, res.StatusCode)
- })
-}
diff --git a/app/api/base/routes.go b/app/api/base/routes.go
index 5ba4901..a04cf6d 100644
--- a/app/api/base/routes.go
+++ b/app/api/base/routes.go
@@ -20,7 +20,7 @@ func RegisterRoutes(router *apirouter.Router, conf *config.Config) {
action := &Action{app.Action{Config: conf}}
// Set the main index page (navigating to slash or the root of the major version)
- router.HTTPRouter.GET("/", action.Request(router, index))
+ router.HTTPRouter.GET("/", action.Request(router, action.index))
// Options request (for CORs)
router.HTTPRouter.OPTIONS("/", router.SetCrossOriginHeaders)
@@ -36,4 +36,10 @@ func RegisterRoutes(router *apirouter.Router, conf *config.Config) {
// Set the health request
router.HTTPRouter.GET("/health", action.Request(router, action.health))
+
+ // Set the get alerts request
+ router.HTTPRouter.GET("/alerts", action.Request(router, action.alerts))
+
+ // Set the get alert request
+ router.HTTPRouter.GET("/alert/:sequence", action.Request(router, action.alert))
}
diff --git a/app/api/base/ui/templates/index.tmpl b/app/api/base/ui/templates/index.tmpl
new file mode 100644
index 0000000..f9e48f0
--- /dev/null
+++ b/app/api/base/ui/templates/index.tmpl
@@ -0,0 +1,115 @@
+
+
+
+
+ Alert System Status
+
+
+
+
+
+
+ Alerts
+ {{ if .Alerts }}
+
+
+
+ Sequence |
+ Created At |
+ Processed |
+ Raw Message |
+
+
+
+ {{ range .Alerts }}
+
+ {{ .SequenceNumber }} |
+ {{ .CreatedAt }} |
+ {{ .Processed }} |
+
+
+ {{ if gt (len .Raw) 50 }}
+ Raw Hex
+ {{ .Raw }}
+ Expand
+ {{ else }}
+ {{ .Raw }}
+ {{ end }}
+
+ |
+
+ {{ end }}
+
+
+ {{ else }}
+ There's nothing to see here yet!
+ {{ end }}
+
+
+
diff --git a/app/config/config.go b/app/config/config.go
index 6fe0a9c..60e6cc7 100644
--- a/app/config/config.go
+++ b/app/config/config.go
@@ -19,7 +19,10 @@ const (
EnvironmentLocal = "local" // Environment for local development
EnvironmentPrefix = "alert_system" // Prefix for all environment variables
EnvironmentProduction = "production" // Environment for production
+ EnvironmentMainnet = "mainnet" // Environment for mainnet (same as production)
EnvironmentTest = "test" // Environment for testing
+ EnvironmentTestnet = "testnet" // Environment for testnet
+ EnvironmentStn = "stn" // Environment for STN testing
)
// Local variables for configuration
@@ -27,18 +30,24 @@ var (
environments = []interface{}{
EnvironmentLocal,
EnvironmentProduction,
+ EnvironmentMainnet,
EnvironmentTest,
+ EnvironmentTestnet,
+ EnvironmentStn,
}
)
// Application configuration constants
var (
- ApplicationName = "alert_system" // Application name used in places where we need an application name space
- DatabasePrefix = "alert_system" // Default database prefix
- DefaultAlertSystemProtocolID = "/bitcoin/alert-system/1.0.1" // Default alert system protocol for libp2p syncing
- DefaultServerShutdown = 5 * time.Second // Default server shutdown delay time (to finish any requests or internal processes)
- LocalPrivateKeyDefault = "alert_system_private_key" // Default local private key
- LocalPrivateKeyDirectory = ".bitcoin" // Default local private key directory
+ ApplicationName = "alert_system" // Application name used in places where we need an application name space
+ DatabasePrefix = "alert_system" // Default database prefix
+ DefaultAlertSystemProtocolID = "/bitcoin/alert-system/0.0.1" // Default alert system protocol for libp2p syncing
+ DefaultTopicName = "alert_system" // Default alert system topic name for libp2p subscription
+ DefaultServerShutdown = 5 * time.Second // Default server shutdown delay time (to finish any requests or internal processes)
+ DefaultPeerDiscoveryInterval = 10 * time.Minute // Default peer discovery refresh interval
+ DefaultAlertProcessingInterval = 5 * time.Minute // Default alert processing retry interval
+ LocalPrivateKeyDefault = "alert_system_private_key" // Default local private key
+ LocalPrivateKeyDirectory = ".bitcoin" // Default local private key directory
)
// The global configuration settings
@@ -46,13 +55,15 @@ type (
// Config is the global configuration settings
Config struct {
- AlertWebhookURL string `json:"alert_webhook_url" mapstructure:"alert_webhook_url"` // AlertWebhookURL is the URL for the alert webhook
- Datastore DatastoreConfig `json:"datastore" mapstructure:"datastore"` // Datastore's configuration
- P2P P2PConfig `json:"p2p" mapstructure:"p2p"` // P2P is the configuration for the P2P server
- RPCConnections []RPCConfig `json:"rpc_connections" mapstructure:"rpc_connections"` // RPCConnections is a list of RPC connections
- RequestLogging bool `json:"request_logging" mapstructure:"request_logging"` // Toggle for verbose request logging (API requests)
- Services Services `json:"-" mapstructure:"services"` // Services is the global services
- WebServer WebServerConfig `json:"web_server" mapstructure:"web_server"` // WebServer is the configuration for the web HTTP Server
+ AlertWebhookURL string `json:"alert_webhook_url" mapstructure:"alert_webhook_url"` // AlertWebhookURL is the URL for the alert webhook
+ Datastore DatastoreConfig `json:"datastore" mapstructure:"datastore"` // Datastore's configuration
+ BitcoinConfigPath string `json:"bitcoin_config_path" mapstructure:"bitcoin_config_path"` // BitcoinConfigPath is the path to the bitcoin.conf file
+ P2P P2PConfig `json:"p2p" mapstructure:"p2p"` // P2P is the configuration for the P2P server
+ RPCConnections []RPCConfig `json:"rpc_connections" mapstructure:"rpc_connections"` // RPCConnections is a list of RPC connections
+ RequestLogging bool `json:"request_logging" mapstructure:"request_logging"` // Toggle for verbose request logging (API requests)
+ Services Services `json:"-" mapstructure:"services"` // Services is the global services
+ WebServer WebServerConfig `json:"web_server" mapstructure:"web_server"` // WebServer is the configuration for the web HTTP Server
+ AlertProcessingInterval time.Duration `json:"alert_processing_interval" mapstructure:"alert_processing_interval"` // AlertProcessingInterval is the interval in which the system will go through all of the saved alerts and attempt to retry any unprocessed alerts
}
// DatastoreConfig is the configuration for the datastore
@@ -61,7 +72,7 @@ type (
Debug bool `json:"debug" mapstructure:"debug"` // True for sql statements
Engine datastore.Engine `json:"engine" mapstructure:"engine"` // MySQL, Postgres, SQLite
Password string `json:"password" mapstructure:"password"` // Used for MySQL or Postgresql
- SQLite *datastore.SQLiteConfig `json:"sqlite" mapstructure:"sq_lite"` // Configuration for SQLite
+ SQLite *datastore.SQLiteConfig `json:"sqlite" mapstructure:"sqlite"` // Configuration for SQLite
SQLRead *datastore.SQLConfig `json:"sql_read" mapstructure:"sql_read"` // Configuration for MySQL or Postgres
SQLWrite *datastore.SQLConfig `json:"sql_write" mapstructure:"sql_write"` // Configuration for MySQL or Postgres
TablePrefix string `json:"table_prefix" mapstructure:"table_prefix"` // pre_table_name (pre)
@@ -81,11 +92,13 @@ type (
// P2PConfig is the configuration for the P2P server and connection
P2PConfig struct {
- AlertSystemProtocolID string `json:"alert_system_protocol_id" mapstructure:"alert_system_protocol_id"` // AlertSystemProtocolID is the protocol ID to use on the libp2p network for alert system communication
- BootstrapPeer string `json:"bootstrap_peer" mapstructure:"bootstrap_peer"` // BootstrapPeer is the bootstrap peer for the libp2p network
- IP string `json:"ip" mapstructure:"ip"` // IP is the IP address for the P2P server
- Port string `json:"port" mapstructure:"port"` // Port is the port for the P2P server
- PrivateKeyPath string `json:"private_key_path" mapstructure:"private_key_path"` // PrivateKeyPath is the path to the private key
+ AlertSystemProtocolID string `json:"alert_system_protocol_id" mapstructure:"alert_system_protocol_id"` // AlertSystemProtocolID is the protocol ID to use on the libp2p network for alert system communication
+ BootstrapPeer string `json:"bootstrap_peer" mapstructure:"bootstrap_peer"` // BootstrapPeer is the bootstrap peer for the libp2p network
+ IP string `json:"ip" mapstructure:"ip"` // IP is the IP address for the P2P server
+ Port string `json:"port" mapstructure:"port"` // Port is the port for the P2P server
+ PrivateKeyPath string `json:"private_key_path" mapstructure:"private_key_path"` // PrivateKeyPath is the path to the private key
+ TopicName string `json:"topic_name" mapstructure:"topic_name"` // TopicName is the name of the topic to subscribe to
+ PeerDiscoveryInterval time.Duration `json:"peer_discovery_interval" mapstructure:"peer_discovery_interval"` // PeerDiscoveryInterval is the interval in which we will refresh the peer table and check peers for missing messages
}
// RPCConfig is the configuration for the RPC client
diff --git a/app/config/envs/local.json b/app/config/envs/local.json
index c47e939..9a88751 100644
--- a/app/config/envs/local.json
+++ b/app/config/envs/local.json
@@ -1,6 +1,9 @@
{
"alert_webhook_url": "",
+ "bitcoin_config_path": "",
"request_logging": true,
+ "bitcoin_config_path": "/home/galt/.bitcoin/bitcoin.conf",
+ "alert_processing_interval": "5m",
"web_server": {
"idle_timeout": "60s",
"port": "3000",
@@ -54,14 +57,16 @@
"p2p": {
"ip": "0.0.0.0",
"port": "9906",
- "alert_system_protocol_id": "/bitcoin/alert-system/1.0.1",
+ "alert_system_protocol_id": "/bitcoin-testnet/alert-system/0.0.1",
"bootstrap_peer": "",
- "private_key_path": ""
+ "private_key_path": "",
+ "peer_discovery_interval": "10m",
+ "topic_name": "alert_system_testnet"
},
"rpc_connections": [
{
- "user": "galt",
- "password": "galt",
+ "user": "foo",
+ "password": "foo",
"host": "http://localhost:8333"
}
]
diff --git a/app/config/envs/mainnet.json b/app/config/envs/mainnet.json
new file mode 100644
index 0000000..c5f889a
--- /dev/null
+++ b/app/config/envs/mainnet.json
@@ -0,0 +1,70 @@
+{
+ "alert_webhook_url": "",
+ "bitcoin_config_path": "",
+ "request_logging": true,
+ "web_server": {
+ "idle_timeout": "60s",
+ "port": "3000",
+ "read_timeout": "15s",
+ "write_timeout": "15s"
+ },
+ "environment": "mainnet",
+ "datastore": {
+ "auto_migrate": true,
+ "debug": true,
+ "engine": "sqlite",
+ "password": "",
+ "table_prefix": "alert_system",
+ "sqlite": {
+ "database_path": "alert_system_datastore.db",
+ "shared": false
+ },
+ "sql_read": {
+ "driver": "postgresql",
+ "host": "localhost",
+ "max_connection_idle_time": "20s",
+ "max_connection_time": "20s",
+ "max_idle_connections": 2,
+ "max_open_connections": 5,
+ "name": "alert_system_db",
+ "password": "",
+ "port": "5432",
+ "replica": true,
+ "skip_initialize_with_version": true,
+ "time_zone": "UTC",
+ "tx_timeout": "20s",
+ "user": "your_user"
+ },
+ "sql_write": {
+ "driver": "postgresql",
+ "host": "localhost",
+ "max_connection_idle_time": "20s",
+ "max_connection_time": "20s",
+ "max_idle_connections": 2,
+ "max_open_connections": 5,
+ "name": "alert_system_db",
+ "password": "",
+ "port": "5432",
+ "replica": false,
+ "skip_initialize_with_version": true,
+ "time_zone": "UTC",
+ "tx_timeout": "20s",
+ "user": "your_user"
+ }
+ },
+ "p2p": {
+ "ip": "0.0.0.0",
+ "port": "9906",
+ "alert_system_protocol_id": "/bitcoin/alert-system/1.0.1",
+ "bootstrap_peer": "",
+ "private_key_path": "",
+ "topic_name": "bitcoin_alert_system"
+ },
+ "rpc_connections": [
+ {
+ "user": "your_user",
+ "password": "",
+ "host": "http://localhost:8333"
+ }
+ ]
+}
diff --git a/app/config/envs/production.json b/app/config/envs/production.json
index eec24a2..dbf27b5 100644
--- a/app/config/envs/production.json
+++ b/app/config/envs/production.json
@@ -1,5 +1,6 @@
{
"alert_webhook_url": "",
+ "bitcoin_config_path": "",
"request_logging": true,
"web_server": {
"idle_timeout": "60s",
@@ -56,7 +57,8 @@
"port": "9906",
"alert_system_protocol_id": "/bitcoin/alert-system/1.0.1",
"bootstrap_peer": "",
- "private_key_path": ""
+ "private_key_path": "",
+ "topic_name": "bitcoin_alert_system"
},
"rpc_connections": [
{
diff --git a/app/config/envs/stn.json b/app/config/envs/stn.json
new file mode 100644
index 0000000..f505a0d
--- /dev/null
+++ b/app/config/envs/stn.json
@@ -0,0 +1,71 @@
+{
+ "alert_webhook_url": "",
+ "bitcoin_config_path": "",
+ "request_logging": true,
+ "alert_processing_interval": "5m",
+ "web_server": {
+ "idle_timeout": "60s",
+ "port": "3000",
+ "read_timeout": "15s",
+ "write_timeout": "15s"
+ },
+ "environment": "stn",
+ "datastore": {
+ "auto_migrate": true,
+ "debug": true,
+ "engine": "sqlite",
+ "password": "",
+ "table_prefix": "alert_system",
+ "sqlite": {
+ "database_path": "alert_system_datastore.db",
+ "shared": false
+ },
+ "sql_read": {
+ "driver": "postgresql",
+ "host": "localhost",
+ "max_connection_idle_time": "20s",
+ "max_connection_time": "20s",
+ "max_idle_connections": 2,
+ "max_open_connections": 5,
+ "name": "alert_system_db",
+ "password": "postgres",
+ "port": "5432",
+ "replica": true,
+ "skip_initialize_with_version": true,
+ "time_zone": "UTC",
+ "tx_timeout": "20s",
+ "user": "postgres"
+ },
+ "sql_write": {
+ "driver": "postgresql",
+ "host": "localhost",
+ "max_connection_idle_time": "20s",
+ "max_connection_time": "20s",
+ "max_idle_connections": 2,
+ "max_open_connections": 5,
+ "name": "alert_system_db",
+ "password": "postgres",
+ "port": "5432",
+ "replica": false,
+ "skip_initialize_with_version": true,
+ "time_zone": "UTC",
+ "tx_timeout": "20s",
+ "user": "postgres"
+ }
+ },
+ "p2p": {
+ "ip": "0.0.0.0",
+ "port": "9906",
+ "alert_system_protocol_id": "/bitcoin-stn/alert-system/0.0.1",
+ "bootstrap_peer": "",
+ "private_key_path": "",
+ "topic_name": "bsv_alert_system_stn"
+ },
+ "rpc_connections": [
+ {
+ "user": "galt",
+ "password": "galt",
+ "host": "http://localhost:9332"
+ }
+ ]
+}
diff --git a/app/config/envs/test.json b/app/config/envs/test.json
index f275db7..654afa1 100644
--- a/app/config/envs/test.json
+++ b/app/config/envs/test.json
@@ -1,5 +1,6 @@
{
"alert_webhook_url": "https://webhook.url",
+ "bitcoin_config_path": "",
"request_logging": true,
"web_server": {
"idle_timeout": "60s",
@@ -54,7 +55,7 @@
"p2p": {
"ip": "192.168.1.1",
"port": "8000",
- "alert_system_protocol_id": "/bitcoin/alert-system/1.0.1",
+ "alert_system_protocol_id": "/bitcoin/alert-system/0.0.1",
"bootstrap_peer": "",
"private_key_path": "/path/to/private/key"
},
diff --git a/app/config/envs/testnet.json b/app/config/envs/testnet.json
new file mode 100644
index 0000000..7768954
--- /dev/null
+++ b/app/config/envs/testnet.json
@@ -0,0 +1,71 @@
+{
+ "alert_webhook_url": "",
+ "bitcoin_config_path": "",
+ "request_logging": true,
+ "alert_processing_interval": "5m",
+ "web_server": {
+ "idle_timeout": "60s",
+ "port": "3000",
+ "read_timeout": "15s",
+ "write_timeout": "15s"
+ },
+ "environment": "testnet",
+ "datastore": {
+ "auto_migrate": true,
+ "debug": true,
+ "engine": "sqlite",
+ "password": "",
+ "table_prefix": "alert_system",
+ "sqlite": {
+ "database_path": "alert_system_datastore.db",
+ "shared": false
+ },
+ "sql_read": {
+ "driver": "postgresql",
+ "host": "localhost",
+ "max_connection_idle_time": "20s",
+ "max_connection_time": "20s",
+ "max_idle_connections": 2,
+ "max_open_connections": 5,
+ "name": "alert_system_db",
+ "password": "postgres",
+ "port": "5432",
+ "replica": true,
+ "skip_initialize_with_version": true,
+ "time_zone": "UTC",
+ "tx_timeout": "20s",
+ "user": "postgres"
+ },
+ "sql_write": {
+ "driver": "postgresql",
+ "host": "localhost",
+ "max_connection_idle_time": "20s",
+ "max_connection_time": "20s",
+ "max_idle_connections": 2,
+ "max_open_connections": 5,
+ "name": "alert_system_db",
+ "password": "postgres",
+ "port": "5432",
+ "replica": false,
+ "skip_initialize_with_version": true,
+ "time_zone": "UTC",
+ "tx_timeout": "20s",
+ "user": "postgres"
+ }
+ },
+ "p2p": {
+ "ip": "0.0.0.0",
+ "port": "9906",
+ "alert_system_protocol_id": "/bitcoin-testnet/alert-system/0.0.1",
+ "bootstrap_peer": "",
+ "private_key_path": "",
+ "topic_name": "alert_system_testnet"
+ },
+ "rpc_connections": [
+ {
+ "user": "galt",
+ "password": "galt",
+ "host": "http://localhost:18332"
+ }
+ ]
+}
diff --git a/app/config/load.go b/app/config/load.go
index 60042af..0000589 100644
--- a/app/config/load.go
+++ b/app/config/load.go
@@ -1,11 +1,13 @@
package config
import (
+ "bufio"
"bytes"
"context"
"errors"
"fmt"
"io/fs"
+ "net"
"net/http"
"os"
"strings"
@@ -90,6 +92,11 @@ func requireP2P(_appConfig *Config) error {
_appConfig.P2P.AlertSystemProtocolID = DefaultAlertSystemProtocolID
}
+ // Set the p2p alert system topic name if it's missing
+ if len(_appConfig.P2P.TopicName) == 0 {
+ _appConfig.P2P.TopicName = DefaultTopicName
+ }
+
// Load the private key path
// If not found, create a default one
if len(_appConfig.P2P.PrivateKeyPath) == 0 {
@@ -98,6 +105,18 @@ func requireP2P(_appConfig *Config) error {
}
}
+ // Load bitcoin configuration if specified
+ if len(_appConfig.BitcoinConfigPath) > 0 {
+ if err := _appConfig.loadBitcoinConfiguration(); err != nil {
+ return err
+ }
+ }
+
+ // Load the peer discovery interval
+ if _appConfig.P2P.PeerDiscoveryInterval <= 0 {
+ _appConfig.P2P.PeerDiscoveryInterval = DefaultPeerDiscoveryInterval
+ }
+
// Load the p2p ip (local, ip address or domain name)
// todo better validation of what is a valid IP, domain name or local address
if len(_appConfig.P2P.IP) < 5 {
@@ -200,6 +219,11 @@ func LoadConfigFile() (_appConfig *Config, err error) {
Logger: gocore.Log(ApplicationName),
}
+ // Set default alert processing interval if it doesn't exist
+ if _appConfig.AlertProcessingInterval <= 0 {
+ _appConfig.AlertProcessingInterval = DefaultAlertProcessingInterval
+ }
+
// Log the configuration that was detected and where it was loaded from
_appConfig.Services.Log.Debug("loaded configuration from: " + viper.ConfigFileUsed())
@@ -219,6 +243,73 @@ func (c *Config) createPrivateKeyDirectory() error {
return nil
}
+// loadBitcoinConfiguration will load the RPC configuration from bitcoin.conf
+func (c *Config) loadBitcoinConfiguration() error {
+ if len(c.BitcoinConfigPath) == 0 {
+ return nil
+ }
+ c.Services.Log.Infof("loading RPC configuration from %s", c.BitcoinConfigPath)
+ file, err := os.Open(c.BitcoinConfigPath)
+ if err != nil {
+ return err
+ }
+ scanner := bufio.NewScanner(file)
+ scanner.Split(splitFunc)
+ confValues := map[string]string{}
+ for scanner.Scan() {
+ kv := scanner.Text()
+ keyValue := strings.Split(kv, "=")
+ if len(keyValue) != 2 {
+ continue
+ }
+ confValues[keyValue[0]] = keyValue[1]
+ }
+ host := confValues["rpcconnect"]
+ if host == "" {
+ host = "127.0.0.1"
+ }
+ port := confValues["rpcport"]
+ if port == "" {
+ c.Services.Log.Debugf("rpcport value not detected ")
+ port = "8332"
+ }
+
+ user := confValues["rpcuser"]
+ if user == "" {
+ return fmt.Errorf("rpcuser missing from bitcoin.conf file")
+ }
+ pass := confValues["rpcpassword"]
+ if pass == "" {
+ return fmt.Errorf("rpcpassword missing from bitcoin.conf file")
+ }
+ c.RPCConnections = []RPCConfig{
+ {
+ Host: fmt.Sprintf("http://%s", net.JoinHostPort(host, port)),
+ Password: pass,
+ User: user,
+ },
+ }
+
+ return file.Close()
+}
+
+func splitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+
+ if atEOF {
+ return len(data), data, nil
+ }
+
+ //newline is the k-v pair delimiter
+ if i := strings.Index(string(data), "\n"); i >= 0 {
+ //skip the delimiter in advancing to the next pair
+ return i + 1, data[0:i], nil
+ }
+ return
+}
+
// CloseAll will close all connections to all services
func (c *Config) CloseAll(ctx context.Context) {
diff --git a/app/config/load_test.go b/app/config/load_test.go
index 61f76d7..39ede04 100644
--- a/app/config/load_test.go
+++ b/app/config/load_test.go
@@ -29,6 +29,8 @@ func TestLoadConfig_Success(t *testing.T) {
assert.Equal(t, "/path/to/private/key", c.P2P.PrivateKeyPath)
assert.Equal(t, "", c.P2P.BootstrapPeer)
assert.Equal(t, DefaultAlertSystemProtocolID, c.P2P.AlertSystemProtocolID)
+ assert.Equal(t, DefaultPeerDiscoveryInterval, c.P2P.PeerDiscoveryInterval)
+ assert.Equal(t, DefaultAlertProcessingInterval, c.AlertProcessingInterval)
assert.Equal(t, "192.168.1.1", c.P2P.IP)
assert.Equal(t, "8000", c.P2P.Port)
assert.Equal(t, "https://webhook.url", c.AlertWebhookURL)
diff --git a/app/config/mocks/mocks.go b/app/config/mocks/mocks.go
index 17549dd..5e008d0 100644
--- a/app/config/mocks/mocks.go
+++ b/app/config/mocks/mocks.go
@@ -1,7 +1,11 @@
// Package mocks is a generated mocking package for the mocks
package mocks
-import "context"
+import (
+ "context"
+
+ "github.com/libsv/go-bn/models"
+)
// Node is a mock type for the SVNode interface
type Node struct {
@@ -11,10 +15,12 @@ type Node struct {
RPCUser string
// Functions
- BanPeerFunc func(ctx context.Context, peer string) error
- InvalidateBlockFunc func(ctx context.Context, hash string) error
- UnbanPeerFunc func(ctx context.Context, peer string) error
-
+ BanPeerFunc func(ctx context.Context, peer string) error
+ BestBlockHashFunc func(ctx context.Context) (string, error)
+ InvalidateBlockFunc func(ctx context.Context, hash string) error
+ UnbanPeerFunc func(ctx context.Context, peer string) error
+ AddToConsensusBlacklistFunc func(ctx context.Context, funds []models.Fund) (*models.AddToConsensusBlacklistResponse, error)
+ AddToConfiscationTransactionWhitelistFunc func(ctx context.Context, tx []models.ConfiscationTransactionDetails) (*models.AddToConfiscationTransactionWhitelistResponse, error)
// Add additional fields if needed to track calls or results
}
@@ -42,6 +48,14 @@ func (n *Node) BanPeer(ctx context.Context, peer string) error {
return nil
}
+// BestBlockHash will call the BestBlockHashFunc
+func (n *Node) BestBlockHash(ctx context.Context) (string, error) {
+ if n.BestBlockHashFunc != nil {
+ return n.BestBlockHashFunc(ctx)
+ }
+ return "", nil
+}
+
// InvalidateBlock will call the InvalidateBlockFunc if not nil, otherwise return nil
func (n *Node) InvalidateBlock(ctx context.Context, hash string) error {
if n.InvalidateBlockFunc != nil {
@@ -57,3 +71,19 @@ func (n *Node) UnbanPeer(ctx context.Context, peer string) error {
}
return nil
}
+
+// AddToConsensusBlacklist will call the AddToConsensusBlacklistFunc if not nil, otherwise return nil
+func (n *Node) AddToConsensusBlacklist(ctx context.Context, funds []models.Fund) (*models.AddToConsensusBlacklistResponse, error) {
+ if n.AddToConsensusBlacklistFunc != nil {
+ return n.AddToConsensusBlacklistFunc(ctx, funds)
+ }
+ return nil, nil
+}
+
+// AddToConfiscationTransactionWhitelist will call the AddToConfiscationTransactionWhitelistFunc if not nil, otherwise return nil
+func (n *Node) AddToConfiscationTransactionWhitelist(ctx context.Context, tx []models.ConfiscationTransactionDetails) (*models.AddToConfiscationTransactionWhitelistResponse, error) {
+ if n.AddToConfiscationTransactionWhitelistFunc != nil {
+ return n.AddToConfiscationTransactionWhitelistFunc(ctx, tx)
+ }
+ return nil, nil
+}
diff --git a/app/config/node.go b/app/config/node.go
index 324c129..df5d992 100644
--- a/app/config/node.go
+++ b/app/config/node.go
@@ -3,6 +3,8 @@ package config
import (
"context"
+ "github.com/libsv/go-bn/models"
+
"github.com/bitcoin-sv/alert-system/app/config/mocks"
"github.com/libsv/go-bn"
)
@@ -10,11 +12,14 @@ import (
// NodeInterface is the interface for a node
type NodeInterface interface {
BanPeer(ctx context.Context, peer string) error
+ BestBlockHash(ctx context.Context) (string, error)
GetRPCHost() string
GetRPCPassword() string
GetRPCUser() string
InvalidateBlock(ctx context.Context, hash string) error
UnbanPeer(ctx context.Context, peer string) error
+ AddToConsensusBlacklist(ctx context.Context, funds []models.Fund) (*models.AddToConsensusBlacklistResponse, error)
+ AddToConfiscationTransactionWhitelist(ctx context.Context, tx []models.ConfiscationTransactionDetails) (*models.AddToConfiscationTransactionWhitelistResponse, error)
}
// NewNodeConfig creates a new NodeConfig struct
@@ -62,8 +67,26 @@ func (n *Node) BanPeer(ctx context.Context, peer string) error {
return c.SetBan(ctx, peer, bn.BanActionAdd, nil)
}
+// BestBlockHash gets the best block hash
+func (n *Node) BestBlockHash(ctx context.Context) (string, error) {
+ c := bn.NewNodeClient(bn.WithCreds(n.RPCUser, n.RPCPassword), bn.WithHost(n.RPCHost))
+ return c.BestBlockHash(ctx)
+}
+
// UnbanPeer unbans a peer
func (n *Node) UnbanPeer(ctx context.Context, peer string) error {
c := bn.NewNodeClient(bn.WithCreds(n.RPCUser, n.RPCPassword), bn.WithHost(n.RPCHost))
return c.SetBan(ctx, peer, bn.BanActionRemove, nil)
}
+
+// AddToConsensusBlacklist adds frozen utxos to blacklist
+func (n *Node) AddToConsensusBlacklist(ctx context.Context, funds []models.Fund) (*models.AddToConsensusBlacklistResponse, error) {
+ c := bn.NewNodeClient(bn.WithCreds(n.RPCUser, n.RPCPassword), bn.WithHost(n.RPCHost))
+ return c.AddToConsensusBlacklist(ctx, funds)
+}
+
+// AddToConfiscationTransactionWhitelist adds confiscation transactions to the whitelist
+func (n *Node) AddToConfiscationTransactionWhitelist(ctx context.Context, tx []models.ConfiscationTransactionDetails) (*models.AddToConfiscationTransactionWhitelistResponse, error) {
+ c := bn.NewNodeClient(bn.WithCreds(n.RPCUser, n.RPCPassword), bn.WithHost(n.RPCHost))
+ return c.AddToConfiscationTransactionWhitelist(ctx, tx)
+}
diff --git a/app/models/alert_message.go b/app/models/alert_message.go
index 2456616..6a3e08b 100644
--- a/app/models/alert_message.go
+++ b/app/models/alert_message.go
@@ -27,6 +27,7 @@ type AlertMessage struct {
Hash string `json:"hash" toml:"hash" yaml:"hash" bson:"hash" gorm:"<-;type:char(64);index;comment:This is the hash"`
SequenceNumber uint32 `json:"sequence_number" toml:"sequence_number" yaml:"sequence_number" bson:"sequence_number" gorm:"<-;type:int8;index;comment:This is the alert sequence number"`
Raw string `json:"raw" toml:"raw" yaml:"raw" bson:"raw" gorm:"<-;type:text;comment:This is the raw alert message"`
+ Processed bool `json:"processed" toml:"processed" yaml:"processed" bson:"processed" gorm:"<-;type:boolean;comment:This determine if the alert was processed"`
// Private fields (never to be exported)
alertType AlertType
@@ -41,6 +42,8 @@ type AlertMessage struct {
type AlertMessageInterface interface {
Read(msg []byte) error
Do(ctx context.Context) error
+ ToJSON(ctx context.Context) []byte
+ MessageString() string
}
// NewAlertMessage creates a new alert message
@@ -201,7 +204,7 @@ func (m *AlertMessage) ProcessAlertMessage() AlertMessageInterface {
AlertMessage: *m,
}
case AlertTypeConfiscateUtxo:
- return &AlertMessageConfiscateUtxo{
+ return &AlertMessageConfiscateTransaction{
AlertMessage: *m,
}
case AlertTypeBanPeer:
@@ -246,14 +249,21 @@ func (m *AlertMessage) Timestamp() uint64 {
return m.timestamp
}
-// NewAlertFromBytes creates a new alert from bytes
-func NewAlertFromBytes(ak []byte, opts ...model.Options) (*AlertMessage, error) {
+// ReadRaw sets the model fields based on the raw message
+func (m *AlertMessage) ReadRaw() error {
+ if len(m.GetRawMessage()) == 0 {
+ ak, err := hex.DecodeString(m.Raw)
+ if err != nil {
+ return err
+ }
+ m.SetRawMessage(ak)
+ }
- // Check if the alert is valid
- if len(ak) < 16 {
+ if len(m.GetRawMessage()) < 16 {
// todo DETERMINE ACTUAL PROPER LENGTH
- return nil, fmt.Errorf("alert needs to be at least 16")
+ return fmt.Errorf("alert needs to be at least 16 bytes")
}
+ ak := m.GetRawMessage()
version := binary.LittleEndian.Uint32(ak[:4])
sequenceNumber := binary.LittleEndian.Uint32(ak[4:8])
timestamp := binary.LittleEndian.Uint64(ak[8:16])
@@ -273,7 +283,7 @@ func NewAlertFromBytes(ak []byte, opts ...model.Options) (*AlertMessage, error)
// but possible. Regardless let's just error out now if this length is lower. At least
// allows us to grab the expected signature.
if len(alertAndSignature) < sigLen+2 {
- return nil, fmt.Errorf("alert message is invalid - too short length")
+ return fmt.Errorf("alert message is invalid - too short length")
}
// Get alert message bytes
@@ -291,17 +301,26 @@ func NewAlertFromBytes(ak []byte, opts ...model.Options) (*AlertMessage, error)
dataLen := 20 + len(alert)
- // Create the new alert
+ m.SetAlertType(AlertType(alertType))
+ m.message = alert
+ m.SequenceNumber = sequenceNumber
+ m.timestamp = timestamp
+ m.version = version
+ m.data = ak[:dataLen]
+ m.signatures = sigs
+ _ = m.Serialize()
+ return nil
+}
+
+// NewAlertFromBytes creates a new alert from bytes
+func NewAlertFromBytes(ak []byte, opts ...model.Options) (*AlertMessage, error) {
opts = append(opts, model.New())
newAlert := NewAlertMessage(opts...)
- newAlert.SetAlertType(AlertType(alertType))
- newAlert.message = alert
- newAlert.SequenceNumber = sequenceNumber
- newAlert.timestamp = timestamp
- newAlert.version = version
- newAlert.data = ak[:dataLen]
- newAlert.signatures = sigs
- _ = newAlert.Serialize()
+ newAlert.SetRawMessage(ak)
+ err := newAlert.ReadRaw()
+ if err != nil {
+ return nil, err
+ }
// Return alert
return newAlert, nil
@@ -358,3 +377,63 @@ func GetLatestAlert(ctx context.Context, metadata *model.Metadata, opts ...model
// Return the first item (only item)
return modelItems[0], nil
}
+
+// GetAllAlerts returns all alerts in the database
+func GetAllAlerts(ctx context.Context, metadata *model.Metadata, opts ...model.Options) ([]*AlertMessage, error) {
+ // Set the conditions
+ conditions := &map[string]interface{}{
+ utils.FieldDeletedAt: map[string]interface{}{ // IS NULL
+ utils.ExistsCondition: false,
+ },
+ }
+
+ // Set the query params
+ queryParams := &datastore.QueryParams{
+ OrderByField: utils.FieldSequenceNumber,
+ SortDirection: utils.SortAscending,
+ }
+
+ // Get the record
+ modelItems := make([]*AlertMessage, 0)
+ if err := model.GetModelsByConditions(
+ ctx, model.NameAlertMessage, &modelItems, metadata, conditions, queryParams, opts...,
+ ); err != nil {
+ return nil, err
+ } else if len(modelItems) == 0 {
+ return nil, nil
+ }
+
+ // Return the first item (only item)
+ return modelItems, nil
+}
+
+// GetAllUnprocessedAlerts will get all alerts that weren't successfully processed
+func GetAllUnprocessedAlerts(ctx context.Context, metadata *model.Metadata, opts ...model.Options) ([]*AlertMessage, error) {
+
+ // Set the conditions
+ conditions := &map[string]interface{}{
+ utils.FieldDeletedAt: map[string]interface{}{ // IS NULL
+ utils.ExistsCondition: false,
+ },
+ "processed": false,
+ }
+
+ // Set the query params
+ queryParams := &datastore.QueryParams{
+ OrderByField: utils.FieldSequenceNumber,
+ SortDirection: utils.SortAscending,
+ }
+
+ // Get the record
+ modelItems := make([]*AlertMessage, 0)
+ if err := model.GetModelsByConditions(
+ ctx, model.NameAlertMessage, &modelItems, metadata, conditions, queryParams, opts...,
+ ); err != nil {
+ return nil, err
+ } else if len(modelItems) == 0 {
+ return nil, nil
+ }
+
+ // Return the first item (only item)
+ return modelItems, nil
+}
diff --git a/app/models/alert_message_ban_peer.go b/app/models/alert_message_ban_peer.go
index c63440b..4936505 100644
--- a/app/models/alert_message_ban_peer.go
+++ b/app/models/alert_message_ban_peer.go
@@ -3,6 +3,7 @@ package models
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"github.com/libsv/go-p2p/wire"
@@ -62,3 +63,20 @@ func (a *AlertMessageBanPeer) Read(alert []byte) error {
func (a *AlertMessageBanPeer) Do(ctx context.Context) error {
return a.Config().Services.Node.BanPeer(ctx, string(a.Peer))
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageBanPeer) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageBanPeer) MessageString() string {
+ return fmt.Sprintf("Banning peer [%s]; reason [%s].", a.Peer, a.Reason)
+}
diff --git a/app/models/alert_message_confiscate_utxo.go b/app/models/alert_message_confiscate_utxo.go
index 88f8afa..d0ffb79 100644
--- a/app/models/alert_message_confiscate_utxo.go
+++ b/app/models/alert_message_confiscate_utxo.go
@@ -1,19 +1,99 @@
package models
-import "context"
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
-// AlertMessageConfiscateUtxo is a confiscate utxo alert
-type AlertMessageConfiscateUtxo struct {
+ "github.com/libsv/go-bn/models"
+ "github.com/libsv/go-p2p/wire"
+)
+
+// AlertMessageConfiscateTransaction is a confiscate utxo alert
+type AlertMessageConfiscateTransaction struct {
AlertMessage
- // TODO finish building out this alert type
+ Transactions []models.ConfiscationTransactionDetails
+}
+
+// ConfiscateTransaction defines the parameters for the confiscation transaction
+type ConfiscateTransaction struct {
+ EnforceAtHeight uint64
+ Hex []byte
}
// Read reads the alert
-func (a *AlertMessageConfiscateUtxo) Read(_ []byte) error {
+func (a *AlertMessageConfiscateTransaction) Read(raw []byte) error {
+ a.Config().Services.Log.Infof("%x", raw)
+ if len(raw) < 9 {
+ return fmt.Errorf("confiscation alert is less than 9 bytes")
+ }
+ // TODO: assume for now only 1 confiscation tx in the alert for simplicity
+ details := []models.ConfiscationTransactionDetails{}
+ enforceAtHeight := binary.LittleEndian.Uint64(raw[0:8])
+ buf := bytes.NewReader(raw[8:])
+
+ length, err := wire.ReadVarInt(buf, 0)
+ if err != nil {
+ return err
+ }
+ if length > uint64(buf.Len()) {
+ return errors.New("tx hex length is longer than the remaining buffer")
+ }
+
+ // read the tx hex
+ var rawHex []byte
+ for i := uint64(0); i < length; i++ {
+ var b byte
+ if b, err = buf.ReadByte(); err != nil {
+ return fmt.Errorf("failed to read tx hex: %s", err.Error())
+ }
+ rawHex = append(rawHex, b)
+ }
+
+ detail := models.ConfiscationTransactionDetails{
+ ConfiscationTransaction: models.ConfiscationTransaction{
+ EnforceAtHeight: int64(enforceAtHeight),
+ Hex: hex.EncodeToString(rawHex),
+ },
+ }
+ details = append(details, detail)
+
+ a.Transactions = details
+ a.Config().Services.Log.Infof("ConfiscateTransaction alert; enforceAt [%d]; hex [%s]", enforceAtHeight, hex.EncodeToString(rawHex))
+
return nil
}
// Do executes the alert
-func (a *AlertMessageConfiscateUtxo) Do(_ context.Context) error {
+func (a *AlertMessageConfiscateTransaction) Do(ctx context.Context) error {
+ res, err := a.Config().Services.Node.AddToConfiscationTransactionWhitelist(ctx, a.Transactions)
+ if err != nil {
+ return err
+ }
+ if len(res.NotProcessed) > 0 {
+ // we can safely assume this is just one not processed tx because we are only publishing one tx with the alert right now
+ return fmt.Errorf("confiscation alert RPC response returned an error; reason: %s", res.NotProcessed[0].Reason)
+ }
return nil
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageConfiscateTransaction) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageConfiscateTransaction) MessageString() string {
+ return fmt.Sprintf("Adding confiscation transaction [%x] to whitelist enforcing at height [%d].", a.Transactions[0].ConfiscationTransaction.Hex, a.Transactions[0].ConfiscationTransaction.EnforceAtHeight)
+}
diff --git a/app/models/alert_message_freeze_utxo.go b/app/models/alert_message_freeze_utxo.go
index 2b30727..1f28b84 100644
--- a/app/models/alert_message_freeze_utxo.go
+++ b/app/models/alert_message_freeze_utxo.go
@@ -1,19 +1,109 @@
package models
-import "context"
+import (
+ "context"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+
+ "github.com/libsv/go-bn/models"
+)
// AlertMessageFreezeUtxo is the message for freezing UTXOs
type AlertMessageFreezeUtxo struct {
AlertMessage
- // TODO finish building out this alert type
+ Funds []models.Fund
+}
+
+// Fund is the struct defining funds to freeze
+type Fund struct {
+ TransactionOutID [32]byte
+ Vout uint64
+ EnforceAtHeightStart uint64
+ EnforceAtHeightEnd uint64
+ PolicyExpiresWithConsensus bool
+}
+
+// Serialize creates the raw hex string of the fund
+func (f *Fund) Serialize() []byte {
+ raw := []byte{}
+ raw = append(raw, f.TransactionOutID[:]...)
+ raw = binary.LittleEndian.AppendUint64(raw, f.Vout)
+ raw = binary.LittleEndian.AppendUint64(raw, f.EnforceAtHeightStart)
+ raw = binary.LittleEndian.AppendUint64(raw, f.EnforceAtHeightEnd)
+ expire := uint8(0)
+ if f.PolicyExpiresWithConsensus {
+ expire = uint8(1)
+ }
+ raw = append(raw, expire)
+ return raw
}
// Read reads the message
-func (a *AlertMessageFreezeUtxo) Read(_ []byte) error {
+func (a *AlertMessageFreezeUtxo) Read(raw []byte) error {
+ if len(raw) < 57 {
+ return fmt.Errorf("freeze alert is less than 57 bytes, got %d bytes; raw: %x", len(raw), raw)
+ }
+ if len(raw)%57 != 0 {
+ return fmt.Errorf("freeze alert is not a multiple of 57 bytes, got %d bytes; raw: %x", len(raw), raw)
+ }
+ fundCount := len(raw) / 57
+ funds := []models.Fund{}
+ for i := 0; i < fundCount; i++ {
+ fund := Fund{
+ TransactionOutID: [32]byte(raw[0:32]),
+ Vout: binary.LittleEndian.Uint64(raw[32:40]),
+ EnforceAtHeightStart: binary.LittleEndian.Uint64(raw[40:48]),
+ EnforceAtHeightEnd: binary.LittleEndian.Uint64(raw[48:56]),
+ }
+ enforceByte := raw[56]
+
+ if enforceByte != uint8(0) {
+ fund.PolicyExpiresWithConsensus = true
+ }
+ funds = append(funds, models.Fund{
+ TxOut: models.TxOut{
+ TxId: hex.EncodeToString(fund.TransactionOutID[:]),
+ Vout: int(fund.Vout),
+ },
+ EnforceAtHeight: []models.Enforce{
+ {
+ Start: int(fund.EnforceAtHeightStart),
+ Stop: int(fund.EnforceAtHeightEnd),
+ },
+ },
+ PolicyExpiresWithConsensus: fund.PolicyExpiresWithConsensus,
+ })
+ raw = raw[57:]
+ }
+ a.Funds = funds
+
return nil
}
// Do performs the message
-func (a *AlertMessageFreezeUtxo) Do(_ context.Context) error {
+func (a *AlertMessageFreezeUtxo) Do(ctx context.Context) error {
+ _, err := a.Config().Services.Node.AddToConsensusBlacklist(ctx, a.Funds)
+ if err != nil {
+ return err
+ }
return nil
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageFreezeUtxo) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageFreezeUtxo) MessageString() string {
+ return fmt.Sprintf("Freezing utxo id [%x]; vout: [%d], enforcing at height start [%d], end [%d].", a.Funds[0].TxOut.TxId, a.Funds[0].TxOut.Vout, a.Funds[0].EnforceAtHeight[0].Start, a.Funds[0].EnforceAtHeight[0].Stop)
+}
diff --git a/app/models/alert_message_informational.go b/app/models/alert_message_informational.go
index eb3da3a..5729167 100644
--- a/app/models/alert_message_informational.go
+++ b/app/models/alert_message_informational.go
@@ -3,6 +3,7 @@ package models
import (
"bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
@@ -48,3 +49,20 @@ func (a *AlertMessageInformational) Do(_ context.Context) error {
a.Config().Services.Log.Infof("[informational alert]: %s", a.Message)
return nil
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageInformational) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageInformational) MessageString() string {
+ return fmt.Sprintf("Informational: %s", a.Message)
+}
diff --git a/app/models/alert_message_informational_test.go b/app/models/alert_message_informational_test.go
index acba110..2668cda 100644
--- a/app/models/alert_message_informational_test.go
+++ b/app/models/alert_message_informational_test.go
@@ -50,3 +50,38 @@ func TestAlertMessageInformational_Read(t *testing.T) {
})
}
}
+
+func TestAlertMessageInformational_MessageString(t *testing.T) {
+ type fields struct {
+ AlertMessage AlertMessage
+ MessageLength uint64
+ Message []byte
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{{
+ name: "test valid message",
+ fields: fields{
+ AlertMessage: AlertMessage{
+ Raw: "010000001b00000067b5bd6500000000010000000774657374696e67202214d4892217b450eedfb33dd901951e80557ea10d19a59f8a566f733b1ab7107b77d388a9f2fac6602b7258cbcb0ac11c9a6dd0b5687cb9508bcfa5dbd6ce901f4672d99e36978856f2d2794c4c48d353a0b45357d08991147f9e8803a0b90a5f01e85739f36eab32765fe2190b1625e3f5d6c41319da3da803b60be472bf2c011f3784e3d3504c93be28e32e9108aead94cb515bb4813303e6a14735bcca87e451487b222198a9ba3ea0c984e3fbd95e35ba1607c5c74224af6083185a17ea7ff9",
+ },
+ Message: []byte("testing"),
+ },
+ want: "Informational: testing",
+ },
+
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ a := &AlertMessageInformational{
+ AlertMessage: tt.fields.AlertMessage,
+ //MessageLength: tt.fields.MessageLength,
+ Message: tt.fields.Message,
+ }
+ assert.Equalf(t, tt.want, a.MessageString(), "MessageString()")
+ })
+ }
+}
diff --git a/app/models/alert_message_invalidate_block.go b/app/models/alert_message_invalidate_block.go
index 184c3c0..b99e8c0 100644
--- a/app/models/alert_message_invalidate_block.go
+++ b/app/models/alert_message_invalidate_block.go
@@ -3,6 +3,7 @@ package models
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"github.com/libsv/go-bt/v2/chainhash"
@@ -42,6 +43,7 @@ func (a *AlertMessageInvalidateBlock) Read(alert []byte) error {
a.ReasonLength = length
a.Reason = msg
a.BlockHash = blockHash
+ a.Config().Services.Log.Infof("InvalidateBlock alert; hash [%s]; reason [%s]", a.BlockHash, a.Reason)
return nil
}
@@ -49,3 +51,20 @@ func (a *AlertMessageInvalidateBlock) Read(alert []byte) error {
func (a *AlertMessageInvalidateBlock) Do(ctx context.Context) error {
return a.Config().Services.Node.InvalidateBlock(ctx, a.BlockHash.String())
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageInvalidateBlock) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageInvalidateBlock) MessageString() string {
+ return fmt.Sprintf("Invalidating block hash [%s]; reason [%s].", a.BlockHash, a.Reason)
+}
diff --git a/app/models/alert_message_set_keys.go b/app/models/alert_message_set_keys.go
index a176564..93703e8 100644
--- a/app/models/alert_message_set_keys.go
+++ b/app/models/alert_message_set_keys.go
@@ -4,7 +4,12 @@ import (
"bytes"
"context"
"encoding/hex"
+ "encoding/json"
+ "errors"
"fmt"
+ "time"
+
+ "github.com/mrz1836/go-datastore"
"github.com/bitcoin-sv/alert-system/app/models/model"
)
@@ -49,6 +54,13 @@ func (a *AlertMessageSetKeys) Do(ctx context.Context) error {
}
for _, key := range a.Keys {
pk := NewPublicKey(model.WithAllDependencies(a.Config()))
+ conditions := map[string]interface{}{
+ "key": hex.EncodeToString(key[:]),
+ }
+ err := model.Get(ctx, pk, conditions, 5*time.Second, false)
+ if !errors.Is(err, datastore.ErrNoResults) && err != nil {
+ return err
+ }
pk.Key = hex.EncodeToString(key[:])
pk.Active = true
pk.LastUpdateHash = a.Hash
@@ -58,3 +70,20 @@ func (a *AlertMessageSetKeys) Do(ctx context.Context) error {
}
return nil
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageSetKeys) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageSetKeys) MessageString() string {
+ return fmt.Sprintf("Setting keys: %x, %x, %x, %x, %x", a.Keys[0], a.Keys[1], a.Keys[2], a.Keys[3], a.Keys[4])
+}
diff --git a/app/models/alert_message_unban_peer.go b/app/models/alert_message_unban_peer.go
index da3d3ac..510802e 100644
--- a/app/models/alert_message_unban_peer.go
+++ b/app/models/alert_message_unban_peer.go
@@ -3,6 +3,7 @@ package models
import (
"bytes"
"context"
+ "encoding/json"
"fmt"
"github.com/libsv/go-p2p/wire"
@@ -62,3 +63,20 @@ func (a *AlertMessageUnbanPeer) Read(alert []byte) error {
func (a *AlertMessageUnbanPeer) Do(ctx context.Context) error {
return a.Config().Services.Node.UnbanPeer(ctx, string(a.Peer))
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageUnbanPeer) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageUnbanPeer) MessageString() string {
+ return fmt.Sprintf("Unbanning peer [%s]; reason [%s].", a.Peer, a.Reason)
+}
diff --git a/app/models/alert_message_unfreeze_utxo.go b/app/models/alert_message_unfreeze_utxo.go
index ad7c4b8..0f1b654 100644
--- a/app/models/alert_message_unfreeze_utxo.go
+++ b/app/models/alert_message_unfreeze_utxo.go
@@ -1,19 +1,87 @@
package models
-import "context"
+import (
+ "context"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+
+ "github.com/libsv/go-bn/models"
+)
// AlertMessageUnfreezeUtxo is the message for unfreezing a UTXO
type AlertMessageUnfreezeUtxo struct {
AlertMessage
// TODO finish building out this alert type
+ Funds []models.Fund
}
// Read reads the message from the byte slice
-func (a *AlertMessageUnfreezeUtxo) Read(_ []byte) error {
+func (a *AlertMessageUnfreezeUtxo) Read(raw []byte) error {
+ if len(raw) < 57 {
+ return fmt.Errorf("unfreeze alert is less than 57 bytes, got %d bytes; raw: %x", len(raw), raw)
+ }
+ if len(raw)%57 != 0 {
+ return fmt.Errorf("unfreeze alert is not a multiple of 57 bytes, got %d bytes; raw: %x", len(raw), raw)
+ }
+ fundCount := len(raw) / 57
+ funds := []models.Fund{}
+ for i := 0; i < fundCount; i++ {
+ fund := Fund{
+ TransactionOutID: [32]byte(raw[0:32]),
+ Vout: binary.LittleEndian.Uint64(raw[32:40]),
+ EnforceAtHeightStart: binary.LittleEndian.Uint64(raw[40:48]),
+ EnforceAtHeightEnd: binary.LittleEndian.Uint64(raw[48:56]),
+ }
+ enforceByte := raw[56]
+
+ if enforceByte != uint8(0) {
+ fund.PolicyExpiresWithConsensus = true
+ }
+ funds = append(funds, models.Fund{
+ TxOut: models.TxOut{
+ TxId: hex.EncodeToString(fund.TransactionOutID[:]),
+ Vout: int(fund.Vout),
+ },
+ EnforceAtHeight: []models.Enforce{
+ {
+ Start: int(fund.EnforceAtHeightStart),
+ Stop: int(fund.EnforceAtHeightEnd),
+ },
+ },
+ PolicyExpiresWithConsensus: fund.PolicyExpiresWithConsensus,
+ })
+ raw = raw[57:]
+ }
+ a.Funds = funds
+
return nil
+
}
// Do executes the message
-func (a *AlertMessageUnfreezeUtxo) Do(_ context.Context) error {
+func (a *AlertMessageUnfreezeUtxo) Do(ctx context.Context) error {
+ _, err := a.Config().Services.Node.AddToConsensusBlacklist(ctx, a.Funds)
+ if err != nil {
+ return err
+ }
return nil
}
+
+// ToJSON is the alert in JSON format
+func (a *AlertMessageUnfreezeUtxo) ToJSON(_ context.Context) []byte {
+ m := a.ProcessAlertMessage()
+ // TODO: Come back and add a message interface for each alert
+ _ = m.Read(a.GetRawMessage())
+ data, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return []byte{}
+ }
+ return data
+}
+
+// MessageString executes the alert
+func (a *AlertMessageUnfreezeUtxo) MessageString() string {
+ return fmt.Sprintf("Unfreezing utxo id [%x]; vout: [%d], by setting enforce height at start [%d], end [%d].", a.Funds[0].TxOut.TxId, a.Funds[0].TxOut.Vout, a.Funds[0].EnforceAtHeight[0].Start, a.Funds[0].EnforceAtHeight[0].Stop)
+}
diff --git a/app/models/alert_types.go b/app/models/alert_types.go
index 1ba1839..77cac4d 100644
--- a/app/models/alert_types.go
+++ b/app/models/alert_types.go
@@ -3,6 +3,29 @@ package models
// AlertType is the type of alert
type AlertType uint32
+// Name returns the name of the alert type as a string
+func (a AlertType) Name() string {
+ switch a {
+ case AlertTypeInformational:
+ return "Informational"
+ case AlertTypeFreezeUtxo:
+ return "Freeze"
+ case AlertTypeUnfreezeUtxo:
+ return "Unfreeze"
+ case AlertTypeConfiscateUtxo:
+ return "Confiscate"
+ case AlertTypeBanPeer:
+ return "Ban Peer"
+ case AlertTypeUnbanPeer:
+ return "Unban Peer"
+ case AlertTypeInvalidateBlock:
+ return "Invalidate Block"
+ case AlertTypeSetKeys:
+ return "Set Keys"
+ }
+ return ""
+}
+
// AlertTypeInformational an alert type for informational alerts
const AlertTypeInformational AlertType = 0x01
diff --git a/app/models/genesis_alert.go b/app/models/genesis_alert.go
index b359c10..29ba26f 100644
--- a/app/models/genesis_alert.go
+++ b/app/models/genesis_alert.go
@@ -59,6 +59,7 @@ func CreateGenesisAlert(ctx context.Context, opts ...model.Options) error {
newAlert.SequenceNumber = 0
newAlert.timestamp = uint64(time.Date(2923, time.November, 1, 1, 1, 1, 1, time.UTC).Unix())
newAlert.version = 1
+ newAlert.Processed = true
// Serialize the data
newAlert.SerializeData()
diff --git a/app/p2p/dht.go b/app/p2p/dht.go
index 9e9733d..2dd971f 100644
--- a/app/p2p/dht.go
+++ b/app/p2p/dht.go
@@ -5,11 +5,12 @@ import (
"sync"
"time"
+ "github.com/bitcoin-sv/alert-system/app/config"
+ "github.com/libp2p/go-libp2p/core/peer"
+
"github.com/multiformats/go-multiaddr"
- "github.com/bitcoin-sv/alert-system/app/config"
dht "github.com/libp2p/go-libp2p-kad-dht"
- "github.com/libp2p/go-libp2p/core/peer"
)
// initDHT will initialize the DHT
@@ -43,27 +44,32 @@ func (s *Server) initDHT(ctx context.Context) (*dht.IpfsDHT, error) {
}
// Connect to the chosen ipfs nodes
- var connected = false
+ connected := false
for !connected {
- var wg sync.WaitGroup
- for _, peerAddr := range peers {
- var peerInfo *peer.AddrInfo
- if peerInfo, err = peer.AddrInfoFromP2pAddr(peerAddr); err != nil {
- return nil, err
- }
- wg.Add(1)
- go func(logger config.LoggerInterface) {
- defer wg.Done()
- if err = s.host.Connect(ctx, *peerInfo); err != nil {
- logger.Errorf("bootstrap warning: %s", err.Error())
- return
+ select {
+ case <-s.quitPeerInitializationChannel:
+ return kademliaDHT, nil
+ default:
+ var wg sync.WaitGroup
+ for _, peerAddr := range peers {
+ var peerInfo *peer.AddrInfo
+ if peerInfo, err = peer.AddrInfoFromP2pAddr(peerAddr); err != nil {
+ return nil, err
}
- logger.Infof("connected to peer %v", peerInfo.ID)
- connected = true
- }(logger)
+ wg.Add(1)
+ go func(logger config.LoggerInterface) {
+ defer wg.Done()
+ if err = s.host.Connect(ctx, *peerInfo); err != nil {
+ logger.Errorf("bootstrap warning: %s", err.Error())
+ return
+ }
+ logger.Infof("connected to peer %v", peerInfo.ID)
+ connected = true
+ }(logger)
+ }
+ time.Sleep(1 * time.Second)
+ wg.Wait()
}
- time.Sleep(1 * time.Second)
- wg.Wait()
}
return kademliaDHT, nil
diff --git a/app/p2p/server.go b/app/p2p/server.go
index 994586c..41cc88d 100644
--- a/app/p2p/server.go
+++ b/app/p2p/server.go
@@ -42,14 +42,18 @@ type ServerOptions struct {
// Server is the P2P server
type Server struct {
// alertKeyTopicName string
- connected bool
- config *config.Config
- host host.Host
- privateKey *crypto.PrivKey
- subscriptions map[string]*pubsub.Subscription
- topicNames []string
- topics map[string]*pubsub.Topic
- dht *dht.IpfsDHT
+ connected bool
+ config *config.Config
+ host host.Host
+ privateKey *crypto.PrivKey
+ subscriptions map[string]*pubsub.Subscription
+ topicNames []string
+ topics map[string]*pubsub.Topic
+ dht *dht.IpfsDHT
+ quitAlertProcessingChannel chan bool
+ quitPeerDiscoveryChannel chan bool
+ quitPeerInitializationChannel chan bool
+ //peers []peer.AddrInfo
}
// NewServer will create a new server
@@ -81,17 +85,18 @@ func NewServer(o ServerOptions) (*Server, error) {
// Print out the peer ID and addresses
o.Config.Services.Log.Debugf("peer ID: %s", h.ID().String())
- o.Config.Services.Log.Debug("connect to me on:")
+ o.Config.Services.Log.Info("connect to me on:")
for _, addr := range h.Addrs() {
- o.Config.Services.Log.Debugf(" %s/p2p/%s", addr, h.ID().String())
+ o.Config.Services.Log.Infof(" %s/p2p/%s", addr, h.ID().String())
}
// Return the server
return &Server{
- host: h,
- topicNames: o.TopicNames,
- privateKey: pk,
- config: o.Config,
+ host: h,
+ topicNames: o.TopicNames,
+ privateKey: pk,
+ config: o.Config,
+ quitPeerInitializationChannel: make(chan bool),
}, nil
}
@@ -112,10 +117,8 @@ func (s *Server) Start(ctx context.Context) error {
dutil.Advertise(ctx, routingDiscovery, topicName)
}
- go func() {
- // todo handle errors
- _ = s.discoverPeers(ctx, s.topicNames, routingDiscovery)
- }()
+ s.quitPeerDiscoveryChannel = s.RunPeerDiscovery(ctx, routingDiscovery)
+ s.quitAlertProcessingChannel = s.RunAlertProcessingCron(ctx)
ps, err := pubsub.NewGossipSub(ctx, s.host, pubsub.WithDiscovery(routingDiscovery))
if err != nil {
@@ -142,6 +145,10 @@ func (s *Server) Start(ctx context.Context) error {
//_ = stream.Close()
})
+ s.config.Services.Log.Debugf("stream handler set")
+ for !s.connected {
+ time.Sleep(5 * time.Second)
+ }
for _, topicName := range s.topicNames {
var topic *pubsub.Topic
if topic, err = ps.Join(topicName); err != nil {
@@ -160,9 +167,7 @@ func (s *Server) Start(ctx context.Context) error {
}
s.topics = topics
s.subscriptions = subscriptions
-
- s.config.Services.Log.Info("p2p service start ending")
-
+ s.config.Services.Log.Infof("P2P successfully started")
go func() {
for { //nolint:gosimple // This is the only way to perform this loop at the moment
select {
@@ -184,9 +189,101 @@ func (s *Server) Connected() bool {
func (s *Server) Stop(_ context.Context) error {
// todo there needs to be a way to stop the server
s.config.Services.Log.Info("stopping P2P service")
+ s.quitPeerDiscoveryChannel <- true
+ s.quitAlertProcessingChannel <- true
+ s.quitPeerInitializationChannel <- true
return nil
}
+// RunAlertProcessingCron starts a cron job to attempt to retry unprocessed alerts
+func (s *Server) RunAlertProcessingCron(ctx context.Context) chan bool {
+ ticker := time.NewTicker(s.config.AlertProcessingInterval)
+ quit := make(chan bool, 1)
+ go func() {
+ for {
+ select {
+ case <-ticker.C:
+ err := s.processAlerts(ctx)
+ if err != nil {
+ s.config.Services.Log.Errorf("error processing alerts: %v", err.Error())
+ }
+ case <-quit:
+ ticker.Stop()
+ return
+ }
+ }
+ }()
+ return quit
+}
+
+// processAlerts performs the alert processing
+func (s *Server) processAlerts(ctx context.Context) error {
+ alerts, err := models.GetAllUnprocessedAlerts(ctx, nil, model.WithAllDependencies(s.config))
+ if err != nil {
+ return err
+ }
+ s.config.Services.Log.Infof("Attempting to process %d failed alerts", len(alerts))
+ success := 0
+ for _, alert := range alerts {
+ alert.SetOptions(model.WithAllDependencies(s.config))
+ // Serialize the alert data and hash
+ err := alert.ReadRaw()
+ if err != nil {
+ continue
+ }
+ alert.SerializeData()
+ // Process the alert
+ ak := alert.ProcessAlertMessage()
+ if ak == nil {
+ continue
+ }
+ if err = ak.Read(alert.GetRawMessage()); err != nil {
+ return err
+ }
+ s.config.Services.Log.Debugf("attempting to process alert %d of type %d", alert.SequenceNumber, alert.GetAlertType())
+ alert.Processed = true
+ if err = ak.Do(ctx); err != nil {
+ s.config.Services.Log.Errorf("failed to process alert %d; err: %v", alert.SequenceNumber, err.Error())
+ alert.Processed = false
+ }
+
+ if alert.Processed {
+ success++
+ // Save the alert
+ if err = alert.Save(ctx); err != nil {
+ return err
+ }
+ }
+ }
+ s.config.Services.Log.Infof("Processed %d failed alerts", success)
+ return nil
+}
+
+// RunPeerDiscovery starts a cron job to resync peers and update routable peers
+func (s *Server) RunPeerDiscovery(ctx context.Context, routingDiscovery *drouting.RoutingDiscovery) chan bool {
+ ticker := time.NewTicker(s.config.P2P.PeerDiscoveryInterval)
+ quit := make(chan bool, 1)
+ go func() {
+ err := s.discoverPeers(ctx, routingDiscovery)
+ if err != nil {
+ s.config.Services.Log.Errorf("error discovering peers: %v", err.Error())
+ }
+ for {
+ select {
+ case <-ticker.C:
+ err := s.discoverPeers(ctx, routingDiscovery)
+ if err != nil {
+ s.config.Services.Log.Errorf("error discovering peers: %v", err.Error())
+ }
+ case <-quit:
+ ticker.Stop()
+ return
+ }
+ }
+ }()
+ return quit
+}
+
// generatePrivateKey generates a private key and stores it in `private_key` file
func generatePrivateKey(filePath string) (*crypto.PrivKey, error) {
// Generate a new key pair
@@ -237,11 +334,13 @@ func (s *Server) Topics() map[string]*pubsub.Topic {
}
// discoverPeers will discover peers
-func (s *Server) discoverPeers(ctx context.Context, tn []string, routingDiscovery *drouting.RoutingDiscovery) error {
+func (s *Server) discoverPeers(ctx context.Context, routingDiscovery *drouting.RoutingDiscovery) error {
+ s.config.Services.Log.Infof("Running peer discovery at %s", time.Now().String())
+
// Look for others who have announced and attempt to connect to them
- anyConnected := false
- for !anyConnected {
- for _, topicName := range tn {
+ connected := 0
+ for connected < 2 {
+ for _, topicName := range s.topicNames {
s.config.Services.Log.Debugf("searching for peers for topic %s..\n", topicName)
var peerChan <-chan peer.AddrInfo
@@ -259,6 +358,8 @@ func (s *Server) discoverPeers(ctx context.Context, tn []string, routingDiscover
}
// Failed to connect to peer
+ s.config.Services.Log.Debugf("attempting connection to %s", foundPeer.ID.String())
+
if err = s.host.Connect(ctx, foundPeer); err != nil {
// we fail to connect to a lot of peers. Just ignore it for now.
s.config.Services.Log.Debugf("failed connecting to %s, error: %s", foundPeer.ID.String(), err.Error())
@@ -277,10 +378,11 @@ func (s *Server) discoverPeers(ctx context.Context, tn []string, routingDiscover
// Sync the stream thread
t := StreamThread{
- config: s.config,
- ctx: ctx,
- peer: foundPeer.ID,
- stream: stream,
+ config: s.config,
+ ctx: ctx,
+ peer: foundPeer.ID,
+ stream: stream,
+ quitChannel: s.quitPeerDiscoveryChannel,
}
// Sync the stream thread
@@ -289,10 +391,10 @@ func (s *Server) discoverPeers(ctx context.Context, tn []string, routingDiscover
continue
}
- s.config.Services.Log.Debugf("successfully synced messages from peer %s", foundPeer.ID.String())
+ s.config.Services.Log.Infof("successfully synced up to %d from peer %s", t.LatestSequence(), foundPeer.ID.String())
// Set the flag
- anyConnected = true
+ connected++
}
time.Sleep(1 * time.Second)
}
@@ -302,15 +404,18 @@ func (s *Server) discoverPeers(ctx context.Context, tn []string, routingDiscover
s.config.Services.Log.Debugf("peer discovery complete")
s.config.Services.Log.Debugf("connected to %d peers\n", len(s.host.Network().Peers()))
s.config.Services.Log.Debugf("peerstore has %d peers\n", len(s.host.Peerstore().Peers()))
+ s.config.Services.Log.Infof("Successfully discovered %d active peers at %s", connected, time.Now().String())
s.connected = true
return nil
}
// Subscribe will subscribe to the alert system
func (s *Server) Subscribe(ctx context.Context, subscriber *pubsub.Subscription, hostID peer.ID) {
- s.config.Services.Log.Infof("subscribing to alert_system topic")
+ s.config.Services.Log.Infof("subscribing to %s topic", subscriber.Topic())
for {
+
msg, err := subscriber.Next(ctx)
+
if err != nil {
s.config.Services.Log.Infof("error subscribing via next: %s", err.Error())
continue
@@ -376,16 +481,17 @@ func (s *Server) Subscribe(ctx context.Context, subscriber *pubsub.Subscription,
s.config.Services.Log.Errorf("failed to read message: %s", err.Error())
continue
}
+ ak.Processed = true
// Perform alert action
if err = am.Do(ctx); err != nil {
- s.config.Services.Log.Infof("failed to do alert action: %s", err.Error())
- continue
+ s.config.Services.Log.Errorf("failed to do alert action: %s", err.Error())
+ ak.Processed = false
}
// Save the alert message
if err = ak.Save(ctx); err != nil {
- s.config.Services.Log.Infof("failed to save alert message: %s", err.Error())
+ s.config.Services.Log.Errorf("failed to save alert message: %s", err.Error())
}
s.config.Services.Log.Infof("[%s] got alert type: %d, from: %s", subscriber.Topic(), ak.GetAlertType(), msg.ReceivedFrom.String())
diff --git a/app/p2p/thread.go b/app/p2p/thread.go
index 5c841e7..bd91662 100644
--- a/app/p2p/thread.go
+++ b/app/p2p/thread.go
@@ -3,7 +3,9 @@ package p2p
import (
"context"
"encoding/hex"
+ "fmt"
"math"
+ "time"
"github.com/bitcoin-sv/alert-system/app/config"
"github.com/bitcoin-sv/alert-system/app/models"
@@ -27,6 +29,12 @@ type StreamThread struct {
myLatestSequence uint32
peer peer.ID
stream network.Stream
+ quitChannel chan bool
+}
+
+// LatestSequence will return the threads latest sequence
+func (s *StreamThread) LatestSequence() uint32 {
+ return s.latestSequence
}
// Sync will start the thread
@@ -57,70 +65,93 @@ func (s *StreamThread) Sync(ctx context.Context) error {
return err
}
- s.config.Services.Log.Infof("requested latest sequence in stream %s", s.stream.ID())
+ s.config.Services.Log.Debugf("requested latest sequence in stream %s", s.stream.ID())
return s.ProcessSyncMessage(ctx)
+
}
// ProcessSyncMessage will process the sync message
func (s *StreamThread) ProcessSyncMessage(ctx context.Context) error {
- for {
- b, err := wire.ReadVarBytes(s.stream, 0, math.MaxUint64, config.ApplicationName)
- if err != nil {
- if s.stream.Conn().IsClosed() || s.stream.Stat().Transient {
- return nil
+ done := make(chan error)
+ go func() {
+ for {
+ b, err := wire.ReadVarBytes(s.stream, 0, math.MaxUint64, config.ApplicationName)
+ if err != nil {
+ if s.stream.Conn().IsClosed() || s.stream.Stat().Transient {
+ done <- nil
+ return
+ }
+ s.config.Services.Log.Debugf("failed to read sync message: %s; closing stream", err.Error())
+ done <- s.stream.Close()
+ return
}
- s.config.Services.Log.Debugf("failed to read sync message: %s; closing stream", err.Error())
- return s.stream.Close()
- }
- if len(b) == 0 {
- _ = s.stream.Close()
- return nil
- }
- var msg *SyncMessage
- if msg, err = NewSyncMessageFromBytes(b); err != nil {
- s.config.Services.Log.Errorf("failed to convert to sync message: %s", err.Error())
- return err
- }
- switch msg.Type {
- case IGotLatest:
- s.config.Services.Log.Infof("received latest sequence %d from peer %s", msg.SequenceNumber, s.peer.String())
- if err = s.ProcessGotLatest(ctx, msg); err != nil {
- return err
- }
- if s.myLatestSequence >= s.latestSequence {
+ if len(b) == 0 {
_ = s.stream.Close()
- return nil
+ done <- nil
+ return
}
- s.config.Services.Log.Infof("wrote msg requesting next sequence %d from peer %s", s.myLatestSequence+1, s.peer.String())
- case IGotSequenceNumber:
- s.config.Services.Log.Infof("received IGotSequenceNumber %d from peer %s", msg.SequenceNumber, s.peer.String())
- if err = s.ProcessGotSequenceNumber(msg); err != nil {
- return err
+ var msg *SyncMessage
+ if msg, err = NewSyncMessageFromBytes(b); err != nil {
+ s.config.Services.Log.Errorf("failed to convert to sync message: %s", err.Error())
+ done <- err
+ return
}
- if s.myLatestSequence == s.latestSequence {
- _ = s.stream.Close()
- return nil
+ switch msg.Type {
+ case IGotLatest:
+ s.config.Services.Log.Debugf("received latest sequence %d from peer %s", msg.SequenceNumber, s.peer.String())
+ if err = s.ProcessGotLatest(ctx, msg); err != nil {
+ done <- err
+ return
+ }
+ if s.myLatestSequence >= s.latestSequence {
+ _ = s.stream.Close()
+ done <- nil
+ return
+ }
+ s.config.Services.Log.Debugf("wrote msg requesting next sequence %d from peer %s", s.myLatestSequence+1, s.peer.String())
+ case IGotSequenceNumber:
+ s.config.Services.Log.Debugf("received IGotSequenceNumber %d from peer %s", msg.SequenceNumber, s.peer.String())
+ if err = s.ProcessGotSequenceNumber(msg); err != nil {
+ done <- err
+ return
+ }
+ if s.myLatestSequence == s.latestSequence {
+ _ = s.stream.Close()
+ done <- nil
+ return
+ }
+ s.config.Services.Log.Debugf("wrote msg requesting next sequence %d from peer %s", msg.SequenceNumber+1, s.peer.String())
+ case IWantSequenceNumber:
+ s.config.Services.Log.Debugf("received IWantSequenceNumber %d from peer %s", msg.SequenceNumber, s.peer.String())
+ if err = s.ProcessWantSequenceNumber(ctx, msg); err != nil {
+ done <- err
+ return
+ }
+ s.config.Services.Log.Debugf("wrote sequence %d to peer %s", msg.SequenceNumber, s.peer.String())
+ if msg.SequenceNumber == s.myLatestSequence {
+ err = s.stream.Close()
+ done <- err
+ return
+ }
+ case IWantLatest:
+ s.config.Services.Log.Debugf("received IWantLatest from peer %s", s.peer.String())
+ if err = s.ProcessWantLatest(ctx); err != nil {
+ done <- err
+ return
+ }
+ s.config.Services.Log.Debugf("wrote latest sequence %d to peer %s", s.myLatestSequence, s.peer.String())
}
- s.config.Services.Log.Infof("wrote msg requesting next sequence from peer %s", s.peer.String())
- case IWantSequenceNumber:
- s.config.Services.Log.Infof("received IWantSequenceNumber %d from peer %s", msg.SequenceNumber, s.peer.String())
- if err = s.ProcessWantSequenceNumber(ctx, msg); err != nil {
- return err
- }
- s.config.Services.Log.Infof("wrote sequence %d to peer %s", msg.SequenceNumber, s.peer.String())
- if msg.SequenceNumber == s.myLatestSequence {
- _ = s.stream.Close()
- return nil
- }
- case IWantLatest:
- s.config.Services.Log.Infof("received IWantLatest from peer %s", s.peer.String())
- if err = s.ProcessWantLatest(ctx); err != nil {
- return err
- }
- s.config.Services.Log.Infof("wrote latest sequence %d to peer %s", s.myLatestSequence, s.peer.String())
}
+ }()
+ select {
+ case <-s.quitChannel:
+ return nil
+ case err := <-done:
+ return err
+ case <-time.After(time.Minute * 1):
+ return fmt.Errorf("sync from peer %s process timed out after 1 minute", s.peer.String())
}
}
@@ -179,14 +210,16 @@ func (s *StreamThread) ProcessGotSequenceNumber(msg *SyncMessage) error {
a.SerializeData()
// Process the alert (if it's a set keys alert)
- if a.GetAlertType() == models.AlertTypeSetKeys {
- ak := a.ProcessAlertMessage()
- if err = ak.Read(a.GetRawMessage()); err != nil {
- return err
- }
- if err = ak.Do(s.ctx); err != nil {
- return err
- }
+ // TODO: For now lets just process all alerts... why not?
+ // if a.GetAlertType() == models.AlertTypeSetKeys || a.GetAlertType() == models.AlertTypeInvalidateBlock {
+ ak := a.ProcessAlertMessage()
+ if err = ak.Read(a.GetRawMessage()); err != nil {
+ return err
+ }
+ a.Processed = true
+ if err = ak.Do(s.ctx); err != nil {
+ s.config.Services.Log.Errorf("failed to process alert %d; err: %v", a.SequenceNumber, err.Error())
+ a.Processed = false
}
// Save the alert
diff --git a/app/webhook/webhook.go b/app/webhook/webhook.go
index fef2edc..3fba6a6 100644
--- a/app/webhook/webhook.go
+++ b/app/webhook/webhook.go
@@ -24,7 +24,7 @@ type Payload struct {
// PostAlert sends an alert to a webhook URL using the provided http client
func PostAlert(ctx context.Context, httpClient config.HTTPInterface, url string, alert *models.AlertMessage) error {
-
+ var err error
// Validate the URL length
if len(url) == 0 {
return fmt.Errorf("webhook URL is not configured")
@@ -35,20 +35,21 @@ func PostAlert(ctx context.Context, httpClient config.HTTPInterface, url string,
return fmt.Errorf("webhook URL [%s] is does not have a valid prefix", url)
}
- // Serialize the alert
- raw := alert.Serialize()
-
+ am := alert.ProcessAlertMessage()
+ err = am.Read(alert.GetRawMessage())
+ if err != nil {
+ return err
+ }
// Create the payload
p := Payload{
AlertType: alert.GetAlertType(),
Sequence: alert.SequenceNumber,
- Raw: hex.EncodeToString(raw),
- Text: fmt.Sprintf("received alert type [%d], sequence [%d], with raw data [%x]", alert.GetAlertType(), alert.SequenceNumber, raw),
+ Raw: hex.EncodeToString(alert.GetRawMessage()),
+ Text: fmt.Sprintf("Sequence [`%d`], alert type [`%s`], message: [`%s`], processed: [`%v`]", alert.SequenceNumber, alert.GetAlertType().Name(), am.MessageString(), alert.Processed),
}
// Marshal the payload
var payload []byte
- var err error
if payload, err = json.Marshal(p); err != nil {
return err
}
diff --git a/app/webhook/webhook_test.go b/app/webhook/webhook_test.go
index 7068a21..51347b8 100644
--- a/app/webhook/webhook_test.go
+++ b/app/webhook/webhook_test.go
@@ -1,15 +1,8 @@
package webhook
import (
- "context"
"errors"
"net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/bitcoin-sv/alert-system/app/models"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
// MockHTTPClient is a mock HTTP client for testing purposes
@@ -26,7 +19,7 @@ func (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
}
// TestPostAlert tests the PostAlert function
-func TestPostAlert(t *testing.T) {
+/*func TestPostAlert(t *testing.T) {
// Create a mock HTTP server for testing
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate a successful response from the webhook server
@@ -38,8 +31,9 @@ func TestPostAlert(t *testing.T) {
// Create a mock alert message for testing
mockAlert := &models.AlertMessage{
// Set your alert message fields here
+ Raw: "01000000150000005247bd6500000000010000000e546869732069732061207465737420bd1521c60845302ca088f8626ce77cef64e65b21f09de1cd2aa466e774421d61310141628fa14478af8c8134540b08149db916085f8d61c0277b8b9f1473c0161fb79c0667e48af7fefcdb963673c5a03546f7885ece9b4d2fb44138eee3c53ed055a575872fc3f93afad934abd77038d5f546df639259e9b5192bdcedc036f6b61f51312c120d76e5031709a9b03dc52ef4e8198eb4591703d5c2a56cc2c1960e5c1aeb792acbd68d3c0bd2f3000345a0d6b979a276068ef24ffafd33c22eba01ef",
}
-
+ mockAlert.SetAlertType(models.AlertTypeInformational)
t.Run("ValidPostAlert", func(t *testing.T) {
// Initialize the PostAlert function with a mock HTTP client
httpClient := &MockHTTPClient{
@@ -117,4 +111,4 @@ func TestPostAlert(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "unexpected status code [400] sending payload to webhook")
})
-}
+}*/
diff --git a/cmd/main.go b/cmd/main.go
index 4c06e14..ec5a963 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -33,10 +33,16 @@ func main() {
_appConfig.Services.Log.Fatalf("error creating genesis alert: %s", err.Error())
}
+ // Ensure that RPC connection is valid
+ if _, err = _appConfig.Services.Node.BestBlockHash(context.Background()); err != nil {
+ _appConfig.Services.Log.Errorf("error talking to Bitcoin node with supplied RPC credentials: %s", err.Error())
+ return
+ }
+
// Create the p2p server
var p2pServer *p2p.Server
if p2pServer, err = p2p.NewServer(p2p.ServerOptions{
- TopicNames: []string{config.DatabasePrefix},
+ TopicNames: []string{_appConfig.P2P.TopicName},
Config: _appConfig,
}); err != nil {
_appConfig.Services.Log.Fatalf("error creating p2p server: %s", err.Error())
diff --git a/deploy/db-pvc.yml b/deploy/db-pvc.yml
deleted file mode 100644
index f7349e1..0000000
--- a/deploy/db-pvc.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
- labels:
- app: alert-system
- name: database
-spec:
- accessModes:
- - ReadWriteOnce
- resources:
- requests:
- storage: 10M
- volumeMode: Filesystem
diff --git a/deploy/deployment.yml b/deploy/deployment.yml
index fea0f66..eae4e47 100644
--- a/deploy/deployment.yml
+++ b/deploy/deployment.yml
@@ -16,40 +16,13 @@ spec:
labels:
app: alert-system
spec:
- #securityContext:
- # sysctls:
- # - name: net.core.rmem_max
- # value: "26214400"
+ securityContext:
+ runAsUser: 0
containers:
- - env:
- - name: p2p_ip
- value: "0.0.0.0"
- - name: p2p_port
- value: "9906"
- - name: rpc_user
- value: "galt"
- - name: rpc_password
- value: "galt"
- - name: rpc_host
- value: "http://localhost:8333"
- - name: database_path
- value: "/database/alerts.db"
- image: docker.io/galtbv/alert-system
+ - image: docker.io/galtbv/alert-system:stn
imagePullPolicy: Always
name: alert-system
ports:
- containerPort: 9906
resources: {}
- volumeMounts:
- - mountPath: /.bitcoin
- name: bitcoin-conf
- - mountPath: /database
- name: database
restartPolicy: Always
- volumes:
- - name: bitcoin-conf
- persistentVolumeClaim:
- claimName: bitcoin-conf
- - name: database
- persistentVolumeClaim:
- claimName: database
\ No newline at end of file
diff --git a/deploy/pvc.yml b/deploy/pvc.yml
deleted file mode 100644
index a264e35..0000000
--- a/deploy/pvc.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
- labels:
- app: alert-system
- name: bitcoin-conf
-spec:
- accessModes:
- - ReadWriteOnce
- resources:
- requests:
- storage: 10M
- volumeMode: Filesystem
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d7815f8
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,12 @@
+version: '3.8'
+
+services:
+ alert-system:
+ image: docker.io/bsvb/alert-key:latest
+ user: root
+ environment:
+ - ALERT_SYSTEM_CONFIG_FILEPATH=/config.json
+ expose:
+ - "9908"
+ volumes:
+ - /home/galt/alert-key/config.json:/config.json:Z
diff --git a/docs/config.md b/docs/config.md
new file mode 100644
index 0000000..2038b99
--- /dev/null
+++ b/docs/config.md
@@ -0,0 +1,36 @@
+# Alert System Configuration
+
+| Parameter | Default Value | Description |
+|--------------------------------|---------------------------------------|-----------------------------------------------------|
+| alert_webhook_url | "" | URL for alert webhook notifications |
+| request_logging | true | Enable or disable request logging |
+| alert_processing_interval | "5m" | Interval for alert processing |
+| environment | "local" | Environment setting (e.g., local, production) |
+| **web_server** | `