├── ansible ├── ansible.cfg ├── config │ ├── node-seed.yml │ └── miner-seed.yml ├── inventories │ └── seed │ │ └── hosts.ini └── playbooks │ ├── visualization.yml │ ├── roles │ ├── logging │ │ ├── tasks │ │ │ ├── 01_install_dependencies.yml │ │ │ ├── main.yml │ │ │ ├── 04_provision_grafana.yml │ │ │ ├── 02_install_loki.yml │ │ │ └── 03_install_promtail.yml │ │ └── files │ │ │ ├── loki-source.yaml │ │ │ ├── loki.service │ │ │ ├── promtail.service │ │ │ ├── dashboard_provision_logging.yml │ │ │ ├── promtail-config.yaml │ │ │ └── loki-config.yaml │ ├── monitoring │ │ ├── files │ │ │ ├── prometheus-source.yaml │ │ │ ├── prom.service │ │ │ └── dashboard_provision_monitoring.yml │ │ ├── tasks │ │ │ ├── main.yml │ │ │ ├── 01_install_dependencies.yml │ │ │ ├── 03_provision_grafana.yml │ │ │ └── 02_install_prometheus.yml │ │ └── templates │ │ │ └── prometheus.yml.j2 │ ├── visualization │ │ ├── tasks │ │ │ ├── main.yml │ │ │ ├── 01_install_grafana.yml │ │ │ ├── 03_provision_grafana.yml │ │ │ └── 02_install_nginx.yml │ │ ├── files │ │ │ ├── dashboard_provision_list.yml │ │ │ └── dashboard-list.json │ │ └── templates │ │ │ ├── dashboard-nginx.j2 │ │ │ └── dashboard-nginx-with-SSL.j2 │ └── blockchain │ │ ├── templates │ │ └── systemd-chain.service.j2 │ │ └── tasks │ │ └── main.yml │ ├── check_grafana.yml │ ├── blockchain.yml │ ├── monitoring.yml │ ├── logging.yml │ └── install-SSL.yml ├── helm ├── templates │ ├── NOTES.txt │ ├── configmap.yaml │ ├── tests │ │ └── test-connection.yaml │ ├── persistentvolumeclaim.yaml │ ├── miner.yaml │ ├── node.yaml │ └── _helpers.tpl ├── values.yaml └── Chart.yaml ├── pkg ├── chain │ ├── bloom │ │ ├── bloom_test.go │ │ └── bloom.go │ ├── iterator │ │ ├── iterator.go │ │ ├── reverse_block.go │ │ ├── reverse_header.go │ │ └── reverse_block_test.go │ └── chain_test.go ├── consensus │ ├── merkletree_test.go │ ├── validator │ │ ├── metrics.go │ │ ├── light_validator_test.go │ │ └── light_validator.go │ ├── consensus.go │ └── merkletree.go ├── common │ └── consts.go ├── wallet │ ├── common_test.go │ ├── hd_wallet │ │ ├── hd_wallet_test.go │ │ ├── account_test.go │ │ └── metadata.go │ ├── common.go │ └── simple_wallet │ │ └── wallet_test.go ├── network │ ├── pubsub │ │ ├── pubsub.go │ │ └── gossip.go │ ├── discovery │ │ ├── discovery.go │ │ ├── dht.go │ │ └── mdns.go │ ├── protobuf │ │ └── kernel.proto │ ├── events │ │ └── host.go │ ├── common.go │ └── p2p_timeout.go ├── crypto │ ├── hash │ │ ├── void.go │ │ └── hash.go │ ├── multi_hash.go │ ├── hashed_signature.go │ └── sign │ │ ├── signature.go │ │ ├── ecdsa_p256_test.go │ │ └── ecdsa_p256.go ├── script │ ├── stack.go │ └── interpreter │ │ └── interpreter.go ├── errs │ └── error.go ├── util │ ├── script │ │ └── script.go │ ├── mutex │ │ └── ctx_mutex.go │ ├── p2pkh │ │ ├── p2pkh_test.go │ │ └── p2pkh.go │ ├── util_test.go │ └── util.go ├── kernel │ ├── utxo.go │ └── block.go ├── mempool │ └── mempool_explorer.go ├── storage │ ├── boltdb_test.go │ └── storage.go ├── encoding │ ├── encoding.go │ ├── gob.go │ └── gob_test.go ├── monitor │ ├── monitor.go │ └── prometheus.go ├── observer │ ├── chain_observer.go │ └── net_observer.go ├── miner │ ├── pow_test.go │ └── pow.go └── utxoset │ └── utxos.go ├── .gitignore ├── cmd ├── cli │ ├── main.go │ └── cmd │ │ ├── cmd.go │ │ └── tx.go ├── nespv │ ├── main.go │ └── cmd │ │ ├── cmd.go │ │ ├── addresses.go │ │ └── send.go ├── miner │ ├── cmd.go │ └── main.go └── node │ ├── cmd.go │ └── main.go ├── tests └── mocks │ ├── chain │ ├── explorer │ │ └── explorer_mock.go │ └── iterator │ │ └── iterator_mock.go │ ├── crypto │ ├── hash │ │ ├── hash_fake.go │ │ └── hash_mock.go │ └── sign │ │ └── sign_mock.go │ ├── consensus │ ├── light_validator_mock.go │ ├── heavy_validator_mock.go │ └── consensus_mock.go │ ├── network │ └── net_wallet.go │ ├── util │ └── block_matcher.go │ ├── encoding │ └── encoding_mock.go │ └── storage │ └── storage_mock.go ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .goreleaser.yaml ├── config └── examples │ ├── docker-config.yaml │ ├── kubernetes-config.yaml │ └── seed-node-config.yaml ├── default-config.yaml └── Makefile /ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ansible/config/node-seed.yml: -------------------------------------------------------------------------------- 1 | target: node 2 | config: ../../config/examples/seed-node-config.yaml -------------------------------------------------------------------------------- /ansible/config/miner-seed.yml: -------------------------------------------------------------------------------- 1 | target: miner 2 | config: ../../config/examples/seed-node-config.yaml 3 | -------------------------------------------------------------------------------- /pkg/chain/bloom/bloom_test.go: -------------------------------------------------------------------------------- 1 | package bloom //nolint:testpackage // don't create separate package for tests 2 | -------------------------------------------------------------------------------- /pkg/consensus/merkletree_test.go: -------------------------------------------------------------------------------- 1 | package consensus //nolint:testpackage // don't create separate package for tests 2 | -------------------------------------------------------------------------------- /ansible/inventories/seed/hosts.ini: -------------------------------------------------------------------------------- 1 | [nodes] 2 | seed-1.chainnet.yago.ninja ansible_user=ubuntu identity_path=~/.ssh/seed-node-1.pem 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __debug_bin* 2 | .idea 3 | bin/ 4 | *.pb.go 5 | *.pem 6 | *.data 7 | _fixture/ 8 | # Added by goreleaser init: 9 | dist/ 10 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/cmd/cli/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /tests/mocks/chain/explorer/explorer_mock.go: -------------------------------------------------------------------------------- 1 | package explorer 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type MockExplorer struct { 6 | mock.Mock 7 | } 8 | -------------------------------------------------------------------------------- /helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ .Release.Name }}-config 5 | data: 6 | config.yaml: |- 7 | {{ .Values.configFile | indent 4 }} -------------------------------------------------------------------------------- /cmd/nespv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/yago-123/chainnet/cmd/nespv/cmd" 6 | ) 7 | 8 | func main() { 9 | cmd.Execute(logrus.New()) 10 | } 11 | -------------------------------------------------------------------------------- /ansible/playbooks/visualization.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Deploy Grafana and Nginx 3 | hosts: all 4 | become: true 5 | become_method: sudo 6 | vars: 7 | domain: dashboard.chainnet.yago.ninja 8 | roles: 9 | - visualization -------------------------------------------------------------------------------- /pkg/common/consts.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/yago-123/chainnet/pkg/kernel" 4 | 5 | const ( 6 | InitialCoinbaseReward = 50 * kernel.ChainnetCoinAmount 7 | HalvingInterval = 210000 8 | MaxNumberHalvings = 64 9 | ) 10 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/tasks/01_install_dependencies.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install dependencies (wget, unzip, curl) 3 | apt: 4 | name: 5 | - wget 6 | - unzip 7 | - curl 8 | state: present 9 | update_cache: yes 10 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/files/loki-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: Loki 4 | type: loki 5 | access: proxy 6 | url: http://127.0.0.1:3100 7 | isDefault: false 8 | jsonData: 9 | maxLines: 1000 10 | timeInterval: 60s 11 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/files/loki.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Loki service 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/loki-linux-amd64 -config.file=/etc/loki/loki-config.yaml 7 | Restart=always 8 | 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /pkg/wallet/common_test.go: -------------------------------------------------------------------------------- 1 | package wallet //nolint:testpackage // don't create separate package for tests 2 | 3 | import "testing" 4 | 5 | func TestGenerateInputs(_ *testing.T) { 6 | // todo: implement 7 | } 8 | 9 | func TestGenerateOutputs(_ *testing.T) { 10 | // todo: implement 11 | } 12 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/files/prometheus-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: "Chainnet Prometheus data source" 4 | type: prometheus 5 | access: proxy 6 | url: http://127.0.0.1:9092 7 | isDefault: true 8 | jsonData: 9 | keepCookies: [] 10 | 11 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/files/promtail.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Promtail service 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/promtail-linux-amd64 -config.file=/etc/promtail/promtail-config.yaml 7 | Restart=always 8 | 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install dependencies 3 | include_tasks: 01_install_dependencies.yml 4 | 5 | - name: Install and configure Prometheus 6 | include_tasks: 02_install_prometheus.yml 7 | 8 | - name: Provision Grafana files for Prometheus 9 | include_tasks: 03_provision_grafana.yml -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/tasks/01_install_dependencies.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install prerequisites 3 | apt: 4 | name: 5 | - wget 6 | - curl 7 | - gnupg 8 | - apt-transport-https 9 | - software-properties-common 10 | - nginx 11 | state: present 12 | update_cache: yes 13 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Grafana 3 | include_tasks: 01_install_grafana.yml 4 | 5 | - name: Install reverse proxy via Nginx 6 | include_tasks: 02_install_nginx.yml 7 | 8 | - name: Provision Grafana files for list of charts 9 | include_tasks: 03_provision_grafana.yml 10 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/files/prom.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus Monitoring 3 | After=network.target 4 | 5 | [Service] 6 | User=root 7 | ExecStart=/usr/local/bin/prometheus \ 8 | --config.file=/etc/prometheus/prometheus.yml \ 9 | --web.listen-address="0.0.0.0:9092" 10 | [Install] 11 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /pkg/network/pubsub/pubsub.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | ) 8 | 9 | type PubSub interface { 10 | NotifyBlockHeaderAdded(ctx context.Context, header kernel.BlockHeader) error 11 | NotifyTransactionAdded(ctx context.Context, tx kernel.Transaction) error 12 | } 13 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/files/dashboard_provision_logging.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'Monitoring Dashboards' 4 | orgId: 1 5 | folder: 'Logging' 6 | type: file 7 | disableDeletion: true 8 | updateIntervalSeconds: 10 9 | options: 10 | path: /etc/grafana/provisioning/dashboards/logging 11 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/files/dashboard_provision_list.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'Chart List' 4 | orgId: 1 5 | folder: '' 6 | type: file 7 | disableDeletion: true 8 | updateIntervalSeconds: 10 9 | options: 10 | path: /etc/grafana/provisioning/dashboards/list/dashboard-list.json 11 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/files/dashboard_provision_monitoring.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'Monitoring Dashboards' 4 | orgId: 1 5 | folder: 'Monitoring' 6 | type: file 7 | disableDeletion: true 8 | updateIntervalSeconds: 10 9 | options: 10 | path: /etc/grafana/provisioning/dashboards/custom/ 11 | -------------------------------------------------------------------------------- /ansible/playbooks/check_grafana.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if Grafana is running 3 | ansible.builtin.systemd: 4 | name: grafana-server 5 | state: started 6 | register: grafana_status 7 | ignore_errors: true 8 | 9 | - name: Set flag to indicate if Grafana is not running 10 | set_fact: 11 | grafana_installed: "{{ grafana_status.failed }}" 12 | -------------------------------------------------------------------------------- /pkg/network/discovery/discovery.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | DiscoveryServiceTag = "node-p2p-discovery" 9 | DiscoveryTimeout = 10 * time.Second 10 | ) 11 | 12 | // Discovery will be used to discover peers in the network level and connect to them 13 | type Discovery interface { 14 | Start() error 15 | Stop() error 16 | Type() string 17 | } 18 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install dependencies 3 | include_tasks: 01_install_dependencies.yml 4 | 5 | - name: Install and configure Loki 6 | include_tasks: 02_install_loki.yml 7 | 8 | - name: Install and configure Promtail 9 | include_tasks: 03_install_promtail.yml 10 | 11 | - name: Provision Grafana files for Loki 12 | include_tasks: 04_provision_grafana.yml 13 | -------------------------------------------------------------------------------- /pkg/chain/iterator/iterator.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | ) 6 | 7 | type BlockIterator interface { 8 | Initialize(reference []byte) error 9 | GetNextBlock() (*kernel.Block, error) 10 | HasNext() bool 11 | } 12 | 13 | type HeaderIterator interface { 14 | Initialize(reference []byte) error 15 | GetNextHeader() (*kernel.BlockHeader, error) 16 | HasNext() bool 17 | } 18 | -------------------------------------------------------------------------------- /ansible/playbooks/blockchain.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Deploy Chainnet blockchain on the network 3 | hosts: all 4 | become: true # Use sudo privileges 5 | become_method: sudo 6 | vars: 7 | app_dir: /var/chainnet 8 | config: 'default-config.yaml' 9 | repo_url: 'https://github.com/yago-123/chainnet.git' 10 | branch: 'master' 11 | go_version: '1.23.0' 12 | target: "{{ node }}" 13 | roles: 14 | - blockchain 15 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/blockchain/templates/systemd-chain.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Chainnet {{ target }} service 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart={{ app_dir }}/bin/chainnet-{{ target }} --config {{ app_dir }}/config.yaml 7 | Restart=always 8 | StandardOutput=append:/var/log/chainnet/service.log 9 | StandardError=append:/var/log/chainnet/service.err.log 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/templates/prometheus.yml.j2: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s # Set the default scrape interval to 15 seconds 3 | scrape_timeout: 5s # (Optional) Timeout for scraping metrics 4 | 5 | scrape_configs: 6 | - job_name: 'local_scrape' # Name of the scrape job 7 | metrics_path: '/metrics' 8 | static_configs: 9 | - targets: ['127.0.0.1:9091', '127.0.0.1:9099'] # Target chain and libp2p core metrics 10 | -------------------------------------------------------------------------------- /pkg/crypto/hash/void.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Void struct { 8 | } 9 | 10 | func NewVoidHasher() *Void { 11 | return &Void{} 12 | } 13 | 14 | func (v *Void) Hash(_ []byte) ([]byte, error) { 15 | return []byte{}, fmt.Errorf("void hasher does not hash anything") 16 | } 17 | 18 | func (v *Void) Verify(_ []byte, _ []byte) (bool, error) { 19 | return false, fmt.Errorf("void hasher does not verify anything") 20 | } 21 | -------------------------------------------------------------------------------- /pkg/script/stack.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | type Stack struct { 4 | items []string 5 | } 6 | 7 | func NewStack() *Stack { 8 | return &Stack{} 9 | } 10 | 11 | func (s *Stack) Push(item string) { 12 | s.items = append(s.items, item) 13 | } 14 | 15 | func (s *Stack) Pop() string { 16 | item := s.items[len(s.items)-1] 17 | s.items = s.items[:len(s.items)-1] 18 | return item 19 | } 20 | 21 | func (s *Stack) Len() uint { 22 | return uint(len(s.items)) 23 | } 24 | -------------------------------------------------------------------------------- /helm/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "chainnet.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "chainnet.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "chainnet.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /cmd/cli/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/spf13/cobra" 6 | "github.com/yago-123/chainnet/config" 7 | ) 8 | 9 | var cfg *config.Config 10 | var logger = logrus.New() 11 | 12 | var rootCmd = &cobra.Command{ 13 | Use: "chainnet-cli", 14 | Run: func(_ *cobra.Command, _ []string) { 15 | 16 | }, 17 | } 18 | 19 | func Execute() { 20 | if err := rootCmd.Execute(); err != nil { 21 | logger.Fatalf("error executing command: %v", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/consensus/validator/metrics.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | type HValidatorMetrics struct { 4 | txMetrics *HValidatorTxMetrics 5 | headerMetrics *HValidatorHeaderMetrics 6 | blockMetrics *HValidatorBlockMetrics 7 | } 8 | 9 | type HValidatorTxMetrics struct { 10 | totalAnalyzed, totalRejected uint64 11 | } 12 | 13 | type HValidatorHeaderMetrics struct { 14 | totalAnalyzed, totalRejected uint64 15 | } 16 | 17 | type HValidatorBlockMetrics struct { 18 | totalAnalyzed, totalRejected uint64 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /pkg/wallet/hd_wallet/hd_wallet_test.go: -------------------------------------------------------------------------------- 1 | package hdwallet //nolint:testpackage // don't create separate package for tests 2 | import "testing" 3 | 4 | // const privateKeyHDTest = ` 5 | // -----BEGIN EC PRIVATE KEY----- 6 | // MHcCAQEEII4Ci5GHuv3rO3q1L+2BHcBHqO/immA45VTmswGXxUYkoAoGCCqGSM49 7 | // AwEHoUQDQgAETm+qq1qRGebJyaGa6lBmgkC0NlaAo4iKOGEDczvj5A3lK6TLLe9u 8 | // 0MF7c9jWuMaNt3/lUjAtu8ja9uIALbQyHw== 9 | // -----END EC PRIVATE KEY----- 10 | // ` 11 | 12 | func TestHDWallet_Sync(_ *testing.T) { 13 | // todo: implement 14 | } 15 | -------------------------------------------------------------------------------- /tests/mocks/crypto/hash/hash_fake.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // FakeHashing adds "-hashed" to the payload provided so the hash can be predictable during tests 8 | type FakeHashing struct { 9 | } 10 | 11 | func (mh *FakeHashing) Hash(payload []byte) ([]byte, error) { 12 | return append(payload, []byte("-hashed")...), nil 13 | } 14 | 15 | func (mh *FakeHashing) Verify(hash []byte, payload []byte) (bool, error) { 16 | h, _ := mh.Hash(payload) 17 | 18 | return bytes.Equal(hash, h), nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/wallet/hd_wallet/account_test.go: -------------------------------------------------------------------------------- 1 | package hdwallet //nolint:testpackage // don't create separate package for tests 2 | import "testing" 3 | 4 | // const privateKeyAccountTest = ` 5 | // -----BEGIN EC PRIVATE KEY----- 6 | // MHcCAQEEII4Ci5GHuv3rO3q1L+2BHcBHqO/immA45VTmswGXxUYkoAoGCCqGSM49 7 | // AwEHoUQDQgAETm+qq1qRGebJyaGa6lBmgkC0NlaAo4iKOGEDczvj5A3lK6TLLe9u 8 | // 0MF7c9jWuMaNt3/lUjAtu8ja9uIALbQyHw== 9 | // -----END EC PRIVATE KEY----- 10 | // ` 11 | 12 | func TestHDAccount_Sync(_ *testing.T) { 13 | // todo: implement 14 | } 15 | -------------------------------------------------------------------------------- /tests/mocks/consensus/light_validator_mock.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type MockLightValidator struct { 10 | mock.Mock 11 | } 12 | 13 | func (m *MockLightValidator) ValidateTxLight(tx *kernel.Transaction) error { 14 | args := m.Called(tx) 15 | return args.Error(0) 16 | } 17 | 18 | func (m *MockLightValidator) ValidateHeader(bh *kernel.BlockHeader) error { 19 | args := m.Called(bh) 20 | return args.Error(0) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/miner/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/config" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "chainnet-miner", 12 | Run: func(cmd *cobra.Command, _ []string) { 13 | cfg = config.InitConfig(cmd) 14 | }, 15 | } 16 | 17 | func Execute(logger *logrus.Logger) { 18 | config.AddConfigFlags(rootCmd) 19 | 20 | if err := rootCmd.Execute(); err != nil { 21 | logger.Fatalf("error executing command: %v", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/node/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/config" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "chainnet-node", 12 | Run: func(cmd *cobra.Command, _ []string) { 13 | cfg = config.InitConfig(cmd) 14 | }, 15 | } 16 | 17 | func Execute(logger *logrus.Logger) { 18 | config.AddConfigFlags(rootCmd) 19 | 20 | if err := rootCmd.Execute(); err != nil { 21 | logger.Fatalf("error executing command: %v", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ansible/playbooks/monitoring.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup Chainnet monitoring 3 | hosts: all 4 | become: true 5 | become_method: sudo 6 | vars: 7 | prometheus_version: '2.47.0' 8 | domain: 'dashboard.chainnet.yago.ninja' 9 | tasks: 10 | - name: Check if Grafana is running 11 | import_tasks: check_grafana.yml 12 | 13 | - name: Install and start Grafana if not running 14 | include_role: 15 | name: visualization 16 | when: grafana_installed 17 | 18 | - name: Set up monitoring 19 | include_role: 20 | name: monitoring 21 | -------------------------------------------------------------------------------- /ansible/playbooks/logging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Loki and Promtail 3 | hosts: all 4 | become: true 5 | become_method: sudo 6 | vars: 7 | loki_version: "v2.8.0" 8 | promtail_version: "v2.8.0" 9 | domain: "dashboard.chainnet.yago.ninja" 10 | tasks: 11 | - name: Check if Grafana is running 12 | import_tasks: check_grafana.yml 13 | 14 | - name: Install and start Grafana if not running 15 | include_role: 16 | name: visualization 17 | when: grafana_installed 18 | 19 | - name: Set up logging 20 | include_role: 21 | name: logging -------------------------------------------------------------------------------- /pkg/errs/error.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import "github.com/pkg/errors" 4 | 5 | // Errors used in the storage package 6 | var ( 7 | ErrStorageElementNotFound = errors.New("not found") 8 | ) 9 | 10 | // Errors used in the wallet package 11 | var ( 12 | ErrWalletInvalidChildPrivateKey = errors.New("derived private key is invalid") 13 | ) 14 | 15 | // Errors used in the crypto package 16 | var ( 17 | ErrCryptoPublicKeyDerivation = errors.New("failed to derive public key from private key") 18 | ) 19 | 20 | // Errors used in the mempool package 21 | var ( 22 | ErrMemPoolFull = errors.New("mempool does not have enough space") 23 | ) 24 | -------------------------------------------------------------------------------- /tests/mocks/crypto/hash/hash_mock.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package hash 3 | 4 | import ( 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | // MockHashing adds "-hashed" to the payload provided so the hash can be predictable during tests 9 | type MockHashing struct { 10 | mock.Mock 11 | } 12 | 13 | func (mh *MockHashing) Hash(payload []byte) ([]byte, error) { 14 | args := mh.Called(payload) 15 | return args.Get(0).([]byte), args.Error(1) 16 | } 17 | 18 | func (mh *MockHashing) Verify(hash []byte, payload []byte) (bool, error) { 19 | args := mh.Called(hash, payload) 20 | return args.Bool(0), args.Error(1) 21 | } 22 | -------------------------------------------------------------------------------- /tests/mocks/consensus/heavy_validator_mock.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type MockHeavyValidator struct { 10 | mock.Mock 11 | } 12 | 13 | func NewMockHeavyValidator() *MockHeavyValidator { 14 | return &MockHeavyValidator{} 15 | } 16 | 17 | func (m *MockHeavyValidator) ValidateTx(_ *kernel.Transaction) error { 18 | return nil 19 | } 20 | 21 | func (m *MockHeavyValidator) ValidateHeader(_ *kernel.BlockHeader) error { 22 | return nil 23 | } 24 | 25 | func (m *MockHeavyValidator) ValidateBlock(_ *kernel.Block) error { 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /tests/mocks/chain/iterator/iterator_mock.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package iterator 3 | 4 | import ( 5 | "github.com/yago-123/chainnet/pkg/kernel" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type MockIterator struct { 11 | mock.Mock 12 | } 13 | 14 | func (i *MockIterator) Initialize(reference []byte) error { 15 | args := i.Called(reference) 16 | return args.Error(0) 17 | } 18 | 19 | func (i *MockIterator) GetNextBlock() (*kernel.Block, error) { 20 | args := i.Called() 21 | return args.Get(0).(*kernel.Block), args.Error(1) 22 | } 23 | 24 | func (i *MockIterator) HasNext() bool { 25 | args := i.Called() 26 | return args.Bool(0) 27 | } 28 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/templates/dashboard-nginx.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name {{ domain }}; 4 | 5 | # Redirect /list to the specific Grafana dashboard 6 | location /list { 7 | rewrite ^/list$ /d/eeazamyajw2kgc/dashboard-list?orgId=1&from=now-24h&to=now&timezone=browser permanent; 8 | } 9 | 10 | # Proxy all other requests to Grafana 11 | location / { 12 | proxy_pass http://localhost:3000; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Forwarded-Proto $scheme; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/tasks/01_install_grafana.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add Grafana GPG key 3 | get_url: 4 | url: https://packages.grafana.com/gpg.key 5 | dest: /etc/apt/trusted.gpg.d/grafana.asc 6 | become: true 7 | 8 | - name: Add Grafana repository 9 | apt_repository: 10 | repo: "deb https://packages.grafana.com/oss/deb stable main" 11 | state: present 12 | 13 | - name: Install Grafana 14 | apt: 15 | name: grafana 16 | state: present 17 | 18 | - name: Configure Grafana service 19 | template: 20 | src: templates/grafana.ini.j2 21 | dest: /etc/grafana/grafana.ini 22 | 23 | - name: Start Grafana service 24 | systemd: 25 | name: grafana-server 26 | enabled: true 27 | state: started -------------------------------------------------------------------------------- /pkg/util/script/script.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/btcsuite/btcutil/base58" 7 | ) 8 | 9 | const ( 10 | // ScriptSigSeparator represents the value used to join script sig arguments into a single string 11 | ScriptSigSeparator = " " 12 | ) 13 | 14 | func EncodeScriptSig(scriptSig [][]byte) string { 15 | ret := []string{} 16 | for _, val := range scriptSig { 17 | ret = append(ret, base58.Encode(val)) 18 | } 19 | 20 | return strings.Join(ret, ScriptSigSeparator) 21 | } 22 | 23 | func DecodeScriptSig(scriptSig string) [][]byte { 24 | ret := [][]byte{} 25 | for _, val := range strings.Split(scriptSig, ScriptSigSeparator) { 26 | ret = append(ret, base58.Decode(val)) 27 | } 28 | 29 | return ret 30 | } 31 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/files/promtail-config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 9080 3 | grpc_listen_port: 0 4 | 5 | clients: 6 | - url: http://localhost:3100/loki/api/v1/push 7 | 8 | positions: 9 | filename: /tmp/positions.yaml 10 | 11 | scrape_configs: 12 | - job_name: system 13 | static_configs: 14 | - targets: 15 | - localhost 16 | labels: 17 | __path__: /var/log/chainnet/*log 18 | pipeline_stages: 19 | - regex: 20 | expression: 'time="(?P[^"]+)" level=(?P[a-zA-Z]+) msg="(?P.*)"' 21 | - timestamp: 22 | source: timestamp 23 | format: "2006-01-02 15:04:05.000" 24 | - labels: 25 | level: 26 | - output: 27 | source: message 28 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | 3 | image: 4 | repository: yagoninja/chainnet-miner 5 | tag: latest 6 | pullPolicy: Always 7 | 8 | imageNode: 9 | repository: yagoninja/chainnet-node 10 | tag: latest 11 | pullPolicy: Always 12 | 13 | service: 14 | name: chainnet-miner 15 | type: ClusterIP 16 | port: 80 17 | targetPort: 80 18 | 19 | serviceNode: 20 | name: chainnet-node 21 | type: ClusterIP 22 | port: 80 23 | targetPort: 80 24 | 25 | configFile: "" 26 | 27 | persistence: 28 | enabled: true 29 | storageClass: "standard" 30 | accessModes: 31 | - ReadWriteOnce 32 | resources: 33 | requests: 34 | storage: 10Gi 35 | 36 | resources: {} 37 | 38 | serviceAccount: 39 | create: true 40 | name: "" # Leave as an empty string to use the default name 41 | -------------------------------------------------------------------------------- /pkg/kernel/utxo.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // UTXO represents the unspent transaction output 9 | type UTXO struct { 10 | TxID []byte 11 | OutIdx uint 12 | Output TxOutput 13 | } 14 | 15 | // EqualInput checks if the input is the same as the given input 16 | func (utxo *UTXO) EqualInput(input TxInput) bool { 17 | return bytes.Equal(utxo.TxID, input.Txid) && utxo.OutIdx == input.Vout 18 | } 19 | 20 | // UniqueKey represents the unique key for the UTXO. Method used for mapping UTXOs and inputs via this unique key 21 | func (utxo *UTXO) UniqueKey() string { 22 | return fmt.Sprintf("%x-%d", utxo.TxID, utxo.OutIdx) 23 | } 24 | 25 | // Amount returns the balance value contained in the UTXO ($$$) 26 | func (utxo *UTXO) Amount() uint { 27 | return utxo.Output.Amount 28 | } 29 | -------------------------------------------------------------------------------- /tests/mocks/crypto/sign/sign_mock.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package sign 3 | 4 | import ( 5 | "bytes" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockSign adds "-signed" to the payload provided so the signature can be predictable during tests 11 | type MockSign struct { 12 | mock.Mock 13 | } 14 | 15 | func (m *MockSign) NewKeyPair() ([]byte, []byte, error) { 16 | args := m.Called() 17 | return args.Get(0).([]byte), args.Get(1).([]byte), args.Error(2) 18 | } 19 | 20 | func (m *MockSign) Sign(payload []byte, _ []byte) ([]byte, error) { 21 | return append(payload, []byte("-signed")...), nil 22 | } 23 | 24 | func (m *MockSign) Verify(signature []byte, payload []byte, _ []byte) (bool, error) { 25 | return bytes.Equal(signature, append(payload, []byte("-signed")...)), nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/crypto/multi_hash.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/yago-123/chainnet/pkg/crypto/hash" 7 | ) 8 | 9 | type MultiHash struct { 10 | hashers []hash.Hashing 11 | } 12 | 13 | func NewMultiHash(hashers []hash.Hashing) *MultiHash { 14 | return &MultiHash{ 15 | hashers: hashers, 16 | } 17 | } 18 | 19 | func (m *MultiHash) Hash(payload []byte) ([]byte, error) { 20 | var err error 21 | for _, hasher := range m.hashers { 22 | payload, err = hasher.Hash(payload) 23 | if err != nil { 24 | return []byte{}, err 25 | } 26 | } 27 | 28 | return payload, nil 29 | } 30 | 31 | func (m *MultiHash) Verify(hash []byte, payload []byte) (bool, error) { 32 | h, err := m.Hash(payload) 33 | if err != nil { 34 | return false, err 35 | } 36 | 37 | return bytes.Equal(hash, h), nil 38 | } 39 | -------------------------------------------------------------------------------- /helm/templates/persistentvolumeclaim.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: chainnet-miner-pvc 5 | labels: 6 | app: chainnet 7 | spec: 8 | accessModes: 9 | - "ReadWriteOnce" # Directly specifying the access mode as it's a constant 10 | resources: 11 | requests: 12 | storage: "10Gi" # Directly specifying the storage size 13 | storageClassName: "standard" # Directly specifying the storage class 14 | --- 15 | apiVersion: v1 16 | kind: PersistentVolumeClaim 17 | metadata: 18 | name: chainnet-node-pvc 19 | labels: 20 | app: chainnet 21 | spec: 22 | accessModes: 23 | - "ReadWriteOnce" # Directly specifying the access mode as it's a constant 24 | resources: 25 | requests: 26 | storage: "10Gi" # Directly specifying the storage size 27 | storageClassName: "standard" # Directly specifying the storage class 28 | -------------------------------------------------------------------------------- /pkg/consensus/consensus.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | ) 6 | 7 | // LightValidator represents a validator that does not require having the whole chain downloaded locally 8 | // like for example the ones performed by wallets before sending transactions to the nodes and miners 9 | type LightValidator interface { 10 | ValidateTxLight(tx *kernel.Transaction) error 11 | ValidateHeader(bh *kernel.BlockHeader) error 12 | } 13 | 14 | // HeavyValidator performs the same validations as LightValidator but also validates the previous 15 | // information like the validity of the chain and transactions without funds. This validator is used 16 | // by nodes and miners 17 | type HeavyValidator interface { 18 | ValidateTx(tx *kernel.Transaction) error 19 | ValidateHeader(bh *kernel.BlockHeader) error 20 | ValidateBlock(b *kernel.Block) error 21 | } 22 | -------------------------------------------------------------------------------- /tests/mocks/network/net_wallet.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package network 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/stretchr/testify/mock" 8 | "github.com/yago-123/chainnet/pkg/kernel" 9 | ) 10 | 11 | type MockWalletNetwork struct { 12 | mock.Mock 13 | } 14 | 15 | func (m *MockWalletNetwork) GetWalletUTXOS(ctx context.Context, address []byte) ([]*kernel.UTXO, error) { 16 | args := m.Called(ctx, address) 17 | return args.Get(0).([]*kernel.UTXO), args.Error(1) 18 | } 19 | 20 | func (m *MockWalletNetwork) GetWalletTxs(ctx context.Context, address []byte) ([]*kernel.Transaction, error) { 21 | args := m.Called(ctx, address) 22 | return args.Get(0).([]*kernel.Transaction), args.Error(1) 23 | } 24 | 25 | func (m *MockWalletNetwork) SendTransaction(ctx context.Context, tx kernel.Transaction) error { 26 | args := m.Called(ctx, tx) 27 | return args.Error(0) 28 | } 29 | -------------------------------------------------------------------------------- /tests/mocks/util/block_matcher.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // MatchByPreviousBlockPointer creates an argument matcher 12 | // based on the PrevBlockHash field of a kernel.Block instance. 13 | func MatchByPreviousBlockPointer(prevBlockHash []byte) interface{} { 14 | return mock.MatchedBy(func(innerBlock *kernel.Block) bool { 15 | return reflect.DeepEqual(innerBlock.Header.PrevBlockHash, prevBlockHash) 16 | }) 17 | } 18 | 19 | // MatchByPreviousBlock creates an argument matcher 20 | // based on the PrevBlockHash field of a kernel.Block instance. 21 | func MatchByPreviousBlock(prevBlockHash []byte) interface{} { 22 | return mock.MatchedBy(func(innerBlock kernel.Block) bool { 23 | return reflect.DeepEqual(innerBlock.Header.PrevBlockHash, prevBlockHash) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/tasks/03_provision_grafana.yml: -------------------------------------------------------------------------------- 1 | - name: Ensure the provisioning directory for list charts exists 2 | file: 3 | path: /etc/grafana/provisioning/dashboards/list 4 | state: directory 5 | owner: grafana 6 | group: grafana 7 | mode: '0755' 8 | 9 | - name: Copy the dashboard provisioning YAML for chart list 10 | copy: 11 | src: files/dashboard_provision_list.yml 12 | dest: /etc/grafana/provisioning/dashboards/dashboard_provision_list.yml 13 | owner: grafana 14 | group: grafana 15 | mode: '0644' 16 | 17 | - name: Copy the list of dashboard JSON files 18 | copy: 19 | src: files/dashboard-list.json 20 | dest: /etc/grafana/provisioning/dashboards/list 21 | owner: grafana 22 | group: grafana 23 | mode: '0644' 24 | 25 | - name: Restart Grafana service 26 | systemd: 27 | name: grafana-server 28 | state: restarted -------------------------------------------------------------------------------- /pkg/mempool/mempool_explorer.go: -------------------------------------------------------------------------------- 1 | package mempool 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | ) 8 | 9 | // MemPoolExplorer is a middleware for MemPool, designed to prevent other packages, such as the network layer, 10 | // from directly accessing its structure. This separation ensures a clear boundary, restricting MemPool manipulation 11 | // to the chain object only 12 | type MemPoolExplorer struct { //nolint:revive // name shutter it's OK here 13 | mempool *MemPool 14 | } 15 | 16 | func NewMemPoolExplorer(mempool *MemPool) *MemPoolExplorer { 17 | return &MemPoolExplorer{ 18 | mempool: mempool, 19 | } 20 | } 21 | 22 | func (me *MemPoolExplorer) RetrieveTx(txID string) (*kernel.Transaction, error) { 23 | me.mempool.mu.Lock() 24 | defer me.mempool.mu.Unlock() 25 | 26 | if _, ok := me.mempool.txIDs[txID]; !ok { 27 | return nil, fmt.Errorf("transaction with ID %s not found", txID) 28 | } 29 | 30 | return me.mempool.txIDs[txID], nil 31 | } 32 | -------------------------------------------------------------------------------- /cmd/nespv/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | "github.com/yago-123/chainnet/config" 9 | "github.com/yago-123/chainnet/pkg/crypto" 10 | "github.com/yago-123/chainnet/pkg/crypto/hash" 11 | "github.com/yago-123/chainnet/pkg/crypto/sign" 12 | ) 13 | 14 | var cfg *config.Config 15 | var logger = logrus.New() 16 | 17 | var ( 18 | // general consensus hasher (tx, block hashes...) 19 | consensusHasherType = hash.SHA256 20 | 21 | // general consensus signer (tx) 22 | consensusSigner = crypto.NewHashedSignature( 23 | sign.NewECDSASignature(), 24 | hash.NewHasher(sha256.New()), 25 | ) 26 | ) 27 | 28 | var rootCmd = &cobra.Command{ 29 | Use: "chainnet-nespv", 30 | Run: func(_ *cobra.Command, _ []string) { 31 | 32 | }, 33 | } 34 | 35 | func Execute(logger *logrus.Logger) { 36 | if err := rootCmd.Execute(); err != nil { 37 | logger.Fatalf("error executing command: %v", err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/chain/iterator/reverse_block.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | "github.com/yago-123/chainnet/pkg/storage" 6 | ) 7 | 8 | // ReverseBlockIterator 9 | type ReverseBlockIterator struct { 10 | prevBlockHash []byte 11 | store storage.Storage 12 | } 13 | 14 | func NewReverseBlockIterator(store storage.Storage) *ReverseBlockIterator { 15 | return &ReverseBlockIterator{ 16 | store: store, 17 | } 18 | } 19 | 20 | func (it *ReverseBlockIterator) Initialize(reference []byte) error { 21 | it.prevBlockHash = reference 22 | return nil 23 | } 24 | 25 | func (it *ReverseBlockIterator) GetNextBlock() (*kernel.Block, error) { 26 | block, err := it.store.RetrieveBlockByHash(it.prevBlockHash) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | it.prevBlockHash = block.Header.PrevBlockHash 32 | 33 | return block, err 34 | } 35 | 36 | func (it *ReverseBlockIterator) HasNext() bool { 37 | return len(it.prevBlockHash) > 0 38 | } 39 | -------------------------------------------------------------------------------- /pkg/chain/iterator/reverse_header.go: -------------------------------------------------------------------------------- 1 | package iterator 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | "github.com/yago-123/chainnet/pkg/storage" 6 | ) 7 | 8 | // ReverseHeaderIterator 9 | type ReverseHeaderIterator struct { 10 | prevHeaderHash []byte 11 | store storage.Storage 12 | } 13 | 14 | func NewReverseHeaderIterator(store storage.Storage) *ReverseHeaderIterator { 15 | return &ReverseHeaderIterator{ 16 | store: store, 17 | } 18 | } 19 | 20 | func (it *ReverseHeaderIterator) Initialize(reference []byte) error { 21 | it.prevHeaderHash = reference 22 | return nil 23 | } 24 | 25 | func (it *ReverseHeaderIterator) GetNextHeader() (*kernel.BlockHeader, error) { 26 | header, err := it.store.RetrieveHeaderByHash(it.prevHeaderHash) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | it.prevHeaderHash = header.PrevBlockHash 32 | 33 | return header, err 34 | } 35 | 36 | func (it *ReverseHeaderIterator) HasNext() bool { 37 | return len(it.prevHeaderHash) > 0 38 | } 39 | -------------------------------------------------------------------------------- /tests/mocks/consensus/consensus_mock.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package consensus 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/yago-123/chainnet/pkg/kernel" 8 | 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // MockConsensus is a mock implementation of Consensus interface for testing purposes 13 | type MockConsensus struct { 14 | mock.Mock 15 | } 16 | 17 | func (m *MockConsensus) ValidateBlock(b *kernel.Block) bool { 18 | args := m.Called(b) 19 | return args.Bool(0) 20 | } 21 | 22 | func (m *MockConsensus) CalculateBlockHash(_ context.Context, b *kernel.Block) ([]byte, uint, error) { 23 | args := m.Called(b) 24 | return args.Get(0).([]byte), args.Get(1).(uint), args.Error(2) 25 | } 26 | 27 | func (m *MockConsensus) ValidateTx(tx *kernel.Transaction) bool { 28 | args := m.Called(tx) 29 | return args.Bool(0) 30 | } 31 | 32 | func (m *MockConsensus) CalculateTxHash(tx *kernel.Transaction) ([]byte, error) { 33 | args := m.Called(tx) 34 | return args.Get(0).([]byte), args.Error(1) 35 | } 36 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/tasks/04_provision_grafana.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure Grafana data source 3 | template: 4 | src: files/loki-source.yaml 5 | dest: /etc/grafana/provisioning/datasources/loki-source.yaml 6 | 7 | - name: Ensure the provisioning directory exists 8 | file: 9 | path: /etc/grafana/provisioning/dashboards/logging 10 | state: directory 11 | owner: grafana 12 | group: grafana 13 | mode: '0755' 14 | 15 | - name: Copy the dashboard provisioning YAML 16 | copy: 17 | src: files/dashboard_provision_logging.yml 18 | dest: /etc/grafana/provisioning/dashboards/dashboard_provision_logging.yml 19 | owner: grafana 20 | group: grafana 21 | mode: '0644' 22 | 23 | - name: Copy the dashboard JSON file 24 | copy: 25 | src: files/dashboard-logging.json 26 | dest: /etc/grafana/provisioning/dashboards/logging/dashboard-logging.json 27 | owner: grafana 28 | group: grafana 29 | mode: '0644' 30 | 31 | - name: Restart Grafana service 32 | systemd: 33 | name: grafana-server 34 | state: restarted -------------------------------------------------------------------------------- /ansible/playbooks/install-SSL.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install SSL certificates for the dashboard server 3 | hosts: all 4 | become: true 5 | become_method: sudo 6 | vars: 7 | domain: dashboard.chainnet.yago.ninja 8 | certificate_email: me@yago.ninja 9 | tasks: 10 | - name: Check if the certificate file exists 11 | stat: 12 | path: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem" 13 | register: cert_file_stat 14 | 15 | - name: Ensure package lists are updated 16 | apt: 17 | update_cache: yes 18 | when: not cert_file_stat.stat.exists 19 | 20 | - name: Install Certbot and the Nginx plugin 21 | apt: 22 | name: 23 | - certbot 24 | - python3-certbot-nginx 25 | state: present 26 | when: not cert_file_stat.stat.exists 27 | 28 | - name: Generate SSL certificate 29 | command: > 30 | certbot --nginx -d {{ domain }} --non-interactive --agree-tos --email {{ certificate_email }} 31 | args: 32 | creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem 33 | when: not cert_file_stat.stat.exists 34 | -------------------------------------------------------------------------------- /pkg/crypto/hashed_signature.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/crypto/hash" 5 | "github.com/yago-123/chainnet/pkg/crypto/sign" 6 | ) 7 | 8 | type HashedSignature struct { 9 | signature sign.Signature 10 | hasher hash.Hashing 11 | } 12 | 13 | func NewHashedSignature(signature sign.Signature, hasher hash.Hashing) *HashedSignature { 14 | return &HashedSignature{ 15 | signature: signature, 16 | hasher: hasher, 17 | } 18 | } 19 | 20 | func (hs *HashedSignature) NewKeyPair() ([]byte, []byte, error) { 21 | return hs.signature.NewKeyPair() 22 | } 23 | 24 | func (hs *HashedSignature) Sign(payload []byte, privKey []byte) ([]byte, error) { 25 | h, err := hs.hasher.Hash(payload) 26 | if err != nil { 27 | return []byte{}, err 28 | } 29 | return hs.signature.Sign(h, privKey) 30 | } 31 | 32 | func (hs *HashedSignature) Verify(signature []byte, payload []byte, pubKey []byte) (bool, error) { 33 | h, err := hs.hasher.Hash(payload) 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | return hs.signature.Verify(signature, h, pubKey) 39 | } 40 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/tasks/02_install_loki.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Download and install Loki binary 3 | get_url: 4 | url: "https://github.com/grafana/loki/releases/download/{{ loki_version }}/loki-linux-amd64.zip" 5 | dest: "/tmp/loki-linux-amd64.zip" 6 | mode: '0644' 7 | 8 | - name: Unzip Loki binary 9 | unarchive: 10 | src: "/tmp/loki-linux-amd64.zip" 11 | dest: "/usr/local/bin/" 12 | remote_src: yes 13 | 14 | - name: Create Loki config directory 15 | file: 16 | path: "/etc/loki" 17 | state: directory 18 | 19 | - name: Copy Loki config file 20 | template: 21 | src: files/loki-config.yaml 22 | dest: /etc/loki/loki-config.yaml 23 | 24 | - name: Provision Loki service 25 | copy: 26 | src: files/loki.service 27 | dest: /etc/systemd/system/loki.service 28 | mode: '0644' 29 | 30 | - name: Reload systemd daemon 31 | command: systemctl daemon-reload 32 | 33 | - name: Enable Loki service 34 | systemd: 35 | name: "loki" 36 | enabled: yes 37 | 38 | - name: Restart Loki service 39 | systemd: 40 | name: "loki" 41 | state: restarted -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/tasks/03_provision_grafana.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure Grafana data source 3 | template: 4 | src: files/prometheus-source.yaml 5 | dest: /etc/grafana/provisioning/datasources/prometheus-source.yaml 6 | 7 | - name: Ensure the provisioning directory exists 8 | file: 9 | path: /etc/grafana/provisioning/dashboards/custom 10 | state: directory 11 | owner: grafana 12 | group: grafana 13 | mode: '0755' 14 | 15 | - name: Copy the dashboard provisioning YAML 16 | copy: 17 | src: files/dashboard_provision_monitoring.yml 18 | dest: /etc/grafana/provisioning/dashboards/dashboard_provision_monitoring.yml 19 | owner: grafana 20 | group: grafana 21 | mode: '0644' 22 | 23 | - name: Copy all dashboard provisioning YAML files 24 | copy: 25 | src: "{{ item }}" 26 | dest: /etc/grafana/provisioning/dashboards/custom 27 | owner: grafana 28 | group: grafana 29 | mode: '0644' 30 | with_fileglob: 31 | - files/dashboards/*.json 32 | 33 | - name: Restart Grafana service 34 | systemd: 35 | name: grafana-server 36 | state: restarted -------------------------------------------------------------------------------- /pkg/crypto/sign/signature.go: -------------------------------------------------------------------------------- 1 | package sign 2 | 3 | import "errors" 4 | 5 | type Signature interface { 6 | NewKeyPair() ([]byte, []byte, error) 7 | // todo() should we add the transaction object here directly instead of payload? 8 | Sign(payload []byte, privKey []byte) ([]byte, error) 9 | Verify(signature []byte, payload []byte, pubKey []byte) (bool, error) 10 | } 11 | 12 | // signInputValidator controls the input validation for the sign method 13 | func signInputValidator(payload []byte, privKey []byte) error { 14 | if len(payload) < 1 { 15 | return errors.New("payload is empty") 16 | } 17 | 18 | if len(privKey) < 1 { 19 | return errors.New("private key is empty") 20 | } 21 | 22 | return nil 23 | } 24 | 25 | // verifyInputValidator controls the input validation for the verify method 26 | func verifyInputValidator(signature []byte, payload []byte, pubKey []byte) error { 27 | if len(signature) < 1 { 28 | return errors.New("signature is empty") 29 | } 30 | 31 | if len(payload) < 1 { 32 | return errors.New("payload is empty") 33 | } 34 | 35 | if len(pubKey) < 1 { 36 | return errors.New("public key is empty") 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/storage/boltdb_test.go: -------------------------------------------------------------------------------- 1 | package storage //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | cerror "github.com/yago-123/chainnet/pkg/errs" 8 | 9 | "github.com/yago-123/chainnet/pkg/encoding" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | const MockStorageFile = "test-file" 16 | 17 | func TestBoltDB_NotFound(t *testing.T) { 18 | defer os.Remove(MockStorageFile) 19 | 20 | bolt, err := NewBoltDB(MockStorageFile, "block-bucket", "header-bucket", encoding.NewGobEncoder()) 21 | require.NoError(t, err) 22 | 23 | _, err = bolt.GetLastBlock() 24 | assert.Equal(t, cerror.ErrStorageElementNotFound, err) 25 | 26 | _, err = bolt.GetGenesisBlock() 27 | assert.Equal(t, cerror.ErrStorageElementNotFound, err) 28 | 29 | _, err = bolt.GetGenesisHeader() 30 | assert.Equal(t, cerror.ErrStorageElementNotFound, err) 31 | 32 | _, err = bolt.RetrieveBlockByHash([]byte("")) 33 | assert.Equal(t, cerror.ErrStorageElementNotFound, err) 34 | 35 | _, err = bolt.RetrieveHeaderByHash([]byte("")) 36 | assert.Equal(t, cerror.ErrStorageElementNotFound, err) 37 | } 38 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/files/loki-config.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | 6 | ingester: 7 | lifecycler: 8 | ring: 9 | kvstore: 10 | store: inmemory 11 | replication_factor: 1 12 | chunk_idle_period: 5m 13 | chunk_retain_period: 30s 14 | max_transfer_retries: 0 15 | 16 | schema_config: 17 | configs: 18 | - from: 2020-10-24 19 | store: boltdb-shipper 20 | object_store: filesystem 21 | schema: v11 22 | index: 23 | prefix: loki_index_ 24 | period: 24h 25 | 26 | storage_config: 27 | boltdb_shipper: 28 | active_index_directory: /tmp/loki/index 29 | shared_store: filesystem 30 | cache_location: /tmp/loki/boltdb-cache 31 | filesystem: 32 | directory: /tmp/loki/chunks 33 | 34 | compactor: 35 | working_directory: /tmp/loki/compactor 36 | shared_store: filesystem 37 | 38 | limits_config: 39 | enforce_metric_name: false 40 | reject_old_samples: true 41 | reject_old_samples_max_age: 168h 42 | 43 | chunk_store_config: 44 | max_look_back_period: 0s 45 | 46 | table_manager: 47 | retention_deletes_enabled: false 48 | retention_period: 0s 49 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/logging/tasks/03_install_promtail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Download and install Promtail binary 3 | get_url: 4 | url: "https://github.com/grafana/loki/releases/download/{{ promtail_version }}/promtail-linux-amd64.zip" 5 | dest: "/tmp/promtail-linux-amd64.zip" 6 | mode: '0644' 7 | 8 | - name: Unzip Promtail binary 9 | unarchive: 10 | src: "/tmp/promtail-linux-amd64.zip" 11 | dest: "/usr/local/bin/" 12 | remote_src: yes 13 | 14 | - name: Create Promtail config directory 15 | file: 16 | path: "/etc/promtail" 17 | state: directory 18 | 19 | - name: Copy Promtail config file 20 | template: 21 | src: files/promtail-config.yaml 22 | dest: /etc/promtail/promtail-config.yaml 23 | 24 | - name: Provision Promtail service 25 | copy: 26 | src: files/promtail.service 27 | dest: /etc/systemd/system/promtail.service 28 | mode: '0644' 29 | 30 | - name: Reload systemd daemon 31 | command: systemctl daemon-reload 32 | 33 | - name: Enable Promtail service 34 | systemd: 35 | name: "promtail" 36 | enabled: yes 37 | 38 | - name: Restart Promtail service 39 | systemd: 40 | name: "promtail" 41 | state: restarted -------------------------------------------------------------------------------- /pkg/network/protobuf/kernel.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/yago-123/chainnet/pkg/chain/p2p"; 4 | 5 | message Transaction { 6 | bytes id = 1; 7 | repeated TxInput vin = 2; 8 | repeated TxOutput vout = 3; 9 | } 10 | 11 | message Transactions { 12 | repeated Transaction transactions = 1; 13 | } 14 | 15 | message TxInput { 16 | bytes txid = 1; 17 | uint64 vout = 2; 18 | string script_sig = 3; 19 | string pub_key = 4; 20 | } 21 | 22 | message TxOutput { 23 | uint64 amount = 1; 24 | string script_pub_key = 2; 25 | string pub_key = 3; 26 | } 27 | 28 | message UTXO { 29 | bytes txid = 1; 30 | uint64 vout = 2; 31 | TxOutput Output = 3; 32 | } 33 | 34 | message UTXOs { 35 | repeated UTXO utxos = 1; 36 | } 37 | 38 | message BlockHeader { 39 | bytes version = 1; 40 | bytes prev_block_hash = 2; 41 | bytes merkle_root = 3; 42 | uint64 height = 4; 43 | int64 timestamp = 5; 44 | uint64 target = 6; 45 | uint64 nonce = 7; 46 | } 47 | 48 | message BlockHeaders { 49 | repeated BlockHeader headers = 1; 50 | } 51 | 52 | message Block { 53 | BlockHeader header = 1; 54 | repeated Transaction transactions = 2; 55 | bytes hash = 3; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/tasks/02_install_nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Nginx 3 | apt: 4 | name: nginx 5 | state: present 6 | 7 | - name: Check if the certificate file exists 8 | stat: 9 | path: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem" 10 | register: cert_file_stat 11 | 12 | - name: Copy Nginx configuration for SSL reverse proxy if the certificate file exists 13 | template: 14 | src: "templates/dashboard-nginx-with-SSL.j2" 15 | dest: "/etc/nginx/sites-available/dashboard" 16 | when: cert_file_stat.stat.exists 17 | 18 | - name: Copy Nginx configuration without SSL reverse proxy if the certificate file does not exist 19 | template: 20 | src: "templates/dashboard-nginx.j2" 21 | dest: "/etc/nginx/sites-available/dashboard" 22 | when: not cert_file_stat.stat.exists 23 | 24 | - name: Start Nginx service 25 | systemd: 26 | name: nginx 27 | enabled: true 28 | state: started 29 | 30 | - name: Enable the dashboard site 31 | file: 32 | src: /etc/nginx/sites-available/dashboard 33 | dest: /etc/nginx/sites-enabled/dashboard 34 | state: link 35 | force: yes 36 | 37 | - name: Reload Nginx 38 | systemd: 39 | name: nginx 40 | state: reloaded -------------------------------------------------------------------------------- /pkg/encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | ) 6 | 7 | const ( 8 | GobEncodingType = "gob" 9 | ProtoEncodingType = "protobuf" 10 | ) 11 | 12 | type Encoding interface { 13 | Type() string 14 | 15 | SerializeBlock(b kernel.Block) ([]byte, error) 16 | SerializeHeader(bh kernel.BlockHeader) ([]byte, error) 17 | SerializeHeaders(bhs []*kernel.BlockHeader) ([]byte, error) 18 | SerializeTransaction(tx kernel.Transaction) ([]byte, error) 19 | SerializeTransactions(txs []*kernel.Transaction) ([]byte, error) 20 | SerializeUTXO(utxo kernel.UTXO) ([]byte, error) 21 | SerializeUTXOs(utxos []*kernel.UTXO) ([]byte, error) 22 | SerializeBool(b bool) ([]byte, error) 23 | 24 | DeserializeBlock(data []byte) (*kernel.Block, error) 25 | DeserializeHeader(data []byte) (*kernel.BlockHeader, error) 26 | DeserializeHeaders(data []byte) ([]*kernel.BlockHeader, error) 27 | DeserializeTransaction(data []byte) (*kernel.Transaction, error) 28 | DeserializeTransactions(data []byte) ([]*kernel.Transaction, error) 29 | DeserializeUTXO(data []byte) (*kernel.UTXO, error) 30 | DeserializeUTXOs(data []byte) ([]*kernel.UTXO, error) 31 | DeserializeBool(data []byte) (bool, error) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/consensus/validator/light_validator_test.go: -------------------------------------------------------------------------------- 1 | package validator //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | "github.com/yago-123/chainnet/pkg/script" 8 | "github.com/yago-123/chainnet/tests/mocks/crypto/hash" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestLValidator_ValidateTxLight(_ *testing.T) { 14 | // todo() once we have RPN done 15 | } 16 | 17 | func TestLValidator_ValidateHeader(_ *testing.T) { 18 | 19 | } 20 | 21 | func TestLValidator_validateTxID(t *testing.T) { 22 | hasher := &hash.FakeHashing{} 23 | tx := kernel.NewTransaction( 24 | []kernel.TxInput{kernel.NewInput([]byte("tx-id-1"), 1, "scriptsig", "pubkey")}, 25 | []kernel.TxOutput{kernel.NewOutput(1, script.P2PK, "pubkey2")}, 26 | ) 27 | 28 | // generate tx hash 29 | txHash, err := hasher.Hash(tx.Assemble()) 30 | require.NoError(t, err) 31 | 32 | lv := LValidator{ 33 | hasher: hasher, 34 | } 35 | // verify that tx hash matches 36 | tx.SetID(txHash) 37 | require.NoError(t, lv.validateTxID(tx)) 38 | 39 | // modify the hash 40 | txHash[0] = 0x0 41 | tx.SetID(txHash) 42 | require.Error(t, lv.validateTxID(tx)) 43 | } 44 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: chainnet 3 | description: A Helm chart for chainnet blockchain 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.1.0" 25 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | project_name: chainnet 4 | 5 | before: 6 | hooks: 7 | - make protobuf 8 | - make test 9 | 10 | builds: 11 | - id: node 12 | main: ./cmd/node 13 | binary: chainnet-node 14 | env: ["CGO_ENABLED=0"] 15 | goos: [linux] 16 | goarch: [amd64] 17 | - id: miner 18 | main: ./cmd/miner 19 | binary: chainnet-miner 20 | env: ["CGO_ENABLED=0"] 21 | goos: [linux] 22 | goarch: [amd64] 23 | - id: nespv 24 | main: ./cmd/nespv 25 | binary: chainnet-nespv 26 | env: ["CGO_ENABLED=0"] 27 | goos: [linux] 28 | goarch: [amd64] 29 | - id: cli 30 | main: ./cmd/cli 31 | binary: chainnet-cli 32 | env: ["CGO_ENABLED=0"] 33 | goos: [linux] 34 | goarch: [amd64] 35 | 36 | archives: 37 | - formats: [ 'tar.gz' ] 38 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 39 | files: 40 | - README.md 41 | 42 | checksum: 43 | name_template: "checksums.txt" 44 | 45 | release: 46 | github: 47 | owner: yago-123 48 | name: chainnet 49 | 50 | nfpms: 51 | - package_name: chainnet 52 | vendor: yago-123 53 | homepage: "https://github.com/yago-123/chainnet" 54 | maintainer: "yago-123 " 55 | formats: [deb] 56 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/monitoring/tasks/02_install_prometheus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Download Prometheus binary 3 | get_url: 4 | url: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz" 5 | dest: /tmp/prometheus.tar.gz 6 | 7 | - name: Extract Prometheus binary 8 | unarchive: 9 | src: /tmp/prometheus.tar.gz 10 | dest: /opt/ 11 | remote_src: yes 12 | 13 | - name: Move Prometheus binaries 14 | command: mv /opt/prometheus-{{ prometheus_version }}.linux-amd64/prometheus /usr/local/bin/ 15 | 16 | - name: Move Prometheus related files 17 | copy: 18 | src: /opt/prometheus-{{ prometheus_version }}.linux-amd64/ 19 | dest: /etc/prometheus/ 20 | remote_src: yes 21 | 22 | - name: Provide Prometheus configuration 23 | template: 24 | src: templates/prometheus.yml.j2 25 | dest: /etc/prometheus/prometheus.yml 26 | mode: '0644' 27 | 28 | - name: Create Prometheus systemd service file 29 | template: 30 | src: files/prom.service 31 | dest: /etc/systemd/system/prometheus.service 32 | mode: '0644' 33 | 34 | - name: Start Prometheus service 35 | systemd: 36 | name: prometheus 37 | enabled: true 38 | state: started 39 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | main-pipeline: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: '1.24' 19 | 20 | - name: Install Protoc 21 | uses: arduino/setup-protoc@v3 22 | with: 23 | version: "23.x" 24 | 25 | - name: Install protoc-gen-go 26 | run: | 27 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0 28 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 29 | 30 | - name: Generate protobuf files 31 | run: make protobuf 32 | 33 | - name: Run golangci-lint 34 | uses: golangci/golangci-lint-action@v2 35 | with: 36 | version: 'v1.64.5' 37 | 38 | - name: Build Node 39 | run: make node 40 | 41 | - name: Build Miner 42 | run: make miner 43 | 44 | - name: Build NESPV 45 | run: make nespv 46 | 47 | - name: Build CLI 48 | run: make cli 49 | 50 | - name: Test code 51 | run: make test 52 | -------------------------------------------------------------------------------- /pkg/chain/bloom/bloom.go: -------------------------------------------------------------------------------- 1 | package bloom 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/yago-123/chainnet/pkg/kernel" 8 | ) 9 | 10 | const ( 11 | BloomObserverID = "bloomfilter-observer" 12 | BloomFilterSize = 32 13 | ) 14 | 15 | type BlockBloomFilter struct { 16 | bloom map[string][]bool 17 | } 18 | 19 | func NewBlockBloomFilter() *BlockBloomFilter { 20 | return &BlockBloomFilter{ 21 | bloom: make(map[string][]bool), 22 | } 23 | } 24 | 25 | func (bf *BlockBloomFilter) AddBlock(block *kernel.Block) { 26 | bf.bloom[string(block.Hash)] = make([]bool, BloomFilterSize) 27 | 28 | for _, tx := range block.Transactions { 29 | index := binary.BigEndian.Uint64(tx.ID) % BloomFilterSize 30 | bf.bloom[string(block.Hash)][index] = true 31 | } 32 | } 33 | 34 | func (bf *BlockBloomFilter) PresentInBlock(txID, blockID []byte) (bool, error) { 35 | if filter, ok := bf.bloom[string(blockID)]; ok { 36 | index := binary.BigEndian.Uint64(txID) % BloomFilterSize 37 | return filter[index], nil 38 | } 39 | 40 | return false, fmt.Errorf("block %s not found in bloom filter", string(blockID)) 41 | } 42 | 43 | func (bf *BlockBloomFilter) ID() string { 44 | return BloomObserverID 45 | } 46 | 47 | func (bf *BlockBloomFilter) OnBlockAddition(block *kernel.Block) { 48 | // calculate the bloom filter of the new block 49 | bf.AddBlock(block) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/network/discovery/dht.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | ds "github.com/ipfs/go-datastore" 8 | dht "github.com/libp2p/go-libp2p-kad-dht" 9 | "github.com/libp2p/go-libp2p/core/host" 10 | ) 11 | 12 | const ( 13 | DHTDiscoveryType = "DHT" 14 | ) 15 | 16 | type DHTDiscovery struct { 17 | dht *dht.IpfsDHT 18 | isActive bool 19 | } 20 | 21 | func NewDHTDiscovery(host host.Host) (*DHTDiscovery, error) { 22 | // todo(): consider adding persistent data store 23 | // todo(): roam around the options available for the DHT initialization 24 | // todo(): add seed nodes to the DHT via options too 25 | d := dht.NewDHT(context.Background(), host, ds.NewMapDatastore()) 26 | 27 | return &DHTDiscovery{ 28 | dht: d, 29 | isActive: false, 30 | }, nil 31 | } 32 | 33 | func (d *DHTDiscovery) Start() error { 34 | if d.isActive { 35 | return nil 36 | } 37 | 38 | err := d.dht.Bootstrap(context.Background()) 39 | if err != nil { 40 | return fmt.Errorf("failed to bootstrap DHT: %w", err) 41 | } 42 | 43 | d.isActive = true 44 | return nil 45 | } 46 | 47 | func (d *DHTDiscovery) Stop() error { 48 | if !d.isActive { 49 | return nil 50 | } 51 | 52 | err := d.dht.Close() 53 | if err != nil { 54 | return fmt.Errorf("failed to stop DHT: %w", err) 55 | } 56 | 57 | d.isActive = false 58 | return nil 59 | } 60 | 61 | func (d *DHTDiscovery) Type() string { 62 | return DHTDiscoveryType 63 | } 64 | -------------------------------------------------------------------------------- /helm/templates/miner.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: chainnet-miner 5 | labels: 6 | app: chainnet 7 | spec: 8 | serviceName: "chainnet-miner" 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: chainnet-miner 13 | template: 14 | metadata: 15 | labels: 16 | app: chainnet-miner 17 | spec: 18 | containers: 19 | - name: chainnet-miner 20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 21 | imagePullPolicy: "{{ .Values.image.pullPolicy }}" 22 | ports: 23 | - containerPort: 80 24 | name: http 25 | volumeMounts: 26 | - name: chainnet-miner-storage 27 | mountPath: /data 28 | - name: config-volume 29 | mountPath: /etc/chainnet/config.yaml 30 | subPath: config.yaml 31 | env: 32 | - name: CONFIG_FILE 33 | value: /etc/chainnet/config.yaml 34 | resources: 35 | {{- toYaml .Values.resources | nindent 12 }} 36 | volumes: 37 | - name: chainnet-miner-storage 38 | persistentVolumeClaim: 39 | claimName: chainnet-miner-pvc 40 | - name: config-volume 41 | configMap: 42 | name: {{ .Release.Name }}-config 43 | items: 44 | - key: config.yaml 45 | path: config.yaml 46 | -------------------------------------------------------------------------------- /helm/templates/node.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: chainnet-node 5 | labels: 6 | app: chainnet 7 | spec: 8 | serviceName: "chainnet-node" 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: chainnet-node 13 | template: 14 | metadata: 15 | labels: 16 | app: chainnet-node 17 | spec: 18 | containers: 19 | - name: chainnet-node 20 | image: "{{ .Values.imageNode.repository }}:{{ .Values.imageNode.tag }}" 21 | imagePullPolicy: "{{ .Values.imageNode.pullPolicy }}" 22 | ports: 23 | - containerPort: 80 24 | name: http 25 | volumeMounts: 26 | - name: chainnet-node-storage 27 | mountPath: /data 28 | - name: config-volume 29 | mountPath: /etc/chainnet/config.yaml 30 | subPath: config.yaml 31 | env: 32 | - name: CONFIG_FILE 33 | value: /etc/chainnet/config.yaml 34 | resources: 35 | {{- toYaml .Values.resources | nindent 12 }} 36 | volumes: 37 | - name: chainnet-node-storage 38 | persistentVolumeClaim: 39 | claimName: chainnet-node-pvc 40 | - name: config-volume 41 | configMap: 42 | name: {{ .Release.Name }}-config 43 | items: 44 | - key: config.yaml 45 | path: config.yaml 46 | -------------------------------------------------------------------------------- /pkg/util/mutex/ctx_mutex.go: -------------------------------------------------------------------------------- 1 | package mutex 2 | 3 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, 4 | // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 5 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 6 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 7 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 8 | // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 9 | // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | 11 | // Extracted from https://h12.io/article/go-pattern-context-aware-lock 12 | 13 | import ( 14 | "context" 15 | ) 16 | 17 | type CtxMutex struct { 18 | ch chan struct{} 19 | } 20 | 21 | func NewCtxMutex(maxConcurrentLocks uint) *CtxMutex { 22 | return &CtxMutex{ch: make(chan struct{}, maxConcurrentLocks)} 23 | } 24 | 25 | func (mu *CtxMutex) Lock(ctx context.Context) bool { 26 | select { 27 | case <-ctx.Done(): 28 | return false 29 | case mu.ch <- struct{}{}: 30 | return true 31 | } 32 | } 33 | 34 | func (mu *CtxMutex) Unlock() { 35 | <-mu.ch 36 | } 37 | 38 | func (mu *CtxMutex) Locked() bool { 39 | return len(mu.ch) > 0 40 | } 41 | -------------------------------------------------------------------------------- /pkg/util/p2pkh/p2pkh_test.go: -------------------------------------------------------------------------------- 1 | package utilp2pkh //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/btcsuite/btcutil/base58" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGenerateP2PKHAddrFromPubKey(t *testing.T) { 12 | p2pkhAddr, err := GenerateP2PKHAddrFromPubKey( 13 | base58.Decode("aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTJdddpT9aV3HbEPRuBpyEXFktCPCgrdp3FEXrfqjz2xoeQwTCqBs8qJtUFNmCLRTyVaTYuy7G8RZnHkABrMpH2cCG"), 14 | 1, 15 | ) 16 | 17 | require.NoError(t, err) 18 | require.Len(t, string(p2pkhAddr), P2PKHAddressLength) 19 | assert.Equal(t, "agr72ArMnsmdm9XTScgCpXnwkhAANyBCd", base58.Encode(p2pkhAddr)) 20 | } 21 | 22 | func TestExtractPubKeyHashedFromP2PKHAddr(t *testing.T) { 23 | pubKeyHash, version, err := ExtractPubKeyHashedFromP2PKHAddr( 24 | base58.Decode("agr72ArMnsmdm9XTScgCpXnwkhAANyBCd"), 25 | ) 26 | 27 | require.NoError(t, err) 28 | assert.Equal(t, 1, int(version)) 29 | assert.Equal(t, "2ajHyKQLikZqXV9rpaSfnV6mh7a5", base58.Encode(pubKeyHash)) 30 | assert.Len(t, pubKeyHash, 20) 31 | 32 | // modify byte to test checksum validation 33 | _, _, err = ExtractPubKeyHashedFromP2PKHAddr( 34 | base58.Decode("agr72ArMnsmd99XTScgCpXnwkhAANyBCd"), 35 | ) 36 | 37 | require.Error(t, err) 38 | 39 | // make sure that length is checked 40 | _, _, err = ExtractPubKeyHashedFromP2PKHAddr( 41 | base58.Decode("agr72ArMnsmd9XTScgCpXnwkhAANyBCd"), 42 | ) 43 | require.Error(t, err) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/network/events/host.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/yago-123/chainnet/pkg/observer" 7 | 8 | "github.com/libp2p/go-libp2p/core/event" 9 | "github.com/libp2p/go-libp2p/core/host" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // todo(): turn this into a struct with administration methods if we rely more on this event bus system from host 14 | // InitializeHostEventsSubscription creates the subscription and the listener for host events 15 | func InitializeHostEventsSubscription(ctx context.Context, logger *logrus.Logger, host host.Host, subject observer.NetSubject) error { 16 | sub, err := host.EventBus().Subscribe(new(event.EvtPeerIdentificationCompleted)) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | go listenForHostEvents(ctx, logger, sub, subject) 22 | 23 | return nil 24 | } 25 | 26 | // listenForHostEvents represents the event listener that reacts to events emitter by the host event bus 27 | func listenForHostEvents(ctx context.Context, logger *logrus.Logger, sub event.Subscription, subject observer.NetSubject) { 28 | for { 29 | select { 30 | case evt := <-sub.Out(): 31 | switch e := evt.(type) { 32 | case event.EvtPeerIdentificationCompleted: 33 | subject.NotifyNodeDiscovered(e.Peer) 34 | default: 35 | logger.Errorf("unhandled event type: %T", evt) 36 | } 37 | case <-ctx.Done(): 38 | // context finished, stop listening for events 39 | logger.Errorf("context canceled, stopping event listener") 40 | return 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/templates/dashboard-nginx-with-SSL.j2: -------------------------------------------------------------------------------- 1 | # HTTP server block (for redirecting HTTP to HTTPS) 2 | server { 3 | listen 80; 4 | server_name {{ domain }}; 5 | 6 | # Redirect all HTTP requests to HTTPS 7 | return 301 https://$host$request_uri; 8 | } 9 | 10 | # HTTPS server block 11 | server { 12 | listen 443 ssl; 13 | server_name {{ domain }}; 14 | 15 | # SSL certificate and key paths (Let's Encrypt) 16 | ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; 17 | ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; 18 | 19 | # Recommended SSL settings (optional, but recommended) 20 | #ssl_protocols TLSv1.2 TLSv1.3; 21 | #ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; 22 | #ssl_prefer_server_ciphers on; 23 | # ssl_dhparam /etc/ssl/certs/dhparam.pem; 24 | 25 | # Redirect /list to the specific Grafana dashboard 26 | location /list { 27 | rewrite ^/list$ /d/eeazamyajw2kgc/dashboard-list?orgId=1&from=now-24h&to=now&timezone=browser permanent; 28 | } 29 | 30 | # Your proxy settings for all other requests 31 | location / { 32 | proxy_pass http://localhost:3000; 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Real-IP $remote_addr; 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header X-Forwarded-Proto $scheme; 37 | proxy_set_header X-Forwarded-Host $host; 38 | proxy_set_header X-Forwarded-Port $server_port; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/examples/docker-config.yaml: -------------------------------------------------------------------------------- 1 | seed-nodes: # List of seed nodes 2 | - address: "seed-1.chainnet.yago.ninja" 3 | peer-id: "QmVQ8bj9KPfTiN23vX7sbqn4oXjTSycfULL4oApAZccWL5" 4 | port: 9100 5 | # - address: "seed-2.chainnet.yago.ninja" 6 | # peer-id: "peerID-2" 7 | # port: 8081 8 | # - address: "seed-3.chainnet.yago.ninja" 9 | # peer-id: "peerID-3" 10 | # port: 8082 11 | 12 | storage-file: "/data/miner-storage" # File used for persisting the chain status 13 | pub-key: # Public wallet key encoded in base58, used for receiving mining rewards 14 | "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTG7ZBzTqdDQvpbDVh5j5yCpKYU6MVZ35PW9KegkuX1JZDLHdkaTAbKXwfx4Pjy2At82Dda9ujs8d5ReXF22QHk2JA" 15 | mining-interval: "1m" # Interval between block creation 16 | 17 | p2p: 18 | enabled: true # Enable or disable network communication 19 | # identity: 20 | # priv-key-path: "ecdsa-priv-key.pem" # ECDSA peer private key path in PEM format (leave empty to generate a random identity) 21 | 22 | peer-port: 9100 # Port used for network communication with other peers 23 | min-conn: 5 # Minimum number of connections 24 | max-conn: 100 # Maximum number of connections 25 | conn-timeout: "60s" # Maximum duration of a connection 26 | write-timeout: "20s" # Maximum duration of a write stream 27 | read-timeout: "20s" # Maximum duration of a read stream 28 | buffer-size: 4096 # Read buffer size over the network 29 | -------------------------------------------------------------------------------- /pkg/monitor/monitor.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | type MetricType int 6 | 7 | const ( 8 | Counter MetricType = iota 9 | Gauge 10 | ) 11 | 12 | type Monitor interface { 13 | ID() string 14 | RegisterMetrics(registry *prometheus.Registry) 15 | } 16 | 17 | // NewMetric registers a new metric with the given name and help string 18 | func NewMetric(registry *prometheus.Registry, typ MetricType, name, help string, executor func() float64) { 19 | switch typ { 20 | case Counter: 21 | registry.MustRegister(prometheus.NewCounterFunc(prometheus.CounterOpts{ 22 | Name: name, 23 | Help: help, 24 | }, executor)) 25 | case Gauge: 26 | registry.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{ 27 | Name: name, 28 | Help: help, 29 | }, executor)) 30 | } 31 | } 32 | 33 | // NewMetricWithLabels registers a new metric with an executor function that must be run asynchronously. This 34 | // is required for modules that contain metrics that are hard to calculate synchronously. 35 | func NewMetricWithLabels( 36 | registry *prometheus.Registry, 37 | typ MetricType, 38 | name, help string, 39 | labels []string, 40 | executor func(metricVec interface{}), 41 | ) { 42 | switch typ { 43 | case Counter: 44 | metric := prometheus.NewCounterVec(prometheus.CounterOpts{ 45 | Name: name, 46 | Help: help, 47 | }, labels) 48 | registry.MustRegister(metric) 49 | go executor(metric) 50 | 51 | case Gauge: 52 | metric := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 53 | Name: name, 54 | Help: help, 55 | }, labels) 56 | registry.MustRegister(metric) 57 | go executor(metric) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/examples/kubernetes-config.yaml: -------------------------------------------------------------------------------- 1 | #seed-nodes: # List of seed nodes 2 | # - address: "seed-1.chainnet.yago.ninja" 3 | # peer-id: "QmVQ8bj9KPfTiN23vX7sbqn4oXjTSycfULL4oApAZccWL5" 4 | # port: 9100 5 | # - address: "seed-2.chainnet.yago.ninja" 6 | # peer-id: "peerID-2" 7 | # port: 8081 8 | # - address: "seed-3.chainnet.yago.ninja" 9 | # peer-id: "peerID-3" 10 | # port: 8082 11 | 12 | storage-file: "/data/miner-storage" # File used for persisting the chain status 13 | pub-key: # Public wallet key encoded in base58, used for receiving mining rewards 14 | "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTG7ZBzTqdDQvpbDVh5j5yCpKYU6MVZ35PW9KegkuX1JZDLHdkaTAbKXwfx4Pjy2At82Dda9ujs8d5ReXF22QHk2JA" 15 | mining-interval: "10s" # Interval between block creation 16 | 17 | p2p: 18 | enabled: true # Enable or disable network communication 19 | # identity: 20 | # priv-key-path: "ecdsa-priv-key.pem" # ECDSA peer private key path in PEM format (leave empty to generate a random identity) 21 | 22 | peer-port: 9100 # Port used for network communication with other peers 23 | min-conn: 5 # Minimum number of connections 24 | max-conn: 100 # Maximum number of connections 25 | conn-timeout: "60s" # Maximum duration of a connection 26 | write-timeout: "20s" # Maximum duration of a write stream 27 | read-timeout: "20s" # Maximum duration of a read stream 28 | buffer-size: 4096 # Read buffer size over the network 29 | -------------------------------------------------------------------------------- /tests/mocks/encoding/encoding_mock.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package encoding 3 | 4 | import ( 5 | "github.com/yago-123/chainnet/pkg/kernel" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type MockEncoding struct { 11 | mock.Mock 12 | } 13 | 14 | func (m *MockEncoding) SerializeBlock(b kernel.Block) ([]byte, error) { 15 | args := m.Called(b) 16 | return args.Get(0).([]byte), args.Error(1) 17 | } 18 | 19 | func (m *MockEncoding) DeserializeBlock(data []byte) (*kernel.Block, error) { 20 | args := m.Called(data) 21 | return args.Get(0).(*kernel.Block), args.Error(1) 22 | } 23 | 24 | func (m *MockEncoding) SerializeHeader(bh kernel.BlockHeader) ([]byte, error) { 25 | args := m.Called(bh) 26 | return args.Get(0).([]byte), args.Error(1) 27 | } 28 | 29 | func (m *MockEncoding) DeserializeHeader(data []byte) (*kernel.BlockHeader, error) { 30 | args := m.Called(data) 31 | return args.Get(0).(*kernel.BlockHeader), args.Error(1) 32 | } 33 | 34 | func (m *MockEncoding) SerializeHeaders(bhs []*kernel.BlockHeader) ([]byte, error) { 35 | args := m.Called(bhs) 36 | return args.Get(0).([]byte), args.Error(1) 37 | } 38 | 39 | func (m *MockEncoding) DeserializeHeaders(data []byte) ([]*kernel.BlockHeader, error) { 40 | args := m.Called(data) 41 | return args.Get(0).([]*kernel.BlockHeader), args.Error(1) 42 | } 43 | 44 | func (m *MockEncoding) SerializeTransaction(tx kernel.Transaction) ([]byte, error) { 45 | args := m.Called(tx) 46 | return args.Get(0).([]byte), args.Error(1) 47 | } 48 | 49 | func (m *MockEncoding) DeserializeTransaction(data []byte) (*kernel.Transaction, error) { 50 | args := m.Called(data) 51 | return args.Get(0).(*kernel.Transaction), args.Error(1) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/wallet/hd_wallet/metadata.go: -------------------------------------------------------------------------------- 1 | package hdwallet 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | type Metadata struct { 11 | NumAccounts uint `json:"num_accounts"` 12 | MetadataAccounts []MetadataAccount `json:"metadata_accounts"` 13 | } 14 | 15 | type MetadataAccount struct { 16 | NumExternalWallets uint `json:"num_external_wallets"` 17 | NumInternalWallets uint `json:"num_internal_wallets"` 18 | } 19 | 20 | func SaveMetadata(path string, metadata *Metadata) error { 21 | // marshal the metadata 22 | jsonData, err := json.MarshalIndent(metadata, "", " ") 23 | if err != nil { 24 | return fmt.Errorf("error marshalling metadata: %w", err) 25 | } 26 | 27 | // create the file 28 | file, err := os.Create(path) 29 | if err != nil { 30 | return fmt.Errorf("error creating file: %w", err) 31 | } 32 | defer file.Close() 33 | 34 | // write the content 35 | _, err = file.Write(jsonData) 36 | if err != nil { 37 | return fmt.Errorf("error writing metadata: %w", err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func LoadMetadata(path string) (*Metadata, error) { 44 | // open the file 45 | file, err := os.Open(path) 46 | if err != nil { 47 | return nil, fmt.Errorf("error opening file: %w", err) 48 | } 49 | defer file.Close() 50 | 51 | // read the file content 52 | fileContent, err := io.ReadAll(file) 53 | if err != nil { 54 | return nil, fmt.Errorf("error reading file: %w", err) 55 | } 56 | 57 | // unmarshal the JSON data 58 | var metadata Metadata 59 | err = json.Unmarshal(fileContent, &metadata) 60 | if err != nil { 61 | return nil, fmt.Errorf("error unmarshalling metadata: %w", err) 62 | } 63 | 64 | return &metadata, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/crypto/sign/ecdsa_p256_test.go: -------------------------------------------------------------------------------- 1 | package sign //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestECDSASigner_Verify(t *testing.T) { 11 | ecdsa := NewECDSASignature() 12 | 13 | // generate public and private keys 14 | pubKey, privKey, err := ecdsa.NewKeyPair() 15 | require.NoError(t, err) 16 | assert.NotEmpty(t, pubKey) 17 | assert.NotEmpty(t, privKey) 18 | 19 | // sign hash 20 | hash := []byte("837c7456ccf4f09aac5ccf1f250449005353c83a92340f16e72d0af1a2504d42444faefa53d4300ec3bb6902b50b94e1b26c506fdcf5342552e387335ec35d91d830baedf34350a89cff4bb04e132156ec15c87abb639c1cc82bf7fc3b82d6eec6215b138100922bb407702c067503912d54eaef8530d0439616e967bd27b7e6e02e921a6124fb34f656ae4b4ff7b91815108a6e47d43b024ce25bb2dc430bfbe80598e0518a17e7c5e309ed3c3905be487816f1f41d86cbe64e4497dbaaccfdb687021f549cfb01384f5ce5a6842d24793a6319a99e6da87c44754042d7411ef88afedd7c81786405ee75d83dea9962cac9da2257598242b60df07cb927df8c32e3dc45fc45d456925739103836858b93df039419a4eda331690a2f76ecb4686c0246d2200b082f549eb2eea4386f1d50c1917e85979c99790915bd70489e85") 21 | signature, err := ecdsa.Sign(hash, privKey) 22 | require.NoError(t, err) 23 | 24 | // make sure that the signature can be verified 25 | verified, err := ecdsa.Verify(signature, hash, pubKey) 26 | require.NoError(t, err) 27 | assert.True(t, verified) 28 | 29 | // modify the message in order to fail verifying 30 | hashModified := hash 31 | hashModified[0] = byte(1) 32 | verified, err = ecdsa.Verify(signature, hashModified, pubKey) 33 | require.NoError(t, err) 34 | assert.False(t, verified) 35 | 36 | // modify the signature in order to fail verifying 37 | signatureModified := signature 38 | signatureModified[0] = byte(1) 39 | verified, err = ecdsa.Verify(signatureModified, hash, pubKey) 40 | require.NoError(t, err) 41 | assert.False(t, verified) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/crypto/sign/ecdsa_p256.go: -------------------------------------------------------------------------------- 1 | package sign 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "math/big" 8 | 9 | util_crypto "github.com/yago-123/chainnet/pkg/util/crypto" 10 | ) 11 | 12 | type ECDSAP256Signer struct { 13 | } 14 | 15 | func NewECDSASignature() *ECDSAP256Signer { 16 | return &ECDSAP256Signer{} 17 | } 18 | 19 | func (ecdsaSign *ECDSAP256Signer) NewKeyPair() ([]byte, []byte, error) { 20 | curve := elliptic.P256() 21 | private, err := ecdsa.GenerateKey(curve, rand.Reader) 22 | if err != nil { 23 | return []byte{}, []byte{}, err 24 | } 25 | 26 | return util_crypto.ConvertECDSAKeysToDERBytes(&private.PublicKey, private) 27 | } 28 | 29 | func (ecdsaSign *ECDSAP256Signer) Sign(payload []byte, privKey []byte) ([]byte, error) { 30 | if err := signInputValidator(payload, privKey); err != nil { 31 | return []byte{}, err 32 | } 33 | 34 | privateKey, err := util_crypto.ConvertDERBytesToECDSAPriv(privKey) 35 | if err != nil { 36 | return []byte{}, err 37 | } 38 | 39 | r, s, err := ecdsa.Sign(rand.Reader, privateKey, payload) 40 | if err != nil { 41 | return []byte{}, err 42 | } 43 | 44 | // consolidate signature 45 | signature := r.Bytes() 46 | signature = append(signature, s.Bytes()...) 47 | 48 | return signature, nil 49 | } 50 | 51 | func (ecdsaSign *ECDSAP256Signer) Verify(signature []byte, payload []byte, pubKey []byte) (bool, error) { 52 | if err := verifyInputValidator(signature, payload, pubKey); err != nil { 53 | return false, err 54 | } 55 | 56 | publicKey, err := util_crypto.ConvertDERBytesToECDSAPub(pubKey) 57 | if err != nil { 58 | return false, err 59 | } 60 | 61 | rLength := len(signature) / 2 //nolint:mnd // we need to divide the signature in half 62 | rBytes := signature[:rLength] 63 | sBytes := signature[rLength:] 64 | 65 | r := new(big.Int).SetBytes(rBytes) 66 | s := new(big.Int).SetBytes(sBytes) 67 | 68 | return ecdsa.Verify(publicKey, payload, r, s), nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/network/common.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/yago-123/chainnet/pkg/encoding" 8 | 9 | "github.com/libp2p/go-libp2p/core/host" 10 | "github.com/libp2p/go-libp2p/core/peer" 11 | "github.com/yago-123/chainnet/config" 12 | ) 13 | 14 | const ( 15 | RouterRetrieveAddressTxs = "/api/v1/address/%s/txs" 16 | RouterRetrieveAddressUTXOs = "/api/v1/address/%s/utxos" 17 | RouterAddressIsActive = "/api/v1/address/%s/active" 18 | RouterSendTx = "/api/v1/sendTx" 19 | 20 | ContentTypeHeader = "Content-Type" 21 | ) 22 | 23 | func extractAddrInfo(addr string, port uint, id string) (*peer.AddrInfo, error) { 24 | addrInfo, err := peer.AddrInfoFromString( 25 | fmt.Sprintf("/dns4/%s/tcp/%d/p2p/%s", addr, port, id), 26 | ) 27 | if err != nil { 28 | return &peer.AddrInfo{}, fmt.Errorf("failed to parse multiaddress: %w", err) 29 | } 30 | 31 | return addrInfo, nil 32 | } 33 | 34 | func connectToSeeds(cfg *config.Config, host host.Host) error { 35 | for _, seed := range cfg.SeedNodes { 36 | addr, err := extractAddrInfo(seed.Address, uint(seed.Port), seed.PeerID) //nolint:gosec // this int to uint is safe 37 | if err != nil { 38 | cfg.Logger.Errorf("failed to extract address info from seed node %s: %v", seed.Address, err) 39 | continue 40 | } 41 | 42 | // todo(): provide this context via argument 43 | err = host.Connect(context.Background(), *addr) 44 | if err != nil { 45 | cfg.Logger.Errorf("failed to connect to seed node %s: %v", addr, err) 46 | continue 47 | } 48 | 49 | cfg.Logger.Infof("connected to seed node %s", addr.ID.String()) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func getContentTypeFrom(encoder encoding.Encoding) string { 56 | switch encoder.Type() { 57 | case encoding.GobEncodingType: 58 | return "application/gob" 59 | case encoding.ProtoEncodingType: 60 | return "application/x-protobuf" 61 | default: 62 | return "application/octet-stream" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/observer/chain_observer.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | ) 8 | 9 | // ChainObserver interface that defines the methods that a block observer should implement 10 | type ChainObserver interface { 11 | ID() string 12 | OnBlockAddition(block *kernel.Block) 13 | OnTxAddition(tx *kernel.Transaction) 14 | } 15 | 16 | // ChainSubject controller that manages the block observers 17 | type ChainSubject interface { 18 | Register(observer ChainObserver) 19 | Unregister(observer ChainObserver) 20 | NotifyBlockAdded(block *kernel.Block) 21 | NotifyTxAdded(tx *kernel.Transaction) 22 | } 23 | 24 | type ChainSubjectController struct { 25 | observers map[string]ChainObserver 26 | mu sync.Mutex 27 | } 28 | 29 | func NewChainSubject() *ChainSubjectController { 30 | return &ChainSubjectController{ 31 | observers: make(map[string]ChainObserver), 32 | } 33 | } 34 | 35 | // Register adds an observer to the list of observers 36 | func (so *ChainSubjectController) Register(observer ChainObserver) { 37 | so.mu.Lock() 38 | defer so.mu.Unlock() 39 | so.observers[observer.ID()] = observer 40 | } 41 | 42 | // Unregister removes an observer from the list of observers 43 | func (so *ChainSubjectController) Unregister(observer ChainObserver) { 44 | so.mu.Lock() 45 | defer so.mu.Unlock() 46 | delete(so.observers, observer.ID()) 47 | } 48 | 49 | // NotifyBlockAdded notifies all observers that a new block has been added 50 | func (so *ChainSubjectController) NotifyBlockAdded(block *kernel.Block) { 51 | so.mu.Lock() 52 | defer so.mu.Unlock() 53 | for _, observer := range so.observers { 54 | observer.OnBlockAddition(block) 55 | } 56 | } 57 | 58 | // NotifyTxAdded notifies all observers that a new transaction has been added 59 | func (so *ChainSubjectController) NotifyTxAdded(tx *kernel.Transaction) { 60 | so.mu.Lock() 61 | defer so.mu.Unlock() 62 | for _, observer := range so.observers { 63 | observer.OnTxAddition(tx) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/miner/pow_test.go: -------------------------------------------------------------------------------- 1 | package miner //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/yago-123/chainnet/pkg/crypto/hash" 8 | "github.com/yago-123/chainnet/pkg/kernel" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestProofOfWork_CalculateBlockHash(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | // check that returns error if the target is bigger than the hash itself 18 | bh := kernel.NewBlockHeader([]byte("1"), 1, []byte("merkle-root"), 1, []byte("prev-block-hash"), 300, 0) 19 | _, err := NewProofOfWork(ctx, bh, hash.SHA256) 20 | require.Error(t, err) 21 | 22 | // calculate simple hash with 1 zero 23 | bh = kernel.NewBlockHeader([]byte("1"), 1, []byte("merkle-root"), 1, []byte("prev-block-hash"), 8, 0) 24 | pow, err := NewProofOfWork(ctx, bh, hash.SHA256) 25 | require.NoError(t, err) 26 | blockHash, nonce, err := pow.CalculateBlockHash() 27 | require.NoError(t, err) 28 | assert.Positive(t, nonce) 29 | assert.Equal(t, []byte{0x0}, blockHash[:1]) 30 | assert.NotEqual(t, []byte{0x0}, blockHash[1:2]) 31 | 32 | // calculate simple hash with 2 zeros 33 | bh = kernel.NewBlockHeader([]byte("1"), 1, []byte("merkle-root"), 1, []byte("prev-block-hash"), 16, 0) 34 | pow, err = NewProofOfWork(ctx, bh, hash.SHA256) 35 | require.NoError(t, err) 36 | blockHash, nonce, err = pow.CalculateBlockHash() 37 | require.NoError(t, err) 38 | assert.Positive(t, nonce) 39 | assert.Equal(t, []byte{0x0, 0x0}, blockHash[:2]) 40 | assert.NotEqual(t, []byte{0x0}, blockHash[2:3]) 41 | 42 | // make suire that proof of work can be cancelled 43 | bh = kernel.NewBlockHeader([]byte("1"), 1, []byte("merkle-root"), 1, []byte("prev-block-hash"), 200, 0) 44 | ctx, cancel := context.WithCancel(context.Background()) 45 | cancel() 46 | pow, err = NewProofOfWork(ctx, bh, hash.SHA256) 47 | require.NoError(t, err) 48 | _, _, err = pow.CalculateBlockHash() 49 | require.Error(t, err) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/cli/cmd/tx.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/yago-123/chainnet/config" 11 | "github.com/yago-123/chainnet/pkg/encoding" 12 | "github.com/yago-123/chainnet/pkg/network" 13 | ) 14 | 15 | const FlagAddress = "address" 16 | 17 | var listTxsCmd = &cobra.Command{ 18 | Use: "list-txs", 19 | Short: "List all transactions", 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | cfg = config.InitConfig(cmd) 22 | 23 | address, _ := cmd.Flags().GetString(FlagAddress) 24 | 25 | if len(address) == 0 { 26 | logger.Fatalf("address must be provided") 27 | } 28 | 29 | url := fmt.Sprintf( 30 | "http://%s/%s", 31 | net.JoinHostPort(cfg.Wallet.ServerAddress, fmt.Sprintf("%d", cfg.Wallet.ServerPort)), 32 | fmt.Sprintf(network.RouterRetrieveAddressTxs, address), 33 | ) 34 | 35 | // send request 36 | resp, err := http.Get(url) //nolint:gosec // this is OK for a CLI tool 37 | if err != nil { 38 | logger.Fatalf("failed to get transactions: %v", err) 39 | } 40 | defer resp.Body.Close() 41 | 42 | if resp.StatusCode != http.StatusOK { 43 | logger.Fatalf("failed to get transactions: %v", resp.Status) 44 | } 45 | 46 | // decode response 47 | encoder := encoding.NewProtobufEncoder() 48 | data, err := io.ReadAll(resp.Body) 49 | if err != nil { 50 | logger.Fatalf("failed to read response body: %v", err) 51 | } 52 | 53 | txs, err := encoder.DeserializeTransactions(data) 54 | if err != nil { 55 | logger.Fatalf("failed to deserialize transactions: %v", err) 56 | } 57 | 58 | // print transactions 59 | for _, tx := range txs { 60 | 61 | logger.Infof("{\n%s}\n", tx.String()) 62 | } 63 | }, 64 | } 65 | 66 | func init() { 67 | // main command 68 | config.AddConfigFlags(listTxsCmd) 69 | rootCmd.AddCommand(listTxsCmd) 70 | 71 | // sub commands 72 | listTxsCmd.Flags().String(FlagAddress, "", "Destination address to send coins") 73 | 74 | // required flags 75 | _ = listTxsCmd.MarkFlagRequired(FlagAddress) 76 | } 77 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "chainnet.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "chainnet.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "chainnet.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "chainnet.labels" -}} 37 | helm.sh/chart: {{ include "chainnet.chart" . }} 38 | {{ include "chainnet.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "chainnet.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "chainnet.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "chainnet.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "chainnet.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /tests/mocks/storage/storage_mock.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck // this is a mock 2 | package storage 3 | 4 | import ( 5 | "github.com/yago-123/chainnet/pkg/kernel" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type MockStorage struct { 11 | mock.Mock 12 | } 13 | 14 | func (ms *MockStorage) PersistBlock(block kernel.Block) error { 15 | args := ms.Called(block) 16 | return args.Error(0) 17 | } 18 | 19 | func (ms *MockStorage) PersistHeader(_ []byte, _ kernel.BlockHeader) error { 20 | return nil 21 | } 22 | 23 | func (ms *MockStorage) GetLastBlock() (*kernel.Block, error) { 24 | args := ms.Called() 25 | return args.Get(0).(*kernel.Block), args.Error(1) 26 | } 27 | 28 | func (ms *MockStorage) GetLastHeader() (*kernel.BlockHeader, error) { 29 | args := ms.Called() 30 | return args.Get(0).(*kernel.BlockHeader), args.Error(1) 31 | } 32 | 33 | func (ms *MockStorage) GetLastBlockHash() ([]byte, error) { 34 | args := ms.Called() 35 | return args.Get(0).([]byte), args.Error(1) 36 | } 37 | 38 | func (ms *MockStorage) GetGenesisBlock() (*kernel.Block, error) { 39 | args := ms.Called() 40 | return args.Get(0).(*kernel.Block), args.Error(1) 41 | } 42 | 43 | func (ms *MockStorage) GetGenesisHeader() (*kernel.BlockHeader, error) { 44 | args := ms.Called() 45 | return args.Get(0).(*kernel.BlockHeader), args.Error(1) 46 | } 47 | 48 | func (ms *MockStorage) RetrieveBlockByHash(hash []byte) (*kernel.Block, error) { 49 | args := ms.Called(hash) 50 | return args.Get(0).(*kernel.Block), args.Error(1) 51 | } 52 | 53 | func (ms *MockStorage) RetrieveHeaderByHash(hash []byte) (*kernel.BlockHeader, error) { 54 | args := ms.Called(hash) 55 | return args.Get(0).(*kernel.BlockHeader), args.Error(1) 56 | } 57 | 58 | func (ms *MockStorage) Typ() string { 59 | return "mock" 60 | } 61 | 62 | func (ms *MockStorage) ID() string { 63 | return ms.Called().String(0) 64 | } 65 | 66 | func (ms *MockStorage) OnBlockAddition(block *kernel.Block) { 67 | ms.Called(block) 68 | } 69 | 70 | func (ms *MockStorage) OnTxAddition(tx *kernel.Transaction) { 71 | ms.Called(tx) 72 | } 73 | 74 | func (ms *MockStorage) Close() error { 75 | args := ms.Called() 76 | return args.Error(0) 77 | } 78 | -------------------------------------------------------------------------------- /config/examples/seed-node-config.yaml: -------------------------------------------------------------------------------- 1 | #seed-nodes: # List of seed nodes 2 | # - address: "seed-1.chainnet.yago.ninja" 3 | # peer-id: "QmVQ8bj9KPfTiN23vX7sbqn4oXjTSycfULL4oApAZccWL5" 4 | # port: 9100 5 | # - address: "seed-2.chainnet.yago.ninja" 6 | # peer-id: "peerID-2" 7 | # port: 8081 8 | # - address: "seed-3.chainnet.yago.ninja" 9 | # peer-id: "peerID-3" 10 | # port: 8082 11 | 12 | storage-file: "bin/miner-storage" # File used for persisting the chain status 13 | miner: 14 | pub-key-reward: # Public wallet key encoded in base58, used for receiving mining rewards 15 | "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTK2r1ViPYeJCMAcSHrt4AEkBouG5vmbAjKMGnZ1RyjP3bPTUhJrRXfEnD3CEhB7Rumao463ayeiU2jbRhjsygwqFp" 16 | mining-interval: "10m" # Interval between block creation 17 | adjustment-interval: 6 # Number of blocks before adjusting the difficulty 18 | 19 | prometheus: 20 | enabled: true # Enable or disable prometheus metrics 21 | port: 9091 # Port exposed for prometheus metrics 22 | libp2p-port: 9099 23 | path: "/metrics" # Path for prometheus metrics endpoint 24 | 25 | p2p: 26 | enabled: true # Enable or disable network communication 27 | identity-path: "/var/chainnet/identity.pem" # ECDSA peer private key path in PEM format (leave empty to generate a random identity) 28 | peer-port: 9100 # Port used for network communication with other peers 29 | http-api-port: 8080 # Port exposed for the router API (required for nespv wallets) 30 | min-conn: 5 # Minimum number of connections 31 | max-conn: 100 # Maximum number of connections 32 | conn-timeout: "60s" # Maximum duration of a connection 33 | write-timeout: "20s" # Maximum duration of a write stream 34 | read-timeout: "20s" # Maximum duration of a read stream 35 | buffer-size: 4096 # Read buffer size over the network 36 | -------------------------------------------------------------------------------- /pkg/network/discovery/mdns.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/yago-123/chainnet/config" 8 | 9 | "github.com/libp2p/go-libp2p/core/host" 10 | "github.com/libp2p/go-libp2p/core/peer" 11 | "github.com/libp2p/go-libp2p/p2p/discovery/mdns" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | const ( 16 | MDNSDiscoveryType = "mDNS" 17 | ) 18 | 19 | type MdnsDiscovery struct { 20 | isActive bool 21 | mdns mdns.Service 22 | } 23 | 24 | // NewMdnsDiscovery creates a new mDNS discovery service 25 | func NewMdnsDiscovery(cfg *config.Config, host host.Host) (*MdnsDiscovery, error) { 26 | // inject the disco notifee logic into the MDNs algorithm 27 | mdnsService := mdns.NewMdnsService(host, DiscoveryServiceTag, newMDNSNotifee(host, cfg.Logger)) 28 | 29 | return &MdnsDiscovery{ 30 | mdns: mdnsService, 31 | isActive: false, 32 | }, nil 33 | } 34 | 35 | func (m *MdnsDiscovery) Start() error { 36 | if m.isActive { 37 | return nil 38 | } 39 | 40 | err := m.mdns.Start() 41 | if err != nil { 42 | return fmt.Errorf("failed to start mDNS service: %w", err) 43 | } 44 | 45 | m.isActive = true 46 | return nil 47 | } 48 | 49 | func (m *MdnsDiscovery) Stop() error { 50 | if !m.isActive { 51 | return nil 52 | } 53 | 54 | err := m.mdns.Close() 55 | if err != nil { 56 | return fmt.Errorf("failed to stop mDNS service: %w", err) 57 | } 58 | 59 | m.isActive = false 60 | return nil 61 | } 62 | 63 | func (m *MdnsDiscovery) Type() string { 64 | return MDNSDiscoveryType 65 | } 66 | 67 | type notifee struct { 68 | host host.Host 69 | logger *logrus.Logger 70 | } 71 | 72 | func newMDNSNotifee(host host.Host, logger *logrus.Logger) notifee { 73 | return notifee{ 74 | host: host, 75 | logger: logger, 76 | } 77 | } 78 | 79 | func (n notifee) HandlePeerFound(pi peer.AddrInfo) { 80 | ctx, cancel := context.WithTimeout(context.Background(), DiscoveryTimeout) 81 | defer cancel() 82 | 83 | // try to connect to the peer and add the peer to the peerstore given that MDNs does not do that by default. 84 | // This way we can the host event bus will emit the peer found event. This addition to the peer store is done 85 | // by default in the case of other discovery types (like DHT) 86 | err := n.host.Connect(ctx, pi) 87 | if err != nil { 88 | n.logger.Errorf("failed to connect to peer %s after mDNS discovery: %s", pi.ID, err) 89 | return 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/crypto/hash/hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "crypto/sha512" 7 | "errors" 8 | "hash" 9 | "sync" 10 | 11 | "golang.org/x/crypto/ripemd160" 12 | ) 13 | 14 | const ( 15 | SHA256 HasherType = iota 16 | SHA512 17 | RipeMD160 18 | ) 19 | 20 | type HasherType uint 21 | 22 | type Hashing interface { 23 | Hash(payload []byte) ([]byte, error) 24 | Verify(hashedPayload []byte, payload []byte) (bool, error) 25 | } 26 | 27 | // GetHasher represents a factory function that returns the hashing algorithm. This 28 | // factory method is used in cases in which we need to use hashing in parallel hashing 29 | // computations given that the algorithms are not thread safe 30 | func GetHasher(i HasherType) Hashing { 31 | switch i { 32 | case SHA256: 33 | return NewHasher(sha256.New()) 34 | case SHA512: 35 | return NewHasher(sha512.New()) 36 | case RipeMD160: 37 | return NewHasher(ripemd160.New()) 38 | default: 39 | return NewVoidHasher() 40 | } 41 | } 42 | 43 | type Hash struct { 44 | h hash.Hash 45 | mu sync.Mutex 46 | } 47 | 48 | func NewHasher(hashAlgo hash.Hash) *Hash { 49 | return &Hash{ 50 | h: hashAlgo, 51 | } 52 | } 53 | 54 | func (hash *Hash) Hash(payload []byte) ([]byte, error) { 55 | if err := hashInputValidator(payload); err != nil { 56 | return []byte{}, err 57 | } 58 | 59 | hash.mu.Lock() 60 | defer hash.mu.Unlock() 61 | 62 | // reset the hasher state 63 | hash.h.Reset() 64 | 65 | _, err := hash.h.Write(payload) 66 | if err != nil { 67 | return []byte{}, err 68 | } 69 | 70 | return hash.h.Sum(nil), nil 71 | } 72 | 73 | func (hash *Hash) Verify(hashedPayload []byte, payload []byte) (bool, error) { 74 | if err := verifyInputValidator(hashedPayload, payload); err != nil { 75 | return false, err 76 | } 77 | 78 | h, err := hash.Hash(payload) 79 | if err != nil { 80 | return false, err 81 | } 82 | 83 | return bytes.Equal(hashedPayload, h), nil 84 | } 85 | 86 | func hashInputValidator(payload []byte) error { 87 | if len(payload) < 1 { 88 | return errors.New("payload is empty") 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func verifyInputValidator(hash []byte, payload []byte) error { 95 | if len(hash) < 1 { 96 | return errors.New("hash is empty") 97 | } 98 | 99 | if len(payload) < 1 { 100 | return errors.New("payload is empty") 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /default-config.yaml: -------------------------------------------------------------------------------- 1 | seed-nodes: # List of seed nodes 2 | - address: "seed-1.chainnet.yago.ninja" 3 | peer-id: "QmVQ8bj9KPfTiN23vX7sbqn4oXjTSycfULL4oApAZccWL5" 4 | port: 9100 5 | # - address: "seed-2.chainnet.yago.ninja" 6 | # peer-id: "peerID-2" 7 | # port: 8081 8 | # - address: "seed-3.chainnet.yago.ninja" 9 | # peer-id: "peerID-3" 10 | # port: 8082 11 | 12 | storage-file: "bin/miner-storage" # File used for persisting the chain status 13 | miner: 14 | pub-key-reward: # Public wallet key encoded in base58, used for receiving mining rewards 15 | "aSq9DsNNvGhYxYyqA9wd2eduEAZ5AXWgJTbTK2r1ViPYeJCMAcSHrt4AEkBouG5vmbAjKMGnZ1RyjP3bPTUhJrRXfEnD3CEhB7Rumao463ayeiU2jbRhjsygwqFp" 16 | mining-interval: "10m" # Interval between block creation 17 | adjustment-interval: 6 # Number of blocks before adjusting the difficulty 18 | 19 | chain: 20 | max-txs-mempool: 10000 # Maximum number of transactions allowed in the mempool 21 | 22 | prometheus: 23 | enabled: true # Enable or disable prometheus metrics 24 | port: 9091 # Port exposed for prometheus metrics 25 | libp2p-port: 9099 # Port exposed for prometheus core libp2p metrics 26 | path: "/metrics" # Path for prometheus metrics endpoint 27 | 28 | p2p: 29 | enabled: true # Enable or disable network communication 30 | #identity-path: "identity.pem" # ECDSA peer private key path in PEM format (leave empty to generate a random identity) 31 | peer-port: 9100 # Port used for network communication with other peers 32 | http-api-port: 8080 # Port exposed for the router API (required for nespv wallets) 33 | min-conn: 5 # Minimum number of connections 34 | max-conn: 100 # Maximum number of connections 35 | conn-timeout: "60s" # Maximum duration of a connection 36 | write-timeout: "20s" # Maximum duration of a write stream 37 | read-timeout: "20s" # Maximum duration of a read stream 38 | buffer-size: 4096 # Read buffer size over the network 39 | 40 | wallet: 41 | wallet-key-path: priv-key.pem # ECDSA wallet private key path in PEM format 42 | server-address: "seed-1.chainnet.yago.ninja" 43 | server-port: 8080 44 | -------------------------------------------------------------------------------- /pkg/network/p2p_timeout.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "github.com/yago-123/chainnet/config" 11 | 12 | "github.com/libp2p/go-libp2p/core/host" 13 | "github.com/libp2p/go-libp2p/core/network" 14 | "github.com/libp2p/go-libp2p/core/peer" 15 | "github.com/libp2p/go-libp2p/core/protocol" 16 | ) 17 | 18 | // TimeoutStream wraps a network.Stream with read and write timeouts 19 | type TimeoutStream struct { 20 | stream network.Stream 21 | readTimeout time.Duration 22 | writeTimeout time.Duration 23 | bufferSize uint 24 | } 25 | 26 | // NewTimeoutStream creates a network.Stream with read and write timeouts 27 | func NewTimeoutStream(ctx context.Context, cfg *config.Config, host host.Host, p peer.ID, pids ...protocol.ID) (*TimeoutStream, error) { 28 | stream, err := host.NewStream(ctx, p, pids...) 29 | if err != nil { 30 | return nil, fmt.Errorf("error enabling stream to %s: %w", p.String(), err) 31 | } 32 | 33 | return AddTimeoutToStream(stream, cfg), nil 34 | } 35 | 36 | // AddTimeoutToStream wraps a network.Stream with TimeoutStream 37 | func AddTimeoutToStream(s network.Stream, cfg *config.Config) *TimeoutStream { 38 | return &TimeoutStream{ 39 | stream: s, 40 | readTimeout: cfg.P2P.ReadTimeout, 41 | writeTimeout: cfg.P2P.WriteTimeout, 42 | bufferSize: cfg.P2P.BufferSize, 43 | } 44 | } 45 | 46 | // ReadWithTimeout reads from the stream with a timeout 47 | func (t *TimeoutStream) ReadWithTimeout() ([]byte, error) { 48 | var data []byte 49 | if t.readTimeout > 0 { 50 | err := t.stream.SetReadDeadline(time.Now().Add(t.readTimeout)) 51 | if err != nil { 52 | return []byte{}, err 53 | } 54 | } 55 | 56 | buf := make([]byte, t.bufferSize) 57 | // read until EOF 58 | for { 59 | n, err := t.stream.Read(buf) 60 | if n > 0 { 61 | data = append(data, buf[:n]...) 62 | } 63 | 64 | if errors.Is(err, io.EOF) { 65 | // end of stream, break the loop 66 | break 67 | } 68 | 69 | if err != nil { 70 | return []byte{}, err 71 | } 72 | } 73 | 74 | return data, nil 75 | } 76 | 77 | // WriteWithTimeout writes to the stream with a timeout 78 | func (t *TimeoutStream) WriteWithTimeout(buf []byte) (int, error) { 79 | if t.writeTimeout > 0 { 80 | err := t.stream.SetWriteDeadline(time.Now().Add(t.writeTimeout)) 81 | if err != nil { 82 | return 0, err 83 | } 84 | } 85 | 86 | return t.stream.Write(buf) 87 | } 88 | 89 | // Close closes the stream 90 | func (t *TimeoutStream) Close() error { 91 | return t.stream.Close() 92 | } 93 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/visualization/files/dashboard-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 1, 22 | "links": [], 23 | "panels": [ 24 | { 25 | "fieldConfig": { 26 | "defaults": {}, 27 | "overrides": [] 28 | }, 29 | "gridPos": { 30 | "h": 15, 31 | "w": 12, 32 | "x": 0, 33 | "y": 0 34 | }, 35 | "id": 1, 36 | "options": { 37 | "folderUID": "eeb08oxzioi68d", 38 | "includeVars": false, 39 | "keepTime": false, 40 | "maxItems": 10, 41 | "query": "", 42 | "showFolderNames": false, 43 | "showHeadings": false, 44 | "showRecentlyViewed": false, 45 | "showSearch": true, 46 | "showStarred": false, 47 | "tags": [] 48 | }, 49 | "pluginVersion": "11.4.0", 50 | "title": "Monitoring List", 51 | "type": "dashlist" 52 | }, 53 | { 54 | "fieldConfig": { 55 | "defaults": {}, 56 | "overrides": [] 57 | }, 58 | "gridPos": { 59 | "h": 15, 60 | "w": 12, 61 | "x": 12, 62 | "y": 0 63 | }, 64 | "id": 2, 65 | "options": { 66 | "folderUID": "beb093zy0dkaoa", 67 | "includeVars": false, 68 | "keepTime": false, 69 | "maxItems": 10, 70 | "query": "", 71 | "showFolderNames": true, 72 | "showHeadings": false, 73 | "showRecentlyViewed": false, 74 | "showSearch": true, 75 | "showStarred": true, 76 | "tags": [] 77 | }, 78 | "pluginVersion": "11.4.0", 79 | "title": "Logging List", 80 | "type": "dashlist" 81 | } 82 | ], 83 | "preload": false, 84 | "schemaVersion": 40, 85 | "tags": [], 86 | "templating": { 87 | "list": [] 88 | }, 89 | "time": { 90 | "from": "now-24h", 91 | "to": "now" 92 | }, 93 | "timepicker": { 94 | "hidden": true 95 | }, 96 | "timezone": "browser", 97 | "title": "Dashboard List", 98 | "uid": "eeazamyajw2kgc", 99 | "version": 1, 100 | "weekStart": "" 101 | } 102 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yago-123/chainnet/pkg/util" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestIsFirstNBytesZero(t *testing.T) { 14 | hash := []byte{0x0, 0xFF, 0xFF} 15 | 16 | require.True(t, util.IsFirstNBitsZero(hash, 8)) 17 | require.False(t, util.IsFirstNBitsZero(hash, 16)) 18 | require.False(t, util.IsFirstNBitsZero(hash, 256)) 19 | 20 | hash = []byte{0x7F, 0xFF, 0xFF} 21 | require.True(t, util.IsFirstNBitsZero(hash, 1)) 22 | require.False(t, util.IsFirstNBitsZero(hash, 2)) 23 | 24 | hash = []byte{0x0, 0x7F, 0xFF} 25 | require.True(t, util.IsFirstNBitsZero(hash, 9)) 26 | require.False(t, util.IsFirstNBitsZero(hash, 10)) 27 | } 28 | 29 | func TestCalculateMiningDifficulty(t *testing.T) { 30 | type args struct { 31 | currentTarget uint 32 | targetTimeSpan float64 33 | actualTimeSpan int64 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want uint 39 | }{ 40 | { 41 | name: "No adjustment needed", 42 | args: args{ 43 | currentTarget: 100, 44 | targetTimeSpan: 600, 45 | actualTimeSpan: 600, 46 | }, 47 | want: 100, 48 | }, 49 | { 50 | name: "Increase difficulty (actual time is less than target time)", 51 | args: args{ 52 | currentTarget: 100, 53 | targetTimeSpan: 600, 54 | actualTimeSpan: 300, 55 | }, 56 | want: 101, 57 | }, 58 | { 59 | name: "Decrease difficulty", 60 | args: args{ 61 | currentTarget: 100, 62 | targetTimeSpan: 600, 63 | actualTimeSpan: 1200, 64 | }, 65 | want: 99, 66 | }, 67 | { 68 | name: "Decrease difficulty by a small margin ", 69 | args: args{ 70 | currentTarget: 100, 71 | targetTimeSpan: 600, 72 | actualTimeSpan: 601, 73 | }, 74 | want: 99, 75 | }, 76 | { 77 | name: "Test lower limits", 78 | args: args{ 79 | currentTarget: 1, 80 | targetTimeSpan: 600, 81 | actualTimeSpan: 700, // twice the target time span 82 | }, 83 | want: 1, 84 | }, 85 | { 86 | name: "Test upper limits", 87 | args: args{ 88 | currentTarget: 255, 89 | targetTimeSpan: 600, 90 | actualTimeSpan: 500, // twice the target time span 91 | }, 92 | want: 255, 93 | }, 94 | } 95 | 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | if got := util.CalculateMiningTarget(tt.args.currentTarget, tt.args.targetTimeSpan, tt.args.actualTimeSpan); got != tt.want { 99 | t.Errorf("CalculateMiningDifficulty() = %v, want %v", got, tt.want) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func TestIsValidHash(t *testing.T) { 106 | hash := "0000006484ffdc39a5ba6cebae9e398878f24bcab93f4c32acf81e246fa2474b" 107 | assert.True(t, util.IsValidHash([]byte(hash))) 108 | } 109 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/yago-123/chainnet/pkg/kernel" 5 | ) 6 | 7 | const ( 8 | FirstBlockKey = "firstblock" 9 | FirstHeaderKey = "firstheader" 10 | LastBlockKey = "lastblock" 11 | LastHeaderKey = "lastheader" 12 | // LastBlockHashKey is updated when persisting a new block header 13 | LastBlockHashKey = "lastblockhash" 14 | 15 | StorageObserverID = "storage-observer" 16 | ) 17 | 18 | type Storage interface { 19 | // PersistBlock stores a new block and updates LastBlockKey 20 | PersistBlock(block kernel.Block) error 21 | // PersistHeader stores a new header and updates LastHeaderKey and LastBlockHashKey. The latter key 22 | // is updated in this function because as soon as the header is written the block has been commited 23 | // to the chain, even if the block itself has not been persisted yet. Refer to GetLastBlockHash 24 | // function for additional information 25 | PersistHeader(blockHash []byte, blockHeader kernel.BlockHeader) error 26 | // GetLastBlock retrieves the block information contained in LastBlockKey 27 | GetLastBlock() (*kernel.Block, error) 28 | // GetLastHeader retrieves the header of the last block. The last header represents the latest block 29 | // commited to the chain (the block may not be persisted yet) 30 | GetLastHeader() (*kernel.BlockHeader, error) 31 | // GetLastBlockHash retrieves the latest block added to the chain. The value retrieved in this function 32 | // is updated when PersistHeader is called, ensuring therefore that as soon as a header is persisted, the 33 | // latest block hash is persisted too (this have to do with the chain observer). 34 | // 35 | // There may be cases in which the block hash is present but the block represented has not been persisted 36 | // yet. This case is well known and the chain package is designed taking that fact into account. 37 | GetLastBlockHash() ([]byte, error) 38 | // GetGenesisBlock retrieves the content stored in FirstBlockKey 39 | GetGenesisBlock() (*kernel.Block, error) 40 | // GetGenesisHeader retrieves the content stored in FirstHeaderKey 41 | GetGenesisHeader() (*kernel.BlockHeader, error) 42 | // RetrieveBlockByHash retrieves the block that corresponds to the hash 43 | RetrieveBlockByHash(hash []byte) (*kernel.Block, error) 44 | // RetrieveHeaderByHash retrieves the block header that corresponds to the block hash 45 | RetrieveHeaderByHash(hash []byte) (*kernel.BlockHeader, error) 46 | // Typ returns the type of storage used 47 | Typ() string 48 | // ID returns the key StorageObserverID used for running Observer code 49 | ID() string 50 | // OnBlockAddition called when a new block is added to the chain, in the case of storage must be async 51 | OnBlockAddition(block *kernel.Block) 52 | // OnTxAddition called when a new tx is added to the mempool, in the case of storage must be async 53 | OnTxAddition(block *kernel.Transaction) 54 | // Close finishes the connection with the DB 55 | Close() error 56 | } 57 | -------------------------------------------------------------------------------- /ansible/playbooks/roles/blockchain/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure apt is up to date and install necessary packages 3 | apt: 4 | update_cache: yes 5 | name: 6 | - make 7 | - protobuf-compiler 8 | state: present 9 | tags: 10 | - update 11 | - packages 12 | 13 | - name: Create application directory 14 | file: 15 | path: "{{ app_dir }}" 16 | state: directory 17 | mode: '0755' 18 | tags: 19 | - directory 20 | 21 | - name: Clone the repository 22 | git: 23 | repo: "{{ repo_url }}" 24 | dest: "{{ app_dir }}" 25 | version: "{{ branch }}" 26 | force: yes 27 | update: yes 28 | tags: 29 | - git 30 | 31 | - name: Download and install Go binary 32 | block: 33 | - name: Download Go binary 34 | get_url: 35 | url: "https://go.dev/dl/go{{ go_version }}.linux-amd64.tar.gz" 36 | dest: "/tmp/go{{ go_version }}.linux-amd64.tar.gz" 37 | 38 | - name: Extract Go binary 39 | unarchive: 40 | src: "/tmp/go{{ go_version }}.linux-amd64.tar.gz" 41 | dest: "/usr/local" 42 | remote_src: yes 43 | tags: 44 | - go 45 | 46 | - name: Install Go tools 47 | shell: | 48 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ 49 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 50 | environment: 51 | PATH: "/usr/local/go/bin" 52 | tags: 53 | - go-tools 54 | 55 | - name: Build the application with make 56 | shell: make {{ target }} 57 | args: 58 | chdir: "{{ app_dir }}" 59 | environment: 60 | PATH: "/usr/bin:{{ ansible_env.PATH }}:/usr/local/go/bin:{{ ansible_env.HOME }}/go/bin" 61 | tags: 62 | - build 63 | 64 | - name: Copy the configuration file 65 | copy: 66 | src: "{{ config }}" 67 | dest: "{{ app_dir }}/config.yaml" 68 | mode: '0644' 69 | 70 | - name: Create logging directory 71 | file: 72 | path: "/var/log/chainnet" 73 | state: directory 74 | mode: '0755' 75 | tags: 76 | - directory 77 | 78 | - name: Check if identity file path is defined 79 | debug: 80 | msg: "The identity file path is not defined for this host." 81 | when: identity_path is not defined 82 | 83 | - name: Copy identity file to the target machine 84 | copy: 85 | src: "{{ identity_path }}" 86 | dest: "{{ app_dir }}/identity.pem" 87 | mode: '0600' 88 | when: identity_path is defined 89 | 90 | - name: Template systemd service file 91 | template: 92 | src: "templates/systemd-chain.service.j2" 93 | dest: "/etc/systemd/system/{{ target }}.service" 94 | mode: '0644' 95 | 96 | - name: Reload systemd daemon 97 | command: systemctl daemon-reload 98 | 99 | - name: Enable service 100 | systemd: 101 | name: "{{ target }}" 102 | enabled: yes 103 | 104 | - name: Restart service 105 | systemd: 106 | name: "{{ target }}" 107 | state: restarted 108 | -------------------------------------------------------------------------------- /cmd/nespv/cmd/addresses.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/btcsuite/btcutil/base58" 5 | "github.com/spf13/cobra" 6 | "github.com/yago-123/chainnet/config" 7 | "github.com/yago-123/chainnet/pkg/consensus/validator" 8 | "github.com/yago-123/chainnet/pkg/crypto/hash" 9 | "github.com/yago-123/chainnet/pkg/encoding" 10 | cerror "github.com/yago-123/chainnet/pkg/errs" 11 | util_crypto "github.com/yago-123/chainnet/pkg/util/crypto" 12 | util_p2pkh "github.com/yago-123/chainnet/pkg/util/p2pkh" 13 | wallt "github.com/yago-123/chainnet/pkg/wallet/simple_wallet" 14 | ) 15 | 16 | var addressesCmd = &cobra.Command{ 17 | Use: "addresses", 18 | Short: "Addresses wallet", 19 | Long: `Retrieve addresses from wallet.`, 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | cfg = config.InitConfig(cmd) 22 | 23 | privKeyCont, _ := cmd.Flags().GetString(FlagPrivKey) 24 | privKeyPath, _ := cmd.Flags().GetString(FlagWalletKey) 25 | 26 | // check if only one private key is provided 27 | if (privKeyCont == "") == (privKeyPath == "") { 28 | logger.Fatalf("specify one argument containing the private key: --priv-key or --wallet-key-path") 29 | } 30 | 31 | var err error 32 | var privKey, pubKey []byte 33 | 34 | // process key from path or from content 35 | if privKeyCont != "" { 36 | privKey = base58.Decode(privKeyCont) 37 | } 38 | 39 | if privKeyPath != "" { 40 | privKey, err = util_crypto.ReadECDSAPemToPrivateKeyDerBytes(privKeyPath) 41 | if err != nil { 42 | logger.Fatalf("error reading private key: %v", err) 43 | } 44 | } 45 | 46 | // derive public key from private key 47 | pubKey, err = util_crypto.DeriveECDSAPubFromPrivateDERBytes(privKey) 48 | if err != nil { 49 | logger.Fatalf("%v: %v", cerror.ErrCryptoPublicKeyDerivation, err) 50 | } 51 | 52 | // create wallet 53 | wallet, err := wallt.NewWalletWithKeys( 54 | cfg, 55 | 1, 56 | validator.NewLightValidator(hash.GetHasher(consensusHasherType)), 57 | consensusSigner, 58 | hash.GetHasher(consensusHasherType), 59 | encoding.NewProtobufEncoder(), 60 | privKey, 61 | pubKey, 62 | ) 63 | if err != nil { 64 | logger.Fatalf("error setting up wallet: %v", err) 65 | } 66 | 67 | logger.Infof("P2PK addr: %s", base58.Encode(wallet.GetP2PKAddress())) 68 | 69 | p2pkhAddr, err := wallet.GetP2PKHAddress() 70 | if err != nil { 71 | logger.Fatalf("error getting P2PKH address: %v", err) 72 | } 73 | logger.Infof("P2PKH addr: %s", base58.Encode(p2pkhAddr)) 74 | 75 | pubKeyHashedAddr, version, err := util_p2pkh.ExtractPubKeyHashedFromP2PKHAddr(p2pkhAddr) 76 | if err != nil { 77 | logger.Fatalf("error extracting pub key hash from P2PKH address: %v", err) 78 | } 79 | logger.Infof("Hashed-only P2PKH address %s, version: %d", base58.Encode(pubKeyHashedAddr), version) 80 | }, 81 | } 82 | 83 | func init() { 84 | // main command 85 | config.AddConfigFlags(addressesCmd) 86 | rootCmd.AddCommand(addressesCmd) 87 | 88 | // sub commands 89 | addressesCmd.Flags().String(FlagPrivKey, "", "Private key") 90 | 91 | // todo(): reestructure this duplication 92 | addressesCmd.Flags().String(FlagWalletKey, "", "Path to private key") 93 | 94 | // required flags 95 | _ = addressesCmd.MarkFlagRequired(FlagAddress) 96 | _ = addressesCmd.MarkFlagRequired(FlagAmount) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/monitor/prometheus.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/prometheus/client_golang/prometheus/collectors" 11 | 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | 14 | "github.com/julienschmidt/httprouter" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/sirupsen/logrus" 17 | "github.com/yago-123/chainnet/config" 18 | ) 19 | 20 | const ( 21 | ReadWriteTimeout = 5 * time.Second 22 | IdleTimeout = 10 * time.Second 23 | 24 | PrometheusExporterShutdownTimeout = 10 * time.Second 25 | ) 26 | 27 | const ( 28 | OperationLabel = "operation" 29 | ProtocolLabel = "protocol" 30 | PeerLabel = "peer_id" 31 | StorageLabel = "storage" 32 | ) 33 | 34 | type PromExporter struct { 35 | monitors []Monitor 36 | r *httprouter.Router 37 | srv *http.Server 38 | 39 | isActive bool 40 | registry *prometheus.Registry 41 | 42 | logger *logrus.Logger 43 | cfg *config.Config 44 | } 45 | 46 | func NewPrometheusExporter(cfg *config.Config, monitors []Monitor) *PromExporter { 47 | r := httprouter.New() 48 | registry := prometheus.NewRegistry() 49 | 50 | r.GET(cfg.Prometheus.Path, func(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 51 | promhttp.HandlerFor(registry, promhttp.HandlerOpts{ 52 | ErrorLog: cfg.Logger, 53 | Timeout: ReadWriteTimeout, 54 | ErrorHandling: promhttp.ContinueOnError, 55 | }).ServeHTTP(w, req) 56 | }) 57 | 58 | // register the metrics for each monitor 59 | for _, monitor := range monitors { 60 | monitor.RegisterMetrics(registry) 61 | } 62 | 63 | // register the metrics for the Go runtime and the current process 64 | registry.MustRegister( 65 | collectors.NewGoCollector(), // metrics about golang runtime: GC stats, memory usage and other 66 | collectors.NewProcessCollector( // metrics about the process: CPU usage, file descriptors and other 67 | collectors.ProcessCollectorOpts{}, 68 | ), 69 | ) 70 | 71 | return &PromExporter{ 72 | monitors: monitors, 73 | r: r, 74 | registry: registry, 75 | logger: cfg.Logger, 76 | cfg: cfg, 77 | } 78 | } 79 | 80 | func (prom *PromExporter) Start() error { 81 | if prom.isActive { 82 | return nil 83 | } 84 | 85 | srv := &http.Server{ 86 | Addr: fmt.Sprintf("127.0.0.1:%d", prom.cfg.Prometheus.Port), 87 | Handler: prom.r, 88 | ReadTimeout: ReadWriteTimeout, 89 | WriteTimeout: ReadWriteTimeout, 90 | IdleTimeout: IdleTimeout, 91 | } 92 | 93 | prom.srv = srv 94 | prom.isActive = true 95 | 96 | go func() { 97 | err := srv.ListenAndServe() 98 | if errors.Is(err, http.ErrServerClosed) { 99 | prom.logger.Infof("prometheus exporter server stopped successfully") 100 | return 101 | } 102 | 103 | if err != nil { 104 | prom.logger.Errorf("prometheus exporter server stopped with error: %s", err) 105 | return 106 | } 107 | }() 108 | 109 | return nil 110 | } 111 | 112 | func (prom *PromExporter) Stop() error { 113 | if !prom.isActive { 114 | return nil 115 | } 116 | 117 | ctx, cancel := context.WithTimeout(context.Background(), PrometheusExporterShutdownTimeout) 118 | defer cancel() 119 | 120 | if err := prom.srv.Shutdown(ctx); err != nil { 121 | return fmt.Errorf("failed to shutdown HTTP server: %w", err) 122 | } 123 | 124 | prom.isActive = false 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/util/p2pkh/p2pkh.go: -------------------------------------------------------------------------------- 1 | package utilp2pkh 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "fmt" 7 | 8 | "golang.org/x/crypto/ripemd160" 9 | 10 | "github.com/yago-123/chainnet/pkg/crypto" 11 | "github.com/yago-123/chainnet/pkg/crypto/hash" 12 | ) 13 | 14 | const ( 15 | P2PKHAddressLength = 1 + 20 + 4 // version + pubKeyHash + checksum 16 | P2PKHPubKeyHashLength = 20 17 | ) 18 | 19 | // GenerateP2PKHAddrFromPubKey generates a P2PKH address from a public key (including a checksum for error detection). 20 | // Returns the P2PKH address as a base58 encoded string. 21 | func GenerateP2PKHAddrFromPubKey(pubKey []byte, version byte) ([]byte, error) { 22 | hasherP2PKH := crypto.NewMultiHash( 23 | []hash.Hashing{hash.NewHasher(sha256.New()), hash.NewHasher(ripemd160.New())}, 24 | ) 25 | 26 | // hash the public key 27 | pubKeyHash, err := hasherP2PKH.Hash(pubKey) 28 | if err != nil { 29 | return []byte{}, fmt.Errorf("could not hash the public key: %w", err) 30 | } 31 | 32 | // add the version to the hashed public key in order to hash again and obtain the checksum 33 | versionedPayload := append([]byte{version}, pubKeyHash...) 34 | // todo() checksum must be a double SHA-256 hash, instead of SHA-256 + RIPEMD-160, but for now is OK 35 | checksum, err := hasherP2PKH.Hash(versionedPayload) 36 | if err != nil { 37 | return []byte{}, fmt.Errorf("could not hash the versioned payload: %w", err) 38 | } 39 | 40 | // add checksum to generate address 41 | payload := append(versionedPayload, checksum[:4]...) //nolint:gocritic // we need to append the checksum to the payload 42 | 43 | return payload, nil 44 | } 45 | 46 | // ExtractPubKeyHashedFromP2PKHAddr extracts the public key hash from a P2PKH address 47 | func ExtractPubKeyHashedFromP2PKHAddr(address []byte) ([]byte, byte, error) { 48 | hasherP2PKH := crypto.NewMultiHash( 49 | []hash.Hashing{hash.NewHasher(sha256.New()), hash.NewHasher(ripemd160.New())}, 50 | ) 51 | 52 | // check that address has at least the minimum valid length (1 version + 1 pubKeyHash + 4 checksum) 53 | // we know that the address should be 20 bytes because it is a RIPEMD hash, but for now this is OK 54 | if len(address) != P2PKHAddressLength { 55 | return nil, 0, fmt.Errorf("invalid P2PKH address length: got %d, want %d", len(string(address)), P2PKHAddressLength) 56 | } 57 | 58 | version := address[0] 59 | 60 | // extract the public key hash (remaining bytes except for the last 4, if available) 61 | pubKeyHash := address[1 : len(address)-4] 62 | 63 | // ensure that the public key hash is not empty 64 | if len(pubKeyHash) != P2PKHPubKeyHashLength { 65 | return nil, 0, fmt.Errorf("invalid public key hash length: got %d, want %d", len(string(pubKeyHash)), P2PKHPubKeyHashLength) 66 | } 67 | 68 | // verify the checksum 69 | checksum := address[len(address)-4:] 70 | err := verifyP2PKHChecksum(version, pubKeyHash, checksum, hasherP2PKH) 71 | if err != nil { 72 | return nil, 0, err 73 | } 74 | 75 | return pubKeyHash, version, nil 76 | } 77 | 78 | func verifyP2PKHChecksum(version byte, pubKeyHash, checksum []byte, hasherP2PKH hash.Hashing) error { 79 | versionPayload := append([]byte{version}, pubKeyHash...) 80 | calculatedChecksum, err := hasherP2PKH.Hash(versionPayload) 81 | if err != nil { 82 | return fmt.Errorf("could not hash the versioned payload: %w", err) 83 | } 84 | 85 | if !bytes.Equal(checksum, calculatedChecksum[:4]) { 86 | return fmt.Errorf("error validating checksum, expected %x, got %x", checksum, calculatedChecksum[:4]) 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/observer/net_observer.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | 8 | "github.com/libp2p/go-libp2p/core/peer" 9 | ) 10 | 11 | // NetObserver interface that defines the methods that a network observer should implement 12 | type NetObserver interface { 13 | ID() string 14 | OnNodeDiscovered(peerID peer.ID) 15 | OnUnconfirmedHeaderReceived(peer peer.ID, header kernel.BlockHeader) 16 | OnUnconfirmedTxReceived(tx kernel.Transaction) error 17 | OnUnconfirmedTxIDReceived(peer peer.ID, txID []byte) 18 | } 19 | 20 | // NetSubject controller that manages the net observers 21 | type NetSubject interface { 22 | Register(observer NetObserver) 23 | Unregister(observer NetObserver) 24 | // NotifyNodeDiscovered notifies the chain when a new node has been discovered via pubsub 25 | NotifyNodeDiscovered(peerID peer.ID) 26 | // NotifyUnconfirmedHeaderReceived notifies the chain when a header is received via pubsub 27 | NotifyUnconfirmedHeaderReceived(peer peer.ID, header kernel.BlockHeader) 28 | // NotifyUnconfirmedTxReceived notifies the chain when a transaction is received via stream 29 | NotifyUnconfirmedTxReceived(tx kernel.Transaction) error 30 | // NotifyUnconfirmedTxIDReceived notifies the chain when a transaction ID is received via pubsub 31 | NotifyUnconfirmedTxIDReceived(peer peer.ID, txID []byte) 32 | } 33 | 34 | type NetSubjectController struct { 35 | observers map[string]NetObserver 36 | mu sync.Mutex 37 | } 38 | 39 | func NewNetSubject() *NetSubjectController { 40 | return &NetSubjectController{ 41 | observers: make(map[string]NetObserver), 42 | } 43 | } 44 | 45 | // Register adds an observer to the list of observers 46 | func (no *NetSubjectController) Register(observer NetObserver) { 47 | no.mu.Lock() 48 | defer no.mu.Unlock() 49 | no.observers[observer.ID()] = observer 50 | } 51 | 52 | // Unregister removes an observer from the list of observers 53 | func (no *NetSubjectController) Unregister(observer NetObserver) { 54 | no.mu.Lock() 55 | defer no.mu.Unlock() 56 | delete(no.observers, observer.ID()) 57 | } 58 | 59 | // NotifyNodeDiscovered notifies all observers that a new node has been discovered 60 | func (no *NetSubjectController) NotifyNodeDiscovered(peerID peer.ID) { 61 | no.mu.Lock() 62 | defer no.mu.Unlock() 63 | for _, observer := range no.observers { 64 | observer.OnNodeDiscovered(peerID) 65 | } 66 | } 67 | 68 | // NotifyUnconfirmedHeaderReceived notifies all observers that a new block has been added 69 | func (no *NetSubjectController) NotifyUnconfirmedHeaderReceived(peer peer.ID, header kernel.BlockHeader) { 70 | no.mu.Lock() 71 | defer no.mu.Unlock() 72 | for _, observer := range no.observers { 73 | observer.OnUnconfirmedHeaderReceived(peer, header) 74 | } 75 | } 76 | 77 | // NotifyUnconfirmedTxReceived notifies all observers that a new unconfirmed transaction has been received 78 | func (no *NetSubjectController) NotifyUnconfirmedTxReceived(tx kernel.Transaction) error { 79 | no.mu.Lock() 80 | defer no.mu.Unlock() 81 | for _, observer := range no.observers { 82 | if err := observer.OnUnconfirmedTxReceived(tx); err != nil { 83 | return err 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // NotifyUnconfirmedTxIDReceived notifies all observers that a new unconfirmed transaction ID has been received 91 | func (no *NetSubjectController) NotifyUnconfirmedTxIDReceived(peer peer.ID, txID []byte) { 92 | no.mu.Lock() 93 | defer no.mu.Unlock() 94 | for _, observer := range no.observers { 95 | observer.OnUnconfirmedTxIDReceived(peer, txID) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Define directories 2 | OUTPUT_DIR := bin 3 | NODE_PROTOBUF_DIR := pkg/network/protobuf 4 | 5 | CLI_BINARY_NAME := chainnet-cli 6 | MINER_BINARY_NAME := chainnet-miner 7 | NESPV_BINARY_NAME := chainnet-nespv 8 | NODE_BINARY_NAME := chainnet-node 9 | BOT_BINARY_NAME := chainnet-bot 10 | 11 | # Define the source file for the CLI application 12 | CLI_SOURCE := $(wildcard cmd/cli/*.go) 13 | MINER_SOURCE := $(wildcard cmd/miner/*.go) 14 | NESPV_SOURCE := $(wildcard cmd/nespv/*go) 15 | NODE_SOURCE := $(wildcard cmd/node/*.go) 16 | BOT_SOURCE := $(wildcard cmd/bot/*.go) 17 | 18 | # Define the source files for other files 19 | NODE_PROTOBUF_SOURCE := $(wildcard $(NODE_PROTOBUF_DIR)/*.proto) 20 | NODE_PROTOBUF_PB_SOURCE := $(wildcard $(NODE_PROTOBUF_DIR)/*.pb.go) 21 | 22 | # Define build flags 23 | GCFLAGS := -gcflags "all=-N -l" 24 | 25 | # Docker image names and paths 26 | DOCKER_IMAGE_MINER := yagoninja/chainnet-miner:latest 27 | DOCKER_IMAGE_NODE := yagoninja/chainnet-node:latest 28 | DOCKERFILE_MINER := ./build/docker/miner/Dockerfile 29 | DOCKERFILE_NODE := ./build/docker/node/Dockerfile 30 | 31 | .PHONY: all 32 | all: test lint miner node nespv cli bot 33 | 34 | .PHONY: miner 35 | miner: protobuf output-dir 36 | @echo "Building chainnet miner..." 37 | @go build $(GCFLAGS) -o $(OUTPUT_DIR)/$(MINER_BINARY_NAME) $(MINER_SOURCE) 38 | 39 | .PHONY: node 40 | node: protobuf output-dir 41 | @echo "Building chainnet node..." 42 | @go build $(GCFLAGS) -o $(OUTPUT_DIR)/$(NODE_BINARY_NAME) $(NODE_SOURCE) 43 | 44 | .PHONY: nespv 45 | nespv: protobuf output-dir 46 | @echo "Building chainnet nespv..." 47 | @go build $(GCFLAGS) -o $(OUTPUT_DIR)/$(NESPV_BINARY_NAME) $(NESPV_SOURCE) 48 | 49 | .PHONY: cli 50 | cli: protobuf output-dir 51 | @echo "Building chainnet CLI..." 52 | @go build $(GCFLAGS) -o $(OUTPUT_DIR)/$(CLI_BINARY_NAME) $(CLI_SOURCE) 53 | 54 | .PHONY: bot 55 | bot: protobuf output-dir 56 | @echo "Building chainnet bot..." 57 | @go build $(GCFLAGS) -o $(OUTPUT_DIR)/$(BOT_BINARY_NAME) $(BOT_SOURCE) 58 | 59 | .PHONY: protobuf 60 | protobuf: 61 | @echo "Generating protobuf files..." 62 | @protoc --go_out=. --go_opt=paths=source_relative $(NODE_PROTOBUF_SOURCE) 63 | 64 | .PHONY: output-dir 65 | output-dir: 66 | @mkdir -p $(OUTPUT_DIR) 67 | 68 | .PHONY: lint 69 | lint: protobuf 70 | @echo "Running linter..." 71 | @golangci-lint run ./... 72 | 73 | .PHONY: test 74 | test: protobuf 75 | @echo "Running tests..." 76 | @go test -v -cover ./... -tags '!e2e' 77 | 78 | .PHONY: clean 79 | clean: 80 | @echo "Cleaning up..." 81 | @rm -rf $(OUTPUT_DIR) 82 | @rm -f __debug_bin* 83 | @rm -f _fixture/* 84 | @rm -f $(NODE_PROTOBUF_PB_SOURCE) 85 | 86 | .PHONY: imports 87 | imports: 88 | @find . -name "*.go" | xargs goimports -w 89 | 90 | .PHONY: fmt 91 | fmt: 92 | @go fmt ./... 93 | 94 | .PHONY: debug 95 | debug: node 96 | dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./bin/chainnet-node -- --config default-config.yaml 97 | 98 | .PHONY: miner-image 99 | miner-image: miner 100 | @echo "Building Docker image for chainnet miner..." 101 | docker build -t $(DOCKER_IMAGE_MINER) -f $(DOCKERFILE_MINER) . 102 | 103 | .PHONY: node-image 104 | node-image: node 105 | @echo "Building Docker image for chainnet node..." 106 | docker build -t $(DOCKER_IMAGE_NODE) -f $(DOCKERFILE_NODE) . 107 | 108 | .PHONY: images 109 | images: miner-image node-image 110 | 111 | .PHONY: push 112 | push: miner-image node-image 113 | @echo "Pushing Docker images to Docker Hub..." 114 | docker push $(DOCKER_IMAGE_MINER) 115 | docker push $(DOCKER_IMAGE_NODE) 116 | -------------------------------------------------------------------------------- /pkg/wallet/common.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/yago-123/chainnet/pkg/kernel" 8 | "github.com/yago-123/chainnet/pkg/script" 9 | util_p2pkh "github.com/yago-123/chainnet/pkg/util/p2pkh" 10 | ) 11 | 12 | // GenerateInputs set up the inputs for the transaction and returns the total balance of the UTXOs that are going to be 13 | // spent in the transaction 14 | func GenerateInputs(utxos []*kernel.UTXO, targetAmount uint) ([]kernel.TxInput, uint, error) { 15 | // for now simple FIFO method, first outputs are the first to be spent 16 | balance := uint(0) 17 | inputs := []kernel.TxInput{} 18 | 19 | for _, utxo := range utxos { 20 | balance += utxo.Output.Amount 21 | inputs = append(inputs, kernel.NewInput(utxo.TxID, utxo.OutIdx, "", utxo.Output.PubKey)) 22 | if balance >= targetAmount { 23 | return inputs, balance, nil 24 | } 25 | } 26 | 27 | return []kernel.TxInput{}, balance, errors.New("not enough funds to perform the transaction") 28 | } 29 | 30 | // GenerateOutputs set up the outputs for the transaction and handle the change if necessary 31 | // Arguments: 32 | // - scriptType: the type of script that is going to be used in the outputs 33 | // - targetAmounts: the amount that is going to be sent to the receivers 34 | // - addresses: were the outputs are sent 35 | // - txFee: the amount that is going to be used as a fee for the transaction 36 | // - totalBalance: the total balance of the UTXOs that are going to be spent 37 | // - changeReceiverPubKey: the public key of the change output (can't paste the address because will change based 38 | // on the payment type (scriptType) 39 | // - changeReceiverVersion: the version of the change output 40 | func GenerateOutputs(scriptType script.ScriptType, targetAmounts []uint, addresses [][]byte, txFee, totalBalance uint, changeReceiverPubKey []byte, changeReceiverVersion byte) ([]kernel.TxOutput, error) { 41 | txOutput := []kernel.TxOutput{} 42 | 43 | // check if the target amount and the addresses have the same length 44 | if len(targetAmounts) != len(addresses) { 45 | return []kernel.TxOutput{}, fmt.Errorf("target amounts (len=%d) and addresses (len=%d) must have the same length", len(targetAmounts), len(addresses)) 46 | } 47 | 48 | // calculate the total amount that is going to be sent to the addresses 49 | totalTargetAmount := uint(0) 50 | for _, amount := range targetAmounts { 51 | totalTargetAmount += amount 52 | } 53 | if totalTargetAmount+txFee > totalBalance { 54 | return []kernel.TxOutput{}, errors.New("not enough funds to perform the transaction") 55 | } 56 | 57 | // calculate the spare change 58 | change := totalBalance - txFee - totalTargetAmount 59 | 60 | // generate one output for each receiver 61 | for i := range addresses { 62 | txOutput = append(txOutput, kernel.NewOutput(targetAmounts[i], scriptType, string(addresses[i]))) 63 | } 64 | 65 | // add output corresponding to the spare changeType. The change address will be calculated based on the scriptType 66 | // desired, in order to do so we calculate the address based on the public key of the change receiver 67 | if change > 0 { 68 | changeAddress := "" 69 | // calculate the address for P2PK 70 | if scriptType == script.P2PK { 71 | changeAddress = string(changeReceiverPubKey) 72 | } 73 | 74 | // calculate the address for P2PKH 75 | if scriptType == script.P2PKH { 76 | changeAddressArray, err := util_p2pkh.GenerateP2PKHAddrFromPubKey(changeReceiverPubKey, changeReceiverVersion) 77 | if err != nil { 78 | return []kernel.TxOutput{}, err 79 | } 80 | 81 | changeAddress = string(changeAddressArray) 82 | } 83 | 84 | txOutput = append(txOutput, kernel.NewOutput(totalBalance-txFee-totalTargetAmount, scriptType, changeAddress)) 85 | } 86 | 87 | return txOutput, nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/kernel/block.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "unsafe" 7 | ) 8 | 9 | const ( 10 | // ChainnetCoinAmount number of smaller units (Channoshis) that represent 1 Chainnet coin 11 | ChainnetCoinAmount = 100000000 12 | MaxNumberTxsPerBlock = 300 13 | ) 14 | 15 | type BlockHeader struct { 16 | Version []byte 17 | PrevBlockHash []byte 18 | MerkleRoot []byte 19 | Height uint 20 | // todo(): use timestamp to determine the difficulty, in a 2 weeks period, if the number of blocks was 21 | // todo(): created too quick, it means that the difficult must be increased 22 | Timestamp int64 23 | // todo(): target could be removed now, mining difficulty can already be determined dynamically 24 | Target uint 25 | Nonce uint 26 | } 27 | 28 | func NewBlockHeader(version []byte, timestamp int64, merkleRoot []byte, height uint, prevBlockHash []byte, target, nonce uint) *BlockHeader { 29 | return &BlockHeader{ 30 | Version: version, 31 | Timestamp: timestamp, 32 | MerkleRoot: merkleRoot, 33 | Height: height, 34 | PrevBlockHash: prevBlockHash, 35 | Target: target, 36 | Nonce: nonce, 37 | } 38 | } 39 | 40 | func (bh *BlockHeader) SetNonce(nonce uint) { 41 | bh.Nonce = nonce 42 | } 43 | 44 | func (bh *BlockHeader) SetTimestamp(timestamp int64) { 45 | bh.Timestamp = timestamp 46 | } 47 | 48 | func (bh *BlockHeader) IsGenesisHeader() bool { 49 | return len(bh.PrevBlockHash) == 0 && bh.Height == 0 50 | } 51 | 52 | func (bh *BlockHeader) String() string { 53 | return fmt.Sprintf("header(version: %s, prev block hash: %x, merkle root: %x, height: %d, timestamp: %d, target: %d, nonce: %d)", 54 | bh.Version, bh.PrevBlockHash, bh.MerkleRoot, bh.Height, bh.Timestamp, bh.Target, bh.Nonce) 55 | } 56 | 57 | func (bh *BlockHeader) Assemble() []byte { 58 | data := [][]byte{ 59 | []byte(fmt.Sprintf("version %s", bh.Version)), 60 | []byte(fmt.Sprintf("prev block hash %x", bh.PrevBlockHash)), 61 | []byte(fmt.Sprintf("merkle root %x", bh.MerkleRoot)), 62 | []byte(fmt.Sprintf("height %d", bh.Height)), 63 | []byte(fmt.Sprintf("timestamp %d", bh.Timestamp)), 64 | []byte(fmt.Sprintf("target %d", bh.Target)), 65 | []byte(fmt.Sprintf("nonce %d", bh.Nonce)), 66 | } 67 | 68 | return bytes.Join(data, []byte{}) 69 | } 70 | 71 | func (bh *BlockHeader) Size() uint { 72 | return uint(len(bh.Version) + len(bh.PrevBlockHash) + len(bh.MerkleRoot) + int(unsafe.Sizeof(uint(0)))*3 + int(unsafe.Sizeof(int64(0)))) //nolint:gosec // this is used for metrics only 73 | } 74 | 75 | type Block struct { 76 | Header *BlockHeader 77 | Transactions []*Transaction 78 | Hash []byte 79 | } 80 | 81 | func NewBlock(blockHeader *BlockHeader, transactions []*Transaction, blockHash []byte) *Block { 82 | block := &Block{ 83 | Header: blockHeader, 84 | Transactions: transactions, 85 | Hash: blockHash, 86 | } 87 | 88 | return block 89 | } 90 | 91 | func NewGenesisBlock(blockHeader *BlockHeader, transactions []*Transaction, blockHash []byte) *Block { 92 | return NewBlock(blockHeader, transactions, blockHash) 93 | } 94 | 95 | func (block *Block) IsGenesisBlock() bool { 96 | return len(block.Header.PrevBlockHash) == 0 && block.Header.Height == 0 97 | } 98 | 99 | func (block *Block) Size() uint { 100 | totalTxSize := uint(0) 101 | for _, tx := range block.Transactions { 102 | totalTxSize += tx.Size() 103 | } 104 | 105 | return totalTxSize + block.Header.Size() + uint(len(block.Hash)) 106 | } 107 | 108 | func ConvertFromChannoshisToCoins(channoshis uint) float64 { 109 | if channoshis == 0 { 110 | return 0.0 111 | } 112 | 113 | return float64(channoshis) / float64(ChainnetCoinAmount) 114 | } 115 | 116 | func ConvertFromCoinsToChannoshis(coins float64) uint { 117 | return uint(coins * float64(ChainnetCoinAmount)) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/consensus/validator/light_validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/yago-123/chainnet/pkg/crypto/hash" 8 | "github.com/yago-123/chainnet/pkg/kernel" 9 | "github.com/yago-123/chainnet/pkg/util" 10 | ) 11 | 12 | type LValidator struct { 13 | hasher hash.Hashing 14 | } 15 | 16 | func NewLightValidator(hasher hash.Hashing) *LValidator { 17 | return &LValidator{ 18 | hasher: hasher, 19 | } 20 | } 21 | 22 | func (lv *LValidator) ValidateTxLight(tx *kernel.Transaction) error { 23 | // check that there is at least one input 24 | if !tx.HaveInputs() { 25 | return errors.New("transaction has no inputs") 26 | } 27 | 28 | // make sure that there are outputs in the transaction 29 | if !tx.HaveOutputs() { 30 | return errors.New("transaction has no outputs") 31 | } 32 | 33 | validations := []TxFunc{ 34 | lv.validateInputsDontMatch, 35 | lv.validateTxID, 36 | lv.validateAllOutputsContainNonZeroAmounts, 37 | // todo(): set limit to the number of inputs and outputs 38 | // todo(): make sure that transaction size is within limits 39 | // todo(): make sure number of sigops is within limits 40 | } 41 | 42 | for _, validate := range validations { 43 | if err := validate(tx); err != nil { 44 | return err 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (lv *LValidator) ValidateHeader(bh *kernel.BlockHeader) error { 52 | validations := []HeaderFunc{ 53 | lv.validateHeaderFieldsWithinLimits, 54 | lv.validateVersion, 55 | lv.validateHeaderHash, 56 | } 57 | 58 | for _, validate := range validations { 59 | if err := validate(bh); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // validateHeaderFieldsWithinLimits makes sure that the fields of the block header contain correct values 68 | func (lv *LValidator) validateHeaderFieldsWithinLimits(bh *kernel.BlockHeader) error { 69 | if bh.Height > 0 && len(bh.PrevBlockHash) == 0 { 70 | return fmt.Errorf("previous block hash is empty") 71 | } 72 | 73 | if len(bh.MerkleRoot) == 0 { 74 | return fmt.Errorf("merkle root is empty") 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // validateHeaderHash checks that the block hash corresponds to the target 81 | func (lv *LValidator) validateHeaderHash(bh *kernel.BlockHeader) error { 82 | headerHash, err := util.CalculateBlockHash(bh, lv.hasher) 83 | if err != nil { 84 | return fmt.Errorf("error calculating header headerHash: %w", err) 85 | } 86 | 87 | if !util.IsFirstNBitsZero(headerHash, bh.Target) { 88 | return fmt.Errorf("block %x has invalid target %d", headerHash, bh.Target) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // validateVersion makes sure that the header version is correct (to be developed) 95 | func (lv *LValidator) validateVersion(_ *kernel.BlockHeader) error { 96 | return nil 97 | } 98 | 99 | // validateInputsDontMatch checks that the inputs don't match creating double spending problems 100 | func (lv *LValidator) validateInputsDontMatch(tx *kernel.Transaction) error { 101 | for i := range len(tx.Vin) { 102 | for j := i + 1; j < len(tx.Vin); j++ { 103 | if tx.Vin[i].EqualInput(tx.Vin[j]) { 104 | return fmt.Errorf("transaction %x has multiple inputs with the same source", tx.ID) 105 | } 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // validateTxID checks that the hash of the transaction matches the ID field 113 | func (lv *LValidator) validateTxID(tx *kernel.Transaction) error { 114 | return util.VerifyTxHash(tx, tx.ID, lv.hasher) 115 | } 116 | 117 | // validateAllOutputsContainNonZeroAmounts make sure that no empty outputs can be accepted 118 | func (lv *LValidator) validateAllOutputsContainNonZeroAmounts(tx *kernel.Transaction) error { 119 | for i, out := range tx.Vout { 120 | if out.Amount == 0 { 121 | return fmt.Errorf("transaction %x contain output %d empty", tx.ID, i) 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /cmd/node/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/yago-123/chainnet/config" 8 | blockchain "github.com/yago-123/chainnet/pkg/chain" 9 | expl "github.com/yago-123/chainnet/pkg/chain/explorer" 10 | "github.com/yago-123/chainnet/pkg/consensus/validator" 11 | "github.com/yago-123/chainnet/pkg/crypto" 12 | "github.com/yago-123/chainnet/pkg/crypto/hash" 13 | "github.com/yago-123/chainnet/pkg/crypto/sign" 14 | "github.com/yago-123/chainnet/pkg/encoding" 15 | "github.com/yago-123/chainnet/pkg/mempool" 16 | "github.com/yago-123/chainnet/pkg/monitor" 17 | "github.com/yago-123/chainnet/pkg/observer" 18 | "github.com/yago-123/chainnet/pkg/storage" 19 | "github.com/yago-123/chainnet/pkg/utxoset" 20 | ) 21 | 22 | var cfg *config.Config 23 | 24 | var ( 25 | // general consensus hasher (tx, block hashes...) 26 | consensusHasherType = hash.SHA256 27 | 28 | // general consensus signer (tx) 29 | consensusSigner = crypto.NewHashedSignature( 30 | sign.NewECDSASignature(), 31 | hash.NewHasher(sha256.New()), 32 | ) 33 | ) 34 | 35 | func main() { 36 | var err error 37 | 38 | // execute the root command 39 | Execute(logrus.New()) 40 | 41 | cfg.Logger.SetLevel(logrus.DebugLevel) 42 | 43 | cfg.Logger.Infof("starting chain node with config %v", cfg) 44 | 45 | // create new observer 46 | netSubject := observer.NewNetSubject() 47 | subjectChain := observer.NewChainSubject() 48 | 49 | // create instance for persisting data 50 | boltdb, err := storage.NewBoltDB(cfg.StorageFile, "block-bucket", "header-bucket", encoding.NewGobEncoder()) 51 | if err != nil { 52 | cfg.Logger.Fatalf("error creating bolt db: %s", err) 53 | } 54 | 55 | // decorator that wraps storage in order to provide metrics 56 | meteredBoltdb := storage.NewMeteredStorage(boltdb) 57 | 58 | // create explorer instance 59 | explorer := expl.NewChainExplorer(meteredBoltdb, hash.GetHasher(consensusHasherType)) 60 | 61 | // create mempool instance 62 | mempool := mempool.NewMemPool(cfg.Chain.MaxTxsMempool) 63 | 64 | // create utxo set instance 65 | utxoSet := utxoset.NewUTXOSet(cfg) 66 | 67 | // create heavy validator 68 | heavyValidator := validator.NewHeavyValidator( 69 | cfg, 70 | validator.NewLightValidator(hash.GetHasher(consensusHasherType)), 71 | explorer, 72 | consensusSigner, 73 | hash.GetHasher(consensusHasherType), 74 | ) 75 | 76 | // define encoder type 77 | encoder := encoding.NewProtobufEncoder() 78 | 79 | // create new chain 80 | chain, err := blockchain.NewBlockchain( 81 | cfg, 82 | meteredBoltdb, 83 | mempool, 84 | utxoSet, 85 | hash.GetHasher(consensusHasherType), 86 | heavyValidator, 87 | subjectChain, 88 | encoder, 89 | ) 90 | if err != nil { 91 | cfg.Logger.Fatalf("error creating blockchain: %s", err) 92 | } 93 | 94 | // register network observers 95 | netSubject.Register(chain) 96 | 97 | // register chain observers 98 | subjectChain.Register(meteredBoltdb) 99 | subjectChain.Register(mempool) 100 | subjectChain.Register(utxoSet) 101 | 102 | // the chain network is an special case regarding prometheus, see why inside the network module 103 | network, err := chain.InitNetwork(netSubject) 104 | if err != nil { 105 | cfg.Logger.Fatalf("error initializing network: %s", err) 106 | } 107 | 108 | // register the block subject to the network 109 | subjectChain.Register(network) 110 | 111 | // add monitoring via Prometheus 112 | monitors := []monitor.Monitor{chain, meteredBoltdb, mempool, utxoSet, network, heavyValidator} 113 | prometheusExporter := monitor.NewPrometheusExporter(cfg, monitors) 114 | 115 | if cfg.Prometheus.Enabled { 116 | if err = prometheusExporter.Start(); err != nil { 117 | cfg.Logger.Fatalf("error starting prometheus exporter: %s", err) 118 | } 119 | 120 | if err == nil { 121 | cfg.Logger.Infof("exposing Prometheus metrics in http://localhost:%d%s", cfg.Prometheus.Port, cfg.Prometheus.Path) 122 | } 123 | } 124 | 125 | select {} 126 | } 127 | -------------------------------------------------------------------------------- /pkg/consensus/merkletree.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yago-123/chainnet/pkg/crypto/hash" 7 | "github.com/yago-123/chainnet/pkg/kernel" 8 | "github.com/yago-123/chainnet/pkg/util" 9 | ) 10 | 11 | // MerkleNode represents a node in the Merkle tree 12 | type MerkleNode struct { 13 | Left *MerkleNode 14 | Right *MerkleNode 15 | Hash []byte 16 | } 17 | 18 | // newMerkleNode creates a new Merkle node 19 | func newMerkleNode(left, right *MerkleNode, data []byte, hasher hash.Hashing) (*MerkleNode, error) { 20 | node := MerkleNode{} 21 | 22 | // in case is leaf node, assign hash directly 23 | if left == nil && right == nil { 24 | node.Hash = data 25 | } 26 | 27 | leftHash := []byte{} 28 | rightHash := []byte{} 29 | // in case is not leaf node, hash the left and right nodes 30 | if left != nil || right != nil { 31 | if left != nil { 32 | leftHash = left.Hash 33 | } 34 | 35 | if right != nil { 36 | rightHash = right.Hash 37 | } 38 | 39 | nodeHash, err := hasher.Hash(append(leftHash, rightHash...)) 40 | if err != nil { 41 | return nil, fmt.Errorf("error hashing left (%s) and right (%s) nodes: %w", leftHash, rightHash, err) 42 | } 43 | node.Hash = nodeHash 44 | } 45 | 46 | node.Left = left 47 | node.Right = right 48 | 49 | return &node, nil 50 | } 51 | 52 | // MerkleTree represents a Merkle tree 53 | type MerkleTree struct { 54 | root *MerkleNode 55 | } 56 | 57 | // newMerkleTreeFromNodes creates a new Merkle tree from a list of Merkle nodes 58 | func newMerkleTreeFromNodes(nodes []MerkleNode, hasher hash.Hashing) (*MerkleTree, error) { 59 | // create the Merkle tree 60 | for len(nodes) > 1 { 61 | var newLevel []MerkleNode 62 | 63 | for i := 0; i < len(nodes); i += 2 { 64 | var left, right MerkleNode 65 | left = nodes[i] 66 | 67 | if i+1 < len(nodes) { 68 | right = nodes[i+1] 69 | } 70 | 71 | if i+1 >= len(nodes) { 72 | right = nodes[i] // in case of odd number of nodes, duplicate the last node 73 | } 74 | 75 | parent, err := newMerkleNode(&left, &right, nil, hasher) 76 | if err != nil { 77 | return nil, fmt.Errorf("error creating Merkle parent node for left (%s) and right (%s) nodes: %w", left.Hash, right.Hash, err) 78 | } 79 | newLevel = append(newLevel, *parent) 80 | } 81 | 82 | nodes = newLevel 83 | } 84 | 85 | tree := MerkleTree{&nodes[0]} 86 | 87 | return &tree, nil 88 | } 89 | 90 | func NewMerkleTreeFromHashes(proofs [][]byte, hasher hash.Hashing) (*MerkleTree, error) { 91 | var nodes []MerkleNode 92 | 93 | if len(proofs) == 0 { 94 | return nil, fmt.Errorf("no proofs were provided") 95 | } 96 | 97 | for _, proof := range proofs { 98 | node, err := newMerkleNode(nil, nil, proof, hasher) 99 | if err != nil { 100 | return nil, fmt.Errorf("error creating Merkle node for hash %s: %w", proof, err) 101 | } 102 | 103 | nodes = append(nodes, *node) 104 | } 105 | 106 | return newMerkleTreeFromNodes(nodes, hasher) 107 | } 108 | 109 | // NewMerkleTreeFromTxs creates a new Merkle tree from a list of transactions 110 | func NewMerkleTreeFromTxs(txs []*kernel.Transaction, hasher hash.Hashing) (*MerkleTree, error) { 111 | var nodes []MerkleNode 112 | 113 | // create leaf nodes using the Assemble method 114 | for _, tx := range txs { 115 | // make sure that the hash of the transaction is correct 116 | txHash, err := util.CalculateTxHash(tx, hasher) 117 | if err != nil { 118 | return nil, fmt.Errorf("error calculating hash for transaction %s: %w", tx.ID, err) 119 | } 120 | 121 | // generate new node 122 | node, err := newMerkleNode(nil, nil, txHash, hasher) 123 | if err != nil { 124 | return nil, fmt.Errorf("error creating Merkle node for transaction %s: %w", tx.ID, err) 125 | } 126 | nodes = append(nodes, *node) 127 | } 128 | 129 | return newMerkleTreeFromNodes(nodes, hasher) 130 | } 131 | 132 | // RootHash returns the root hash of the Merkle tree 133 | func (mt *MerkleTree) RootHash() []byte { 134 | return mt.root.Hash 135 | } 136 | -------------------------------------------------------------------------------- /pkg/utxoset/utxos.go: -------------------------------------------------------------------------------- 1 | package utxoset 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "unsafe" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/yago-123/chainnet/pkg/monitor" 10 | 11 | "github.com/sirupsen/logrus" 12 | "github.com/yago-123/chainnet/config" 13 | 14 | "github.com/yago-123/chainnet/pkg/kernel" 15 | ) 16 | 17 | const UTXOSObserverID = "utxos-observer" 18 | 19 | type UTXOSet struct { 20 | mu sync.Mutex 21 | utxos map[string]kernel.UTXO 22 | 23 | logger *logrus.Logger 24 | cfg *config.Config 25 | } 26 | 27 | func NewUTXOSet(cfg *config.Config) *UTXOSet { 28 | return &UTXOSet{ 29 | mu: sync.Mutex{}, 30 | utxos: make(map[string]kernel.UTXO), 31 | logger: cfg.Logger, 32 | cfg: cfg, 33 | } 34 | } 35 | 36 | // AddBlock invalidates the new inputs of the block and adds the new outputs to the UTXO set 37 | func (u *UTXOSet) AddBlock(block *kernel.Block) error { 38 | u.mu.Lock() 39 | defer u.mu.Unlock() 40 | 41 | for _, tx := range block.Transactions { 42 | // invalidate inputs used in the block 43 | for _, input := range tx.Vin { 44 | // skip Coinbase transactions 45 | if tx.IsCoinbase() { 46 | continue 47 | } 48 | 49 | _, ok := u.utxos[input.UniqueTxoKey()] 50 | if !ok { 51 | // if the utxo is not found, return error (impossible scenario in theory) 52 | return fmt.Errorf("transaction %s not found in the UTXO set", tx.ID) 53 | } 54 | 55 | // delete the utxo from the set 56 | delete(u.utxos, input.UniqueTxoKey()) 57 | } 58 | 59 | // add new outputs to the set 60 | for index, output := range tx.Vout { 61 | utxo := kernel.UTXO{ 62 | TxID: tx.ID, 63 | OutIdx: uint(index), //nolint:gosec // int to uint is safe 64 | Output: output, 65 | } 66 | 67 | // store utxo in the set 68 | u.utxos[utxo.UniqueKey()] = utxo 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // RetrieveInputsBalance from the inputs provided 76 | func (u *UTXOSet) RetrieveInputsBalance(inputs []kernel.TxInput) (uint, error) { 77 | u.mu.Lock() 78 | defer u.mu.Unlock() 79 | 80 | balance := uint(0) 81 | for _, input := range inputs { 82 | utxo, ok := u.utxos[input.UniqueTxoKey()] 83 | if !ok { 84 | return 0, fmt.Errorf("input %s not found in the UTXO set", input.UniqueTxoKey()) 85 | } 86 | 87 | balance += utxo.Output.Amount 88 | } 89 | 90 | return balance, nil 91 | } 92 | 93 | // ID returns the observer id 94 | func (u *UTXOSet) ID() string { 95 | return UTXOSObserverID 96 | } 97 | 98 | // OnBlockAddition is called when a new block is added to the blockchain via the observer pattern 99 | func (u *UTXOSet) OnBlockAddition(block *kernel.Block) { 100 | err := u.AddBlock(block) 101 | if err != nil { 102 | u.logger.Errorf("error adding block to UTXO set: %s", err) 103 | return 104 | } 105 | } 106 | 107 | // OnTxAddition is called when a new tx is added to the mempool via the observer pattern 108 | func (u *UTXOSet) OnTxAddition(_ *kernel.Transaction) { 109 | // do nothing 110 | } 111 | 112 | // RegisterMetrics registers the UTXO set metrics to the prometheus registry 113 | func (u *UTXOSet) RegisterMetrics(register *prometheus.Registry) { 114 | monitor.NewMetric(register, monitor.Gauge, "utxo_set_num_outputs", "Number of outputs in the UTXO set", 115 | func() float64 { 116 | return float64(len(u.utxos)) 117 | }, 118 | ) 119 | 120 | monitor.NewMetric(register, monitor.Gauge, "utxo_set_storage_size", "Size of the UTXO set in bytes", 121 | func() float64 { 122 | storage := uint(0) 123 | for _, utxo := range u.utxos { 124 | storage += utxo.Output.Size() + uint(len(utxo.TxID)) + uint(unsafe.Sizeof(uint(0))) 125 | } 126 | 127 | return float64(storage) 128 | }, 129 | ) 130 | 131 | monitor.NewMetric(register, monitor.Gauge, "utxo_set_output_balance", "A gauge containing the total balance of the UTXO set", 132 | func() float64 { 133 | totalBalance := uint(0) 134 | for _, utxo := range u.utxos { 135 | totalBalance += utxo.Amount() 136 | } 137 | 138 | return kernel.ConvertFromChannoshisToCoins(totalBalance) 139 | }, 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /pkg/encoding/gob.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | 8 | "github.com/yago-123/chainnet/pkg/kernel" 9 | ) 10 | 11 | type GobEncoder struct { 12 | } 13 | 14 | func NewGobEncoder() *GobEncoder { 15 | return &GobEncoder{} 16 | } 17 | 18 | func (gobenc *GobEncoder) Type() string { 19 | return GobEncodingType 20 | } 21 | 22 | func (gobenc *GobEncoder) serialize(data interface{}) ([]byte, error) { 23 | var result bytes.Buffer 24 | encoder := gob.NewEncoder(&result) 25 | if err := encoder.Encode(data); err != nil { 26 | return nil, fmt.Errorf("error serializing data: %w", err) 27 | } 28 | return result.Bytes(), nil 29 | } 30 | 31 | func (gobenc *GobEncoder) deserialize(data []byte, out interface{}) error { 32 | decoder := gob.NewDecoder(bytes.NewReader(data)) 33 | if err := decoder.Decode(out); err != nil { 34 | return fmt.Errorf("error deserializing data: %w", err) 35 | } 36 | return nil 37 | } 38 | 39 | func (gobenc *GobEncoder) SerializeBlock(b kernel.Block) ([]byte, error) { 40 | return gobenc.serialize(b) 41 | } 42 | 43 | func (gobenc *GobEncoder) DeserializeBlock(data []byte) (*kernel.Block, error) { 44 | var b kernel.Block 45 | if err := gobenc.deserialize(data, &b); err != nil { 46 | return nil, fmt.Errorf("error deserializing block: %w", err) 47 | } 48 | return &b, nil 49 | } 50 | 51 | func (gobenc *GobEncoder) SerializeHeader(bh kernel.BlockHeader) ([]byte, error) { 52 | return gobenc.serialize(bh) 53 | } 54 | 55 | func (gobenc *GobEncoder) DeserializeHeader(data []byte) (*kernel.BlockHeader, error) { 56 | var bh kernel.BlockHeader 57 | if err := gobenc.deserialize(data, &bh); err != nil { 58 | return nil, fmt.Errorf("error deserializing block header: %w", err) 59 | } 60 | return &bh, nil 61 | } 62 | 63 | func (gobenc *GobEncoder) SerializeHeaders(headers []*kernel.BlockHeader) ([]byte, error) { 64 | return gobenc.serialize(headers) 65 | } 66 | 67 | func (gobenc *GobEncoder) DeserializeHeaders(data []byte) ([]*kernel.BlockHeader, error) { 68 | var headers []*kernel.BlockHeader 69 | if err := gobenc.deserialize(data, &headers); err != nil { 70 | return nil, fmt.Errorf("error deserializing block headers: %w", err) 71 | } 72 | return headers, nil 73 | } 74 | 75 | func (gobenc *GobEncoder) SerializeTransaction(tx kernel.Transaction) ([]byte, error) { 76 | return gobenc.serialize(tx) 77 | } 78 | 79 | func (gobenc *GobEncoder) DeserializeTransaction(data []byte) (*kernel.Transaction, error) { 80 | var tx kernel.Transaction 81 | if err := gobenc.deserialize(data, &tx); err != nil { 82 | return nil, fmt.Errorf("error deserializing transaction: %w", err) 83 | } 84 | return &tx, nil 85 | } 86 | 87 | func (gobenc *GobEncoder) SerializeTransactions(txs []*kernel.Transaction) ([]byte, error) { 88 | return gobenc.serialize(txs) 89 | } 90 | 91 | func (gobenc *GobEncoder) DeserializeTransactions(data []byte) ([]*kernel.Transaction, error) { 92 | var txs []*kernel.Transaction 93 | if err := gobenc.deserialize(data, &txs); err != nil { 94 | return nil, fmt.Errorf("error deserializing transactions: %w", err) 95 | } 96 | return txs, nil 97 | } 98 | 99 | func (gobenc *GobEncoder) SerializeUTXO(utxo kernel.UTXO) ([]byte, error) { 100 | return gobenc.serialize(utxo) 101 | } 102 | 103 | func (gobenc *GobEncoder) DeserializeUTXO(data []byte) (*kernel.UTXO, error) { 104 | var utxo kernel.UTXO 105 | if err := gobenc.deserialize(data, &utxo); err != nil { 106 | return nil, fmt.Errorf("error deserializing UTXO: %w", err) 107 | } 108 | return &utxo, nil 109 | } 110 | 111 | func (gobenc *GobEncoder) SerializeUTXOs(utxos []*kernel.UTXO) ([]byte, error) { 112 | return gobenc.serialize(utxos) 113 | } 114 | 115 | func (gobenc *GobEncoder) DeserializeUTXOs(data []byte) ([]*kernel.UTXO, error) { 116 | var utxos []*kernel.UTXO 117 | if err := gobenc.deserialize(data, &utxos); err != nil { 118 | return nil, fmt.Errorf("error deserializing UTXOs: %w", err) 119 | } 120 | return utxos, nil 121 | } 122 | 123 | func (gobenc *GobEncoder) SerializeBool(b bool) ([]byte, error) { 124 | return gobenc.serialize(b) 125 | } 126 | 127 | func (gobenc *GobEncoder) DeserializeBool(data []byte) (bool, error) { 128 | var b bool 129 | if err := gobenc.deserialize(data, &b); err != nil { 130 | return false, fmt.Errorf("error deserializing bool: %w", err) 131 | } 132 | 133 | return b, nil 134 | } 135 | -------------------------------------------------------------------------------- /cmd/nespv/cmd/send.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | 8 | cerror "github.com/yago-123/chainnet/pkg/errs" 9 | util_crypto "github.com/yago-123/chainnet/pkg/util/crypto" 10 | 11 | "github.com/btcsuite/btcutil/base58" 12 | "github.com/yago-123/chainnet/pkg/script" 13 | 14 | "github.com/spf13/cobra" 15 | "github.com/yago-123/chainnet/config" 16 | "github.com/yago-123/chainnet/pkg/consensus/validator" 17 | "github.com/yago-123/chainnet/pkg/crypto/hash" 18 | "github.com/yago-123/chainnet/pkg/encoding" 19 | wallt "github.com/yago-123/chainnet/pkg/wallet/simple_wallet" 20 | ) 21 | 22 | const ( 23 | FlagPayType = "pay-type" 24 | FlagAddress = "address" 25 | FlagAmount = "amount" 26 | FlagFee = "fee" 27 | FlagPrivKey = "priv-key" 28 | FlagWalletKey = "wallet-key-path" 29 | ) 30 | 31 | var sendCmd = &cobra.Command{ 32 | Use: "send", 33 | Short: "Send transaction", 34 | Long: `Send transactions from wallets.`, 35 | Run: func(cmd *cobra.Command, _ []string) { 36 | cfg = config.InitConfig(cmd) 37 | 38 | scriptTypeStr, _ := cmd.Flags().GetString(FlagPayType) 39 | address, _ := cmd.Flags().GetString(FlagAddress) 40 | amount, _ := cmd.Flags().GetFloat64(FlagAmount) 41 | fee, _ := cmd.Flags().GetFloat64(FlagFee) 42 | privKeyCont, _ := cmd.Flags().GetString(FlagPrivKey) 43 | privKeyPath, _ := cmd.Flags().GetString(FlagWalletKey) 44 | 45 | // check if only one private key is provided 46 | if (privKeyCont == "") == (privKeyPath == "") { 47 | logger.Fatalf("specify one argument containing the private key: --priv-key or --wallet-key-path") 48 | } 49 | 50 | var err error 51 | var privKey, pubKey []byte 52 | var payType script.ScriptType 53 | 54 | // process key from path or from content 55 | if privKeyCont != "" { 56 | privKey = base58.Decode(privKeyCont) 57 | } 58 | 59 | if privKeyPath != "" { 60 | privKey, err = util_crypto.ReadECDSAPemToPrivateKeyDerBytes(privKeyPath) 61 | if err != nil { 62 | logger.Fatalf("error reading private key: %v", err) 63 | } 64 | } 65 | 66 | if scriptTypeStr != "" { 67 | payType = script.ReturnScriptTypeFromStringType(scriptTypeStr) 68 | } 69 | 70 | // derive public key from private key 71 | pubKey, err = util_crypto.DeriveECDSAPubFromPrivateDERBytes(privKey) 72 | if err != nil { 73 | logger.Fatalf("%v: %v", cerror.ErrCryptoPublicKeyDerivation, err) 74 | } 75 | 76 | // create wallet 77 | wallet, err := wallt.NewWalletWithKeys( 78 | cfg, 79 | 1, 80 | validator.NewLightValidator(hash.GetHasher(consensusHasherType)), 81 | consensusSigner, 82 | hash.GetHasher(consensusHasherType), 83 | encoding.NewProtobufEncoder(), 84 | privKey, 85 | pubKey, 86 | ) 87 | if err != nil { 88 | logger.Fatalf("error setting up wallet: %v", err) 89 | } 90 | 91 | utxos, err := wallet.GetWalletUTXOS() 92 | if err != nil { 93 | logger.Fatalf("error getting wallet UTXOS: %v", err) 94 | } 95 | 96 | tx, err := wallet.GenerateNewTransaction(payType, base58.Decode(address), kernel.ConvertFromCoinsToChannoshis(amount), kernel.ConvertFromCoinsToChannoshis(fee), utxos) 97 | if err != nil { 98 | logger.Fatalf("error generating transaction: %v", err) 99 | } 100 | 101 | context, cancel := context.WithTimeout(context.Background(), cfg.P2P.ConnTimeout) 102 | defer cancel() 103 | 104 | err = wallet.SendTransaction(context, tx) 105 | if err != nil { 106 | logger.Fatalf("error sending transaction: %v", err) 107 | } 108 | 109 | logger.Infof("Sent transaction: %s", tx.String()) 110 | }, 111 | } 112 | 113 | func init() { 114 | // main command 115 | config.AddConfigFlags(sendCmd) 116 | rootCmd.AddCommand(sendCmd) 117 | 118 | // sub commands 119 | sendCmd.Flags().String(FlagPayType, "P2PK", "Type of address to send coins to") 120 | sendCmd.Flags().String(FlagAddress, "", "Destination address to send coins") 121 | sendCmd.Flags().Float64(FlagAmount, 0.0, "Amount of coins to send") 122 | sendCmd.Flags().Float64(FlagFee, 0.0, "Amount of fee to send") 123 | sendCmd.Flags().String(FlagPrivKey, "", "Private key") 124 | 125 | // todo(): reestructure this duplication 126 | sendCmd.Flags().String(FlagWalletKey, "", "Path to private key") 127 | 128 | // required flags 129 | _ = sendCmd.MarkFlagRequired(FlagAddress) 130 | _ = sendCmd.MarkFlagRequired(FlagAmount) 131 | } 132 | -------------------------------------------------------------------------------- /pkg/miner/pow.go: -------------------------------------------------------------------------------- 1 | package miner 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "math/big" 9 | "runtime" 10 | "sync" 11 | 12 | "github.com/yago-123/chainnet/pkg/crypto/hash" 13 | "github.com/yago-123/chainnet/pkg/kernel" 14 | "github.com/yago-123/chainnet/pkg/util" 15 | ) 16 | 17 | const ( 18 | HashLength = 256 19 | MaxNonce = ^uint(0) 20 | 21 | CPUDivisionFactor = 2 22 | ) 23 | 24 | var ErrMiningCancelled = errors.New("mining cancelled by context") 25 | 26 | type miningResult struct { 27 | hash []byte 28 | nonce uint 29 | err error 30 | } 31 | 32 | // ProofOfWork holds the components needed for mining 33 | type ProofOfWork struct { 34 | externalCtx context.Context 35 | results chan miningResult 36 | wg sync.WaitGroup 37 | hashDifficulty *big.Int 38 | 39 | hasherType hash.HasherType 40 | 41 | bh *kernel.BlockHeader 42 | } 43 | 44 | // NewProofOfWork creates a new ProofOfWork instance 45 | func NewProofOfWork(ctx context.Context, bh *kernel.BlockHeader, hasherType hash.HasherType) (*ProofOfWork, error) { 46 | if bh.Target >= HashLength { 47 | return nil, errors.New("target is bigger than the hash length") 48 | } 49 | 50 | hashDifficulty := big.NewInt(1) 51 | hashDifficulty.Lsh(hashDifficulty, HashLength-bh.Target) 52 | 53 | return &ProofOfWork{ 54 | externalCtx: ctx, 55 | results: make(chan miningResult), 56 | hashDifficulty: hashDifficulty, 57 | hasherType: hasherType, 58 | bh: bh, 59 | }, nil 60 | } 61 | 62 | // CalculateBlockHash calculates the hash of a block in parallel 63 | func (pow *ProofOfWork) CalculateBlockHash() ([]byte, uint, error) { 64 | if pow.bh.Target >= HashLength { 65 | return nil, 0, errors.New("target is bigger than the hash length") 66 | } 67 | 68 | // calculate the number of goroutines to use. We use half of the available CPUs because in our case the miner 69 | // will have other tasks to do (like listening for new blocks) and we don't want to starve the system (in miner only 70 | // scenarios, we could use all CPUs) 71 | numGoroutines := int(math.Max(1, float64(runtime.NumCPU()/CPUDivisionFactor))) 72 | nonceRange := MaxNonce / uint(numGoroutines) //nolint:gosec // int to uint is safe 73 | 74 | // if one of the goroutines finds a block, use this context to propagate the cancellation. This cancellation 75 | // is an overlap of the pow.externalCtx given that in case of block addition, the observer code will trigger 76 | // the cancellation too (however, this one is more specific and more responsive) 77 | blockMinedCtx, cancel := context.WithCancel(context.Background()) 78 | defer cancel() 79 | 80 | // split work and ranges among go routines 81 | for i := range make([]int, numGoroutines) { 82 | pow.wg.Add(1) 83 | go pow.startMining(blockMinedCtx, *pow.bh, nonceRange, uint(i)*nonceRange) //nolint:gosec // int to uint is safe 84 | } 85 | 86 | // wait for all go routines to finish 87 | go func() { 88 | pow.wg.Wait() 89 | close(pow.results) 90 | }() 91 | 92 | // wait for the first result to be returned 93 | for result := range pow.results { 94 | if result.err == nil { 95 | // retrieve block data mined 96 | return result.hash, result.nonce, nil 97 | } 98 | 99 | // if mining have been cancelled, return error 100 | if errors.Is(result.err, ErrMiningCancelled) { 101 | return nil, 0, result.err 102 | } 103 | } 104 | 105 | // at this point no go routine have found a valid block 106 | return nil, 0, errors.New("could not find hash for block being mined") 107 | } 108 | 109 | // startMining starts a mining process in a goroutine 110 | func (pow *ProofOfWork) startMining(blockMinedCtx context.Context, bh kernel.BlockHeader, nonceRange uint, startNonce uint) { 111 | defer pow.wg.Done() 112 | // initialize hash function in each goroutine because is not thread safe by default 113 | hasher := hash.GetHasher(pow.hasherType) 114 | 115 | // iterate over the nonce range and calculate the hash 116 | for nonce := startNonce; nonce < startNonce+nonceRange && nonce < MaxNonce; nonce++ { 117 | select { 118 | case <-pow.externalCtx.Done(): 119 | // if the context is cancelled, return immediately 120 | pow.results <- miningResult{nil, 0, ErrMiningCancelled} 121 | return 122 | case <-blockMinedCtx.Done(): 123 | pow.results <- miningResult{nil, 0, ErrMiningCancelled} 124 | return 125 | default: 126 | // calculate the hash of the block 127 | bh.SetNonce(nonce) 128 | blockHash, err := util.CalculateBlockHash(&bh, hasher) 129 | if err != nil { 130 | pow.results <- miningResult{nil, 0, fmt.Errorf("did not found hash for block: %w", err)} 131 | return 132 | } 133 | 134 | if util.IsFirstNBitsZero(blockHash, bh.Target) { 135 | pow.results <- miningResult{blockHash, nonce, nil} 136 | return 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /cmd/miner/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "time" 6 | 7 | "github.com/yago-123/chainnet/pkg/monitor" 8 | "github.com/yago-123/chainnet/pkg/utxoset" 9 | 10 | expl "github.com/yago-123/chainnet/pkg/chain/explorer" 11 | 12 | "github.com/yago-123/chainnet/config" 13 | blockchain "github.com/yago-123/chainnet/pkg/chain" 14 | "github.com/yago-123/chainnet/pkg/consensus/validator" 15 | "github.com/yago-123/chainnet/pkg/crypto" 16 | "github.com/yago-123/chainnet/pkg/crypto/hash" 17 | "github.com/yago-123/chainnet/pkg/crypto/sign" 18 | "github.com/yago-123/chainnet/pkg/encoding" 19 | "github.com/yago-123/chainnet/pkg/kernel" 20 | "github.com/yago-123/chainnet/pkg/mempool" 21 | "github.com/yago-123/chainnet/pkg/miner" 22 | "github.com/yago-123/chainnet/pkg/observer" 23 | "github.com/yago-123/chainnet/pkg/storage" 24 | 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | var cfg *config.Config 29 | 30 | var ( 31 | // general consensus hasher (tx, block hashes...) 32 | consensusHasherType = hash.SHA256 33 | 34 | // general consensus signer (tx) 35 | consensusSigner = crypto.NewHashedSignature( 36 | sign.NewECDSASignature(), 37 | hash.NewHasher(sha256.New()), 38 | ) 39 | ) 40 | 41 | func main() { //nolint:funlen // it's OK to be long here 42 | var block *kernel.Block 43 | 44 | // execute the root command 45 | Execute(logrus.New()) 46 | 47 | cfg.Logger.SetLevel(logrus.DebugLevel) 48 | 49 | cfg.Logger.Infof("starting chain node with config %v", cfg) 50 | 51 | // create observer controllers 52 | subjectChain := observer.NewChainSubject() 53 | subjectNet := observer.NewNetSubject() 54 | 55 | // create instance for persisting data 56 | boltdb, err := storage.NewBoltDB(cfg.StorageFile, "block-bucket", "header-bucket", encoding.NewGobEncoder()) 57 | if err != nil { 58 | cfg.Logger.Fatalf("Error creating bolt db: %s", err) 59 | } 60 | 61 | // decorator that wraps storage in order to provide metrics 62 | meteredBoltdb := storage.NewMeteredStorage(boltdb) 63 | 64 | // create explorer instance 65 | explorer := expl.NewChainExplorer(meteredBoltdb, hash.GetHasher(consensusHasherType)) 66 | 67 | // create mempool instance 68 | mempool := mempool.NewMemPool(cfg.Chain.MaxTxsMempool) 69 | 70 | // create utxo set instance 71 | utxoSet := utxoset.NewUTXOSet(cfg) 72 | 73 | // create heavy validator 74 | heavyValidator := validator.NewHeavyValidator( 75 | cfg, 76 | validator.NewLightValidator(hash.GetHasher(consensusHasherType)), 77 | explorer, 78 | consensusSigner, 79 | hash.GetHasher(consensusHasherType), 80 | ) 81 | 82 | // define encoder type 83 | encoder := encoding.NewProtobufEncoder() 84 | 85 | // create new chain 86 | chain, err := blockchain.NewBlockchain( 87 | cfg, 88 | meteredBoltdb, 89 | mempool, 90 | utxoSet, 91 | hash.GetHasher(consensusHasherType), 92 | heavyValidator, 93 | subjectChain, 94 | encoder, 95 | ) 96 | if err != nil { 97 | cfg.Logger.Fatalf("Error creating blockchain: %s", err) 98 | } 99 | 100 | // create new miner 101 | mine, err := miner.NewMiner(cfg, chain, consensusHasherType, explorer) 102 | if err != nil { 103 | cfg.Logger.Fatalf("error initializing miner: %s", err) 104 | } 105 | 106 | // register network observers 107 | subjectNet.Register(chain) 108 | 109 | // register chain observers 110 | subjectChain.Register(mine) 111 | subjectChain.Register(meteredBoltdb) 112 | subjectChain.Register(mempool) 113 | subjectChain.Register(utxoSet) 114 | 115 | network, err := chain.InitNetwork(subjectNet) 116 | if err != nil { 117 | cfg.Logger.Fatalf("error initializing network: %s", err) 118 | } 119 | 120 | // register the block subject to the network 121 | subjectChain.Register(network) 122 | 123 | // add monitoring via Prometheus 124 | monitors := []monitor.Monitor{chain, meteredBoltdb, mempool, utxoSet, network, heavyValidator} 125 | prometheusExporter := monitor.NewPrometheusExporter(cfg, monitors) 126 | 127 | if cfg.Prometheus.Enabled { 128 | if err = prometheusExporter.Start(); err != nil { 129 | cfg.Logger.Fatalf("error starting prometheus exporter: %s", err) 130 | } 131 | 132 | if err == nil { 133 | cfg.Logger.Infof("exposing Prometheus metrics in http://localhost:%d%s", cfg.Prometheus.Port, cfg.Prometheus.Path) 134 | } 135 | } 136 | 137 | for { 138 | // start mining block 139 | block, err = mine.MineBlock() 140 | if err != nil { 141 | cfg.Logger.Errorf("stopped mining block: %s", err) 142 | continue 143 | } 144 | 145 | miningTime := time.Unix(block.Header.Timestamp, 0).Format(time.RFC3339) 146 | 147 | if block.IsGenesisBlock() { 148 | cfg.Logger.Infof( 149 | "genesis block mined successfully: hash %x, number txs %d, time %s, height %d, target %d, nonce %d", 150 | block.Hash, len(block.Transactions), miningTime, block.Header.Height, block.Header.Target, block.Header.Nonce, 151 | ) 152 | continue 153 | } 154 | 155 | cfg.Logger.Infof( 156 | "block mined successfully: hash %x, previous hash %x, number txs %d, time %s, height %d, target %d, nonce %d", 157 | block.Hash, block.Header.PrevBlockHash, len(block.Transactions), miningTime, block.Header.Height, block.Header.Target, block.Header.Nonce, 158 | ) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pkg/script/interpreter/interpreter.go: -------------------------------------------------------------------------------- 1 | package interpreter 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "strconv" 7 | 8 | "golang.org/x/crypto/ripemd160" 9 | 10 | "github.com/yago-123/chainnet/pkg/crypto" 11 | 12 | "github.com/yago-123/chainnet/pkg/crypto/hash" 13 | "github.com/yago-123/chainnet/pkg/crypto/sign" 14 | "github.com/yago-123/chainnet/pkg/kernel" 15 | "github.com/yago-123/chainnet/pkg/script" 16 | 17 | util_script "github.com/yago-123/chainnet/pkg/util/script" 18 | ) 19 | 20 | const ( 21 | opCheckSigVerifyMinStackLength = 2 22 | opDupMinStackLength = 1 23 | opHash160MinStackLength = 1 24 | opEqualVerifyMinStackLength = 2 25 | ) 26 | 27 | // RPNInterpreter represents the interpreter for the Reverse Polish Notation (RPN) script 28 | type RPNInterpreter struct { 29 | signer sign.Signature 30 | } 31 | 32 | func NewScriptInterpreter(signer sign.Signature) *RPNInterpreter { 33 | return &RPNInterpreter{ 34 | signer: signer, 35 | } 36 | } 37 | 38 | // GenerateScriptSig evaluates the scriptPubKey requirement and generates the scriptSig that will unlock the scriptPubKey 39 | func (rpn *RPNInterpreter) GenerateScriptSig(scriptPubKey string, pubKey, privKey []byte, tx *kernel.Transaction) (string, error) { 40 | var scriptSig [][]byte 41 | 42 | // converts script pub key into list of tokens and list of strings 43 | scriptTokens, _, err := script.StringToScript(scriptPubKey) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | signature, err := rpn.signer.Sign(tx.AssembleForSigning(), privKey) 49 | if err != nil { 50 | return "", fmt.Errorf("couldn't sign transaction: %w", err) 51 | } 52 | 53 | scriptType := script.DetermineScriptType(scriptTokens) 54 | 55 | switch scriptType { 56 | case script.P2PK: 57 | scriptSig = append(scriptSig, signature) 58 | case script.P2PKH: 59 | scriptSig = append(scriptSig, signature, pubKey) 60 | case script.UndefinedScriptType: 61 | return "", fmt.Errorf("undefined script type %d", scriptType) 62 | default: 63 | return "", fmt.Errorf("unsupported script type %d", scriptType) 64 | } 65 | 66 | return util_script.EncodeScriptSig(scriptSig), nil 67 | } 68 | 69 | // VerifyScriptPubKey verifies the scriptPubKey by reconstructing the script and evaluating it 70 | func (rpn *RPNInterpreter) VerifyScriptPubKey(scriptPubKey string, scriptSig string, tx *kernel.Transaction) (bool, error) { //nolint:gocognit // allow this function to be complex 71 | stack := script.NewStack() 72 | 73 | // converts script pub key into list of tokens and list of strings 74 | scriptTokens, scriptString, err := script.StringToScript(scriptPubKey) 75 | if err != nil { 76 | return false, err 77 | } 78 | 79 | // iterate over the scriptSig and push values to the stack 80 | for _, element := range util_script.DecodeScriptSig(scriptSig) { 81 | stack.Push(string(element)) 82 | } 83 | 84 | // start evaluation of scriptPubKey 85 | for index, token := range scriptTokens { 86 | if token.IsUndefined() { 87 | return false, fmt.Errorf("undefined token %s in position %d", scriptString[index], index) 88 | } 89 | 90 | if token.IsOperator() { //nolint:nestif // allow this nesting to be "complex" 91 | // perform operation based on operator with a and b 92 | switch token { //nolint:exhaustive // only check operators 93 | case script.OpChecksig: 94 | if stack.Len() < opCheckSigVerifyMinStackLength { 95 | return false, fmt.Errorf("invalid stack length for OP_CHECKSIG") 96 | } 97 | 98 | var ret bool 99 | pubKey := stack.Pop() 100 | sig := stack.Pop() 101 | 102 | // verify the signature 103 | ret, err = rpn.signer.Verify([]byte(sig), tx.AssembleForSigning(), []byte(pubKey)) 104 | if err != nil { 105 | return false, fmt.Errorf("couldn't verify signature: %w", err) 106 | } 107 | stack.Push(strconv.FormatBool(ret)) 108 | case script.OpDup: 109 | if stack.Len() < opDupMinStackLength { 110 | return false, fmt.Errorf("invalid stack length for OP_DUP") 111 | } 112 | 113 | val := stack.Pop() 114 | stack.Push(val) 115 | stack.Push(val) 116 | case script.OpHash160: 117 | if stack.Len() < opHash160MinStackLength { 118 | return false, fmt.Errorf("invalid stack length for OP_HASH160") 119 | } 120 | 121 | var hashedVal []byte 122 | val := stack.Pop() 123 | 124 | hasher := crypto.NewMultiHash( 125 | []hash.Hashing{hash.NewHasher(sha256.New()), hash.NewHasher(ripemd160.New())}, 126 | ) 127 | hashedVal, err = hasher.Hash([]byte(val)) 128 | if err != nil { 129 | return false, fmt.Errorf("couldn't hash value: %w", err) 130 | } 131 | 132 | stack.Push(string(hashedVal)) 133 | case script.OpEqualVerify: 134 | if stack.Len() < opEqualVerifyMinStackLength { 135 | return false, fmt.Errorf("invalid stack length for OP_EQUALVERIFY") 136 | } 137 | 138 | val1 := stack.Pop() 139 | val2 := stack.Pop() 140 | 141 | if val1 != val2 { 142 | return false, fmt.Errorf("OP_EQUAL_VERIFY failed, values are not equal: %s != %s", val1, val2) 143 | } 144 | default: 145 | } 146 | } 147 | 148 | if !token.IsOperator() { 149 | stack.Push(scriptString[index]) 150 | } 151 | } 152 | 153 | if stack.Len() != 1 { 154 | return false, fmt.Errorf("invalid stack length after script execution") 155 | } 156 | 157 | return stack.Pop() == "true", nil 158 | } 159 | -------------------------------------------------------------------------------- /pkg/chain/iterator/reverse_block_test.go: -------------------------------------------------------------------------------- 1 | package iterator //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yago-123/chainnet/pkg/kernel" 7 | mockStorage "github.com/yago-123/chainnet/tests/mocks/storage" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | const ( 14 | InitialCoinbaseReward = 50 15 | ) 16 | 17 | func TestReverseIterator(t *testing.T) { 18 | // set up genesis kernel with coinbase transaction 19 | coinbaseTx := kernel.NewCoinbaseTransaction("address-1", InitialCoinbaseReward, 0) 20 | coinbaseTx.SetID([]byte("coinbase-transaction-genesis-id")) 21 | blockHeader := kernel.NewBlockHeader([]byte{}, 0, []byte{}, 0, []byte{}, 0, 0) 22 | blockHeader.SetNonce(1) 23 | genesisBlock := kernel.NewGenesisBlock(blockHeader, []*kernel.Transaction{coinbaseTx}, []byte("genesis-kernel-hash")) 24 | 25 | // set up kernel 1 with one coinbase transaction 26 | coinbaseTx2 := kernel.NewCoinbaseTransaction("address-2", InitialCoinbaseReward, 0) 27 | coinbaseTx2.SetID([]byte("coinbase-transaction-kernel-1-id")) 28 | blockHeader = kernel.NewBlockHeader([]byte{}, 0, []byte{}, 0, genesisBlock.Hash, 0, 0) 29 | blockHeader.SetNonce(1) 30 | block1 := kernel.NewBlock(blockHeader, []*kernel.Transaction{coinbaseTx2}, []byte("kernel-hash-1")) 31 | 32 | // set up kernel 2 with one coinbase transaction and one regular transaction 33 | coinbaseTx3 := kernel.NewCoinbaseTransaction("address-3", InitialCoinbaseReward, 0) 34 | coinbaseTx3.SetID([]byte("coinbase-transaction-kernel-2-id")) 35 | regularTx := kernel.NewTransaction( 36 | []kernel.TxInput{ 37 | { 38 | Txid: []byte("coinbase-transaction-kernel-1-id"), 39 | Vout: 0, 40 | ScriptSig: "ScriptSig", 41 | }, 42 | }, 43 | []kernel.TxOutput{ 44 | {Amount: 1, ScriptPubKey: "ScriptPubKey"}, 45 | }) 46 | regularTx.SetID([]byte("regular-tx-2-id")) 47 | blockHeader = kernel.NewBlockHeader([]byte{}, 0, []byte{}, 0, block1.Hash, 0, 0) 48 | blockHeader.SetNonce(1) 49 | block2 := kernel.NewBlock(blockHeader, []*kernel.Transaction{coinbaseTx3, regularTx}, []byte("kernel-hash-2")) 50 | 51 | // set up the store mock with the corresponding returns 52 | storage := mockStorage.MockStorage{} 53 | storage. 54 | On("RetrieveBlockByHash", block2.Hash). 55 | Return(block2, nil) 56 | storage. 57 | On("RetrieveBlockByHash", block1.Hash). 58 | Return(block1, nil) 59 | storage. 60 | On("RetrieveBlockByHash", genesisBlock.Hash). 61 | Return(genesisBlock, nil) 62 | 63 | // check that the iterator works as expected 64 | reverseIterator := NewReverseBlockIterator(&storage) 65 | 66 | // initialize iterator with the last kernel hash 67 | err := reverseIterator.Initialize(block2.Hash) 68 | require.NoError(t, err) 69 | 70 | // check if we have next element and retrieve kernel 2 71 | assert.True(t, reverseIterator.HasNext(), "error checking if next kernel exists") 72 | b, err := reverseIterator.GetNextBlock() 73 | require.NoError(t, err) 74 | assert.Equal(t, []byte("kernel-hash-2"), b.Hash, "failure retrieving kernel 2") 75 | 76 | // check if we have next element and retrieve kernel 1 77 | assert.True(t, reverseIterator.HasNext(), "error checking if next kernel exists") 78 | b, err = reverseIterator.GetNextBlock() 79 | require.NoError(t, err) 80 | assert.Equal(t, []byte("kernel-hash-1"), b.Hash, "failure retrieving kernel 1") 81 | 82 | // check if we have next element and retrieve genesis kernel 83 | assert.True(t, reverseIterator.HasNext(), "error checking if next kernel exists") 84 | b, err = reverseIterator.GetNextBlock() 85 | require.NoError(t, err) 86 | assert.Equal(t, []byte("genesis-kernel-hash"), b.Hash, "failure retrieving genesis kernel") 87 | 88 | // verify that we don't have more elements available 89 | assert.False(t, reverseIterator.HasNext(), "more elements were found when the chain must have ended") 90 | } 91 | 92 | func TestReverseIteratorWithOnlyGenesisBlock(t *testing.T) { 93 | // set up genesis kernel with coinbase transaction 94 | coinbaseTx := kernel.NewCoinbaseTransaction("address-1", InitialCoinbaseReward, 0) 95 | coinbaseTx.SetID([]byte("coinbase-genesis-kernel-id")) 96 | blockHeader := kernel.NewBlockHeader([]byte{}, 0, []byte{}, 0, []byte{}, 0, 0) 97 | blockHeader.SetNonce(1) 98 | genesisBlock := kernel.NewGenesisBlock(blockHeader, []*kernel.Transaction{coinbaseTx}, []byte("genesis-kernel-hash")) 99 | 100 | // set up the store mock with the corresponding returns 101 | storage := mockStorage.MockStorage{} 102 | storage. 103 | On("RetrieveBlockByHash", genesisBlock.Hash). 104 | Return(genesisBlock, nil) 105 | 106 | // check that the iterator works as expected 107 | reverseIterator := NewReverseBlockIterator(&storage) 108 | 109 | // initialize iterator with the last kernel hash 110 | err := reverseIterator.Initialize(genesisBlock.Hash) 111 | require.NoError(t, err) 112 | 113 | // check if we have next element and retrieve genesis kernel 114 | assert.True(t, reverseIterator.HasNext(), "error checking if next kernel exists") 115 | b, err := reverseIterator.GetNextBlock() 116 | require.NoError(t, err) 117 | assert.Equal(t, []byte("genesis-kernel-hash"), b.Hash, "failure retrieving genesis kernel") 118 | 119 | // verify that we don't have more elements available 120 | assert.False(t, reverseIterator.HasNext(), "more elements were found when the chain must have ended") 121 | } 122 | -------------------------------------------------------------------------------- /pkg/chain/chain_test.go: -------------------------------------------------------------------------------- 1 | package blockchain //nolint:testpackage // don't create separate package for tests 2 | import ( 3 | "os" 4 | "testing" 5 | 6 | cerror "github.com/yago-123/chainnet/pkg/errs" 7 | 8 | "github.com/yago-123/chainnet/pkg/utxoset" 9 | 10 | "github.com/yago-123/chainnet/config" 11 | "github.com/yago-123/chainnet/pkg/encoding" 12 | "github.com/yago-123/chainnet/pkg/kernel" 13 | "github.com/yago-123/chainnet/pkg/mempool" 14 | "github.com/yago-123/chainnet/pkg/observer" 15 | "github.com/yago-123/chainnet/pkg/storage" 16 | "github.com/yago-123/chainnet/tests/mocks/consensus" 17 | mockHash "github.com/yago-123/chainnet/tests/mocks/crypto/hash" 18 | mockStorage "github.com/yago-123/chainnet/tests/mocks/storage" 19 | 20 | "github.com/sirupsen/logrus" 21 | 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | var block1 = &kernel.Block{ //nolint:gochecknoglobals // ignore linter in this case 27 | Header: &kernel.BlockHeader{ 28 | Version: []byte("1"), 29 | PrevBlockHash: []byte{}, 30 | MerkleRoot: []byte{}, 31 | Height: 0, 32 | Timestamp: 0, 33 | Target: 1, 34 | Nonce: 0, 35 | }, 36 | Transactions: []*kernel.Transaction{ 37 | kernel.NewCoinbaseTransaction("pubkey", 50, 0), 38 | }, 39 | Hash: []byte("block-1-hash"), 40 | } 41 | 42 | var block2 = &kernel.Block{ //nolint:gochecknoglobals // ignore linter in this case 43 | Header: &kernel.BlockHeader{ 44 | Version: []byte("1"), 45 | PrevBlockHash: []byte("block-1-hash"), 46 | MerkleRoot: []byte{}, 47 | Height: 1, 48 | Timestamp: 0, 49 | Target: 1, 50 | Nonce: 0, 51 | }, 52 | Transactions: []*kernel.Transaction{ 53 | kernel.NewCoinbaseTransaction("pubkey", 50, 0), 54 | }, 55 | Hash: []byte("block-2-hash"), 56 | } 57 | 58 | var block3 = &kernel.Block{ //nolint:gochecknoglobals // ignore linter in this case 59 | Header: &kernel.BlockHeader{ 60 | Version: []byte("1"), 61 | PrevBlockHash: []byte("block-2-hash"), 62 | MerkleRoot: []byte{}, 63 | Height: 2, 64 | Timestamp: 0, 65 | Target: 1, 66 | Nonce: 0, 67 | }, 68 | Transactions: []*kernel.Transaction{ 69 | kernel.NewCoinbaseTransaction("pubkey", 50, 0), 70 | }, 71 | Hash: []byte("block-3-hash"), 72 | } 73 | 74 | var block4 = &kernel.Block{ //nolint:gochecknoglobals // ignore linter in this case 75 | Header: &kernel.BlockHeader{ 76 | Version: []byte("1"), 77 | PrevBlockHash: []byte("block-3-hash"), 78 | MerkleRoot: []byte{}, 79 | Height: 3, 80 | Timestamp: 0, 81 | Target: 1, 82 | Nonce: 0, 83 | }, 84 | Transactions: []*kernel.Transaction{ 85 | kernel.NewCoinbaseTransaction("pubkey", 50, 0), 86 | }, 87 | Hash: []byte("block-4-hash"), 88 | } 89 | 90 | // tests the NewBlockchain method when there is not any previous chain addition 91 | func TestBlockchain_InitializationFromScratch(t *testing.T) { 92 | store := &mockStorage.MockStorage{} 93 | store. 94 | On("GetLastHeader"). 95 | Return(&kernel.BlockHeader{}, cerror.ErrStorageElementNotFound) 96 | 97 | cfg := &config.Config{Logger: logrus.New()} 98 | 99 | chain, err := NewBlockchain( 100 | cfg, 101 | store, 102 | mempool.NewMemPool(1000), 103 | utxoset.NewUTXOSet(cfg), 104 | &mockHash.FakeHashing{}, 105 | &consensus.MockHeavyValidator{}, 106 | observer.NewChainSubject(), 107 | encoding.NewGobEncoder(), 108 | ) 109 | 110 | require.NoError(t, err) 111 | assert.Equal(t, uint(0), chain.lastHeight) 112 | assert.Empty(t, chain.lastBlockHash, 0) 113 | assert.Empty(t, chain.headers, 0) 114 | } 115 | 116 | // tests the NewBlockchain method when there has been additions to the chain before 117 | func TestBlockchain_InitializationRecovery(t *testing.T) { 118 | boltdb, err := storage.NewBoltDB("temp-file", "block-bucket", "header-bucket", encoding.NewGobEncoder()) 119 | require.NoError(t, err) 120 | defer os.Remove("temp-file") 121 | 122 | // persist headers in store 123 | require.NoError(t, boltdb.PersistHeader(block1.Hash, *block1.Header)) 124 | require.NoError(t, boltdb.PersistHeader(block2.Hash, *block2.Header)) 125 | require.NoError(t, boltdb.PersistHeader(block3.Hash, *block3.Header)) 126 | require.NoError(t, boltdb.PersistHeader(block4.Hash, *block4.Header)) 127 | 128 | // persist blocks in store 129 | require.NoError(t, boltdb.PersistBlock(*block1)) 130 | require.NoError(t, boltdb.PersistBlock(*block2)) 131 | require.NoError(t, boltdb.PersistBlock(*block3)) 132 | require.NoError(t, boltdb.PersistBlock(*block4)) 133 | 134 | mockHashing := &mockHash.MockHashing{} 135 | mockHashing. 136 | On("Hash", block4.Header.Assemble()). 137 | Return([]byte("block-4-hash"), nil) 138 | 139 | cfg := &config.Config{Logger: logrus.New()} 140 | // initialize chain and make sure that the values are retrieved correctly 141 | chain, err := NewBlockchain( 142 | cfg, 143 | boltdb, 144 | mempool.NewMemPool(1000), 145 | utxoset.NewUTXOSet(cfg), 146 | mockHashing, 147 | &consensus.MockHeavyValidator{}, 148 | observer.NewChainSubject(), 149 | encoding.NewGobEncoder(), 150 | ) 151 | 152 | require.NoError(t, err) 153 | assert.NotNil(t, chain) 154 | assert.Equal(t, []byte("block-4-hash"), chain.lastBlockHash) 155 | assert.Equal(t, uint(4), chain.lastHeight) 156 | assert.Len(t, chain.headers, 4) 157 | assert.Equal(t, []byte("block-3-hash"), chain.headers["block-4-hash"].PrevBlockHash) 158 | } 159 | -------------------------------------------------------------------------------- /pkg/wallet/simple_wallet/wallet_test.go: -------------------------------------------------------------------------------- 1 | package simplewallet //nolint:testpackage // don't create separate package for tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yago-123/chainnet/config" 7 | "github.com/yago-123/chainnet/pkg/consensus/validator" 8 | "github.com/yago-123/chainnet/pkg/encoding" 9 | "github.com/yago-123/chainnet/pkg/kernel" 10 | "github.com/yago-123/chainnet/pkg/script" 11 | mockHash "github.com/yago-123/chainnet/tests/mocks/crypto/hash" 12 | mockSign "github.com/yago-123/chainnet/tests/mocks/crypto/sign" 13 | 14 | "github.com/btcsuite/btcutil/base58" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | var utxos = []*kernel.UTXO{ //nolint:gochecknoglobals // data that is used across all test funcs 21 | {TxID: []byte("random-id-0"), OutIdx: 1, Output: kernel.NewOutput(1, script.P2PK, "pubkey-2")}, 22 | {TxID: []byte("random-id-1"), OutIdx: 3, Output: kernel.NewOutput(2, script.P2PK, "pubkey-2")}, 23 | {TxID: []byte("random-id-2"), OutIdx: 1, Output: kernel.NewOutput(5, script.P2PK, "pubkey-2")}, 24 | {TxID: []byte("random-id-3"), OutIdx: 8, Output: kernel.NewOutput(5, script.P2PK, "pubkey-2")}, 25 | } 26 | 27 | func TestWallet_SendTransaction(t *testing.T) { 28 | var err error 29 | 30 | signer := mockSign.MockSign{} 31 | signer. 32 | On("NewKeyPair"). 33 | Return([]byte("pubkey-2"), []byte("privkey-2"), nil) 34 | 35 | wallet, err := NewWallet(config.NewConfig(), 1, validator.NewLightValidator(&mockHash.FakeHashing{}), &signer, &mockHash.FakeHashing{}, encoding.NewProtobufEncoder()) 36 | require.NoError(t, err) 37 | 38 | // send transaction with a target amount bigger than utxos amount 39 | _, err = wallet.GenerateNewTransaction(script.P2PK, []byte("pubkey-1"), 100, 1, utxos) 40 | require.Error(t, err) 41 | 42 | // send transaction with a txFee bigger than utxos amount 43 | _, err = wallet.GenerateNewTransaction(script.P2PK, []byte("pubkey-1"), 1, 100, utxos) 44 | require.Error(t, err) 45 | 46 | // send transaction without utxos 47 | _, err = wallet.GenerateNewTransaction(script.P2PK, []byte("pubkey-1"), 10, 1, []*kernel.UTXO{}) 48 | require.Error(t, err) 49 | 50 | // send transaction with incorrect utxos unlocking scripts 51 | signer2 := mockSign.MockSign{} 52 | signer2. 53 | On("NewKeyPair"). 54 | Return([]byte("pubkey-5"), []byte("privkey-5"), nil) 55 | } 56 | 57 | func TestWallet_SendTransactionCheckOutputTx(t *testing.T) { 58 | var err error 59 | 60 | hasher := &mockHash.FakeHashing{} 61 | signer := mockSign.MockSign{} 62 | signer. 63 | On("NewKeyPair"). 64 | Return([]byte("pubkey-2"), []byte("privkey-2"), nil) 65 | 66 | wallet, err := NewWallet(config.NewConfig(), 1, validator.NewLightValidator(hasher), &signer, hasher, encoding.NewProtobufEncoder()) 67 | require.NoError(t, err) 68 | // send transaction with correct target and empty tx fee 69 | tx, err := wallet.GenerateNewTransaction(script.P2PK, []byte("pubkey-1"), 10, 0, utxos) 70 | expectedTx := &kernel.Transaction{ 71 | ID: tx.ID, 72 | Vin: []kernel.TxInput{ 73 | kernel.NewInput([]byte("random-id-0"), 1, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo68 OP_CHECKSIGpubkey-13\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 74 | kernel.NewInput([]byte("random-id-1"), 3, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo68 OP_CHECKSIGpubkey-13\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 75 | kernel.NewInput([]byte("random-id-2"), 1, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo68 OP_CHECKSIGpubkey-13\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 76 | kernel.NewInput([]byte("random-id-3"), 8, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo68 OP_CHECKSIGpubkey-13\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 77 | }, 78 | Vout: []kernel.TxOutput{ 79 | kernel.NewOutput(10, script.P2PK, "pubkey-1"), 80 | kernel.NewOutput(3, script.P2PK, "pubkey-2"), 81 | }, 82 | } 83 | require.NoError(t, err) 84 | assert.Equal(t, expectedTx, tx) 85 | 86 | // send transaction with correct target and some tx fee 87 | tx, err = wallet.GenerateNewTransaction(script.P2PK, []byte("pubkey-3"), 10, 2, utxos) 88 | expectedTx2 := &kernel.Transaction{ 89 | ID: tx.ID, 90 | Vin: []kernel.TxInput{ 91 | kernel.NewInput([]byte("random-id-0"), 1, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo6A OP_CHECKSIGpubkey-31\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 92 | kernel.NewInput([]byte("random-id-1"), 3, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo6A OP_CHECKSIGpubkey-31\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 93 | kernel.NewInput([]byte("random-id-2"), 1, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo6A OP_CHECKSIGpubkey-31\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 94 | kernel.NewInput([]byte("random-id-3"), 8, base58.Encode([]byte("Inputs:random-id-01random-id-13random-id-21random-id-38Outputs:10\x00KozLnpdoo6A OP_CHECKSIGpubkey-31\x00KozLnpdoo69 OP_CHECKSIGpubkey-2-signed")), "pubkey-2"), 95 | }, 96 | Vout: []kernel.TxOutput{ 97 | kernel.NewOutput(10, script.P2PK, "pubkey-3"), 98 | kernel.NewOutput(1, script.P2PK, "pubkey-2"), 99 | }, 100 | } 101 | require.NoError(t, err) 102 | assert.Equal(t, expectedTx2, tx) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/encoding/gob_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "testing" 7 | 8 | "github.com/yago-123/chainnet/pkg/encoding" 9 | "github.com/yago-123/chainnet/pkg/kernel" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | var testBlock = &kernel.Block{ //nolint:gochecknoglobals // data that is used across all test funcs 16 | Header: &kernel.BlockHeader{ 17 | Version: []byte("v1"), 18 | PrevBlockHash: []byte("prevhash"), 19 | MerkleRoot: []byte("merkleroot"), 20 | Height: 123, 21 | Timestamp: 1610000000, 22 | Target: 456, 23 | Nonce: 789, 24 | }, 25 | Transactions: []*kernel.Transaction{ 26 | { 27 | ID: []byte("tx1"), 28 | Vin: []kernel.TxInput{ 29 | {Txid: []byte("tx0"), Vout: 0, ScriptSig: "script1", PubKey: "pubkey1"}, 30 | {Txid: []byte("tx0"), Vout: 1, ScriptSig: "script2", PubKey: "pubkey2"}, 31 | }, 32 | Vout: []kernel.TxOutput{ 33 | {Amount: 50, ScriptPubKey: "scriptpubkey1", PubKey: "pubkey1"}, 34 | {Amount: 30, ScriptPubKey: "scriptpubkey2", PubKey: "pubkey2"}, 35 | }, 36 | }, 37 | { 38 | ID: []byte("tx2"), 39 | Vin: []kernel.TxInput{ 40 | {Txid: []byte("tx1"), Vout: 0, ScriptSig: "script3", PubKey: "pubkey3"}, 41 | }, 42 | Vout: []kernel.TxOutput{ 43 | {Amount: 20, ScriptPubKey: "scriptpubkey3", PubKey: "pubkey3"}, 44 | {Amount: 10, ScriptPubKey: "scriptpubkey4", PubKey: "pubkey4"}, 45 | }, 46 | }, 47 | }, 48 | Hash: []byte("blockhash"), 49 | } 50 | 51 | var testTransaction = kernel.Transaction{ //nolint:gochecknoglobals // data that is used across all test funcs 52 | ID: []byte("tx1"), 53 | Vin: []kernel.TxInput{ 54 | {Txid: []byte("tx0"), Vout: 0, ScriptSig: "script1", PubKey: "pubkey1"}, 55 | {Txid: []byte("tx0"), Vout: 1, ScriptSig: "script2", PubKey: "pubkey2"}, 56 | }, 57 | Vout: []kernel.TxOutput{ 58 | {Amount: 50, ScriptPubKey: "scriptpubkey1", PubKey: "pubkey1"}, 59 | {Amount: 30, ScriptPubKey: "scriptpubkey2", PubKey: "pubkey2"}, 60 | }, 61 | } 62 | 63 | var testBlockHeaders = []*kernel.BlockHeader{ //nolint:gochecknoglobals // data that is used across all test funcs 64 | { 65 | Version: []byte("v1"), 66 | PrevBlockHash: []byte("prevhash"), 67 | MerkleRoot: []byte("merkleroot"), 68 | Height: 123, 69 | Timestamp: 1610000000, 70 | Target: 456, 71 | Nonce: 789, 72 | }, 73 | { 74 | Version: []byte("v2"), 75 | PrevBlockHash: []byte("prevhash2"), 76 | MerkleRoot: []byte("merkleroot2"), 77 | Height: 456, 78 | Timestamp: 1620000000, 79 | Target: 789, 80 | Nonce: 101112, 81 | }, 82 | } 83 | 84 | func TestSerializeBlock(t *testing.T) { 85 | gobenc := encoding.NewGobEncoder() 86 | 87 | data, err := gobenc.SerializeBlock(*testBlock) 88 | require.NoError(t, err) 89 | 90 | var block kernel.Block 91 | decoder := gob.NewDecoder(bytes.NewReader(data)) 92 | err = decoder.Decode(&block) 93 | require.NoError(t, err) 94 | 95 | assert.Equal(t, *testBlock, block) 96 | } 97 | 98 | func TestDeserializeBlock(t *testing.T) { 99 | gobenc := encoding.NewGobEncoder() 100 | 101 | data, err := gobenc.SerializeBlock(*testBlock) 102 | require.NoError(t, err) 103 | 104 | block, err := gobenc.DeserializeBlock(data) 105 | require.NoError(t, err) 106 | 107 | assert.Equal(t, testBlock, block) 108 | } 109 | 110 | func TestSerializeHeader(t *testing.T) { 111 | gobenc := encoding.NewGobEncoder() 112 | 113 | data, err := gobenc.SerializeHeader(*testBlock.Header) 114 | require.NoError(t, err) 115 | 116 | var header kernel.BlockHeader 117 | decoder := gob.NewDecoder(bytes.NewReader(data)) 118 | err = decoder.Decode(&header) 119 | require.NoError(t, err) 120 | 121 | assert.Equal(t, *testBlock.Header, header) 122 | } 123 | 124 | func TestDeserializeHeader(t *testing.T) { 125 | gobenc := encoding.NewGobEncoder() 126 | 127 | data, err := gobenc.SerializeHeader(*testBlock.Header) 128 | require.NoError(t, err) 129 | 130 | header, err := gobenc.DeserializeHeader(data) 131 | require.NoError(t, err) 132 | 133 | assert.Equal(t, testBlock.Header, header) 134 | } 135 | 136 | func TestSerializeHeaders(t *testing.T) { 137 | gobenc := encoding.NewGobEncoder() 138 | 139 | data, err := gobenc.SerializeHeaders(testBlockHeaders) 140 | require.NoError(t, err) 141 | 142 | var headers []*kernel.BlockHeader 143 | decoder := gob.NewDecoder(bytes.NewReader(data)) 144 | err = decoder.Decode(&headers) 145 | require.NoError(t, err) 146 | 147 | assert.ElementsMatch(t, testBlockHeaders, headers) 148 | } 149 | 150 | func TestDeserializeHeaders(t *testing.T) { 151 | gobenc := encoding.NewGobEncoder() 152 | 153 | data, err := gobenc.SerializeHeaders(testBlockHeaders) 154 | require.NoError(t, err) 155 | 156 | headers, err := gobenc.DeserializeHeaders(data) 157 | require.NoError(t, err) 158 | 159 | assert.ElementsMatch(t, testBlockHeaders, headers) 160 | } 161 | 162 | func TestSerializeTransaction(t *testing.T) { 163 | gobenc := encoding.NewGobEncoder() 164 | 165 | data, err := gobenc.SerializeTransaction(testTransaction) 166 | require.NoError(t, err) 167 | 168 | var transaction kernel.Transaction 169 | decoder := gob.NewDecoder(bytes.NewReader(data)) 170 | err = decoder.Decode(&transaction) 171 | require.NoError(t, err) 172 | 173 | assert.Equal(t, testTransaction, transaction) 174 | } 175 | 176 | func TestDeserializeTransaction(t *testing.T) { 177 | gobenc := encoding.NewGobEncoder() 178 | 179 | data, err := gobenc.SerializeTransaction(testTransaction) 180 | require.NoError(t, err) 181 | 182 | transaction, err := gobenc.DeserializeTransaction(data) 183 | require.NoError(t, err) 184 | 185 | assert.Equal(t, &testTransaction, transaction) 186 | } 187 | -------------------------------------------------------------------------------- /pkg/network/pubsub/gossip.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/yago-123/chainnet/config" 8 | "github.com/yago-123/chainnet/pkg/encoding" 9 | "github.com/yago-123/chainnet/pkg/kernel" 10 | "github.com/yago-123/chainnet/pkg/observer" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | pubSubP2P "github.com/libp2p/go-libp2p-pubsub" 15 | "github.com/libp2p/go-libp2p/core/host" 16 | ) 17 | 18 | const ( 19 | // todo(): BlackListedNodes? 20 | // todo(): add topic for txs added to mempool? 21 | TxAddedPubSubTopic = "tx-added-topic" 22 | BlockAddedPubSubTopic = "block-added-topic" 23 | ) 24 | 25 | type gossipHandler struct { 26 | ctx context.Context 27 | logger *logrus.Logger 28 | host host.Host 29 | encoder encoding.Encoding 30 | netSubject observer.NetSubject 31 | } 32 | 33 | func newGossipHandler(ctx context.Context, cfg *config.Config, host host.Host, encoder encoding.Encoding, netSubject observer.NetSubject) *gossipHandler { 34 | return &gossipHandler{ 35 | ctx: ctx, 36 | logger: cfg.Logger, 37 | host: host, 38 | encoder: encoder, 39 | netSubject: netSubject, 40 | } 41 | } 42 | 43 | // listenForBlocksAdded represents the handler for the block added topic 44 | func (h *gossipHandler) listenForBlocksAdded(sub *pubSubP2P.Subscription) { 45 | for { 46 | msg, err := sub.Next(h.ctx) 47 | if err != nil { 48 | h.logger.Errorf("stopping listening for blocks added: %v", err) 49 | return 50 | } 51 | 52 | // ignore those messages that come from the same node 53 | if h.host.ID() == msg.ReceivedFrom { 54 | continue 55 | } 56 | 57 | header, err := h.encoder.DeserializeHeader(msg.Data) 58 | if err != nil { 59 | h.logger.Errorf("failed deserializing header from %s: %v", msg.ReceivedFrom, err) 60 | continue 61 | } 62 | 63 | h.logger.Tracef("received block from %s with block ID %v", msg.ReceivedFrom, header) 64 | 65 | h.netSubject.NotifyUnconfirmedHeaderReceived(msg.ReceivedFrom, *header) 66 | } 67 | } 68 | 69 | // listenForTxMempool represents the handler for the tx mempool topic 70 | func (h *gossipHandler) listenForTxAdded(sub *pubSubP2P.Subscription) { 71 | for { 72 | msg, err := sub.Next(h.ctx) 73 | if err != nil { 74 | h.logger.Errorf("stopping listening for transactions: %v", err) 75 | return 76 | } 77 | 78 | // ignore those messages that come from the same node 79 | if h.host.ID() == msg.ReceivedFrom { 80 | continue 81 | } 82 | 83 | // todo(): verify that is a transaction ID 84 | 85 | h.logger.Infof("received transaction from %s with tx ID %x", msg.ReceivedFrom, msg.Data) 86 | h.netSubject.NotifyUnconfirmedTxIDReceived(msg.ReceivedFrom, msg.Data) 87 | } 88 | } 89 | 90 | type GossipPubSub struct { 91 | ctx context.Context 92 | pubsub *pubSubP2P.PubSub 93 | 94 | encoder encoding.Encoding 95 | 96 | netSubject observer.NetSubject 97 | topicStore map[string]*pubSubP2P.Topic 98 | } 99 | 100 | func NewGossipPubSub(ctx context.Context, cfg *config.Config, host host.Host, encoder encoding.Encoding, netSubject observer.NetSubject, topics []string, enableSubscribe bool) (*GossipPubSub, error) { 101 | pubsub, err := pubSubP2P.NewGossipSub(ctx, host) 102 | if err != nil { 103 | return nil, fmt.Errorf("failed to create pubsub module: %w", err) 104 | } 105 | 106 | handler := newGossipHandler(ctx, cfg, host, encoder, netSubject) 107 | 108 | // initialize handlers for the topics available 109 | topicHandlers := map[string]func(sub *pubSubP2P.Subscription){ 110 | TxAddedPubSubTopic: handler.listenForTxAdded, 111 | BlockAddedPubSubTopic: handler.listenForBlocksAdded, 112 | } 113 | 114 | topicStore := make(map[string]*pubSubP2P.Topic) 115 | // join the topics and subscribe/initialize handler if required 116 | for _, topicName := range topics { 117 | topic, errJoin := pubsub.Join(topicName) 118 | if errJoin != nil { 119 | return nil, fmt.Errorf("error joining pubsub topic %s: %w", topicName, errJoin) 120 | } 121 | 122 | // if subscribe is enabled, subscribe to the topic and initialize the handler. Otherwise, just join the 123 | // topic. Subscribe is not enabled for the cases in which we only want to publish to the topic (like wallets) 124 | // but not listen 125 | // todo: put enableSubscribe as flag 126 | if enableSubscribe { 127 | // subscribe to the topic to listen 128 | sub, errSub := topic.Subscribe() 129 | if errSub != nil { 130 | return nil, fmt.Errorf("error subscribing to pubsub topic %s: %w", topicName, errSub) 131 | } 132 | 133 | // start handlers 134 | if handlerFunc, ok := topicHandlers[topicName]; !ok { 135 | return nil, fmt.Errorf("unable to initialize handler for topic %s", topicName) 136 | } else if ok { 137 | go handlerFunc(sub) 138 | } 139 | } 140 | 141 | // save the topics 142 | topicStore[topicName] = topic 143 | } 144 | 145 | return &GossipPubSub{ 146 | ctx: ctx, 147 | pubsub: pubsub, 148 | encoder: encoder, 149 | netSubject: netSubject, 150 | topicStore: topicStore, 151 | }, nil 152 | } 153 | 154 | // NotifyBlockHeaderAdded used for notifying the pubsub network that a local block has been added to the blockchain 155 | func (g *GossipPubSub) NotifyBlockHeaderAdded(ctx context.Context, header kernel.BlockHeader) error { 156 | topic, ok := g.topicStore[BlockAddedPubSubTopic] 157 | if !ok { 158 | return fmt.Errorf("topic %s not registered", BlockAddedPubSubTopic) 159 | } 160 | 161 | data, err := g.encoder.SerializeHeader(header) 162 | if err != nil { 163 | return fmt.Errorf("failed to serialize transaction: %w", err) 164 | } 165 | 166 | return topic.Publish(ctx, data) 167 | } 168 | 169 | // NotifyTransactionAdded used for notifying the pubsub network that a local transaction has been added to 170 | // the mempool. It propagates the transaction ID only. If nodes want to retrieve the transaction details, 171 | // they will ask the node that sent the transaction. 172 | func (g *GossipPubSub) NotifyTransactionAdded(ctx context.Context, tx kernel.Transaction) error { 173 | topic, ok := g.topicStore[TxAddedPubSubTopic] 174 | if !ok { 175 | return fmt.Errorf("topic %s not registered", TxAddedPubSubTopic) 176 | } 177 | 178 | return topic.Publish(ctx, tx.ID) 179 | } 180 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "sync" 9 | 10 | "github.com/yago-123/chainnet/pkg/crypto/hash" 11 | "github.com/yago-123/chainnet/pkg/kernel" 12 | ) 13 | 14 | const ( 15 | NumBitsInByte = 8 16 | BiggestByteMask = 0xFF 17 | 18 | TargetAdjustmentUnit = uint(1) 19 | InitialBlockTarget = uint(1) 20 | MinimumTarget = uint(1) 21 | MaximumTarget = uint(255) 22 | 23 | MinLengthHash = 16 24 | MaxLengthHash = 256 25 | ) 26 | 27 | // CalculateTxHash calculates the hash of a transaction 28 | func CalculateTxHash(tx *kernel.Transaction, hasher hash.Hashing) ([]byte, error) { 29 | // todo(): move this to the NewTransaction function instead? 30 | return hasher.Hash(tx.Assemble()) 31 | } 32 | 33 | // VerifyTxHash verifies the hash of a transaction 34 | func VerifyTxHash(tx *kernel.Transaction, hash []byte, hasher hash.Hashing) error { 35 | ret, err := hasher.Verify(hash, tx.Assemble()) 36 | if err != nil { 37 | return fmt.Errorf("verify tx hash failed: %w", err) 38 | } 39 | 40 | if !ret { 41 | return errors.New("tx hash verification failed") 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // CalculateBlockHash calculates the hash of a block header 48 | func CalculateBlockHash(bh *kernel.BlockHeader, hasher hash.Hashing) ([]byte, error) { 49 | return hasher.Hash(bh.Assemble()) 50 | } 51 | 52 | // VerifyBlockHash verifies the hash of a block header 53 | func VerifyBlockHash(bh *kernel.BlockHeader, hash []byte, hasher hash.Hashing) error { 54 | ret, err := hasher.Verify(hash, bh.Assemble()) 55 | if err != nil { 56 | return fmt.Errorf("block hashing failed: %w", err) 57 | } 58 | 59 | if !ret { 60 | return errors.New("block header hash verification failed") 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // SafeUintToInt converts uint to int safely, returning an error if it would overflow 67 | func SafeUintToInt(u uint) (int, error) { 68 | if u > uint(int(^uint(0)>>1)) { // Check if u exceeds the maximum int value 69 | return 0, errors.New("uint value exceeds int range") 70 | } 71 | 72 | return int(u), nil 73 | } 74 | 75 | // IsFirstNBitsZero checks if the first n bits of the array are zero 76 | func IsFirstNBitsZero(arr []byte, n uint) bool { 77 | if n == 0 { 78 | return true // if n is 0, trivially true 79 | } 80 | 81 | fullBytes, err := SafeUintToInt(n / NumBitsInByte) 82 | if err != nil { 83 | return false 84 | } 85 | remainingBits := n % NumBitsInByte 86 | 87 | arrLen := len(arr) 88 | if arrLen < fullBytes || (arrLen == fullBytes && remainingBits > 0) { 89 | return false 90 | } 91 | 92 | // check full bytes 93 | for i := range make([]struct{}, fullBytes) { 94 | if arr[i] != 0 { 95 | return false 96 | } 97 | } 98 | 99 | // check remaining bits in the next byte if there are any 100 | if remainingBits > 0 { 101 | nextByte := arr[fullBytes] 102 | mask := byte(BiggestByteMask << (NumBitsInByte - remainingBits)) 103 | if nextByte&mask != 0 { 104 | return false 105 | } 106 | } 107 | 108 | return true 109 | } 110 | 111 | // CalculateMiningTarget calculates the new mining target based on the time required for mining the blocks 112 | // vs. the time expected to mine the blocks: 113 | // - if required > expected -> decrease the target by 1 unit 114 | // - if required < expected -> increase the target by 1 unit 115 | // - if required = expected -> do not change the target 116 | // 117 | // The mechanism used is simplified to prevent high fluctuations. 118 | func CalculateMiningTarget(currentTarget uint, targetTimeSpan float64, actualTimeSpan int64) uint { 119 | // determine the adjustment factor based on the actual and expected time spans 120 | timeAdjustmentFactor := float64(actualTimeSpan) / targetTimeSpan 121 | 122 | newTarget := currentTarget 123 | 124 | if timeAdjustmentFactor > 1.0 { 125 | // actual mining time is longer than expected, make it harder to mine 126 | newTarget = currentTarget - TargetAdjustmentUnit 127 | } 128 | 129 | if timeAdjustmentFactor < 1.0 { 130 | // actual mining time is shorter than expected, make it harder to mine 131 | newTarget = currentTarget + TargetAdjustmentUnit 132 | } 133 | 134 | // ensure the new target is within the valid range (there is no Min and Max for uint...) 135 | return uint(math.Min(math.Max(float64(newTarget), float64(MinimumTarget)), float64(MaximumTarget))) 136 | } 137 | 138 | func IsValidAddress(_ []byte) bool { 139 | // todo(): develop a proper address validation mechanism 140 | return true 141 | } 142 | 143 | func IsValidHash(hash []byte) bool { 144 | // convert raw []byte to a hexadecimal string 145 | hexString := fmt.Sprintf("%x", hash) 146 | 147 | // check length constraint 148 | if len(hexString) < MinLengthHash || len(hexString) > MaxLengthHash { 149 | return false 150 | } 151 | 152 | return true 153 | } 154 | 155 | // ProcessConcurrently processes a list of items concurrently with a maximum number of goroutines 156 | func ProcessConcurrently[T any]( 157 | ctx context.Context, 158 | items []T, 159 | maxConcurrency int, 160 | cancel context.CancelFunc, 161 | process func(ctx context.Context, item T) error, 162 | ) error { 163 | semaphore := make(chan struct{}, maxConcurrency) 164 | var wg sync.WaitGroup 165 | var overallErr error 166 | var overallErrMu sync.Mutex // protects access to overallErr 167 | 168 | for _, item := range items { 169 | semaphore <- struct{}{} // acquire a slot 170 | wg.Add(1) 171 | 172 | go func(it T) { 173 | defer wg.Done() 174 | defer func() { <-semaphore }() // release the slot 175 | 176 | if err := process(ctx, it); err != nil { 177 | overallErrMu.Lock() 178 | if overallErr == nil { // capture the first error 179 | overallErr = err 180 | if cancel != nil { 181 | cancel() // stop other operations if a cancel function is provided 182 | } 183 | } 184 | overallErrMu.Unlock() 185 | } 186 | }(item) 187 | } 188 | 189 | wg.Wait() // wait for all goroutines to finish 190 | return overallErr 191 | } 192 | 193 | // GetBalanceUTXOs calculates the total balance of a list of UTXOs 194 | func GetBalanceUTXOs(utxos []*kernel.UTXO) uint { 195 | var balance uint 196 | for _, utxo := range utxos { 197 | balance += utxo.Amount() 198 | } 199 | 200 | return balance 201 | } 202 | --------------------------------------------------------------------------------