Initial commit
This commit is contained in:
commit
2d4d7759e0
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
out/
|
||||
Makefile
|
||||
README.md
|
||||
data/
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
out/
|
||||
data/
|
||||
megalinter-reports/
|
||||
.env
|
10
.mega-linter.yml
Normal file
10
.mega-linter.yml
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
ADDITIONAL_EXCLUDED_DIRECTORIES:
|
||||
- data/
|
||||
CONSOLE_REPORTER: true
|
||||
DISABLE:
|
||||
- SPELL
|
||||
- MARKDOWN
|
||||
- REPOSITORY
|
||||
APPLY_FIXES:
|
||||
- YAML_PRETTIER
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM docker.io/golang:1.19 as builder
|
||||
WORKDIR /usr/local/src
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o kermago
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /usr/local/src/kermago /kermago
|
||||
EXPOSE 18018
|
||||
ENTRYPOINT ["/kermago"]
|
59
Makefile
Normal file
59
Makefile
Normal file
@ -0,0 +1,59 @@
|
||||
WORKDIR=$(shell pwd)
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
cd $(WORKDIR) && go build -o out/kermago
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
cd $(WORKDIR) && go test -v -coverpkg=./... ./...
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
cd $(WORKDIR) && gofmt -l -s -w .
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf /var/local/badkerma/*.json
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
cd $(WORKDIR) && go run .
|
||||
|
||||
.PHONY: clean-run
|
||||
clean-run: clean run
|
||||
|
||||
.PHONY: debug
|
||||
debug:
|
||||
cd $(WORKDIR) && KERMA_DEBUG=true go run .
|
||||
|
||||
.PHONY: clean-debug
|
||||
clean-debug: clean debug
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build:
|
||||
docker-compose build
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: docker-build
|
||||
docker-compose push
|
||||
|
||||
.PHONY: docker-debug
|
||||
docker-debug: docker-build
|
||||
KERMA_DEBUG=true docker-compose up
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: docker-build
|
||||
docker-compose up -d
|
||||
|
||||
.PHONY: docker-stop
|
||||
docker-stop:
|
||||
docker-compose down
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
docker pull docker.io/oxsecurity/megalinter:latest
|
||||
docker run -e DEFAULT_WORKSPACE='/src' -v "$(shell pwd):/src" docker.io/oxsecurity/megalinter:latest
|
89
README.md
Normal file
89
README.md
Normal file
@ -0,0 +1,89 @@
|
||||
KermaGo
|
||||
===
|
||||
|
||||
Prerequisites
|
||||
---
|
||||
|
||||
GNU Make and docker/Go installed on the machine
|
||||
|
||||
Config via environment variables
|
||||
---
|
||||
|
||||
| Name | Description | Default value |
|
||||
|-|-|-|
|
||||
`KERMA_DEBUG` | Run in debug mode | `false` |
|
||||
`KERMA_IP_ADDR` | Public IP Address to return to other nodes (if node behind port-forwarding) | `3.126.74.45` |
|
||||
`KERMA_PORT` | Port to listen on | 18018 |
|
||||
`KERMA_CONN_TIMEOUT` | Connection timeout | 10 |
|
||||
`KERMA_INITIAL_PEER_LIST` | Comma-separated string of initial peers to connect to | `""` |
|
||||
`KERMA_STORE_BASE_DIR` | Base directory to store data. The docker container will mount this under `./data` in the current directory | `/var/local/badkerma` |
|
||||
`KERMA_PEER_LIST_STORE` | Filename of the peer list | `peers.json` |
|
||||
`KERMA_BLOCK_STORE` | Filename of the blockchain | `blocks.json` |
|
||||
`KERMA_TRANSACTION_STORE` | Filename of the transactions | `transactions.json` |
|
||||
`KERMA_PRIVATE_KEY_FILE` | Filename of the private key file | `private.pem` |
|
||||
|
||||
You can create a `.env` file and docker will read the variables from there.
|
||||
|
||||
**NOTE**: You **need** to set `KERMA_INITIAL_PEER_LIST` to a valid node address when running the node for the **first time** or **after you have cleaned the persistent store**. Thereafter, the node gossips a known node list and keeps it up-to-date.
|
||||
|
||||
Run it locally
|
||||
---
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
Compile it locally
|
||||
---
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Test it locally
|
||||
---
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
You can also test with netcat. Start the program and run:
|
||||
|
||||
```bash
|
||||
cat test.txt | nc localhost 18018 -p 50001
|
||||
```
|
||||
|
||||
Run in debug mode locally
|
||||
---
|
||||
|
||||
```bash
|
||||
make debug
|
||||
```
|
||||
|
||||
Build the docker image
|
||||
---
|
||||
|
||||
```bash
|
||||
DOCKER_REGISTRY=localhost make docker-build
|
||||
```
|
||||
|
||||
Run docker container in debug mode
|
||||
---
|
||||
|
||||
```bash
|
||||
DOCKER_REGISTRY=localhost make docker-debug
|
||||
```
|
||||
|
||||
Run docker container in background
|
||||
---
|
||||
|
||||
```bash
|
||||
DOCKER_REGISTRY=localhost make docker-run
|
||||
```
|
||||
|
||||
Stop running docker container
|
||||
---
|
||||
|
||||
```bash
|
||||
make docker-stop
|
||||
```
|
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@ -0,0 +1,23 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
badkerma:
|
||||
image: ${DOCKER_REGISTRY}/badkerma:latest
|
||||
volumes:
|
||||
- ./data:${KERMA_STORE_BASE_DIR}
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
KERMA_DEBUG: ${KERMA_DEBUG}
|
||||
KERMA_IP_ADDR: ${KERMA_IP_ADDR}
|
||||
KERMA_PORT: ${KERMA_PORT}
|
||||
KERMA_CONN_TIMEOUT: ${KERMA_CONN_TIMEOUT}
|
||||
KERMA_INITIAL_PEER_LIST: ${KERMA_INITIAL_PEER_LIST}
|
||||
KERMA_STORE_BASE_DIR: ${KERMA_STORE_BASE_DIR}
|
||||
KERMA_PEER_LIST_STORE: ${KERMA_PEER_LIST_STORE}
|
||||
KERMA_BLOCK_STORE: ${KERMA_BLOCK_STORE}
|
||||
KERMA_TRANSACTION_STORE: ${KERMA_TRANSACTION_STORE}
|
||||
KERMA_PRIVATE_KEY_FILE: ${KERMA_PRIVATE_KEY_FILE}
|
||||
ports:
|
||||
- 18018:18018
|
||||
env_file: .env
|
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module kerma
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/docker/go v1.5.1-1
|
||||
github.com/smartystreets/goconvey v1.7.2
|
||||
golang.org/x/exp v0.0.0-20221026153819-32f3d567a233
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
||||
)
|
17
go.sum
Normal file
17
go.sum
Normal file
@ -0,0 +1,17 @@
|
||||
github.com/docker/go v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k=
|
||||
github.com/docker/go v1.5.1-1/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20221026153819-32f3d567a233 h1:9bNbSKT4RPLEzne0Xh1v3NaNecsa1DKjkOuTbY6V9rI=
|
||||
golang.org/x/exp v0.0.0-20221026153819-32f3d567a233/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
98
helpers/config.go
Normal file
98
helpers/config.go
Normal file
@ -0,0 +1,98 @@
|
||||
// Package helpers provides useful helper structures for KermaGo
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ipAddress = "3.126.74.45"
|
||||
port = 18018
|
||||
connTimeout = 10
|
||||
storeBaseDir = "/var/local/badkerma"
|
||||
peerListStore = "peers.json"
|
||||
blockStore = "blocks.json"
|
||||
transactionStore = "transactions.json"
|
||||
privateKeyFile = "private.pem"
|
||||
listenAddr = "0.0.0.0"
|
||||
initialPeerList = ""
|
||||
)
|
||||
|
||||
/*
|
||||
A Config is a helper used for storing and obtaining the application configuration.
|
||||
Supported configuration includes:
|
||||
- IPAddress
|
||||
- Port
|
||||
- ListenAddr - address to listen on
|
||||
- ConnTimeout - timeout for the client connections
|
||||
- PeerListStore - filename for the peer list store
|
||||
- BlockStore - filename for the blockchain store
|
||||
- TransactionStore - filename for the transaction store
|
||||
- PrivateKeyFile - filename for the private key
|
||||
- InitialPeerList - initial peer list for building up
|
||||
*/
|
||||
type Config struct {
|
||||
IPAddress string
|
||||
Port int
|
||||
PeerListStore string
|
||||
ConnTimeout int
|
||||
PrivateKeyFile string
|
||||
BlockStore string
|
||||
TransactionStore string
|
||||
ListenAddr string
|
||||
InitialPeerList string
|
||||
}
|
||||
|
||||
/*
|
||||
Construct fetches the configuration from the KERMA_* environment variables or sets sensible defaults
|
||||
*/
|
||||
func (c *Config) Construct() {
|
||||
c.IPAddress = ipAddress
|
||||
c.Port = port
|
||||
c.PeerListStore = peerListStore
|
||||
c.ConnTimeout = connTimeout
|
||||
c.PrivateKeyFile = privateKeyFile
|
||||
c.BlockStore = blockStore
|
||||
c.TransactionStore = transactionStore
|
||||
c.InitialPeerList = initialPeerList
|
||||
|
||||
baseDir := storeBaseDir
|
||||
|
||||
if os.Getenv("KERMA_IP_ADDR") != "" {
|
||||
c.IPAddress = os.Getenv("KERMA_IP_ADDR")
|
||||
}
|
||||
if os.Getenv("KERMA_PORT") != "" {
|
||||
c.Port, _ = strconv.Atoi(os.Getenv("KERMA_PORT"))
|
||||
}
|
||||
if os.Getenv("KERMA_CONN_TIMEOUT") != "" {
|
||||
c.ConnTimeout, _ = strconv.Atoi(os.Getenv("KERMA_CONN_TIMEOUT"))
|
||||
}
|
||||
if os.Getenv("KERMA_INITIAL_PEER_LIST") != "" {
|
||||
c.InitialPeerList = os.Getenv("KERMA_INITIAL_PEER_LIST")
|
||||
}
|
||||
if os.Getenv("KERMA_STORE_BASE_DIR") != "" {
|
||||
baseDir = os.Getenv("KERMA_STORE_BASE_DIR")
|
||||
baseDir = strings.TrimSuffix(baseDir, "/")
|
||||
}
|
||||
if os.Getenv("KERMA_PEER_LIST_STORE") != "" {
|
||||
c.PeerListStore = os.Getenv("KERMA_PEER_LIST_STORE")
|
||||
}
|
||||
if os.Getenv("KERMA_BLOCK_STORE") != "" {
|
||||
c.BlockStore = os.Getenv("KERMA_BLOCK_STORE")
|
||||
}
|
||||
if os.Getenv("KERMA_TRANSACTION_STORE") != "" {
|
||||
c.TransactionStore = os.Getenv("KERMA_TRANSACTION_STORE")
|
||||
}
|
||||
if os.Getenv("KERMA_PRIVATE_KEY_FILE") != "" {
|
||||
c.PrivateKeyFile = os.Getenv("KERMA_PRIVATE_KEY_FILE")
|
||||
}
|
||||
|
||||
c.PeerListStore = baseDir + "/" + c.PeerListStore
|
||||
c.BlockStore = baseDir + "/" + c.BlockStore
|
||||
c.TransactionStore = baseDir + "/" + c.TransactionStore
|
||||
c.PrivateKeyFile = baseDir + "/" + c.PrivateKeyFile
|
||||
c.ListenAddr = listenAddr + ":" + fmt.Sprint(c.Port)
|
||||
}
|
57
helpers/logger.go
Normal file
57
helpers/logger.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Package helpers provides useful helper structures for KermaGo
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
A Logger is a helper used for logging messages to stdout and stderr
|
||||
*/
|
||||
type Logger struct{}
|
||||
|
||||
/*
|
||||
Debug logs a debug message to stdout if the KERMA_DEBUG environment variable is set to true
|
||||
*/
|
||||
func (l *Logger) Debug(msg string) {
|
||||
if os.Getenv("KERMA_DEBUG") == "true" {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintln(os.Stdout, t.Format(time.RFC1123)+" [DEBUG] "+msg)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Info logs an info message to stdout
|
||||
*/
|
||||
func (l *Logger) Info(msg string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintln(os.Stdout, t.Format(time.RFC1123)+" [INFO] "+msg)
|
||||
}
|
||||
|
||||
/*
|
||||
Warn logs a warning message to stderr
|
||||
*/
|
||||
func (l *Logger) Warn(msg string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintln(os.Stderr, t.Format(time.RFC1123)+" [WARN] "+msg)
|
||||
}
|
||||
|
||||
/*
|
||||
Error logs an error message to stderr
|
||||
*/
|
||||
func (l *Logger) Error(msg string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintln(os.Stderr, t.Format(time.RFC1123)+" [ERROR] "+msg)
|
||||
}
|
||||
|
||||
/*
|
||||
Fatal logs a fatal message to stderr and terminates the application
|
||||
*/
|
||||
func (l *Logger) Fatal(msg string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintln(os.Stderr, t.Format(time.RFC1123)+" [FATAL] "+msg)
|
||||
os.Exit(1)
|
||||
}
|
91
helpers/tests/validator_test.go
Normal file
91
helpers/tests/validator_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"kerma/helpers"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
Convey("Given a new validator", t, func() {
|
||||
var validator helpers.Validator
|
||||
validator.Construct()
|
||||
|
||||
Convey("When the IsValidFQDN method is called", func() {
|
||||
Convey("When the input is an invalid FQDN, 'false' should be returned", func() {
|
||||
So(validator.IsValidFQDN("!!!$"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When the input is a valid FQDN, 'true' should be returned", func() {
|
||||
So(validator.IsValidFQDN("example.com"), ShouldEqual, true)
|
||||
So(validator.IsValidFQDN("sub.example.com"), ShouldEqual, true)
|
||||
So(validator.IsValidFQDN("sub1.example.com"), ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the IsValidIP method is called", func() {
|
||||
Convey("When the input is an invalid IP, 'false' should be returned", func() {
|
||||
So(validator.IsValidIP("!!!$"), ShouldEqual, false)
|
||||
So(validator.IsValidIP("example.com"), ShouldEqual, false)
|
||||
So(validator.IsValidIP("127.0.0.1"), ShouldEqual, false)
|
||||
So(validator.IsValidIP(validator.Config.IPAddress), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When the input is a valid FQDN, 'true' should be returned", func() {
|
||||
So(validator.IsValidIP("1.1.1.1"), ShouldEqual, true)
|
||||
So(validator.IsValidIP("172.16.2.1"), ShouldEqual, true)
|
||||
So(validator.IsValidIP("192.168.150.2"), ShouldEqual, true)
|
||||
So(validator.IsValidIP("10.11.231.2"), ShouldEqual, true)
|
||||
So(validator.IsValidIP("254.254.252.251"), ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the IsValidPort method is called", func() {
|
||||
Convey("When the input is an invalid port, 'false' should be returned", func() {
|
||||
So(validator.IsValidPort("-1"), ShouldEqual, false)
|
||||
So(validator.IsValidPort("123456"), ShouldEqual, false)
|
||||
So(validator.IsValidPort("0"), ShouldEqual, false)
|
||||
So(validator.IsValidPort("example.com"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When the input is a valid port, 'true' should be returned", func() {
|
||||
So(validator.IsValidPort("18018"), ShouldEqual, true)
|
||||
So(validator.IsValidPort("65535"), ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the IsValidPeerName method is called", func() {
|
||||
Convey("When the input is an invalid peer name, 'false' should be returned", func() {
|
||||
So(validator.IsValidPeerName("192.168.150.2:-1"), ShouldEqual, false)
|
||||
So(validator.IsValidPeerName("172.16.2.1:123456"), ShouldEqual, false)
|
||||
So(validator.IsValidPeerName("127.0.0.1:18018"), ShouldEqual, false)
|
||||
So(validator.IsValidPeerName("example.com:111111"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When the input is a valid peer name, 'true' should be returned", func() {
|
||||
So(validator.IsValidPeerName("example.com:18018"), ShouldEqual, true)
|
||||
So(validator.IsValidPeerName("sub.example.com:11111"), ShouldEqual, true)
|
||||
So(validator.IsValidPeerName("172.16.2.1:18018"), ShouldEqual, true)
|
||||
So(validator.IsValidPeerName("192.168.150.2:18018"), ShouldEqual, true)
|
||||
So(validator.IsValidPeerName("10.11.231.2:18018"), ShouldEqual, true)
|
||||
So(validator.IsValidPeerName("254.254.252.251:18018"), ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the CheckIfFileExists method is called", func() {
|
||||
Convey("When the input is a non-existent file, 'false' should be returned", func() {
|
||||
So(validator.CheckIfFileExists("nonexisting.test"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When the input is an existing file, 'true' should be returned", func() {
|
||||
buff := make([]byte, 100)
|
||||
ioutil.WriteFile("existing.test", buff, 0666)
|
||||
So(validator.CheckIfFileExists("existing.test"), ShouldEqual, true)
|
||||
os.Remove("existing.test")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
83
helpers/validator.go
Normal file
83
helpers/validator.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Package helpers provides useful helper structures for KermaGo
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
A Validator is a helper used to validate inputs based on a specified config
|
||||
*/
|
||||
type Validator struct {
|
||||
Config Config
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new validator
|
||||
*/
|
||||
func (v *Validator) Construct() {
|
||||
v.Config.Construct()
|
||||
}
|
||||
|
||||
/*
|
||||
IsValidFQDN checks if the string provided corresponds to a valid domain name
|
||||
*/
|
||||
func (v *Validator) IsValidFQDN(fqdn string) bool {
|
||||
// see: https://www.socketloop.com/tutorials/golang-use-regular-expression-to-validate-domain-name
|
||||
RegExp := regexp.MustCompile(`^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z
|
||||
]{2,3})$`)
|
||||
|
||||
return RegExp.MatchString(fqdn)
|
||||
}
|
||||
|
||||
/*
|
||||
IsValidPeerName checks if the string provided is a valid peer name. This includes valid domain + port combinations, valid IP + port combinations as well as only domains and IPs with default port 18018
|
||||
*/
|
||||
func (v *Validator) IsValidPeerName(peerName string) bool {
|
||||
addr := ""
|
||||
port := ""
|
||||
|
||||
if strings.Contains(peerName, ":") {
|
||||
buf := strings.Split(peerName, ":")
|
||||
addr = buf[0]
|
||||
port = buf[1]
|
||||
} else {
|
||||
addr = peerName
|
||||
port = "18018"
|
||||
}
|
||||
|
||||
if !v.IsValidIP(addr) {
|
||||
return v.IsValidFQDN(addr) && v.IsValidPort(port)
|
||||
}
|
||||
return v.IsValidPort(port)
|
||||
}
|
||||
|
||||
/*
|
||||
IsValidPort checks if the string provided is a port in the valid port range
|
||||
*/
|
||||
func (v *Validator) IsValidPort(port string) bool {
|
||||
portInt, err := strconv.ParseInt(port, 10, 32)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return portInt > 0 && portInt <= 65535
|
||||
}
|
||||
|
||||
/*
|
||||
IsValidIP checks if the string provided is a valid IP address. Valid IP addresses exclude 127.0.0.1 and the configured listen address
|
||||
*/
|
||||
func (v *Validator) IsValidIP(ip string) bool {
|
||||
return net.ParseIP(ip) != nil && ip != "127.0.0.1" && ip != v.Config.IPAddress
|
||||
}
|
||||
|
||||
/*
|
||||
CheckIfFileExists checks if the path specified is an existing file
|
||||
*/
|
||||
func (v *Validator) CheckIfFileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
88
main.go
Normal file
88
main.go
Normal file
@ -0,0 +1,88 @@
|
||||
// Package main is the main package of the Kerma node
|
||||
package main
|
||||
|
||||
import (
|
||||
"kerma/helpers"
|
||||
"kerma/models"
|
||||
"kerma/utils"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
var logger helpers.Logger
|
||||
|
||||
func main() {
|
||||
var config helpers.Config
|
||||
config.Construct()
|
||||
|
||||
logger.Debug("!!!!! NODE RUNNING IN DEBUG MODE !!!!!")
|
||||
|
||||
// Bind port
|
||||
logger.Info("Listening on " + config.ListenAddr)
|
||||
listener, err := net.Listen("tcp", config.ListenAddr)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("Failed binding port: " + err.Error())
|
||||
}
|
||||
|
||||
// Define our in-memory state
|
||||
var state models.State
|
||||
state.Construct()
|
||||
|
||||
// Close listener on exit
|
||||
defer listener.Close()
|
||||
|
||||
// Accept connections and handle requests
|
||||
logger.Info("Performing handshake with known peers")
|
||||
for {
|
||||
go initNode(&state)
|
||||
startServer(&state, listener)
|
||||
}
|
||||
}
|
||||
|
||||
func initNode(state *models.State) {
|
||||
for _, peer := range state.PeerList {
|
||||
if !state.CheckForHandshake(peer.Name) {
|
||||
timeout := state.Config.ConnTimeout * int(time.Second)
|
||||
d := net.Dialer{Timeout: time.Duration(timeout)}
|
||||
conn, err := d.Dial("tcp", peer.Name)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Failed connecting to peer: " + err.Error())
|
||||
state.RemovePeerByName(peer.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
var handler utils.TCPHandler
|
||||
var client utils.Client
|
||||
handler.Construct(conn)
|
||||
client.Construct(state, handler)
|
||||
|
||||
client.InitHandshake(peer.Name)
|
||||
client.DiscoverPeers(peer.Name)
|
||||
client.DiscoverChainTip(peer.Name)
|
||||
client.DiscoverMempool(peer.Name)
|
||||
|
||||
var server utils.Server
|
||||
server.Construct(state, handler)
|
||||
go server.Handle()
|
||||
go client.GossipTransactions(peer.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startServer(state *models.State, l net.Listener) {
|
||||
conn, err := l.Accept()
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("Failed listening on port: " + err.Error())
|
||||
}
|
||||
|
||||
var handler utils.TCPHandler
|
||||
var server utils.Server
|
||||
|
||||
handler.Construct(conn)
|
||||
server.Construct(state, handler)
|
||||
|
||||
go server.Handle()
|
||||
}
|
231
models/block.go
Normal file
231
models/block.go
Normal file
@ -0,0 +1,231 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/exp/utf8string"
|
||||
)
|
||||
|
||||
/*
|
||||
A Block is a struct that represents a block as per the Kerma specification
|
||||
*/
|
||||
type Block struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Txids []string `json:"txids" binding:"required"`
|
||||
Nonce string `json:"nonce" binding:"required"`
|
||||
Previd *string `json:"previd"`
|
||||
Created int64 `json:"created" binding:"required"`
|
||||
Target string `json:"T" binding:"required"`
|
||||
Miner string `json:"miner,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Height uint64 `json:"-"`
|
||||
UTXOSet map[string]uint64 `json:"-"`
|
||||
}
|
||||
|
||||
/*
|
||||
Hash returns the sha256 hash of the canonical JSON of the block
|
||||
*/
|
||||
func (b *Block) Hash() (string, error) {
|
||||
block, err := b.MarshalJson()
|
||||
if err != nil {
|
||||
return "", errors.New("could not parse block as json")
|
||||
}
|
||||
hashSum := sha256.Sum256(block)
|
||||
|
||||
return hex.EncodeToString(hashSum[:]), nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetID returns the block id
|
||||
*/
|
||||
func (b *Block) GetID() string {
|
||||
id, err := b.Hash()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
/*
|
||||
GetType returns the type of the block
|
||||
*/
|
||||
func (b *Block) GetType() string {
|
||||
return b.Type
|
||||
}
|
||||
|
||||
/*
|
||||
GetEntity returns the block
|
||||
*/
|
||||
func (b *Block) GetEntity() interface{} {
|
||||
return b
|
||||
}
|
||||
|
||||
/*
|
||||
Validate is responsible for validating the block; returns an error if unsuccessful
|
||||
*/
|
||||
func (b *Block) Validate() error {
|
||||
if b.Type != "block" {
|
||||
return errors.New("object not a block")
|
||||
}
|
||||
|
||||
if b.IsGenesisBlock() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.Previd == nil && !b.IsGenesisBlock() {
|
||||
return errors.New("illegal genesis block detected")
|
||||
}
|
||||
|
||||
if b.Target != GetGenesisBlock().Target {
|
||||
return errors.New("incorrect target supplied")
|
||||
}
|
||||
|
||||
if b.Created < GetGenesisBlock().Created {
|
||||
return errors.New("timestamp before genesis block")
|
||||
}
|
||||
|
||||
// TODO: check if this should be millis
|
||||
if b.Created > time.Now().UnixMilli() {
|
||||
return errors.New("block created in the future")
|
||||
}
|
||||
|
||||
if b.Miner != "" {
|
||||
if !utf8string.NewString(b.Miner).IsASCII() {
|
||||
return errors.New("miner is not an ascii printable string")
|
||||
}
|
||||
|
||||
if len(b.Miner) > 128 {
|
||||
return errors.New("miner length incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
if b.Note != "" {
|
||||
if !utf8string.NewString(b.Note).IsASCII() {
|
||||
return errors.New("note is not an ascii printable string")
|
||||
}
|
||||
|
||||
if len(b.Note) > 128 {
|
||||
return errors.New("note length incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
_, err := hex.DecodeString(b.Nonce)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = hex.DecodeString(*b.Previd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bid, err := b.Hash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bidBytes, _ := hex.DecodeString(bid)
|
||||
bint := binary.BigEndian.Uint64(bidBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tidBytes, _ := hex.DecodeString(b.Target)
|
||||
tid := binary.BigEndian.Uint64(tidBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bint >= tid {
|
||||
return errors.New("proof-of-work equation not satisfied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
String returns the canonical JSON of the block as a string
|
||||
*/
|
||||
func (b *Block) String() string {
|
||||
res, _ := b.MarshalJson()
|
||||
return string(res)
|
||||
}
|
||||
|
||||
/*
|
||||
ContainsTransaction checks if the block contains the specified transaction
|
||||
*/
|
||||
func (b *Block) ContainsTransaction(txid string) bool {
|
||||
return slices.Contains(b.Txids, txid)
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical JSON of the block
|
||||
*/
|
||||
func (b *Block) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(b)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON creates a block from the input JSON byte array
|
||||
*/
|
||||
func (b *Block) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp Block
|
||||
err := json.Unmarshal(data, (*tmp)(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
IsGenesisBlock checks if the block is the genesis block
|
||||
*/
|
||||
func (b *Block) IsGenesisBlock() bool {
|
||||
return b.String() == GetGenesisBlock().String()
|
||||
}
|
||||
|
||||
/*
|
||||
GetGenesisBlock returns the genesis block
|
||||
*/
|
||||
func GetGenesisBlock() *Block {
|
||||
genesis := Block{
|
||||
Type: "block",
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Created: 1624219079,
|
||||
Miner: "dionyziz",
|
||||
Nonce: "0000000000000000000000000000000000000000000000000000002634878840",
|
||||
Note: "The Economist 2021-06-20: Crypto-miners are probably to blame for the graphics-chip shortage",
|
||||
Previd: nil,
|
||||
Txids: []string{},
|
||||
Height: 0,
|
||||
UTXOSet: map[string]uint64{},
|
||||
}
|
||||
return &genesis
|
||||
}
|
||||
|
||||
/*
|
||||
GetBlockReward returns the block reward
|
||||
*/
|
||||
func GetBlockReward() uint64 {
|
||||
// 5 * (10^13)
|
||||
return 5 * uint64(math.Pow(10, 13))
|
||||
}
|
46
models/chaintip.go
Normal file
46
models/chaintip.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
A Chaintip is an object that has a type "chaintip" and a block ID
|
||||
*/
|
||||
type Chaintip struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
BlockID string `json:"blockid" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new Chaintip object for this instance
|
||||
*/
|
||||
func (c *Chaintip) Construct(bid string) {
|
||||
c.Type = "chaintip"
|
||||
c.BlockID = bid
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical json of the chaintip object
|
||||
*/
|
||||
func (c *Chaintip) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(c)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON returns a new Chaintip object from a byte array
|
||||
*/
|
||||
func (c *Chaintip) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp Chaintip
|
||||
err := json.Unmarshal(data, (*tmp)(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
27
models/error.go
Normal file
27
models/error.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import "github.com/docker/go/canonical/json"
|
||||
|
||||
/*
|
||||
An Error represents a struct that has a type and an error message as a string
|
||||
*/
|
||||
type Error struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Error string `json:"error" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new error object with the given string as message
|
||||
*/
|
||||
func (e *Error) Construct(errorMsg string) {
|
||||
e.Type = "error"
|
||||
e.Error = errorMsg
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical json of the error object
|
||||
*/
|
||||
func (e *Error) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(e)
|
||||
}
|
39
models/generic.go
Normal file
39
models/generic.go
Normal file
@ -0,0 +1,39 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import "github.com/docker/go/canonical/json"
|
||||
|
||||
/*
|
||||
A Generic represents a struct that only has a type
|
||||
*/
|
||||
type Generic struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical json of the generic object
|
||||
*/
|
||||
func (g *Generic) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(g)
|
||||
}
|
||||
|
||||
/*
|
||||
BuildPeerRequest creates a new generic object with type "getpeers"
|
||||
*/
|
||||
func (g *Generic) BuildPeerRequest() {
|
||||
g.Type = "getpeers"
|
||||
}
|
||||
|
||||
/*
|
||||
BuildChainTipRequest creates a new generic object with type "getchaintip"
|
||||
*/
|
||||
func (g *Generic) BuildChainTipRequest() {
|
||||
g.Type = "getchaintip"
|
||||
}
|
||||
|
||||
/*
|
||||
BuildMempoolRequest creates a new generic object with type "getmempool"
|
||||
*/
|
||||
func (g *Generic) BuildMempoolRequest() {
|
||||
g.Type = "getmempool"
|
||||
}
|
69
models/hello.go
Normal file
69
models/hello.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
A Hello represents a struct that has a type, a version and a user agent
|
||||
*/
|
||||
type Hello struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
Agent string `json:"agent"`
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new Hello object for this instance
|
||||
*/
|
||||
func (h *Hello) Construct() {
|
||||
h.Type = "hello"
|
||||
h.Version = "0.8.0"
|
||||
h.Agent = "BadKerma Go Client 0.8.x"
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical json of the hello object
|
||||
*/
|
||||
func (h *Hello) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(h)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON returns a new Hello request from a byte array
|
||||
*/
|
||||
func (h *Hello) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp Hello
|
||||
err := json.Unmarshal(data, (*tmp)(h))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !h.verify() {
|
||||
return errors.New("hello request not valid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hello) verify() bool {
|
||||
if h.Type == "hello" && h.isValidVersion() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Hello) isValidVersion() bool {
|
||||
RegExp := regexp.MustCompile(`^0\.8\.[0-9]$`)
|
||||
|
||||
return RegExp.MatchString(h.Version)
|
||||
}
|
29
models/interfaces.go
Normal file
29
models/interfaces.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
/*
|
||||
An IEntity is an interface that represents a Kerma entity - either a Block or a Transaction
|
||||
*/
|
||||
type IEntity interface {
|
||||
Hash() (string, error)
|
||||
GetType() string
|
||||
GetID() string
|
||||
GetEntity() interface{}
|
||||
Validate() error
|
||||
String() string
|
||||
}
|
||||
|
||||
/*
|
||||
An IObject is an interface that represents a Kerma object
|
||||
*/
|
||||
type IObject interface {
|
||||
GetObjectValue() (IEntity, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
/*
|
||||
A CustomMarshaler is an interface that provides custom marshaling functions
|
||||
*/
|
||||
type CustomMarshaler interface {
|
||||
MarshalJson() ([]byte, error)
|
||||
}
|
57
models/mempool.go
Normal file
57
models/mempool.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
/*
|
||||
A Mempool represents a struct that has a type, and a list of tx ids
|
||||
*/
|
||||
type Mempool struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Txids []string `json:"txids" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new Mempool object for this instance
|
||||
*/
|
||||
func (m *Mempool) Construct(mempool []string) {
|
||||
m.Type = "mempool"
|
||||
m.Txids = slices.Clone(mempool)
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical json of the mempool object
|
||||
*/
|
||||
func (m *Mempool) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(m)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON returns a new Mempool request from a byte array
|
||||
*/
|
||||
func (m *Mempool) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp Mempool
|
||||
err := json.Unmarshal(data, (*tmp)(m))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !m.verify() {
|
||||
return errors.New("mempool request not valid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mempool) verify() bool {
|
||||
return m.Type == "mempool"
|
||||
}
|
128
models/object.go
Normal file
128
models/object.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
An Object is a struct that represents a Kerma object. The object has a type and the object specification as a raw JSON
|
||||
*/
|
||||
type Object struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Object json.RawMessage `json:"object" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
An ObjectWrapper is a struct that wraps a Kerma object. The wrapper has a type and an object ID
|
||||
*/
|
||||
type ObjectWrapper struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
ObjectID string `json:"objectid" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
BuildObjectRequest creates a new ObjectWrapper with type "getobject" and the supplied Object ID
|
||||
*/
|
||||
func (o *ObjectWrapper) BuildObjectRequest(oid string) {
|
||||
o.Type = "getobject"
|
||||
o.ObjectID = oid
|
||||
}
|
||||
|
||||
/*
|
||||
BuildGossipObject creates a new ObjectWrapper with type "ihaveobject" and the supplied Object ID
|
||||
*/
|
||||
func (o *ObjectWrapper) BuildGossipObject(oid string) {
|
||||
o.Type = "ihaveobject"
|
||||
o.ObjectID = oid
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical JSON of the object wrapper
|
||||
*/
|
||||
func (o *ObjectWrapper) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(o)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON creates a new ObjectWrapper from the input JSON byte array
|
||||
*/
|
||||
func (o *ObjectWrapper) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp ObjectWrapper
|
||||
err := json.Unmarshal(data, (*tmp)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Type != "getobject" && o.Type != "ihaveobject" {
|
||||
return errors.New("inccorect type for object supplied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetObjectValue returns the value of the object as an IEntity
|
||||
*/
|
||||
func (o *Object) GetObjectValue() (IEntity, error) {
|
||||
msg, err := o.Object.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var transaction Transaction
|
||||
terr := transaction.UnmarshalJSON(msg)
|
||||
if terr == nil {
|
||||
return &transaction, nil
|
||||
}
|
||||
|
||||
var block Block
|
||||
berr := block.UnmarshalJSON(msg)
|
||||
if berr == nil {
|
||||
return &block, nil
|
||||
}
|
||||
|
||||
return nil, berr
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical JSON of the object
|
||||
*/
|
||||
func (o *Object) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(o)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON creates a new object from the input JSON byte array
|
||||
*/
|
||||
func (o *Object) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp Object
|
||||
err := json.Unmarshal(data, (*tmp)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Type != "object" {
|
||||
return errors.New("inccorect type for object supplied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
String returns the canonical JSON of the object as a string
|
||||
*/
|
||||
func (o *Object) String() string {
|
||||
res, _ := o.MarshalJson()
|
||||
return string(res)
|
||||
}
|
770
models/state.go
Normal file
770
models/state.go
Normal file
@ -0,0 +1,770 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"kerma/helpers"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
/*
|
||||
A State is an object that contains the current state of the application.
|
||||
State-related fields include:
|
||||
- PeerList - list of discovered peers
|
||||
- Chain - chain of known valid blocks
|
||||
- Transactions - hashmap of known valid transactions
|
||||
- PrivateKey - ED25519 private key of the Kerma node
|
||||
*/
|
||||
type State struct {
|
||||
PeerList []Peer
|
||||
Validator helpers.Validator
|
||||
Config helpers.Config
|
||||
Chain []Block
|
||||
Transactions map[string]*Transaction
|
||||
PrivateKey ed25519.PrivateKey
|
||||
Logger helpers.Logger
|
||||
Height uint64
|
||||
}
|
||||
|
||||
/*
|
||||
A Peer represents a peer on the Kerma network. It has a name and an indicator of whether there was a handshake
|
||||
*/
|
||||
type Peer struct {
|
||||
Name string
|
||||
Active bool
|
||||
}
|
||||
|
||||
/*
|
||||
A PeerListResponse represents an object with a type and a string array, containing the addresses of all known peers
|
||||
*/
|
||||
type PeerListResponse struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Peers []string `json:"peers"`
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new state object
|
||||
*/
|
||||
func (s *State) Construct() {
|
||||
s.PeerList = []Peer{}
|
||||
s.Validator.Construct()
|
||||
s.Config.Construct()
|
||||
|
||||
privKey, err := s.parsePKIFile()
|
||||
if err != nil {
|
||||
s.Logger.Warn(err.Error())
|
||||
s.Logger.Info("Generating new key...")
|
||||
_, privKey, _ = ed25519.GenerateKey(nil)
|
||||
}
|
||||
s.PrivateKey = privKey
|
||||
s.dumpPKIFile()
|
||||
|
||||
s.Logger.Info("Reading known peers from dump file")
|
||||
dumpErr := s.parsePeerListStore()
|
||||
|
||||
if dumpErr != nil {
|
||||
s.Logger.Warn(dumpErr.Error() + ", reverting to defaults...")
|
||||
s.parseInitialPeerList()
|
||||
}
|
||||
|
||||
s.Logger.Info("Restoring previous known state")
|
||||
s.Chain = []Block{}
|
||||
s.Transactions = map[string]*Transaction{}
|
||||
|
||||
terr := s.parseTransactionStore()
|
||||
if terr != nil {
|
||||
s.Logger.Debug(terr.Error())
|
||||
}
|
||||
|
||||
berr := s.parseBlockStore()
|
||||
if berr != nil {
|
||||
s.Logger.Debug(berr.Error())
|
||||
}
|
||||
|
||||
if len(s.Chain) == 0 {
|
||||
s.Chain = append(s.Chain, *GetGenesisBlock())
|
||||
s.Height = 0
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==============================
|
||||
|
||||
HANDSHAKE OPERATIONS
|
||||
|
||||
==============================
|
||||
*/
|
||||
|
||||
/*
|
||||
CheckForHandshake checks whether the node has completed a handshake with the specified peer
|
||||
*/
|
||||
func (s *State) CheckForHandshake(peerName string) bool {
|
||||
for _, peer := range s.PeerList {
|
||||
if peer.Name == peerName {
|
||||
return peer.Active
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
CompleteHandshake completes a handshake with the specified peer
|
||||
*/
|
||||
func (s *State) CompleteHandshake(peerName string) {
|
||||
peer := s.FindPeerByName(peerName)
|
||||
|
||||
if peer != nil {
|
||||
peer.Active = true
|
||||
} else {
|
||||
peer = &Peer{
|
||||
Name: peerName,
|
||||
Active: true,
|
||||
}
|
||||
s.PeerList = append(s.PeerList, *peer)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
======================
|
||||
|
||||
PEER OPERATIONS
|
||||
|
||||
======================
|
||||
*/
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical json of the PeerListResponse object
|
||||
*/
|
||||
func (p *PeerListResponse) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(p)
|
||||
}
|
||||
|
||||
/*
|
||||
FetchPeerListResponse returns a new PeerListResponse object from the local peer list
|
||||
*/
|
||||
func (s *State) FetchPeerListResponse() PeerListResponse {
|
||||
list := PeerListResponse{
|
||||
Type: "peers",
|
||||
Peers: []string{},
|
||||
}
|
||||
|
||||
for _, p := range s.PeerList {
|
||||
list.Peers = append(list.Peers, p.Name)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
/*
|
||||
FindPeerByName returns the specified peer from the peer list
|
||||
*/
|
||||
func (s *State) FindPeerByName(peerName string) *Peer {
|
||||
for p, peer := range s.PeerList {
|
||||
if peer.Name == peerName {
|
||||
return &s.PeerList[p]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
RemovePeerByName removes the specified peer from the peer list
|
||||
*/
|
||||
func (s *State) RemovePeerByName(peerName string) {
|
||||
for i, peer := range s.PeerList {
|
||||
if peer.Name == peerName {
|
||||
slices.Delete(s.PeerList, i, i)
|
||||
}
|
||||
}
|
||||
s.DumpPeerListStore()
|
||||
}
|
||||
|
||||
/*
|
||||
ParsePeerListResponse parses the given PeerListResponse object to the local peer list
|
||||
*/
|
||||
func (s *State) ParsePeerListResponse(p *PeerListResponse) {
|
||||
for _, peer := range p.Peers {
|
||||
if s.Validator.IsValidPeerName(peer) && s.FindPeerByName(peer) == nil {
|
||||
p := Peer{
|
||||
Name: peer,
|
||||
Active: false,
|
||||
}
|
||||
s.PeerList = append(s.PeerList, p)
|
||||
}
|
||||
}
|
||||
s.DumpPeerListStore()
|
||||
}
|
||||
|
||||
/*
|
||||
DumpPeerListStore stores the local peer list to a JSON file
|
||||
*/
|
||||
func (s *State) DumpPeerListStore() {
|
||||
list := []string{}
|
||||
for _, p := range s.PeerList {
|
||||
if s.Validator.IsValidPeerName(p.Name) {
|
||||
list = append(list, p.Name)
|
||||
}
|
||||
}
|
||||
file, _ := json.MarshalCanonical(list)
|
||||
_ = os.WriteFile(s.Config.PeerListStore, file, 0644)
|
||||
}
|
||||
|
||||
func (s *State) parsePeerListStore() error {
|
||||
if s.Validator.CheckIfFileExists(s.Config.PeerListStore) {
|
||||
file, err := os.ReadFile(s.Config.PeerListStore)
|
||||
if err != nil {
|
||||
return errors.New("cannot read peer list store " + s.Config.PeerListStore)
|
||||
}
|
||||
peerList := []string{}
|
||||
err = json.Unmarshal([]byte(file), &peerList)
|
||||
if err != nil {
|
||||
return errors.New("cannot parse peer list store at " + s.Config.PeerListStore)
|
||||
}
|
||||
for _, peer := range peerList {
|
||||
if s.Validator.IsValidPeerName(peer) {
|
||||
p := Peer{
|
||||
Name: peer,
|
||||
Active: false,
|
||||
}
|
||||
s.PeerList = append(s.PeerList, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot find peer list store at " + s.Config.PeerListStore)
|
||||
}
|
||||
|
||||
func (s *State) parseInitialPeerList() {
|
||||
s.PeerList = []Peer{}
|
||||
|
||||
if strings.Contains(s.Config.InitialPeerList, ",") {
|
||||
peers := strings.Split(s.Config.InitialPeerList, ",")
|
||||
for _, peerName := range peers {
|
||||
if s.Validator.IsValidPeerName(peerName) {
|
||||
p := Peer{
|
||||
Name: peerName,
|
||||
Active: false,
|
||||
}
|
||||
s.PeerList = append(s.PeerList, p)
|
||||
} else {
|
||||
s.Logger.Warn("Failed parsing the initial peer list. Please check your configuration")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if s.Validator.IsValidPeerName(s.Config.InitialPeerList) {
|
||||
p := Peer{
|
||||
Name: s.Config.InitialPeerList,
|
||||
Active: false,
|
||||
}
|
||||
s.PeerList = append(s.PeerList, p)
|
||||
} else {
|
||||
s.Logger.Warn("Failed parsing the initial peer list. Please check your configuration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
======================
|
||||
|
||||
PKI OPERATIONS
|
||||
|
||||
======================
|
||||
*/
|
||||
|
||||
func (s *State) parsePKIFile() (ed25519.PrivateKey, error) {
|
||||
privKeyBytes, err := os.ReadFile(s.Config.PrivateKeyFile)
|
||||
if err != nil {
|
||||
return nil, errors.New("cannot read private key file at " + s.Config.PrivateKeyFile)
|
||||
}
|
||||
|
||||
res, err := x509.ParsePKCS8PrivateKey(privKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.New("cannot parse private key from " + s.Config.PrivateKeyFile)
|
||||
}
|
||||
|
||||
privKey := res.(ed25519.PrivateKey)
|
||||
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func (s *State) dumpPKIFile() {
|
||||
privKeyBytes, _ := x509.MarshalPKCS8PrivateKey(s.PrivateKey)
|
||||
|
||||
_ = os.WriteFile(s.Config.PrivateKeyFile, privKeyBytes, 0600)
|
||||
}
|
||||
|
||||
/*
|
||||
======================
|
||||
|
||||
BLOCK OPERATIONS
|
||||
|
||||
======================
|
||||
*/
|
||||
|
||||
func (s *State) validateBlock(b *Block) error {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.IsGenesisBlock() {
|
||||
return nil
|
||||
}
|
||||
|
||||
bid := b.GetID()
|
||||
|
||||
prevBlock := s.Chain[len(s.Chain)-1]
|
||||
prevHash, err := prevBlock.Hash()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *b.Previd != prevHash || prevBlock.Created > b.Created {
|
||||
return errors.New("invalid chain extension, block invalid: " + bid)
|
||||
}
|
||||
|
||||
cbid := "invalid"
|
||||
for i, txid := range b.Txids {
|
||||
tx := s.Transactions[txid]
|
||||
if tx != nil {
|
||||
err = tx.Validate()
|
||||
if err != nil {
|
||||
return errors.New("detected invalid transaction, block invalid: " + bid)
|
||||
}
|
||||
|
||||
if tx.IsCoinbaseTx() {
|
||||
if i != 0 {
|
||||
return errors.New("invalid coinbase transaction, block invalid: " + bid)
|
||||
}
|
||||
|
||||
if tx.Outputs[0].Value > s.getTransactionFeesSum(b)+GetBlockReward() {
|
||||
return errors.New("law of conservation for coinbase transaction broken, block invalid: " + bid)
|
||||
}
|
||||
|
||||
txBlock := s.findBlockForTransaction(txid)
|
||||
if txBlock != nil && *tx.Height != txBlock.Height {
|
||||
return errors.New("incorrect height on coinbase transaction, block invalid: " + bid)
|
||||
}
|
||||
|
||||
cbid, _ = tx.Hash()
|
||||
|
||||
} else {
|
||||
if cbid != "invalid" && tx.IsTxInput(cbid) {
|
||||
return errors.New("coinbase transaction cannot be spent in same block, block invalid: " + bid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
AppendToChain adds the specified block to the chain
|
||||
*/
|
||||
func (s *State) AppendToChain(b *Block) error {
|
||||
if len(s.Chain) == 0 {
|
||||
s.Chain = append(s.Chain, *GetGenesisBlock())
|
||||
s.Height = 0
|
||||
}
|
||||
|
||||
bid, err := b.Hash()
|
||||
if err != nil {
|
||||
s.Logger.Debug(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if s.GetBlock(bid) != nil {
|
||||
s.Logger.Debug("Block already in chain: " + bid)
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Logger.Debug("Validating block: " + bid)
|
||||
err = s.validateBlock(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.Previd != nil {
|
||||
prevBlock := s.GetBlock(*b.Previd)
|
||||
b.Height = prevBlock.Height + 1
|
||||
} else {
|
||||
if b.IsGenesisBlock() {
|
||||
b.Height = GetGenesisBlock().Height
|
||||
} else {
|
||||
return errors.New("invalid genesis block")
|
||||
}
|
||||
}
|
||||
|
||||
if b.Height > s.Height {
|
||||
s.Logger.Info("New longest chain extension found: " + bid)
|
||||
s.Height = b.Height
|
||||
err = s.computeUTXOSet(b)
|
||||
|
||||
if err == nil {
|
||||
s.Chain = append(s.Chain, *b)
|
||||
for _, txid := range b.Txids {
|
||||
s.confirmTransaction(txid)
|
||||
}
|
||||
s.DumpTransactionStore()
|
||||
} else {
|
||||
s.Logger.Debug(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) computeUTXOSet(b *Block) error {
|
||||
if b.IsGenesisBlock() {
|
||||
b.UTXOSet = GetGenesisBlock().UTXOSet
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Logger.Debug("Computing UTXO set for block")
|
||||
prevBlock := s.GetBlock(*b.Previd)
|
||||
|
||||
b.UTXOSet = make(map[string]uint64)
|
||||
maps.Copy(prevBlock.UTXOSet, b.UTXOSet)
|
||||
|
||||
for _, txid := range b.Txids {
|
||||
s.Logger.Debug("Computing UTXO for transaction " + txid)
|
||||
transaction := s.GetTransaction(txid)
|
||||
err := transaction.Validate()
|
||||
|
||||
if err != nil {
|
||||
return errors.New("invalid utxo set for block " + b.GetID() + ", transaction " + txid)
|
||||
}
|
||||
|
||||
if transaction.IsCoinbaseTx() {
|
||||
output := transaction.Outputs[0]
|
||||
b.UTXOSet[output.Pubkey] = output.Value
|
||||
} else {
|
||||
for _, input := range transaction.Inputs {
|
||||
output := s.getOutputFromTransactionInput(&input)
|
||||
var balance uint64
|
||||
balance = output.Value
|
||||
if val, ok := b.UTXOSet[output.Pubkey]; ok {
|
||||
balance = val + output.Value
|
||||
}
|
||||
|
||||
for _, o := range transaction.Outputs {
|
||||
balance -= o.Value
|
||||
}
|
||||
|
||||
if balance > 0 {
|
||||
b.UTXOSet[output.Pubkey] = balance
|
||||
} else {
|
||||
delete(b.UTXOSet, output.Pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetBlock returns the block specified by the supplied ID or nil
|
||||
*/
|
||||
func (s *State) GetBlock(bid string) *Block {
|
||||
for _, b := range s.Chain {
|
||||
if bid == b.GetID() {
|
||||
return &b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetChainTip returns the tip of our local chain as an object
|
||||
*/
|
||||
func (s *State) GetChainTip() *Block {
|
||||
if len(s.Chain) == 0 {
|
||||
return GetGenesisBlock()
|
||||
}
|
||||
|
||||
return &s.Chain[len(s.Chain)-1]
|
||||
}
|
||||
|
||||
func (s *State) getTransactionFeesSum(b *Block) uint64 {
|
||||
var fees uint64
|
||||
fees = 0
|
||||
|
||||
for _, txid := range b.Txids {
|
||||
fees += s.getTransactionFees(txid)
|
||||
}
|
||||
|
||||
return fees
|
||||
}
|
||||
|
||||
/*
|
||||
GetMissingTransactionsInBlock returns the transaction ids from the block that are not present in the mempool
|
||||
*/
|
||||
func (s *State) GetMissingTransactionsInBlock(b *Block) ([]string, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
res := []string{}
|
||||
|
||||
for _, txid := range b.Txids {
|
||||
tx := s.Transactions[txid]
|
||||
if tx == nil {
|
||||
res = append(res, txid)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
/*
|
||||
DumpBlockStore stores the local block chain to a JSON file
|
||||
*/
|
||||
func (s *State) DumpBlockStore() {
|
||||
file, _ := json.MarshalCanonical(s.Chain)
|
||||
_ = os.WriteFile(s.Config.BlockStore, file, 0644)
|
||||
}
|
||||
|
||||
func (s *State) parseBlockStore() error {
|
||||
if s.Validator.CheckIfFileExists(s.Config.BlockStore) {
|
||||
file, err := os.ReadFile(s.Config.BlockStore)
|
||||
if err != nil {
|
||||
return errors.New("cannot read block store " + s.Config.BlockStore)
|
||||
}
|
||||
chain := []Block{}
|
||||
err = json.Unmarshal([]byte(file), &chain)
|
||||
if err != nil {
|
||||
return errors.New("cannot parse block store at " + s.Config.BlockStore)
|
||||
}
|
||||
|
||||
for i, block := range chain {
|
||||
if block.IsGenesisBlock() {
|
||||
continue
|
||||
}
|
||||
block.Height = uint64(i)
|
||||
s.AppendToChain(&block)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot find block store at " + s.Config.BlockStore)
|
||||
}
|
||||
|
||||
/*
|
||||
=============================
|
||||
|
||||
TRANSACTION OPERATIONS
|
||||
|
||||
=============================
|
||||
*/
|
||||
|
||||
func (s *State) validateTransaction(t *Transaction) error {
|
||||
txid, err := t.Hash()
|
||||
|
||||
if err != nil {
|
||||
s.Logger.Debug(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.Outputs) == 0 {
|
||||
return errors.New("outputs cannot be empty, transaction invalid: " + txid)
|
||||
}
|
||||
|
||||
if t.Height != nil {
|
||||
s.Logger.Debug("Processing a coinbase transaction: " + txid)
|
||||
} else {
|
||||
inputValues := 0
|
||||
outputValues := 0
|
||||
|
||||
if len(t.Inputs) == 0 {
|
||||
return errors.New("inputs cannot be empty in a non-coinbase transaction, transaction invalid: " + txid)
|
||||
}
|
||||
|
||||
for _, input := range t.Inputs {
|
||||
prevTransaction := s.Transactions[input.Outpoint.Txid]
|
||||
if prevTransaction == nil {
|
||||
return errors.New("could not find output transaction in pool, transaction invalid: " + txid)
|
||||
}
|
||||
|
||||
if len(prevTransaction.Outputs)-1 < int(input.Outpoint.Index) {
|
||||
return errors.New("could not find output specified by index in pool, transaction invalid: " + txid)
|
||||
}
|
||||
|
||||
outpoint := prevTransaction.Outputs[int(input.Outpoint.Index)]
|
||||
|
||||
if !t.VerifySign(input.Sig, outpoint.Pubkey) {
|
||||
return errors.New("could not validate input signatures, transaction invalid: " + txid)
|
||||
}
|
||||
|
||||
inputValues += int(outpoint.Value)
|
||||
}
|
||||
|
||||
for _, output := range t.Outputs {
|
||||
outputValues += int(output.Value)
|
||||
}
|
||||
|
||||
if inputValues != 0 && outputValues > inputValues {
|
||||
return errors.New("overspending, transaction invalid: " + txid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
AppendTransaction adds the specified transaction to the store
|
||||
*/
|
||||
func (s *State) AppendTransaction(t *Transaction) error {
|
||||
txid, err := t.Hash()
|
||||
if err != nil {
|
||||
s.Logger.Debug(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
s.Logger.Debug("Validating transaction: " + txid)
|
||||
|
||||
err = s.validateTransaction(t)
|
||||
if err != nil {
|
||||
s.Logger.Debug(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
s.Logger.Debug("Appending transaction to store: " + txid)
|
||||
|
||||
if t.IsCoinbaseTx() {
|
||||
t.Confirmed = true
|
||||
} else {
|
||||
t.Confirmed = false
|
||||
}
|
||||
|
||||
s.Transactions[txid] = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) findBlockForTransaction(txid string) *Block {
|
||||
for _, block := range s.Chain {
|
||||
if block.ContainsTransaction(txid) {
|
||||
return &block
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetTransaction returns the specified transaction from the store
|
||||
*/
|
||||
func (s *State) GetTransaction(txid string) *Transaction {
|
||||
if v, ok := s.Transactions[txid]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetMempoolTransactionIDs returns the list of transaction IDs in the mempool
|
||||
*/
|
||||
func (s *State) GetMempoolTransactionIDs() []string {
|
||||
res := []string{}
|
||||
|
||||
for txid, tx := range s.Transactions {
|
||||
if !tx.Confirmed {
|
||||
res = append(res, txid)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *State) getConfirmedTransactions() map[string]*Transaction {
|
||||
res := map[string]*Transaction{}
|
||||
|
||||
for txid, tx := range s.Transactions {
|
||||
if tx.Confirmed {
|
||||
res[txid] = tx
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *State) confirmTransaction(txid string) {
|
||||
tx := s.GetTransaction(txid)
|
||||
if tx != nil {
|
||||
tx.Confirmed = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) getTransactionFees(txid string) uint64 {
|
||||
var fees uint64
|
||||
fees = 0
|
||||
|
||||
transaction := s.GetTransaction(txid)
|
||||
|
||||
if transaction.IsCoinbaseTx() {
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, input := range transaction.Inputs {
|
||||
prevOutput := s.getOutputFromTransactionInput(&input)
|
||||
fees += prevOutput.Value
|
||||
}
|
||||
|
||||
for _, output := range transaction.Outputs {
|
||||
fees -= output.Value
|
||||
}
|
||||
|
||||
return fees
|
||||
}
|
||||
|
||||
func (s *State) getOutputFromTransactionInput(i *Input) *Output {
|
||||
txid := i.Outpoint.Txid
|
||||
index := i.Outpoint.Index
|
||||
|
||||
outputTx := s.GetTransaction(txid)
|
||||
for i, tx := range outputTx.Outputs {
|
||||
if int64(i) == index {
|
||||
return &tx
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
DumpTransactionStore stores the local store to a JSON file
|
||||
*/
|
||||
func (s *State) DumpTransactionStore() {
|
||||
file, _ := json.MarshalCanonical(s.getConfirmedTransactions())
|
||||
_ = os.WriteFile(s.Config.TransactionStore, file, 0644)
|
||||
}
|
||||
|
||||
func (s *State) parseTransactionStore() error {
|
||||
if s.Validator.CheckIfFileExists(s.Config.TransactionStore) {
|
||||
file, err := os.ReadFile(s.Config.TransactionStore)
|
||||
if err != nil {
|
||||
return errors.New("cannot read transaction store " + s.Config.TransactionStore)
|
||||
}
|
||||
var pool map[string]*Transaction
|
||||
|
||||
err = json.Unmarshal([]byte(file), &pool)
|
||||
if err != nil {
|
||||
return errors.New("cannot parse transaction store at " + s.Config.TransactionStore)
|
||||
}
|
||||
for id, transaction := range pool {
|
||||
s.Logger.Debug("Appending local transaction to store: " + id + ", tx: " + transaction.String())
|
||||
transaction.Confirmed = true
|
||||
s.Transactions[id] = transaction
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot find transaction store at " + s.Config.TransactionStore)
|
||||
}
|
292
models/tests/block_test.go
Normal file
292
models/tests/block_test.go
Normal file
@ -0,0 +1,292 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"kerma/models"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestBlock(t *testing.T) {
|
||||
Convey("Given a new block", t, func() {
|
||||
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
txid := "1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af"
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: 1649827795114,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "svatsan",
|
||||
Note: "First block. Yayy, I have 50 bu now!!",
|
||||
}
|
||||
|
||||
Convey("When GetType is called, 'block' should be returned", func() {
|
||||
So(block.GetType(), ShouldEqual, "block")
|
||||
})
|
||||
|
||||
Convey("When GetEntity is called, the block should be returned", func() {
|
||||
So(block.GetEntity(), ShouldResemble, &block)
|
||||
})
|
||||
|
||||
Convey("When Validate is called, no error should be returned", func() {
|
||||
So(block.Validate(), ShouldEqual, nil)
|
||||
})
|
||||
|
||||
Convey("When ContainsTransaction is called with a correct id, 'true' should be returned", func() {
|
||||
So(block.ContainsTransaction(txid), ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When ContainsTransaction is called with an incorrect id, 'false' should be returned", func() {
|
||||
So(block.ContainsTransaction("123"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When Hash is called, the hash of the cannonical json and no error should be returned", func() {
|
||||
blockJSON, _ := json.MarshalCanonical(block)
|
||||
hashSum := sha256.Sum256(blockJSON)
|
||||
|
||||
res, err := block.Hash()
|
||||
So(hex.EncodeToString(hashSum[:]), ShouldEqual, res)
|
||||
So(err, ShouldEqual, nil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given the genesis block", t, func() {
|
||||
block := models.GetGenesisBlock()
|
||||
gid := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
|
||||
Convey("When GetID is called, the block id should be correct", func() {
|
||||
bid := block.GetID()
|
||||
So(bid, ShouldEqual, gid)
|
||||
})
|
||||
|
||||
Convey("When Validate is called, no error should be returned", func() {
|
||||
So(block.Validate(), ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a new incorrect block", t, func() {
|
||||
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
txid := "1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af"
|
||||
|
||||
Convey("When the block is a genesis", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: nil,
|
||||
Created: 1649827795114,
|
||||
Target: "reallyillegalgenesisblock",
|
||||
Miner: "svatsan",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "illegal genesis block detected")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With an incorrect genesis", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: 1649827795114,
|
||||
Target: "reallyillegalgenesisblock",
|
||||
Miner: "svatsan",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "incorrect target supplied")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a timestamp before genesis", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: 0,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "svatsan",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "timestamp before genesis block")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a timestamp in the future", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: time.Now().UnixMilli() + 10000000,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "svatsan",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "block created in the future")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a miner that is a non-ASCII printible string", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: time.Now().UnixMilli(),
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "Not@nħCIIStrIng",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "miner is not an ascii printable string")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a miner that is more than 128 characters long", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: time.Now().UnixMilli(),
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongMinerName",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "miner length incorrect")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a note that is a non-ASCII printible string", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: time.Now().UnixMilli(),
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "",
|
||||
Note: "Not@nħCIIStrIng",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "note is not an ascii printable string")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a note that is more than 128 characters long", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: time.Now().UnixMilli(),
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "",
|
||||
Note: "LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongNote",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldEqual, "note length incorrect")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a non-hex encoded nonce", func() {
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "AReallyIncorrectNonce",
|
||||
Previd: &previd,
|
||||
Created: time.Now().UnixMilli(),
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a non-hex encoded previd", func() {
|
||||
incorrectPrevId := "incorrectprevid"
|
||||
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &incorrectPrevId,
|
||||
Created: time.Now().UnixMilli(),
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "",
|
||||
Note: "",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With an incorrect proof-of-work equation", func() {
|
||||
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
txid := "1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e37412"
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: 1649827795114,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "svatsan",
|
||||
Note: "First block. Yayy, I have 50 bu now!!",
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(block.Validate().Error(), ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
36
models/tests/chaintip_test.go
Normal file
36
models/tests/chaintip_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestChaintip(t *testing.T) {
|
||||
Convey("Given a new chaintip", t, func() {
|
||||
blockID := models.GetGenesisBlock().GetID()
|
||||
var chaintip models.Chaintip
|
||||
|
||||
Convey("When the constructor is called with a value", func() {
|
||||
chaintip.Construct(blockID)
|
||||
|
||||
Convey("The value should be set", func() {
|
||||
So(chaintip.Type, ShouldEqual, "chaintip")
|
||||
So(chaintip.BlockID, ShouldEqual, blockID)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the MarshalJson is called", func() {
|
||||
chaintip.Construct(blockID)
|
||||
|
||||
Convey("The canonical json and no error should be returned", func() {
|
||||
chaintipJSON, err := chaintip.MarshalJson()
|
||||
canonJSON := `{"blockid":"` + blockID + `","type":"chaintip"}`
|
||||
|
||||
So(string(chaintipJSON), ShouldEqualJSON, canonJSON)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
24
models/tests/error_test.go
Normal file
24
models/tests/error_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
Convey("Given a new error", t, func() {
|
||||
errString := "testing error"
|
||||
var err models.Error
|
||||
|
||||
Convey("When the constructor is called with a value", func() {
|
||||
err.Construct(errString)
|
||||
|
||||
Convey("The value should be set", func() {
|
||||
So(err.Type, ShouldEqual, "error")
|
||||
So(err.Error, ShouldEqual, errString)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
22
models/tests/generic_test.go
Normal file
22
models/tests/generic_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestGeneric(t *testing.T) {
|
||||
Convey("Given a new generic request", t, func() {
|
||||
var generic models.Generic
|
||||
|
||||
Convey("When the BuildPeerRequest method is called", func() {
|
||||
generic.BuildPeerRequest()
|
||||
|
||||
Convey("The type should be 'getpeers'", func() {
|
||||
So(generic.Type, ShouldEqual, "getpeers")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
36
models/tests/hello_test.go
Normal file
36
models/tests/hello_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
Convey("Given a new hello request", t, func() {
|
||||
var hello models.Hello
|
||||
|
||||
Convey("When the Construct method is called", func() {
|
||||
hello.Construct()
|
||||
|
||||
Convey("A correct hello request should be created", func() {
|
||||
So(hello.Type, ShouldEqual, "hello")
|
||||
So(hello.Version, ShouldEqual, "0.8.0")
|
||||
So(hello.Agent, ShouldEqual, "BadKerma Go Client 0.8.x")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When an invalid request is created", t, func() {
|
||||
var hello models.Hello
|
||||
|
||||
msgJSON := `{"type": "hello", "version": "0.9.0"}`
|
||||
|
||||
Convey("When UnmarshalJSON is called, it should return an error", func() {
|
||||
err := errors.New("hello request not valid")
|
||||
So(hello.UnmarshalJSON([]byte(msgJSON)), ShouldResemble, err)
|
||||
})
|
||||
})
|
||||
}
|
55
models/tests/mempool_test.go
Normal file
55
models/tests/mempool_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestMempool(t *testing.T) {
|
||||
Convey("Given a new regular transaction", t, func() {
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: 0,
|
||||
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
|
||||
},
|
||||
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
|
||||
},
|
||||
},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
txid := transaction.GetID()
|
||||
var mempool models.Mempool
|
||||
|
||||
Convey("When the constructor is called with a value", func() {
|
||||
mempool.Construct([]string{txid})
|
||||
|
||||
Convey("The value should be set", func() {
|
||||
So(mempool.Type, ShouldEqual, "mempool")
|
||||
So(mempool.Txids[0], ShouldEqual, txid)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the MarshalJson is called", func() {
|
||||
mempool.Construct([]string{txid})
|
||||
|
||||
Convey("The canonical json and no error should be returned", func() {
|
||||
mempoolJSON, err := mempool.MarshalJson()
|
||||
canonJSON := `{"txids":["` + txid + `"],"type":"mempool"}`
|
||||
|
||||
So(string(mempoolJSON), ShouldEqualJSON, canonJSON)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
147
models/tests/object_test.go
Normal file
147
models/tests/object_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestObject(t *testing.T) {
|
||||
Convey("Given a new object wrapper", t, func() {
|
||||
var wrapper models.ObjectWrapper
|
||||
toHash := "thisstringwillbehashed"
|
||||
hashSum := sha256.Sum256([]byte(toHash))
|
||||
oid := hex.EncodeToString(hashSum[:])
|
||||
|
||||
Convey("When the BuildObjectRequest method is called", func() {
|
||||
wrapper.BuildObjectRequest(oid)
|
||||
|
||||
Convey("The object wrapper should be a valid 'getobject' object", func() {
|
||||
So(wrapper.Type, ShouldEqual, "getobject")
|
||||
So(wrapper.ObjectID, ShouldEqual, oid)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the BuildGossipObject method is called", func() {
|
||||
wrapper.BuildGossipObject(oid)
|
||||
|
||||
Convey("The object wrapper should be a valid 'ihaveobject' object", func() {
|
||||
So(wrapper.Type, ShouldEqual, "ihaveobject")
|
||||
So(wrapper.ObjectID, ShouldEqual, oid)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a new object", t, func() {
|
||||
var object models.Object
|
||||
|
||||
Convey("When the object is a valid transaction", func() {
|
||||
raw := json.RawMessage(`
|
||||
{
|
||||
"height":1,
|
||||
"outputs":[
|
||||
{
|
||||
"pubkey":"62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
|
||||
"value":50000000000000
|
||||
}
|
||||
],
|
||||
"type":"transaction"
|
||||
}`)
|
||||
|
||||
height := uint64(1)
|
||||
|
||||
object = models.Object{
|
||||
Type: "object",
|
||||
Object: raw,
|
||||
}
|
||||
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
|
||||
Value: 50000000000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("Calling GetObjectValue should return the transaction and no error", func() {
|
||||
res, err := object.GetObjectValue()
|
||||
So(res, ShouldResemble, &transaction)
|
||||
So(err, ShouldEqual, nil)
|
||||
|
||||
Convey("Calling String should return the string representation of the transaction", func() {
|
||||
So(res.String(), ShouldEqual, transaction.String())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the object is a valid block", func() {
|
||||
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
raw := json.RawMessage(`
|
||||
{
|
||||
"type": "block",
|
||||
"txids": [
|
||||
"1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af"
|
||||
],
|
||||
"nonce": "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
"previd": "` + previd + `",
|
||||
"created": 1649827795114,
|
||||
"T": "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
"miner": "svatsan",
|
||||
"note": "First block. Yayy, I have 50 bu now!!"
|
||||
}`)
|
||||
|
||||
object = models.Object{
|
||||
Type: "object",
|
||||
Object: raw,
|
||||
}
|
||||
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
"1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af",
|
||||
},
|
||||
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
|
||||
Previd: &previd,
|
||||
Created: 1649827795114,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "svatsan",
|
||||
Note: "First block. Yayy, I have 50 bu now!!",
|
||||
}
|
||||
|
||||
Convey("Calling GetObjectValue should return the block and no error", func() {
|
||||
res, err := object.GetObjectValue()
|
||||
So(err, ShouldEqual, nil)
|
||||
So(res, ShouldResemble, &block)
|
||||
|
||||
Convey("Calling String should return the string representation of the block", func() {
|
||||
So(res.String(), ShouldEqual, block.String())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the object is a random object", func() {
|
||||
raw := json.RawMessage(`
|
||||
{
|
||||
"key": "value"
|
||||
}`)
|
||||
|
||||
object = models.Object{
|
||||
Type: "object",
|
||||
Object: raw,
|
||||
}
|
||||
|
||||
Convey("Calling GetObjectValue should return a nil object and an error", func() {
|
||||
res, err := object.GetObjectValue()
|
||||
So(res, ShouldEqual, nil)
|
||||
So(err, ShouldNotEqual, nil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
421
models/tests/state_test.go
Normal file
421
models/tests/state_test.go
Normal file
@ -0,0 +1,421 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"kerma/helpers"
|
||||
"kerma/models"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestState(t *testing.T) {
|
||||
// Prepare
|
||||
testDir := t.TempDir()
|
||||
t.Setenv("KERMA_STORE_BASE_DIR", testDir)
|
||||
t.Setenv("KERMA_INITIAL_PEER_LIST", "example.com:18018")
|
||||
|
||||
var validator helpers.Validator
|
||||
var config helpers.Config
|
||||
|
||||
validator.Construct()
|
||||
config.Construct()
|
||||
|
||||
peerList := []models.Peer{
|
||||
{
|
||||
Name: "example.com:18018",
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
Convey("Given a new state object", t, func() {
|
||||
var state models.State
|
||||
|
||||
Convey("When the Construct method is called", func() {
|
||||
state.Construct()
|
||||
|
||||
Convey("A valid object should be created", func() {
|
||||
So(state.Validator, ShouldResemble, validator)
|
||||
So(state.Config, ShouldResemble, config)
|
||||
So(state.PeerList, ShouldResemble, peerList)
|
||||
So(state.Chain, ShouldResemble, []models.Block{*models.GetGenesisBlock()})
|
||||
So(state.Transactions, ShouldResemble, map[string]*models.Transaction{})
|
||||
})
|
||||
})
|
||||
|
||||
state.Construct()
|
||||
Convey("When the CompleteHandshake method is called", func() {
|
||||
Convey("When the peer is already in the list, it should be marked as active", func() {
|
||||
state.CompleteHandshake("example.com:18018")
|
||||
So(state.PeerList[0].Active, ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When the peer is not in the list, it should be created and marked as active", func() {
|
||||
state.CompleteHandshake("valid.host:18018")
|
||||
So(state.PeerList[1].Name, ShouldEqual, "valid.host:18018")
|
||||
So(state.PeerList[1].Active, ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the CheckForHandshake method is called", func() {
|
||||
Convey("When there is a finished handshake, 'true' should be returned", func() {
|
||||
state.PeerList[0].Active = true
|
||||
So(state.CheckForHandshake("example.com:18018"), ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When there is no finished handshake, 'false' should be returned", func() {
|
||||
So(state.CheckForHandshake("missing.host:18018"), ShouldEqual, false)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the FetchPeerListResponse method is called, the list of peers should be returned", func() {
|
||||
res := state.FetchPeerListResponse()
|
||||
|
||||
peerListResponse := models.PeerListResponse{
|
||||
Type: "peers",
|
||||
Peers: []string{
|
||||
"example.com:18018",
|
||||
},
|
||||
}
|
||||
|
||||
So(res, ShouldResemble, peerListResponse)
|
||||
})
|
||||
|
||||
Convey("When the FindPeerByName method is called", func() {
|
||||
Convey("When the peer exists, it should be returned", func() {
|
||||
res := state.FindPeerByName("example.com:18018")
|
||||
So(res, ShouldEqual, &state.PeerList[0])
|
||||
})
|
||||
|
||||
Convey("When the peer does not exist, nil should be returned", func() {
|
||||
res := state.FindPeerByName("example1.com:18018")
|
||||
So(res, ShouldEqual, nil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the RemovePeerByName method is called", func() {
|
||||
SkipConvey("When the peer exists, it should be removed", func() {
|
||||
prevLen := len(state.PeerList)
|
||||
state.RemovePeerByName("example.com:18018")
|
||||
So(len(state.PeerList), ShouldEqual, prevLen-1)
|
||||
})
|
||||
|
||||
Convey("When the peer does not exist, nothing should be done", func() {
|
||||
prevLen := len(state.PeerList)
|
||||
state.RemovePeerByName("example1.com:18018")
|
||||
So(len(state.PeerList), ShouldEqual, prevLen)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the ParsePeerListResponse method is called", func() {
|
||||
Convey("With a non-empty input, all new peers should be added to the peer list", func() {
|
||||
peerListResponse := models.PeerListResponse{
|
||||
Type: "peers",
|
||||
Peers: []string{
|
||||
"sub.example.com:18018",
|
||||
"sub1.example.com:18018",
|
||||
},
|
||||
}
|
||||
|
||||
state.ParsePeerListResponse(&peerListResponse)
|
||||
So(state.FindPeerByName("sub.example.com:18018"), ShouldResemble, &models.Peer{Name: "sub.example.com:18018", Active: false})
|
||||
So(state.FindPeerByName("sub1.example.com:18018"), ShouldResemble, &models.Peer{Name: "sub1.example.com:18018", Active: false})
|
||||
})
|
||||
|
||||
Convey("With an empty input, no new peers should be added to the peer list", func() {
|
||||
peerListResponse := models.PeerListResponse{
|
||||
Type: "peers",
|
||||
Peers: []string{},
|
||||
}
|
||||
|
||||
prevLen := len(state.PeerList)
|
||||
state.ParsePeerListResponse(&peerListResponse)
|
||||
So(len(state.PeerList), ShouldEqual, prevLen)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When the DumpPeerListStore method is called, the peer list should be saved to the peer list store file", func() {
|
||||
state.DumpPeerListStore()
|
||||
|
||||
res, oserr := os.Stat(state.Config.PeerListStore)
|
||||
|
||||
file, _ := os.ReadFile(state.Config.PeerListStore)
|
||||
peerList := []string{}
|
||||
err := json.Unmarshal([]byte(file), &peerList)
|
||||
|
||||
So(oserr, ShouldBeNil)
|
||||
So(res.Size(), ShouldNotEqual, 0)
|
||||
So(res.IsDir(), ShouldBeFalse)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(peerList), ShouldEqual, 3)
|
||||
So(peerList[0], ShouldEqual, "example.com:18018")
|
||||
})
|
||||
|
||||
Convey("Given a correct block", func() {
|
||||
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
txid := "2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df"
|
||||
height := uint64(1)
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60",
|
||||
Value: 50000000000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
state.Transactions[txid] = &transaction
|
||||
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "000000000000000000000000000000000000000000000000000000009d8b60ea",
|
||||
Previd: &previd,
|
||||
Created: 1624220079,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "Snekel testminer",
|
||||
Note: "First block after genesis with CBTX",
|
||||
}
|
||||
|
||||
Convey("When AppendToChain is called", func() {
|
||||
err := state.AppendToChain(&block)
|
||||
|
||||
Convey("No error should be returned", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When GetChainTip is called, the new block should be returned", func() {
|
||||
So(state.GetChainTip(), ShouldResemble, &block)
|
||||
})
|
||||
|
||||
Convey("When GetBlock is called, the new block should be returned", func() {
|
||||
So(state.GetBlock(block.GetID()), ShouldResemble, &block)
|
||||
})
|
||||
|
||||
Convey("When GetMissingTransactionsInBlock is called, an empty array and no error should be returned", func() {
|
||||
res, err := state.GetMissingTransactionsInBlock(&block)
|
||||
So(res, ShouldBeEmpty)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When the DumpBlockStore method is called, the blockchain should be saved to the blockchain store file", func() {
|
||||
state.DumpBlockStore()
|
||||
|
||||
res, oserr := os.Stat(state.Config.BlockStore)
|
||||
|
||||
file, _ := os.ReadFile(state.Config.BlockStore)
|
||||
chain := []models.Block{}
|
||||
err := json.Unmarshal([]byte(file), &chain)
|
||||
|
||||
// Set UTXO of both chain blocks and Height, as they are always recalculated
|
||||
genesis := models.GetGenesisBlock()
|
||||
genesis.UTXOSet = map[string]uint64(nil)
|
||||
block.UTXOSet = map[string]uint64(nil)
|
||||
block.Height = 0
|
||||
|
||||
So(oserr, ShouldBeNil)
|
||||
So(res.Size(), ShouldNotEqual, 0)
|
||||
So(res.IsDir(), ShouldBeFalse)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(chain), ShouldEqual, 2)
|
||||
So(&chain[0], ShouldResemble, genesis)
|
||||
So(&chain[1], ShouldResemble, &block)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an incorrect block", func() {
|
||||
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
|
||||
txid := "2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df"
|
||||
height := uint64(1)
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60",
|
||||
Value: 50000000000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
state.Transactions[txid] = &transaction
|
||||
|
||||
block := models.Block{
|
||||
Type: "block",
|
||||
Txids: []string{
|
||||
txid,
|
||||
},
|
||||
Nonce: "000000000000000000000000000000000000000000000000000000009d8b60e2",
|
||||
Previd: &previd,
|
||||
Created: 1624220079,
|
||||
Target: "00000002af000000000000000000000000000000000000000000000000000000",
|
||||
Miner: "Snekel testminer",
|
||||
Note: "First block after genesis with CBTX",
|
||||
}
|
||||
|
||||
Convey("When AppendToChain is called", func() {
|
||||
err := state.AppendToChain(&block)
|
||||
|
||||
Convey("An error should be returned", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a valid coinbase transaction", func() {
|
||||
height := uint64(1)
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
|
||||
Value: 50000000000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When AppendTransaction is called", func() {
|
||||
err := state.AppendTransaction(&transaction)
|
||||
|
||||
Convey("No error should be returned", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("GetTransaction should return the transaction", func() {
|
||||
So(state.GetTransaction(transaction.GetID()), ShouldResemble, &transaction)
|
||||
})
|
||||
|
||||
Convey("GetMempoolTransactionIDs should return an empty array", func() {
|
||||
So(state.GetMempoolTransactionIDs(), ShouldBeEmpty)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an invalid coinbase transaction", func() {
|
||||
height := uint64(1)
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Outputs: []models.Output{},
|
||||
}
|
||||
|
||||
Convey("When AppendTransaction is called", func() {
|
||||
err := state.AppendTransaction(&transaction)
|
||||
|
||||
Convey("An error should be returned", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetMempoolTransactionIDs should return an empty array", func() {
|
||||
So(state.GetMempoolTransactionIDs(), ShouldBeEmpty)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a valid regular transaction", func() {
|
||||
height := uint64(1)
|
||||
coinbase := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Confirmed: true,
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60",
|
||||
Value: 50000000000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
state.Transactions[coinbase.GetID()] = &coinbase
|
||||
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: 0,
|
||||
Txid: "2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df",
|
||||
},
|
||||
Sig: "49cc4f9a1fb9d600a7debc99150e7909274c8c74edd7ca183626dfe49eb4aa21c6ff0e4c5f0dc2a328ad6b8ba10bf7169d5f42993a94bf67e13afa943b749c0b",
|
||||
},
|
||||
},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "c7c2c13afd02be7986dee0f4630df01abdbc950ea379055f1a423a6090f1b2b3",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When AppendTransaction is called", func() {
|
||||
err := state.AppendTransaction(&transaction)
|
||||
|
||||
Convey("No error should be returned", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("GetTransaction should return the transaction", func() {
|
||||
So(state.GetTransaction(transaction.GetID()), ShouldResemble, &transaction)
|
||||
})
|
||||
|
||||
Convey("GetMempoolTransactionIDs should return an array with the transaction", func() {
|
||||
So(state.GetMempoolTransactionIDs(), ShouldNotBeEmpty)
|
||||
So(len(state.GetMempoolTransactionIDs()), ShouldEqual, 1)
|
||||
So(state.GetMempoolTransactionIDs()[0], ShouldEqual, transaction.GetID())
|
||||
})
|
||||
|
||||
Convey("When the DumpTransactionStore method is called, the confirmed transactions should be saved to the store file", func() {
|
||||
state.DumpTransactionStore()
|
||||
|
||||
res, oserr := os.Stat(state.Config.TransactionStore)
|
||||
|
||||
file, _ := os.ReadFile(state.Config.TransactionStore)
|
||||
store := map[string]*models.Transaction{}
|
||||
err := json.Unmarshal([]byte(file), &store)
|
||||
|
||||
// Set confirmed to false, as this is set in the parse function
|
||||
coinbase.Confirmed = false
|
||||
|
||||
So(oserr, ShouldBeNil)
|
||||
So(res.Size(), ShouldNotEqual, 0)
|
||||
So(res.IsDir(), ShouldBeFalse)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(store), ShouldEqual, 1)
|
||||
So(store[coinbase.GetID()], ShouldResemble, &coinbase)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an invalid regular transaction", func() {
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "c7c2c13afd02be7986dee0f4630df01abdbc950ea379055f1a423a6090f1b2b3",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When AppendTransaction is called", func() {
|
||||
err := state.AppendTransaction(&transaction)
|
||||
|
||||
Convey("An error should be returned", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("GetMempoolTransactionIDs should return an empty array", func() {
|
||||
So(state.GetMempoolTransactionIDs(), ShouldBeEmpty)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
202
models/tests/transaction_test.go
Normal file
202
models/tests/transaction_test.go
Normal file
@ -0,0 +1,202 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"kerma/models"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestTransaction(t *testing.T) {
|
||||
Convey("Given a new coinbase transaction", t, func() {
|
||||
height := uint64(1)
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Height: &height,
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
|
||||
Value: 50000000000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When GetType is called, 'transaction' should be returned", func() {
|
||||
So(transaction.GetType(), ShouldEqual, "transaction")
|
||||
})
|
||||
|
||||
Convey("When GetEntity is called, the transaction should be returned", func() {
|
||||
So(transaction.GetEntity(), ShouldResemble, &transaction)
|
||||
})
|
||||
|
||||
Convey("When Validate is called, no error should be returned", func() {
|
||||
So(transaction.Validate(), ShouldEqual, nil)
|
||||
})
|
||||
|
||||
Convey("When IsCoinbaseTx is called, 'true' should be returned", func() {
|
||||
So(transaction.IsCoinbaseTx(), ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When IsTxInput is called, 'false' should be returned", func() {
|
||||
So(transaction.IsTxInput("dasdfasfds"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When Hash is called, the hash of the cannonical json and no error should be returned", func() {
|
||||
transactionJSON, _ := json.MarshalCanonical(transaction)
|
||||
hashSum := sha256.Sum256(transactionJSON)
|
||||
|
||||
res, err := transaction.Hash()
|
||||
So(hex.EncodeToString(hashSum[:]), ShouldEqual, res)
|
||||
So(err, ShouldEqual, nil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a new regular transaction", t, func() {
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: 0,
|
||||
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
|
||||
},
|
||||
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
|
||||
},
|
||||
},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When GetType is called, 'transaction' should be returned", func() {
|
||||
So(transaction.GetType(), ShouldEqual, "transaction")
|
||||
})
|
||||
|
||||
Convey("When GetEntity is called, the transaction should be returned", func() {
|
||||
So(transaction.GetEntity(), ShouldResemble, &transaction)
|
||||
})
|
||||
|
||||
Convey("When Validate is called, no error should be returned", func() {
|
||||
So(transaction.Validate(), ShouldEqual, nil)
|
||||
})
|
||||
|
||||
Convey("When IsCoinbaseTx is called, 'false' should be returned", func() {
|
||||
So(transaction.IsCoinbaseTx(), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When IsTxInput is called with an input, 'true' should be returned", func() {
|
||||
So(transaction.IsTxInput("2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d"), ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When IsTxInput is called with a non-input, 'false' should be returned", func() {
|
||||
So(transaction.IsTxInput("dasdfasfds"), ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("When Hash is called, the hash of the cannonical json and no error should be returned", func() {
|
||||
transactionJSON, _ := json.MarshalCanonical(transaction)
|
||||
hashSum := sha256.Sum256(transactionJSON)
|
||||
|
||||
res, err := transaction.Hash()
|
||||
So(hex.EncodeToString(hashSum[:]), ShouldEqual, res)
|
||||
So(err, ShouldEqual, nil)
|
||||
})
|
||||
|
||||
Convey("When VerifySign is called with a correct signature, 'true' should be returned", func() {
|
||||
So(transaction.VerifySign("1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909", "57558a6dae91ac3ab8caf3f543eac9c51cba4ac680ba5ba0d81b5575dc06bc46"), ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When VerifySign is called with an incorrect signature, 'false' should be returned", func() {
|
||||
So(transaction.VerifySign("", ""), ShouldEqual, false)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a new incorrect regular transaction", t, func() {
|
||||
Convey("With two inputs pointing to the same outpoint", func() {
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: 0,
|
||||
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
|
||||
},
|
||||
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
|
||||
},
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: 0,
|
||||
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
|
||||
},
|
||||
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
|
||||
},
|
||||
},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(transaction.Validate().Error(), ShouldEqual, "multiple transaction inputs with same outpoint found")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With negative outpoint index value", func() {
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: -10,
|
||||
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
|
||||
},
|
||||
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
|
||||
},
|
||||
},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(transaction.Validate().Error(), ShouldEqual, "transaction input index cannot be negative")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a pubkey that is not hex string", func() {
|
||||
transaction := models.Transaction{
|
||||
Type: "transaction",
|
||||
Inputs: []models.Input{
|
||||
{
|
||||
Outpoint: models.Outpoint{
|
||||
Index: 0,
|
||||
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
|
||||
},
|
||||
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
|
||||
},
|
||||
},
|
||||
Outputs: []models.Output{
|
||||
{
|
||||
Pubkey: "VeryInvalidPubkeyValueWOWThisIsSoInvalidOhMyGod",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Convey("When Validate is called, an error should be returned", func() {
|
||||
So(transaction.Validate(), ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
251
models/transaction.go
Normal file
251
models/transaction.go
Normal file
@ -0,0 +1,251 @@
|
||||
// Package models provides the models for the KermaGo application
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"kerma/helpers"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
An Outpoint is a struct that represents an outpoint as per the Kerma specification
|
||||
*/
|
||||
type Outpoint struct {
|
||||
Txid string `json:"txid" binding:"required"`
|
||||
Index int64 `json:"index" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
An Input is a struct that represents an input as per the Kerma specification
|
||||
*/
|
||||
type Input struct {
|
||||
Outpoint Outpoint `json:"outpoint" binding:"required"`
|
||||
Sig string `json:"sig" binding:"required"`
|
||||
}
|
||||
|
||||
type inputVerifier struct {
|
||||
Outpoint Outpoint `json:"outpoint" binding:"required"`
|
||||
Sig *string `json:"sig" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
An Output is a struct that represents an output as per the Kerma specification
|
||||
*/
|
||||
type Output struct {
|
||||
Pubkey string `json:"pubkey" binding:"required"`
|
||||
Value uint64 `json:"value" binding:"required"`
|
||||
}
|
||||
|
||||
/*
|
||||
A Transaction is a struct that represents a transaction as per the Kerma specification
|
||||
*/
|
||||
type Transaction struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Height *uint64 `json:"height,omitempty"`
|
||||
Inputs []Input `json:"inputs,omitempty"`
|
||||
Outputs []Output `json:"outputs,omitempty"`
|
||||
Confirmed bool `json:"-"`
|
||||
}
|
||||
|
||||
type transactionVerifier struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Inputs []inputVerifier `json:"inputs,omitempty"`
|
||||
Outputs []Output `json:"outputs,omitempty"`
|
||||
}
|
||||
|
||||
/*
|
||||
Hash returns the sha256 hash of the canonical JSON of the transaction
|
||||
*/
|
||||
func (t *Transaction) Hash() (string, error) {
|
||||
transaction, err := t.MarshalJson()
|
||||
if err != nil {
|
||||
return "", errors.New("could not parse transaction as json")
|
||||
}
|
||||
hashSum := sha256.Sum256(transaction)
|
||||
|
||||
return hex.EncodeToString(hashSum[:]), nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetID returns the transaction id
|
||||
*/
|
||||
func (b *Transaction) GetID() string {
|
||||
id, err := b.Hash()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
/*
|
||||
GetType returns the type of the transaction
|
||||
*/
|
||||
func (t *Transaction) GetType() string {
|
||||
return t.Type
|
||||
}
|
||||
|
||||
/*
|
||||
GetEntity returns the transaction
|
||||
*/
|
||||
func (t *Transaction) GetEntity() interface{} {
|
||||
return t
|
||||
}
|
||||
|
||||
/*
|
||||
MarshalJson returns the canonical JSON of the transaction
|
||||
*/
|
||||
func (t *Transaction) MarshalJson() ([]byte, error) {
|
||||
return json.MarshalCanonical(t)
|
||||
}
|
||||
|
||||
/*
|
||||
UnmarshalJSON creates a transaction from the input JSON byte array
|
||||
*/
|
||||
func (t *Transaction) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type tmp Transaction
|
||||
err := json.Unmarshal(data, (*tmp)(t))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Validate is responsible for validating the transaction; returns an error if unsuccessful
|
||||
*/
|
||||
func (t *Transaction) Validate() error {
|
||||
if t.Type != "transaction" {
|
||||
return errors.New("object not a transaction")
|
||||
}
|
||||
|
||||
if t.IsCoinbaseTx() {
|
||||
if len(t.Inputs) > 0 {
|
||||
return errors.New("coinbase transactions cannot have an input")
|
||||
}
|
||||
|
||||
if len(t.Outputs) != 1 {
|
||||
return errors.New("coinbase transactions must have exactly one output")
|
||||
}
|
||||
}
|
||||
|
||||
txOutpoints := map[string]int{}
|
||||
|
||||
for _, input := range t.Inputs {
|
||||
if input.Outpoint.Index < 0 {
|
||||
return errors.New("transaction input index cannot be negative")
|
||||
}
|
||||
|
||||
outpointId := input.Outpoint.Txid + "-" + strconv.FormatInt(input.Outpoint.Index, 10)
|
||||
if _, ok := txOutpoints[outpointId]; ok {
|
||||
return errors.New("multiple transaction inputs with same outpoint found")
|
||||
}
|
||||
|
||||
txOutpoints[outpointId] = 1
|
||||
}
|
||||
|
||||
for _, output := range t.Outputs {
|
||||
if output.Value < 0 {
|
||||
return errors.New("transaction output value cannot be negative")
|
||||
}
|
||||
|
||||
pubkeyBytes, err := hex.DecodeString(output.Pubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ed25519.PublicKey(string(pubkeyBytes)) == nil {
|
||||
return errors.New("invalid public key value, transaction invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
String returns the canonical JSON of the transaction as a string
|
||||
*/
|
||||
func (t *Transaction) String() string {
|
||||
res, _ := t.MarshalJson()
|
||||
return string(res)
|
||||
}
|
||||
|
||||
/*
|
||||
VerifySign verifies whether the signature of the transaction with the provided pubkey matches the signature provided
|
||||
*/
|
||||
func (t *Transaction) VerifySign(sig string, pubkeyString string) bool {
|
||||
var logger helpers.Logger
|
||||
|
||||
newTransaction := transactionVerifier{
|
||||
Type: t.Type,
|
||||
Inputs: []inputVerifier{},
|
||||
Outputs: t.Outputs,
|
||||
}
|
||||
|
||||
for _, i := range t.Inputs {
|
||||
newInput := inputVerifier{
|
||||
Outpoint: i.Outpoint,
|
||||
Sig: nil,
|
||||
}
|
||||
|
||||
newTransaction.Inputs = append(newTransaction.Inputs, newInput)
|
||||
}
|
||||
|
||||
txBytes := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(txBytes)
|
||||
encoder.Canonical()
|
||||
encoder.Encode(newTransaction)
|
||||
|
||||
pubkeyBytes, err := hex.DecodeString(pubkeyString)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
sigBytes, err := hex.DecodeString(sig)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
logger.Debug("Checking transaction signature with params: SIGNATURE(" + sig + "), PUBKEY(" + pubkeyString + "), TRANSACTION(" + txBytes.String() + ")")
|
||||
|
||||
pubkey := ed25519.PublicKey(string(pubkeyBytes))
|
||||
if len(pubkey) != ed25519.PublicKeySize {
|
||||
return false
|
||||
}
|
||||
|
||||
return ed25519.Verify(ed25519.PublicKey(pubkey), txBytes.Bytes(), sigBytes)
|
||||
}
|
||||
|
||||
/*
|
||||
IsTxInput checks if the transaction ID supplied is an input of the current transaction
|
||||
*/
|
||||
func (t *Transaction) IsTxInput(txid string) bool {
|
||||
for _, input := range t.Inputs {
|
||||
if txid == input.Outpoint.Txid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
IsCoinbaseTx checks if the transaction is a coinbase transaction
|
||||
*/
|
||||
func (t *Transaction) IsCoinbaseTx() bool {
|
||||
return t.Height != nil
|
||||
}
|
10
run.sh
Executable file
10
run.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Please read the README for a full list of environment variables
|
||||
echo "DOCKER_REGISTRY=localhost" >.env
|
||||
echo "KERMA_STORE_BASE_DIR=/var/local/badkerma" >>.env
|
||||
echo "=================================================="
|
||||
echo "Starting with an empty peer list for readability. Please add KERMA_INITIAL_PEER_LIST to .env if you'd like to emulate a host run."
|
||||
echo "=================================================="
|
||||
|
||||
make docker-debug
|
13
test.txt
Normal file
13
test.txt
Normal file
@ -0,0 +1,13 @@
|
||||
{"type":"hello","version":"0.8.0"}
|
||||
{"type":"peers","peers":[]}
|
||||
{"type":"mempool", "txids": ["2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df"]}
|
||||
{"type":"object", "object":{"T":"00000002af000000000000000000000000000000000000000000000000000000","created":1624221079,"miner":"Snekel testminer","nonce":"00000000000000000000000000000000000000000000000000000000182b95ea","note":"Second block after genesis with CBTX and TX","previd":"0000000108bdb42de5993bcf5f7d92557585dd6abfe9fb68e796518fe7f2ed2e","txids":["73231cc901774ddb4196ee7e9e6b857b208eea04aee26ced038ac465e1e706d2","7ef80f2da40b3f681a5aeb7962731beddccea25fa51e6e7ae6fbce8a58dbe799"],"type":"block"}}
|
||||
{"type":"object","object":{"height":1,"outputs":[{"pubkey":"f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60","value":50000000000000}],"type":"transaction"}}
|
||||
{"type":"object", "object":{"T":"00000002af000000000000000000000000000000000000000000000000000000","created":1624220079,"miner":"Snekel testminer","nonce":"000000000000000000000000000000000000000000000000000000009d8b60ea","note":"First block after genesis with CBTX","previd":"00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e","txids":["2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df"],"type":"block"}}
|
||||
{"type":"object", "object":{"height":2,"outputs":[{"pubkey":"c7c2c13afd02be7986dee0f4630df01abdbc950ea379055f1a423a6090f1b2b3","value":50000000000000}],"type":"transaction"}}
|
||||
{"type":"object", "object":{"inputs":[{"outpoint":{"index":0,"txid":"2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df"},"sig":"49cc4f9a1fb9d600a7debc99150e7909274c8c74edd7ca183626dfe49eb4aa21c6ff0e4c5f0dc2a328ad6b8ba10bf7169d5f42993a94bf67e13afa943b749c0b"}],"outputs":[{"pubkey":"c7c2c13afd02be7986dee0f4630df01abdbc950ea379055f1a423a6090f1b2b3","value":50}],"type":"transaction"}}
|
||||
{"type": "getmempool"}
|
||||
{"object":{"T":"00000002af000000000000000000000000000000000000000000000000000000","created":1652761335,"miner":"grader","nonce":"0000000000000000000000000000000000000000000000000000000015d586f3","previd":"000000001d70ea8d2a7997407b62551b37b88ed0075bba0615ab32cba37ef7d4","txids":[],"type":"block"},"type":"object"}
|
||||
{"object":{"T":"00000002af000000000000000000000000000000000000000000000000000000","created":1652761333,"miner":"grader","nonce":"0000000000000000000000000000000000000000000000000000000000a78010","previd":"000000020ac07f9f5960e593a74843bb184ff5c867e3d20f8466fd74ecfc4b35","txids":[],"type":"block"},"type":"object"}
|
||||
{"object":{"T":"00000002af000000000000000000000000000000000000000000000000000000","created":1652760150,"miner":"grader","nonce":"000000000000000000000000000000000000000000000000000000010508fbb0","previd":"00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e","txids":[],"type":"block"},"type":"object"}
|
||||
{"objectid":"000000006af19d99c7a2ad8ab05a6bd2496c5a3f4676019a376a72bfb287536e","type":"getobject"}
|
167
utils/client.go
Normal file
167
utils/client.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Package utils provides utilities that are needed for the functioning of Kerma
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"kerma/helpers"
|
||||
"kerma/models"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
A Client represents a Kerma client with the following properties:
|
||||
- State - the current state of the application
|
||||
- Handler - a TCPHandler that is used to handle requests to and from remote peers
|
||||
- Logger
|
||||
*/
|
||||
type Client struct {
|
||||
State *models.State
|
||||
Handler TCPHandler
|
||||
Logger helpers.Logger
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new client with the specified state and handler objects
|
||||
*/
|
||||
func (c *Client) Construct(state *models.State, h TCPHandler) {
|
||||
c.State = state
|
||||
c.Handler = h
|
||||
}
|
||||
|
||||
/*
|
||||
InitHandshake initiates a handshake on the specified connection
|
||||
*/
|
||||
func (c *Client) InitHandshake(peerName string) {
|
||||
var resp models.Hello
|
||||
resp.Construct()
|
||||
respJSON, _ := json.MarshalCanonical(resp)
|
||||
|
||||
c.Handler.Output(respJSON)
|
||||
c.Handler.Conn.SetDeadline(time.Now().Add(time.Duration(c.State.Config.ConnTimeout) * time.Second))
|
||||
in, err := bufio.NewReader(c.Handler.Conn).ReadBytes('\n')
|
||||
if err != nil {
|
||||
c.Logger.Error(err.Error())
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
c.State.RemovePeerByName(peerName)
|
||||
} else {
|
||||
c.Handler.Fail("Failed reading message")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := c.Handler.Input(in)
|
||||
if err != nil {
|
||||
c.Logger.Error(err.Error())
|
||||
c.Handler.Fail("Failed parsing message")
|
||||
return
|
||||
}
|
||||
|
||||
msgJSON, err := json.MarshalCanonical(msg)
|
||||
if err != nil {
|
||||
c.Logger.Error("Failed parsing message " + err.Error())
|
||||
c.Handler.Fail("Failed parsing message")
|
||||
return
|
||||
}
|
||||
|
||||
if msgType, ok := msg["type"]; ok {
|
||||
if msgType == "hello" {
|
||||
var helloReq models.Hello
|
||||
err := helloReq.UnmarshalJSON(msgJSON)
|
||||
|
||||
if err == nil {
|
||||
c.Logger.Info("Constructing handshake with " + c.Handler.GetRemotePeer())
|
||||
c.State.CompleteHandshake(peerName)
|
||||
} else {
|
||||
c.Handler.Fail("Hello response invalid")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.Handler.Fail("Response type not expected " + msgType.(string))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.Handler.Fail("Response type not specified")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DiscoverPeers requests remote peers from a known peer
|
||||
*/
|
||||
func (c *Client) DiscoverPeers(peerName string) {
|
||||
if c.State.CheckForHandshake(peerName) {
|
||||
var req models.Generic
|
||||
req.BuildPeerRequest()
|
||||
reqJSON, _ := req.MarshalJson()
|
||||
c.Handler.Output(reqJSON)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DiscoverChainTip requests chaintip from a known peer
|
||||
*/
|
||||
func (c *Client) DiscoverChainTip(peerName string) {
|
||||
if c.State.CheckForHandshake(peerName) {
|
||||
var req models.Generic
|
||||
req.BuildChainTipRequest()
|
||||
reqJSON, _ := req.MarshalJson()
|
||||
c.Handler.Output(reqJSON)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DiscoverMempool requests mempool from a known peer
|
||||
*/
|
||||
func (c *Client) DiscoverMempool(peerName string) {
|
||||
if c.State.CheckForHandshake(peerName) {
|
||||
var req models.Generic
|
||||
req.BuildMempoolRequest()
|
||||
reqJSON, _ := req.MarshalJson()
|
||||
c.Handler.Output(reqJSON)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
DiscoverObject requests remote objects from a known peer
|
||||
*/
|
||||
func (c *Client) DiscoverObject(peerName string, objectID string) {
|
||||
if c.State.CheckForHandshake(peerName) {
|
||||
var req models.ObjectWrapper
|
||||
req.BuildObjectRequest(objectID)
|
||||
reqJSON, _ := req.MarshalJson()
|
||||
c.Handler.Output(reqJSON)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
GossipBlocks sends information about local blocks to peer
|
||||
*/
|
||||
func (c *Client) GossipBlocks(peerName string) {
|
||||
if c.State.CheckForHandshake(peerName) {
|
||||
for _, block := range c.State.Chain {
|
||||
var req models.ObjectWrapper
|
||||
bid := block.GetID()
|
||||
req.BuildGossipObject(bid)
|
||||
reqJSON, _ := req.MarshalJson()
|
||||
c.Handler.Output(reqJSON)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
GossipTransactions sends information about local transactions to peer
|
||||
*/
|
||||
func (c *Client) GossipTransactions(peerName string) {
|
||||
if c.State.CheckForHandshake(peerName) {
|
||||
for txid := range c.State.Transactions {
|
||||
var req models.ObjectWrapper
|
||||
req.BuildGossipObject(txid)
|
||||
reqJSON, _ := req.MarshalJson()
|
||||
c.Handler.Output(reqJSON)
|
||||
}
|
||||
}
|
||||
}
|
89
utils/handler.go
Normal file
89
utils/handler.go
Normal file
@ -0,0 +1,89 @@
|
||||
// Package utils provides utilities that are needed for the functioning of Kerma
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"kerma/helpers"
|
||||
"kerma/models"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
A TCPHandler is a struct used for communicating with other Kerma nodes/clients
|
||||
It consists of:
|
||||
- Conn - a connection for sending and receiving requests
|
||||
- Logger
|
||||
- Config - the configuration of the application
|
||||
*/
|
||||
type TCPHandler struct {
|
||||
Conn net.Conn
|
||||
Logger helpers.Logger
|
||||
Config helpers.Config
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new handler on the specified connection
|
||||
*/
|
||||
func (h *TCPHandler) Construct(conn net.Conn) {
|
||||
h.Conn = conn
|
||||
h.Config.Construct()
|
||||
}
|
||||
|
||||
/*
|
||||
Output handles outgoing data in a canonical JSON format
|
||||
*/
|
||||
func (h *TCPHandler) Output(respJSON []byte) {
|
||||
h.Logger.Info("OUT: " + h.GetRemotePeer() + " " + string(respJSON))
|
||||
respJSON = append(respJSON, '\n')
|
||||
h.Conn.Write(respJSON)
|
||||
}
|
||||
|
||||
/*
|
||||
Error handles outgoing errors in a canonical JSON format
|
||||
*/
|
||||
func (h *TCPHandler) Error(errorMsg string) {
|
||||
var resp models.Error
|
||||
resp.Construct(errorMsg)
|
||||
respJSON, _ := resp.MarshalJson()
|
||||
h.Output(respJSON)
|
||||
}
|
||||
|
||||
/*
|
||||
Fail handles outgoing errors in a canonical JSON format and terminates the connection
|
||||
*/
|
||||
func (h *TCPHandler) Fail(errorMsg string) {
|
||||
h.Error(errorMsg)
|
||||
h.Conn.Close()
|
||||
}
|
||||
|
||||
/*
|
||||
Input handles input data in a canonical JSON format
|
||||
*/
|
||||
func (h *TCPHandler) Input(in []byte) (map[string](interface{}), error) {
|
||||
var msg map[string](interface{})
|
||||
|
||||
err := json.Unmarshal(in, &msg)
|
||||
if err != nil {
|
||||
h.Logger.Error("Failed decoding message " + err.Error())
|
||||
return nil, errors.New("failed decoding message")
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
/*
|
||||
GetRemotePeer returns tha address of the remote peer as a string
|
||||
*/
|
||||
func (h *TCPHandler) GetRemotePeer() string {
|
||||
return h.Conn.RemoteAddr().String()
|
||||
}
|
||||
|
||||
/*
|
||||
GetLocalPeer returns the address + port combination of the local peer as a string
|
||||
*/
|
||||
func (h *TCPHandler) GetLocalPeer() string {
|
||||
return h.Config.IPAddress + ":" + strconv.Itoa(h.Config.Port)
|
||||
}
|
352
utils/server.go
Normal file
352
utils/server.go
Normal file
@ -0,0 +1,352 @@
|
||||
// Package utils provides utilities that are needed for the functioning of Kerma
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"kerma/helpers"
|
||||
"kerma/models"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go/canonical/json"
|
||||
)
|
||||
|
||||
/*
|
||||
A Server represents a Kerma server with the following properties:
|
||||
- State - the current state of the application
|
||||
- Handler - a TCPHandler that is used to handle requests to and from remote peers
|
||||
- Logger
|
||||
- Client - a new client that uses the same state and handler as this server
|
||||
*/
|
||||
type Server struct {
|
||||
State *models.State
|
||||
Handler TCPHandler
|
||||
Logger helpers.Logger
|
||||
Client Client
|
||||
}
|
||||
|
||||
/*
|
||||
Construct creates a new server with the specified state and handler objects
|
||||
*/
|
||||
func (s *Server) Construct(state *models.State, h TCPHandler) {
|
||||
s.State = state
|
||||
s.Handler = h
|
||||
s.Client.Construct(s.State, s.Handler)
|
||||
}
|
||||
|
||||
/*
|
||||
Handle is responsible for handling requests from other Kerma nodes/clients
|
||||
*/
|
||||
func (s *Server) Handle() {
|
||||
// Decode message received from reader, fail on error
|
||||
|
||||
scanner := bufio.NewScanner(s.Handler.Conn)
|
||||
|
||||
for scanner.Scan() {
|
||||
msg, err := s.Handler.Input(scanner.Bytes())
|
||||
if err != nil {
|
||||
s.Handler.Fail(err.Error())
|
||||
}
|
||||
|
||||
// Parse received message, fail on error
|
||||
msgJSON, err := json.MarshalCanonical(msg)
|
||||
if err != nil {
|
||||
s.Logger.Error("Failed parsing message " + err.Error())
|
||||
s.Handler.Fail("Failed parsing message")
|
||||
}
|
||||
|
||||
s.Logger.Info("IN: " + s.Handler.GetRemotePeer() + " " + string(msgJSON))
|
||||
|
||||
remotePeer := s.Handler.GetRemotePeer()
|
||||
|
||||
// check message type, proceed accordingly
|
||||
if msgType, ok := msg["type"]; ok {
|
||||
switch msgType {
|
||||
case "error":
|
||||
break
|
||||
case "hello":
|
||||
// check if peer has already handshaked
|
||||
if !s.State.CheckForHandshake(remotePeer) {
|
||||
// check if hello request is valid
|
||||
var helloReq models.Hello
|
||||
err := helloReq.UnmarshalJSON(msgJSON)
|
||||
|
||||
if err == nil {
|
||||
// finish handshake
|
||||
s.Logger.Info("Constructing handshake with " + remotePeer)
|
||||
var resp models.Hello
|
||||
resp.Construct()
|
||||
respJSON, _ := resp.MarshalJson()
|
||||
s.Handler.Output(respJSON)
|
||||
s.State.CompleteHandshake(remotePeer)
|
||||
s.Client.DiscoverPeers(remotePeer)
|
||||
} else {
|
||||
s.Handler.Fail("Hello request invalid")
|
||||
}
|
||||
}
|
||||
break
|
||||
case "getpeers":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// create list of known peers, send to requester
|
||||
resp := s.State.FetchPeerListResponse()
|
||||
resp.Peers = append(resp.Peers, s.Handler.GetLocalPeer())
|
||||
respJSON, _ := resp.MarshalJson()
|
||||
s.Handler.Output(respJSON)
|
||||
break
|
||||
case "peers":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var peers models.PeerListResponse
|
||||
json.Unmarshal(msgJSON, &peers)
|
||||
s.State.ParsePeerListResponse(&peers)
|
||||
break
|
||||
case "ihaveobject":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var obj models.ObjectWrapper
|
||||
err = obj.UnmarshalJSON(msgJSON)
|
||||
if err != nil {
|
||||
s.Handler.Fail("Could not parse request")
|
||||
break
|
||||
}
|
||||
|
||||
if s.State.Transactions[obj.ObjectID] == nil {
|
||||
resp := models.ObjectWrapper{
|
||||
Type: "getobject",
|
||||
ObjectID: obj.ObjectID,
|
||||
}
|
||||
|
||||
respJSON, _ := resp.MarshalJson()
|
||||
s.Handler.Output(respJSON)
|
||||
}
|
||||
case "getobject":
|
||||
err = s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var obj models.ObjectWrapper
|
||||
err := obj.UnmarshalJSON(msgJSON)
|
||||
if err != nil {
|
||||
s.Handler.Fail("Could not parse request")
|
||||
break
|
||||
}
|
||||
|
||||
transaction := s.State.GetTransaction(obj.ObjectID)
|
||||
if transaction != nil {
|
||||
s.Logger.Debug("Found transaction: " + obj.ObjectID)
|
||||
resp := json.RawMessage(`{"type":"object","object":` + transaction.String() + `}`)
|
||||
|
||||
respJSON, _ := resp.MarshalJSON()
|
||||
s.Handler.Output(respJSON)
|
||||
}
|
||||
|
||||
block := s.State.GetBlock(obj.ObjectID)
|
||||
if block != nil {
|
||||
s.Logger.Debug("Found block: " + obj.ObjectID)
|
||||
resp := json.RawMessage(`{"type":"object","object":` + block.String() + `}`)
|
||||
|
||||
respJSON, _ := resp.MarshalJSON()
|
||||
s.Handler.Output(respJSON)
|
||||
} else {
|
||||
s.Handler.Error("Could not find object " + obj.ObjectID)
|
||||
}
|
||||
break
|
||||
case "object":
|
||||
err = s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var object models.Object
|
||||
|
||||
err := object.UnmarshalJSON(msgJSON)
|
||||
if err != nil {
|
||||
s.Logger.Debug("Could not unmarshal: " + err.Error())
|
||||
s.Handler.Fail("Object request invalid")
|
||||
break
|
||||
}
|
||||
|
||||
val, err := object.GetObjectValue()
|
||||
if err != nil {
|
||||
s.Logger.Error("Could not parse object: " + err.Error())
|
||||
s.Handler.Fail("Could not parse object request")
|
||||
break
|
||||
}
|
||||
|
||||
switch val.GetType() {
|
||||
case "transaction":
|
||||
err := s.State.AppendTransaction(val.GetEntity().(*models.Transaction))
|
||||
if err != nil {
|
||||
s.Logger.Debug("Failed appending transaction: " + err.Error())
|
||||
s.Handler.Fail("Error appending transaction")
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
case "block":
|
||||
block := val.(*models.Block)
|
||||
|
||||
go s.appendBlockToChain(block)
|
||||
break
|
||||
default:
|
||||
s.Logger.Debug("Something went wrong with " + string(msgJSON))
|
||||
s.Handler.Fail("Could not parse object request")
|
||||
}
|
||||
break
|
||||
case "getchaintip":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
res := s.State.GetChainTip()
|
||||
|
||||
var chaintip models.Chaintip
|
||||
chaintip.Construct(res.GetID())
|
||||
respJSON, _ := chaintip.MarshalJson()
|
||||
s.Handler.Output(respJSON)
|
||||
break
|
||||
case "chaintip":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var chaintip models.Chaintip
|
||||
|
||||
err = chaintip.UnmarshalJSON(msgJSON)
|
||||
if err != nil {
|
||||
s.Logger.Debug("Could not unmarshal: " + err.Error())
|
||||
s.Handler.Fail("Object request invalid")
|
||||
break
|
||||
}
|
||||
|
||||
res := s.State.GetChainTip()
|
||||
if res.GetID() != chaintip.BlockID {
|
||||
s.Client.DiscoverObject(remotePeer, chaintip.BlockID)
|
||||
}
|
||||
break
|
||||
case "getmempool":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
res := s.State.GetMempoolTransactionIDs()
|
||||
|
||||
var mempool models.Mempool
|
||||
mempool.Construct(res)
|
||||
respJSON, _ := mempool.MarshalJson()
|
||||
s.Handler.Output(respJSON)
|
||||
break
|
||||
case "mempool":
|
||||
err := s.terminateIfNoHandshake(remotePeer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var mempool models.Mempool
|
||||
|
||||
err = mempool.UnmarshalJSON(msgJSON)
|
||||
if err != nil {
|
||||
s.Logger.Debug("Could not unmarshal: " + err.Error())
|
||||
s.Handler.Fail("Object request invalid")
|
||||
break
|
||||
}
|
||||
|
||||
for _, txid := range mempool.Txids {
|
||||
res := s.State.GetTransaction(txid)
|
||||
if res == nil {
|
||||
s.Client.DiscoverObject(remotePeer, txid)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
s.Handler.Fail("Request type not supported " + msgType.(string))
|
||||
}
|
||||
} else {
|
||||
s.Handler.Fail("Request type not specified")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) terminateIfNoHandshake(remotePeer string) error {
|
||||
if !s.State.CheckForHandshake(remotePeer) {
|
||||
s.Handler.Fail("Handshake not completed")
|
||||
return errors.New("handshake not completed with peer " + remotePeer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) appendBlockToChain(block *models.Block) {
|
||||
dontAppend := false
|
||||
missingTx, err := s.State.GetMissingTransactionsInBlock(block)
|
||||
if err != nil {
|
||||
s.Logger.Debug("Failed appending block: " + err.Error())
|
||||
s.Handler.Fail("Error appending block")
|
||||
return
|
||||
}
|
||||
|
||||
if len(missingTx) != 0 {
|
||||
for _, txid := range missingTx {
|
||||
s.Client.DiscoverObject(s.Handler.GetRemotePeer(), txid)
|
||||
waited := 0
|
||||
for s.State.GetTransaction(txid) == nil {
|
||||
s.Logger.Debug("Waiting for transactions " + txid)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
waited += 500
|
||||
if waited*int(time.Millisecond) >= s.State.Config.ConnTimeout*int(time.Second) {
|
||||
s.Handler.Fail("Did not receive requested transactions in time, failed appending block")
|
||||
dontAppend = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.getMissingBlocks(block)
|
||||
|
||||
if !dontAppend {
|
||||
err = s.State.AppendToChain(block)
|
||||
if err != nil {
|
||||
s.Logger.Debug("Failed appending block: " + err.Error())
|
||||
s.Handler.Fail("Error appending block")
|
||||
}
|
||||
|
||||
s.State.DumpBlockStore()
|
||||
for _, peer := range s.State.PeerList {
|
||||
if peer.Active {
|
||||
s.Client.GossipBlocks(peer.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getMissingBlocks(block *models.Block) {
|
||||
blockId := *block.Previd
|
||||
if s.State.GetBlock(blockId) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Logger.Debug("Block missing from chain: " + blockId)
|
||||
for _, peer := range s.State.PeerList {
|
||||
s.Client.DiscoverObject(peer.Name, blockId)
|
||||
}
|
||||
|
||||
for s.State.GetBlock(blockId) == nil {
|
||||
s.Logger.Debug("Waiting for block " + blockId)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
38
utils/tests/client_test.go
Normal file
38
utils/tests/client_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"kerma/helpers"
|
||||
"kerma/models"
|
||||
"kerma/utils"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
host := "127.0.0.1:1338"
|
||||
net.Listen("tcp", host)
|
||||
conn, _ := net.Dial("tcp", host)
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
var config helpers.Config
|
||||
config.Construct()
|
||||
|
||||
var handler utils.TCPHandler
|
||||
handler.Construct(conn)
|
||||
|
||||
var state models.State
|
||||
state.Construct()
|
||||
|
||||
Convey("Given a new client", t, func() {
|
||||
var client utils.Client
|
||||
client.Construct(&state, handler)
|
||||
|
||||
Convey("When Construct is called, a new object is returned", func() {
|
||||
So(client.State, ShouldResemble, &state)
|
||||
So(client.Handler, ShouldResemble, handler)
|
||||
})
|
||||
})
|
||||
}
|
51
utils/tests/handler_test.go
Normal file
51
utils/tests/handler_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"kerma/helpers"
|
||||
"kerma/utils"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
host := "127.0.0.1:1337"
|
||||
net.Listen("tcp", host)
|
||||
conn, _ := net.Dial("tcp", host)
|
||||
scanner := bufio.NewScanner(conn)
|
||||
|
||||
var config helpers.Config
|
||||
config.Construct()
|
||||
|
||||
Convey("Given a new handler", t, func() {
|
||||
var handler utils.TCPHandler
|
||||
handler.Construct(conn)
|
||||
|
||||
Convey("When Construct is called, a new object is returned", func() {
|
||||
So(handler.Conn, ShouldResemble, conn)
|
||||
So(handler.Config, ShouldResemble, config)
|
||||
})
|
||||
|
||||
Convey("When Output is called, a JSON string should be returned on the connection", func() {
|
||||
jsonString := `{"foo": "bar"}`
|
||||
handler.Output([]byte(jsonString))
|
||||
resp := string(scanner.Bytes())
|
||||
|
||||
So(resp, ShouldEqual, resp)
|
||||
})
|
||||
|
||||
Convey("When Input is called, a JSON string and no error should be obtained from the connection", func() {
|
||||
jsonString := `{"foo": "bar"}`
|
||||
resp, err := handler.Input([]byte(jsonString))
|
||||
|
||||
So(resp, ShouldEqual, resp)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When GetRemotePeer is called, the local connection should be returned", func() {
|
||||
So(handler.GetRemotePeer(), ShouldEqual, host)
|
||||
})
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user