commit cda7a31e4e0b1494652661f2fd8ce07a79629b75 Author: BlockChainDev Date: Mon Feb 25 22:44:14 2019 +0000 Initial commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..6382bd874 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +FROM vidsyhq/go-base:latest diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 000000000..b91339241 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,69 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + +[[constraint]] + branch = "master" + name = "github.com/syndtr/goleveldb" + +[[constraint]] + name = "github.com/urfave/cli" + version = "1.20.0" + +[prune] + go-tests = true + unused-packages = true + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" + +[[constraint]] + branch = "master" + name = "github.com/anthdm/rfc6979" + +[[constraint]] + name = "golang.org/x/text" + version = "0.3.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" + +[[constraint]] + name = "github.com/sirupsen/logrus" + version = "1.0.5" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[[constraint]] + name = "github.com/go-yaml/yaml" + version = "2.1.1" + +[[constraint]] + name = "github.com/go-redis/redis" + version = "6.10.2" \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..a2b7338e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +BRANCH = "master" +BUILD_TIME = "$(shell date -u +\"%Y-%m-%dT%H:%M:%SZ\")" +VERSION = $(shell cat ./VERSION) +REPONAME = "neo-go" +NETMODE ?= "privnet" + +build: + @echo "=> Building darwin binary" + @go build -i -ldflags "-X github.com/CityOfZion/neo-go/config.Version=${VERSION}-dev -X github.com/CityOfZion/neo-go/config.BuildTime=${BUILD_TIME}" -o ./bin/neo-go ./cli/main.go + +build-image: + docker build -t cityofzion/neo-go --build-arg VERSION=${VERSION} . + +build-linux: + @echo "=> Building linux binary" + @GOOS=linux go build -i -ldflags "-X github.com/CityOfZion/neo-go/config.Version=${VERSION}-dev -X github.com/CityOfZion/neo-go/config.BuildTime=${BUILD_TIME}" -o ./bin/neo-go ./cli/main.go + +check-version: + git fetch && (! git rev-list ${VERSION}) + +clean-cluster: + @echo "=> Removing all containers and chain storage" + @rm -rf chains/privnet-docker-one chains/privnet-docker-two chains/privnet-docker-three chains/privnet-docker-four + @docker-compose stop + @docker-compose rm -f + +deps: + @dep ensure + +push-tag: + git checkout ${BRANCH} + git pull origin ${BRANCH} + git tag ${VERSION} + git push origin ${VERSION} + +push-to-registry: + @docker login -e ${DOCKER_EMAIL} -u ${DOCKER_USER} -p ${DOCKER_PASS} + @docker tag CityOfZion/${REPONAME}:latest CityOfZion/${REPONAME}:${CIRCLE_TAG} + @docker push CityOfZion/${REPONAME}:${CIRCLE_TAG} + @docker push CityOfZion/${REPONAME} + +run: build + ./bin/neo-go node -config-path ./config -${NETMODE} + +run-cluster: build-linux + @echo "=> Starting docker-compose cluster" + @echo "=> Building container image" + @docker-compose build + @docker-compose up -d + @echo "=> Tailing logs, exiting this prompt will not stop the cluster" + @docker-compose logs -f + +test: + @go test ./... -cover + +vet: + @go vet ./... \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..25e8d483f --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.44.8 \ No newline at end of file diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..47255ab3e --- /dev/null +++ b/circle.yml @@ -0,0 +1,152 @@ +version: 2 +jobs: + install_deps: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: vidsyhq/go-builder:latest + environment: + BUILD: false + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - run: /scripts/build.sh + - save_cache: + key: dependency-cache-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + paths: + - vendor + - /go/pkg + build_image: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: alpine + steps: + - run: apk update && apk add git make curl tar + - checkout + - restore_cache: + keys: + - dependency-cache-{{ .Revision }} + - restore_cache: + keys: + - dependency-cache-cli-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - setup_remote_docker + - run: + name: Install Docker client + command: | + set -x + VER="17.03.0-ce" + curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz + tar -xz -C /tmp -f /tmp/docker-$VER.tgz + mv /tmp/docker/* /usr/bin + - run: make build-image + test: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: vidsyhq/go-builder:latest + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - run: make test + vet: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: vidsyhq/go-builder:latest + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - run: make vet + check_version: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: vidsyhq/go-builder:latest + steps: + - checkout + - run: make check-version + build_cli: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: vidsyhq/go-builder:latest + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - run: make build + - save_cache: + key: dependency-cache-cli-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + paths: + - bin/neo-go + deploy: + working_directory: /go/src/github.com/CityOfZion/neo-go + docker: + - image: alpine + steps: + - run: apk update && apk add git make curl tar + - checkout + - restore_cache: + key: dependency-cache-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - restore_cache: + key: dependency-cache-cli-{{ checksum "Gopkg.toml" }}-{{ checksum "VERSION" }} + - setup_remote_docker + - run: + name: Install Docker client + command: | + set -x + VER="17.03.0-ce" + curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz + tar -xz -C /tmp -f /tmp/docker-$VER.tgz + mv /tmp/docker/* /usr/bin + - run: make build-image + - deploy: + name: deploy + command: make push-to-registry +workflows: + version: 2 + workflow: + jobs: + - install_deps: + filters: + tags: + only: /[0-9]+\.[0-9]+\.[0-9]+/ + - test: + requires: + - install_deps + filters: + tags: + only: /[0-9]+\.[0-9]+\.[0-9]+/ + - vet: + requires: + - install_deps + filters: + tags: + only: /[0-9]+\.[0-9]+\.[0-9]+/ + - check_version: + filters: + branches: + ignore: master + - build_cli: + requires: + - install_deps + filters: + tags: + only: /[0-9]+\.[0-9]+\.[0-9]+/ + - build_image: + requires: + - install_deps + - build_cli + filters: + tags: + only: /[0-9]+\.[0-9]+\.[0-9]+/ + - deploy: + requires: + - build_image + - test + - vet + - check_version + filters: + tags: + only: + - /[0-9]+\.[0-9]+\.[0-9]+/ + branches: + ignore: /.*/ \ No newline at end of file diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 000000000..790580777 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/docs/contribution.md b/docs/contribution.md new file mode 100644 index 000000000..890e615f3 --- /dev/null +++ b/docs/contribution.md @@ -0,0 +1,28 @@ +# Contributions + + +This document will state guidelines for submitting code. + +## Pre-requisites + +In order to contribute to Golang, it is essential that you know Golang or language similar to Golang. It is also required that you are familiar with NEO infrastructure. + +Reading: + + http://docs.neo.org/en-us/network/network-protocol.html + + https://tour.golang.org/welcome/1 + + https://golang.org/doc/effective_go.html#introduction + +## 1. Open an Issue Prior + +Please open an issue prior to working on the feature, stating the feature you would like to work on. + +## 2. Unit test + +Unit tests are important, when submitting a PR please ensure that you have at least 70% test coverage. + +## 3. Avoid CGo + +Due to the complications that come with using CGo, if using it, please provide justification before using it. If using CGo, can provide significant speed boosts and or a large part of the code can be processed in CGo, then we should be able to come to consensus on using it. Generally, it is better to write everything in Golang, so that the code can be maintained by other Gophers. \ No newline at end of file diff --git a/docs/conventions.md b/docs/conventions.md new file mode 100644 index 000000000..e5a6f5eab --- /dev/null +++ b/docs/conventions.md @@ -0,0 +1,17 @@ +# Conventions + +This document will list conventions that this repo should follow. These are guidelines and if you believe that one should not be followed, then please state why in your PR. If you believe that a piece of code does not follow one of the conventions listed, then please open an issue before making any changes. + +When submitting a new convention, please open an issue for discussion, if possible please highlight parts in the code where this convention could help the code readiblity or simplicity. + + +## Avoid Named return paramters + + +func example(test int) (num int) { + a = test + 1 + num = a * test + return +} + +In the above function we have used a named return paramter, which allows you to include a simple return statement without the variables you are returning. This practice can cause confusion when functions become large or the logic becomes complex, so these should be avoided. \ No newline at end of file diff --git a/pkg/Readme.md b/pkg/Readme.md new file mode 100644 index 000000000..803fb5ffe --- /dev/null +++ b/pkg/Readme.md @@ -0,0 +1,12 @@ +# ReadMe + +Currently this package is in Development. + + +## References + +btcd https://github.com/btcsuite/btcd + +geth https://github.com/ethereum/go-ethereum + +aeternity https://github.com/aeternity/elixir-node \ No newline at end of file diff --git a/pkg/addrmgr/addrmgr.go b/pkg/addrmgr/addrmgr.go new file mode 100644 index 000000000..639fd6dff --- /dev/null +++ b/pkg/addrmgr/addrmgr.go @@ -0,0 +1,285 @@ +package addrmgr + +import ( + "errors" + "fmt" + "math/rand" + "sync" + "time" + + "github.com/CityOfZion/neo-go/pkg/peer" + "github.com/CityOfZion/neo-go/pkg/wire/payload" +) + +const ( + maxTries = 5 // Try to connect five times, if we cannot then we say it is bad + // We will keep bad addresses, so that we do not attempt to connect to them in the future. + // nodes at the moment seem to send a large percentage of bad nodes. + + maxFailures = 12 // This will be incremented when we connect to a node and for whatever reason, they keep disconnecting. + // The number is high because there could be a period of time, where a node is behaving unrepsonsively and + // we do not want to immmediately put it inside of the bad bucket. + + // This is the maximum amount of addresses that the addrmgr will hold + maxAllowedAddrs = 2000 +) + +type addrStats struct { + tries uint8 + failures uint8 + permanent bool // permanent = inbound, temp = outbound + lastTried time.Time + lastSuccess time.Time +} + +type Addrmgr struct { + addrmtx sync.RWMutex + goodAddrs map[*payload.Net_addr]addrStats + newAddrs map[*payload.Net_addr]struct{} + badAddrs map[*payload.Net_addr]addrStats + knownList map[string]*payload.Net_addr // this contains all of the addresses in badAddrs, newAddrs and goodAddrs +} + +func New() *Addrmgr { + + return &Addrmgr{ + sync.RWMutex{}, + make(map[*payload.Net_addr]addrStats, 100), + make(map[*payload.Net_addr]struct{}, 100), + make(map[*payload.Net_addr]addrStats, 100), + make(map[string]*payload.Net_addr, 100), + } +} + +//AddAddrs will add new add new addresses into the newaddr list +// This is safe for concurrent access. +func (a *Addrmgr) AddAddrs(newAddrs []*payload.Net_addr) { + + newAddrs = removeDuplicates(newAddrs) + + var nas []*payload.Net_addr + for _, addr := range newAddrs { + a.addrmtx.Lock() + + if _, ok := a.knownList[addr.IPPort()]; !ok { // filter unique addresses + nas = append(nas, addr) + } + a.addrmtx.Unlock() + } + + for _, addr := range nas { + a.addrmtx.Lock() + a.newAddrs[addr] = struct{}{} + a.knownList[addr.IPPort()] = addr + a.addrmtx.Unlock() + } +} + +// Good returns the good addresses. +// A good address is: +// - Known +// - has been successfully connected to in the last week +// - Note: that while users are launching full nodes from their laptops, the one week marker will need to be modified. +func (a *Addrmgr) Good() []payload.Net_addr { + + var goodAddrs []payload.Net_addr + + var oneWeekAgo = time.Now().Add(((time.Hour * 24) * 7) * -1) + + a.addrmtx.RLock() + // TODO: sort addresses, permanent ones go first + for addr, stat := range a.goodAddrs { + if stat.lastTried.Before(oneWeekAgo) { + continue + } + goodAddrs = append(goodAddrs, *addr) + + } + a.addrmtx.RUnlock() + + return goodAddrs +} + +// Unconnected Addrs are addresses in the newAddr List +func (a *Addrmgr) Unconnected() []payload.Net_addr { + + var unconnAddrs []payload.Net_addr + + a.addrmtx.RLock() + for addr := range a.newAddrs { + unconnAddrs = append(unconnAddrs, *addr) + } + a.addrmtx.RUnlock() + return unconnAddrs +} + +// Bad Addrs are addresses in the badAddr List +// They are put there if we have tried to connect +// to them in the past and it failed. +func (a *Addrmgr) Bad() []payload.Net_addr { + + var badAddrs []payload.Net_addr + + a.addrmtx.RLock() + for addr := range a.badAddrs { + badAddrs = append(badAddrs, *addr) + } + a.addrmtx.RUnlock() + return badAddrs +} + +// FetchMoreAddresses will return true if the numOfKnownAddrs are less than 100 +// This number is kept low because at the moment, there are not a lot of Good Addresses +// Tests have shown that at most there are < 100 +func (a *Addrmgr) FetchMoreAddresses() bool { + + var numOfKnownAddrs int + + a.addrmtx.RLock() + numOfKnownAddrs = len(a.knownList) + a.addrmtx.RUnlock() + + return numOfKnownAddrs < maxAllowedAddrs +} + +// ConnectionComplete will be called by the server when we have done the handshake +// and Not when we have successfully Connected with net.Conn +// It is to tell the AddrMgr that we have connected to a peer +// This peer should already be known to the AddrMgr because +// We send it to the Connmgr +func (a *Addrmgr) ConnectionComplete(addressport string, inbound bool) { + a.addrmtx.Lock() + defer a.addrmtx.Unlock() + + // if addrmgr does not know this, then we just return + if _, ok := a.knownList[addressport]; !ok { + fmt.Println("Connected to an unknown address:port ", addressport) + return + } + + na := a.knownList[addressport] + + // move it from newAddrs List to GoodAddr List + stats := a.goodAddrs[na] + stats.lastSuccess = time.Now() + stats.lastTried = time.Now() + stats.permanent = inbound + stats.tries++ + a.goodAddrs[na] = stats + + // remove it from new and bad, if it was there + delete(a.newAddrs, na) + delete(a.badAddrs, na) + + // Unfortunately, Will have a lot of bad nodes because of people connecting to nodes + // from their laptop. TODO Sort function in good will mitigate. + +} + +// Failed will be called by ConnMgr +// It is used to tell the AddrMgr that they had tried connecting an address an have failed +// This is concurrent safe +func (a *Addrmgr) Failed(addressport string) { + a.addrmtx.Lock() + defer a.addrmtx.Unlock() + + // if addrmgr does not know this, then we just return + if _, ok := a.knownList[addressport]; !ok { + fmt.Println("Connected to an unknown address:port ", addressport) + return + } + + na := a.knownList[addressport] + + // HMM: logic here could be simpler if we make it one list instead + + if stats, ok := a.badAddrs[na]; ok { + stats.lastTried = time.Now() + stats.failures++ + stats.tries++ + if float32(stats.failures)/float32(stats.tries) > 0.8 && stats.tries > 5 { + delete(a.badAddrs, na) + return + } + a.badAddrs[na] = stats + return + } + + if stats, ok := a.goodAddrs[na]; ok { + fmt.Println("We have a good addr", na.IPPort()) + stats.lastTried = time.Now() + stats.failures++ + stats.tries++ + if float32(stats.failures)/float32(stats.tries) > 0.5 && stats.tries > 10 { + delete(a.goodAddrs, na) + a.badAddrs[na] = stats + return + } + a.goodAddrs[na] = stats + return + } + + if _, ok := a.newAddrs[na]; ok { + delete(a.newAddrs, na) + a.badAddrs[na] = addrStats{} + } + +} + +//OnAddr is the responder for the Config file +// when a OnAddr is received by a peer +func (a *Addrmgr) OnAddr(p *peer.Peer, msg *payload.AddrMessage) { + a.AddAddrs(msg.AddrList) +} + +// OnGetAddr Is called when a peer sends a request for the addressList +// We will give them the best addresses we have from good. +func (a *Addrmgr) OnGetAddr(p *peer.Peer, msg *payload.GetAddrMessage) { + a.addrmtx.RLock() + defer a.addrmtx.RUnlock() + // Push most recent peers to peer + addrMsg, err := payload.NewAddrMessage() + if err != nil { + return + } + for _, add := range a.Good() { + addrMsg.AddNetAddr(&add) + } + + p.Write(addrMsg) +} + +// NewAddr will return an address for the external caller to +// connect to. In our case, it will be the connection manager. +func (a *Addrmgr) NewAddr() (string, error) { + // For now it just returns a random value from unconnected + // TODO: When an address is tried, the address manager is notified. + // When asked for a new address, this should be taken into account + // when choosing a new one, also the number of retries. + unconnected := a.Unconnected() + if len(unconnected) == 0 { + return "", errors.New("No Addresses to give") + } + rand := rand.Intn(len(unconnected)) + return unconnected[rand].IPPort(), nil +} + +// https://www.dotnetperls.com/duplicates-go +func removeDuplicates(elements []*payload.Net_addr) []*payload.Net_addr { + + encountered := map[string]bool{} + result := []*payload.Net_addr{} + + for _, element := range elements { + if encountered[element.IPPort()] == true { + // Do not add duplicate. + } else { + // Record this element as an encountered element. + encountered[element.IPPort()] = true + // Append to result slice. + result = append(result, element) + } + } + // Return the new slice. + return result +} diff --git a/pkg/addrmgr/addrmgr_test.go b/pkg/addrmgr/addrmgr_test.go new file mode 100644 index 000000000..5a35ea26b --- /dev/null +++ b/pkg/addrmgr/addrmgr_test.go @@ -0,0 +1,167 @@ +package addrmgr_test + +import ( + "crypto/rand" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + + "github.com/CityOfZion/neo-go/pkg/addrmgr" + "github.com/stretchr/testify/assert" +) + +func TestNewAddrs(t *testing.T) { + + addrmgr := addrmgr.New() + assert.NotEqual(t, nil, addrmgr) +} +func TestAddAddrs(t *testing.T) { + + addrmgr := addrmgr.New() + + ip := [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + addr, _ := payload.NewNetAddr(0, ip, 1033, protocol.NodePeerService) + + ip = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // same + addr2, _ := payload.NewNetAddr(0, ip, 1033, protocol.NodePeerService) + + ip = [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr3, _ := payload.NewNetAddr(0, ip, 1033, protocol.NodePeerService) + + addrs := []*payload.Net_addr{addr, addr2, addr, addr3} + + addrmgr.AddAddrs(addrs) + + assert.Equal(t, 2, len(addrmgr.Unconnected())) + assert.Equal(t, 0, len(addrmgr.Good())) + assert.Equal(t, 0, len(addrmgr.Bad())) +} + +func TestFetchMoreAddress(t *testing.T) { + + addrmgr := addrmgr.New() + + addrs := []*payload.Net_addr{} + + ip := make([]byte, 16) + + for i := 0; i <= 2000; i++ { // Add more than maxAllowedAddrs + rand.Read(ip) + + var nip [16]byte + copy(nip[:], ip[:16]) + + addr, _ := payload.NewNetAddr(0, nip, 1033, protocol.NodePeerService) + addrs = append(addrs, addr) + } + + addrmgr.AddAddrs(addrs) + + assert.Equal(t, false, addrmgr.FetchMoreAddresses()) + +} +func TestConnComplete(t *testing.T) { + + addrmgr := addrmgr.New() + + ip := [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr, _ := payload.NewNetAddr(0, ip, 1033, protocol.NodePeerService) + + ip2 := [16]byte{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr2, _ := payload.NewNetAddr(0, ip2, 1033, protocol.NodePeerService) + + ip3 := [16]byte{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr3, _ := payload.NewNetAddr(0, ip3, 1033, protocol.NodePeerService) + + addrs := []*payload.Net_addr{addr, addr2, addr3} + + addrmgr.AddAddrs(addrs) + + assert.Equal(t, len(addrs), len(addrmgr.Unconnected())) + + // a successful connection + addrmgr.ConnectionComplete(addr.IPPort(), true) + addrmgr.ConnectionComplete(addr.IPPort(), true) // should have no change + + assert.Equal(t, len(addrs)-1, len(addrmgr.Unconnected())) + assert.Equal(t, 1, len(addrmgr.Good())) + + // another successful connection + addrmgr.ConnectionComplete(addr2.IPPort(), true) + + assert.Equal(t, len(addrs)-2, len(addrmgr.Unconnected())) + assert.Equal(t, 2, len(addrmgr.Good())) + +} +func TestAttempted(t *testing.T) { + + addrmgr := addrmgr.New() + + ip := [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr, _ := payload.NewNetAddr(0, ip, 1033, protocol.NodePeerService) + + addrs := []*payload.Net_addr{addr} + + addrmgr.AddAddrs(addrs) + + addrmgr.Failed(addr.IPPort()) + + assert.Equal(t, 1, len(addrmgr.Bad())) // newAddrs was attmepted and failed. Move to Bad + +} +func TestAttemptedMoveFromGoodToBad(t *testing.T) { + + addrmgr := addrmgr.New() + + ip := [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr, _ := payload.NewNetAddr(0, ip, 1043, protocol.NodePeerService) + + addrs := []*payload.Net_addr{addr} + + addrmgr.AddAddrs(addrs) + + addrmgr.ConnectionComplete(addr.IPPort(), true) + addrmgr.ConnectionComplete(addr.IPPort(), true) + addrmgr.ConnectionComplete(addr.IPPort(), true) + + assert.Equal(t, 1, len(addrmgr.Good())) + + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + addrmgr.Failed(addr.IPPort()) + // over threshhold, and will be classed as a badAddr L251 + + assert.Equal(t, 0, len(addrmgr.Good())) + assert.Equal(t, 1, len(addrmgr.Bad())) +} + +func TestGetAddress(t *testing.T) { + + addrmgr := addrmgr.New() + + ip := [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr, _ := payload.NewNetAddr(0, ip, 10333, protocol.NodePeerService) + ip2 := [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr2, _ := payload.NewNetAddr(0, ip2, 10334, protocol.NodePeerService) + ip3 := [16]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // different + addr3, _ := payload.NewNetAddr(0, ip3, 10335, protocol.NodePeerService) + + addrs := []*payload.Net_addr{addr, addr2, addr3} + + addrmgr.AddAddrs(addrs) + + fetchAddr, err := addrmgr.NewAddr() + assert.Equal(t, nil, err) + + ipports := []string{addr.IPPort(), addr2.IPPort(), addr3.IPPort()} + + assert.Contains(t, ipports, fetchAddr) + +} diff --git a/pkg/addrmgr/peers.json b/pkg/addrmgr/peers.json new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/addrmgr/readme.md b/pkg/addrmgr/readme.md new file mode 100644 index 000000000..1121e7972 --- /dev/null +++ b/pkg/addrmgr/readme.md @@ -0,0 +1,23 @@ +# Package - Address Manager + +This package can be used as a standalone to manage addresses on the NEO network. Although you can use it for other chains, the config parameters have been modified for the state of NEO as of now. + +## Responsibility + +To manage the data that the node knows about addresses; data on good addresses, retry rates, failures, and lastSuccessful connection will be managed by the address manager. Also, If a service wants to fetch good address then it will be asked for from the address manager. + + +## Features + +- On GetAddr it will give a list of good addresses to connect to + +- On Addr it will receive addresses and remove any duplicates. + +- General Management of Addresses + +- Periodically saves the peers and metadata about peer into a .json file for retrieval (Not implemented yet) + + +## Note + +The Address manager will not deal with making connections to nodes. Please check the tests for the use cases for this package. diff --git a/pkg/blockchain/blockchain.go b/pkg/blockchain/blockchain.go new file mode 100644 index 000000000..b63641675 --- /dev/null +++ b/pkg/blockchain/blockchain.go @@ -0,0 +1,187 @@ +package blockchain + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + + "github.com/CityOfZion/neo-go/pkg/chainparams" + + "github.com/CityOfZion/neo-go/pkg/database" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" +) + +var ( + ErrBlockValidation = errors.New("Block failed sanity check") + ErrBlockVerification = errors.New("Block failed to be consistent with the current blockchain") +) + +// Blockchain holds the state of the chain +type Chain struct { + db *database.LDB + net protocol.Magic +} + +func New(db *database.LDB, net protocol.Magic) *Chain { + + marker := []byte("HasBeenInitialisedAlready") + + _, err := db.Get(marker) + + if err != nil { + // This is a new db + fmt.Println("New Database initialisation") + db.Put(marker, []byte{}) + + // We add the genesis block into the db + // along with the indexes for it + if net == protocol.MainNet { + + genesisBlock, err := hex.DecodeString(chainparams.GenesisBlock) + if err != nil { + fmt.Println("Could not add genesis header into db") + db.Delete(marker) + return nil + } + r := bytes.NewReader(genesisBlock) + b := payload.Block{} + err = b.Decode(r) + + if err != nil { + fmt.Println("could not Decode genesis block") + db.Delete(marker) + return nil + } + err = db.AddHeader(&b.BlockBase) + if err != nil { + fmt.Println("Could not add genesis header") + db.Delete(marker) + return nil + } + err = db.AddTransactions(b.Hash, b.Txs) + if err != nil { + fmt.Println("Could not add Genesis Transactions") + db.Delete(marker) + return nil + } + } + if net == protocol.TestNet { + fmt.Println("TODO: Setup the genesisBlock for TestNet") + return nil + } + + } + return &Chain{ + db, + net, + } +} +func (c *Chain) AddBlock(msg *payload.BlockMessage) error { + if !validateBlock(msg) { + return ErrBlockValidation + } + + if !c.verifyBlock(msg) { + return ErrBlockVerification + } + + fmt.Println("Block Hash is ", msg.Hash.String()) + + buf := new(bytes.Buffer) + err := msg.Encode(buf) + if err != nil { + return err + } + return c.db.Put(msg.Hash.Bytes(), buf.Bytes()) + +} + +// validateBlock will check the transactions, +// merkleroot is good, signature is good,every that does not require state +// This may be moved to the syncmanager. This function should not be done in a seperate go-routine +// We are intentionally blocking here because if the block is invalid, we will +// disconnect from the peer. +// We could have this return an error instead; where the error could even +// say where the validation failed, for the logs. +func validateBlock(msg *payload.BlockMessage) bool { + return true +} + +func (c *Chain) verifyBlock(msg *payload.BlockMessage) bool { + return true +} + +// This will add a header into the db, +// indexing it also, this method will not +// run any checks, like if it links with a header +// previously in the db +// func (c *Chain) addHeaderNoCheck(header *payload.BlockBase) error { + +// } + +//addHeaders is not safe for concurrent access +func (c *Chain) ValidateHeaders(msg *payload.HeadersMessage) error { + + table := database.NewTable(c.db, database.HEADER) + + latestHash, err := table.Get(database.LATESTHEADER) + if err != nil { + return err + } + + key := latestHash + val, err := table.Get(key) + + lastHeader := &payload.BlockBase{} + err = lastHeader.Decode(bytes.NewReader(val)) + if err != nil { + return err + } + + // TODO?:Maybe we should sort these headers using the Index + // We should not get them in mixed order, but doing it would not be expensive + // If they are already in order + + // Do checks on headers + for _, currentHeader := range msg.Headers { + + if lastHeader == nil { + // This should not happen as genesis header is added if new + // database, however we check nonetheless + return errors.New("Previous Header is nil") + } + + // Check current hash links with previous + if currentHeader.PrevHash != lastHeader.Hash { + return errors.New("Last Header hash != current header Prev hash") + } + + // Check current Index is one more than the previous Index + if currentHeader.Index != lastHeader.Index+1 { + return errors.New("Last Header Index != current header Index") + } + + // Check current timestamp is more than the previous header's timestamp + if lastHeader.Timestamp > currentHeader.Timestamp { + return errors.New("Timestamp of Previous Header is more than Timestamp of current Header") + } + + // NONONO:Do not check if current is more than 15 secs in future + // some blocks had delay from forks in past. + + // NOTE: These are the only non-contextual checks we can do without the blockchain state + lastHeader = currentHeader + } + return nil +} + +func (c *Chain) AddHeaders(msg *payload.HeadersMessage) error { + for _, header := range msg.Headers { + if err := c.db.AddHeader(header); err != nil { + return err + } + } + return nil +} diff --git a/pkg/chainparams/asset.go b/pkg/chainparams/asset.go new file mode 100644 index 000000000..99ff0248b --- /dev/null +++ b/pkg/chainparams/asset.go @@ -0,0 +1,7 @@ +package chainparams + +// GAS is the scripthash native neo asset: GAS +var GAS = "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7" + +// NEO is the scripthash native neo asset: NEO +var NEO = "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b" diff --git a/pkg/chainparams/config.go b/pkg/chainparams/config.go new file mode 100644 index 000000000..8ec89d99f --- /dev/null +++ b/pkg/chainparams/config.go @@ -0,0 +1,51 @@ +package chainparams + +// For now we will just use this to store the +// peers, to test peer data +// Once complete, it will store the genesis params for testnet and mainnet + +var mainnetSeedList = []string{ + "seed1.neo.org:10333", // NOP + "seed2.neo.org:10333", // YEP + "seed3.neo.org:10333", // YEP + "seed4.neo.org:10333", // NOP + "seed5.neo.org:10333", // YEP + "13.59.52.94:10333", // NOP + "18.220.214.143:10333", // NOP + "13.58.198.112:10333", // NOP + "13.59.14.206:10333", // NOP + "18.216.9.7:10333", // NOP +} + +//MainnetSeedList is a string slice containing the initial seeds from protocol.mainnet +// That are replying +var MainnetSeedList = []string{ + "seed2.neo.org:10333", + "seed3.neo.org:10333", + "seed5.neo.org:10333", + "http://seed1.ngd.network:10333", + "http://seed2.ngd.network:10333", + "http://seed3.ngd.network:10333", + "http://seed4.ngd.network:10333", + "http://seed5.ngd.network:10333", + "http://seed6.ngd.network:10333", + "http://seed7.ngd.network:10333", + "http://seed8.ngd.network:10333", + "http://seed9.ngd.network:10333", + "http://seed10.ngd.network:10333", +} + +var testNetSeedList = []string{ + + "18.218.97.227:20333", + "18.219.30.120:20333", + "18.219.13.91:20333", + "13.59.116.121:20333", + "18.218.255.178:20333", +} + +// GenesisHash for mainnet +var GenesisHash = "d42561e3d30e15be6400b6df2f328e02d2bf6354c41dce433bc57687c82144bf" + +// GenesisBlock for mainnet +var GenesisBlock = "000000000000000000000000000000000000000000000000000000000000000000000000f41bc036e39b0d6b0579c851c6fde83af802fa4e57bec0bc3365eae3abf43f8065fc8857000000001dac2b7c0000000059e75d652b5d3827bf04c165bbe9ef95cca4bf55010001510400001dac2b7c00000000400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000400001445b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e5b881227d2c7b226c616e67223a22656e222c226e616d65223a22416e74436f696e227d5d0000c16ff286230008009f7fd096d37ed2c0e3f7f0cfc924beef4ffceb680000000001000000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc50000c16ff28623005fa99d93303775fe50ca119c327759313eccfa1c01000151" diff --git a/pkg/connmgr/config.go b/pkg/connmgr/config.go new file mode 100644 index 000000000..e73f99245 --- /dev/null +++ b/pkg/connmgr/config.go @@ -0,0 +1,25 @@ +package connmgr + +import ( + "net" +) + +// Config contains all methods which will be set by the caller to setup the connection manager. +type Config struct { + // GetAddress will return a single address for the connection manager to connect to + GetAddress func() (string, error) + + // OnConnection is called by the connection manager when + // we successfully connect to a peer + // The caller should ideally inform the address manager that we have connected to this address in this function + OnConnection func(conn net.Conn, addr string) + + // OnAccept will take a established connection + OnAccept func(net.Conn) + + // Port is the port in the format "10333" + Port string + + // DialTimeout is the amount of time, before we can disconnect a pending dialed connection + DialTimeout int +} diff --git a/pkg/connmgr/connmgr.go b/pkg/connmgr/connmgr.go new file mode 100644 index 000000000..4d75b0c09 --- /dev/null +++ b/pkg/connmgr/connmgr.go @@ -0,0 +1,272 @@ +package connmgr + +import ( + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/util/ip" +) + +var ( + // maxOutboundConn is the maximum number of active peers + // that the connection manager will try to have + maxOutboundConn = 10 + + // maxRetries is the maximum amount of successive retries that + // we can have before we stop dialing that peer + maxRetries = uint8(5) +) + +// Connmgr manages pending/active/failed cnnections +type Connmgr struct { + config Config + PendingList map[string]*Request + ConnectedList map[string]*Request + actionch chan func() +} + +//New creates a new connection manager +func New(cfg Config) *Connmgr { + cnnmgr := &Connmgr{ + cfg, + make(map[string]*Request), + make(map[string]*Request), + make(chan func(), 300), + } + + go func() { + + ip := iputils.GetLocalIP() + addrPort := ip.String() + ":" + cfg.Port + + listener, err := net.Listen("tcp", addrPort) + + if err != nil { + fmt.Println("Error connecting to outbound ", err) + } + + defer func() { + listener.Close() + }() + + for { + + conn, err := listener.Accept() + + if err != nil { + continue + } + // TODO(kev): in the OnAccept the connection address will be added to AddrMgr + go cfg.OnAccept(conn) + } + + }() + + return cnnmgr +} + +// NewRequest will make a new connection +// Gets the address from address func in config +// Then dials it and assigns it to pending +func (c *Connmgr) NewRequest() { + + // Fetch address + addr, err := c.config.GetAddress() + if err != nil { + fmt.Println("Error getting address", err) + } + + // empty request item + r := &Request{} + + r.Addr = addr + fmt.Println("Connecting") + c.Connect(r) + +} + +func (c *Connmgr) Connect(r *Request) error { + + r.Retries++ + + conn, err := c.Dial(r.Addr) + if err != nil { + c.failed(r) + return err + } + + r.Conn = conn + r.Inbound = true + + // r.Permanent is set by the caller. default is false + // The permanent connections will be the ones that are hardcoded, e.g seed3.ngd.network + + return c.connected(r) +} + +func (cm *Connmgr) Disconnect(addr string) { + + // fetch from connected list + r, ok := cm.ConnectedList[addr] + + if !ok { + // If not in connected, check pending + r, ok = cm.PendingList[addr] + } + + cm.disconnected(r) + +} + +// Dial is used to dial up connections given the addres and ip in the form address:port +func (c *Connmgr) Dial(addr string) (net.Conn, error) { + dialTimeout := 1 * time.Second + conn, err := net.DialTimeout("tcp", addr, dialTimeout) + if err != nil { + if !isConnected() { + return nil, errors.New("Fatal Error: You do not seem to be connected to the internet") + } + return conn, err + } + return conn, nil +} +func (cm *Connmgr) failed(r *Request) { + + cm.actionch <- func() { + // priority to check if it is permanent or inbound + // if so then these peers are valuable in NEO and so we will just retry another time + if r.Inbound || r.Permanent { + + multiplier := time.Duration(r.Retries * 10) + time.AfterFunc(multiplier*time.Second, + func() { + cm.Connect(r) + }, + ) + // if not then we should check if this request has had maxRetries + // if it has then get a new address + // if not then call Connect on it again + } else if r.Retries > maxRetries { + if cm.config.GetAddress != nil { + go cm.NewRequest() + } + fmt.Println("This peer has been tried the maximum amount of times and a source of new address has not been specified.") + } else { + go cm.Connect(r) + } + + } + +} + +// Disconnected is called when a peer disconnects. +// we take the addr from peer, which is also it's key in the map +// and we use it to remove it from the connectedList +func (c *Connmgr) disconnected(r *Request) error { + + errChan := make(chan error, 0) + + c.actionch <- func() { + + var err error + + if r == nil { + err = errors.New("Request object is nil") + } + + r2 := *r // dereference it, so that r.Addr is not lost on delete + + // if for some reason the underlying connection is not closed, close it + r.Conn.Close() + r.Conn = nil + // if for some reason it is in pending list, remove it + delete(c.PendingList, r.Addr) + delete(c.ConnectedList, r.Addr) + c.failed(&r2) + errChan <- err + } + + return <-errChan +} + +//Connected is called when the connection manager +// makes a successful connection. +func (c *Connmgr) connected(r *Request) error { + + errorChan := make(chan error, 0) + + c.actionch <- func() { + + var err error + + // This should not be the case, since we connected + // Keeping it here to be safe + if r == nil { + err = errors.New("Request object as nil inside of the connected function") + } + + // reset retries to 0 + r.Retries = 0 + + // add to connectedList + c.ConnectedList[r.Addr] = r + + // remove from pending if it was there + delete(c.PendingList, r.Addr) + + if c.config.OnConnection != nil { + c.config.OnConnection(r.Conn, r.Addr) + } + + fmt.Println("Error connected", err) + + errorChan <- err + } + return <-errorChan +} + +// Pending is synchronous, we do not want to continue with logic +// until we are certain it has been added to the pendingList +func (c *Connmgr) pending(r *Request) error { + + errChan := make(chan error, 0) + + c.actionch <- func() { + + var err error + + if r == nil { + err = errors.New("Error : Request object is nil") + } + + c.PendingList[r.Addr] = r + errChan <- err + } + + return <-errChan +} + +func (c *Connmgr) Run() { + go c.loop() +} + +func (c *Connmgr) loop() { + for { + select { + case f := <-c.actionch: + f() + } + } +} + +// https://stackoverflow.com/questions/50056144/check-for-internet-connection-from-application +func isConnected() (ok bool) { + _, err := http.Get("http://clients3.google.com/generate_204") + if err != nil { + return false + } + return true +} diff --git a/pkg/connmgr/connmgr_test.go b/pkg/connmgr/connmgr_test.go new file mode 100644 index 000000000..fbc591263 --- /dev/null +++ b/pkg/connmgr/connmgr_test.go @@ -0,0 +1,106 @@ +package connmgr_test + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/connmgr" + "github.com/stretchr/testify/assert" +) + +func TestDial(t *testing.T) { + cfg := connmgr.Config{ + GetAddress: nil, + OnConnection: nil, + OnAccept: nil, + Port: "", + DialTimeout: 0, + } + + cm := connmgr.New(cfg) + cm.Run() + + ipport := "google.com:80" // google unlikely to go offline, a better approach to test Dialing is welcome. + + conn, err := cm.Dial(ipport) + assert.Equal(t, nil, err) + assert.NotEqual(t, nil, conn) +} +func TestConnect(t *testing.T) { + cfg := connmgr.Config{ + GetAddress: nil, + OnConnection: nil, + OnAccept: nil, + Port: "", + DialTimeout: 0, + } + + cm := connmgr.New(cfg) + cm.Run() + + ipport := "google.com:80" + + r := connmgr.Request{Addr: ipport} + + cm.Connect(&r) + + assert.Equal(t, 1, len(cm.ConnectedList)) + +} +func TestNewRequest(t *testing.T) { + + address := "google.com:80" + + var getAddr = func() (string, error) { + return address, nil + } + + cfg := connmgr.Config{ + GetAddress: getAddr, + OnConnection: nil, + OnAccept: nil, + Port: "", + DialTimeout: 0, + } + + cm := connmgr.New(cfg) + + cm.Run() + + cm.NewRequest() + + if _, ok := cm.ConnectedList[address]; ok { + assert.Equal(t, true, ok) + assert.Equal(t, 1, len(cm.ConnectedList)) + return + } + + assert.Fail(t, "Could not find the address in the connected lists") + +} +func TestDisconnect(t *testing.T) { + + address := "google.com:80" + + var getAddr = func() (string, error) { + return address, nil + } + + cfg := connmgr.Config{ + GetAddress: getAddr, + OnConnection: nil, + OnAccept: nil, + Port: "", + DialTimeout: 0, + } + + cm := connmgr.New(cfg) + + cm.Run() + + cm.NewRequest() + + cm.Disconnect(address) + + assert.Equal(t, 0, len(cm.ConnectedList)) + +} diff --git a/pkg/connmgr/readme.md b/pkg/connmgr/readme.md new file mode 100644 index 000000000..d421b55f3 --- /dev/null +++ b/pkg/connmgr/readme.md @@ -0,0 +1,26 @@ +# Package - Connection Manager + +## Responsibility + +- Manages the active, failed and pending connections for the node. + +## Features + +- Takes an address, dials it and packages it into a request to manage. + +- Retry failed connections. + +- Uses one function as a source for it's addresses. It does not manage addresses. + + +## Usage + +The following methods are exposed from the Connection manager: + +- NewRequest() : This will fetch a new address and connect to it. + +- Connect(r *Request) : This takes a Request object and connects to it. It follow the same logic as NewRequest() however instead of getting the address from the datasource given upon initialisation, you directly feed the address you want to connect to. + +- Disconnect(addrport string) : Given an address:port, this will disconnect it, close the connection and remove it from the connected and pending list, if it was there. + +- Dial(addrport string) (net.Conn, error) : Given an address:port, this will connect to it and return a pointer to a connection plus a nil error if successful, or nil with an error. \ No newline at end of file diff --git a/pkg/connmgr/request.go b/pkg/connmgr/request.go new file mode 100644 index 000000000..5062064be --- /dev/null +++ b/pkg/connmgr/request.go @@ -0,0 +1,13 @@ +package connmgr + +import ( + "net" +) + +type Request struct { + Conn net.Conn + Addr string + Permanent bool + Inbound bool + Retries uint8 // should not be trying more than 255 tries +} diff --git a/pkg/crypto/aes/aes256.go b/pkg/crypto/aes/aes256.go new file mode 100755 index 000000000..6ef1b5c74 --- /dev/null +++ b/pkg/crypto/aes/aes256.go @@ -0,0 +1,93 @@ +package aes + +import ( + "crypto/aes" + "crypto/cipher" +) + +// AESEncrypt encrypts the key with the given source. +func Encrypt(src, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + ecb := newECBEncrypter(block) + out := make([]byte, len(src)) + ecb.CryptBlocks(out, src) + + return out, nil +} + +// AESDecrypt decrypts the encrypted source with the given key. +func Decrypt(crypted, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + blockMode := newECBDecrypter(block) + out := make([]byte, len(crypted)) + blockMode.CryptBlocks(out, crypted) + return out, nil +} + +type ecb struct { + b cipher.Block + blockSize int +} + +func newECB(b cipher.Block) *ecb { + return &ecb{ + b: b, + blockSize: b.BlockSize(), + } +} + +type ecbEncrypter ecb + +func newECBEncrypter(b cipher.Block) cipher.BlockMode { + return (*ecbEncrypter)(newECB(b)) +} + +func (ecb *ecbEncrypter) BlockSize() int { + return ecb.blockSize +} + +func (ecb *ecbEncrypter) CryptBlocks(dst, src []byte) { + if len(src)%ecb.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + ecb.b.Encrypt(dst, src[:ecb.blockSize]) + src = src[ecb.blockSize:] + dst = dst[ecb.blockSize:] + } +} + +type ecbDecrypter ecb + +func newECBDecrypter(b cipher.Block) cipher.BlockMode { + return (*ecbDecrypter)(newECB(b)) +} + +func (ecb ecbDecrypter) BlockSize() int { + return ecb.blockSize +} + +func (ecb *ecbDecrypter) CryptBlocks(dst, src []byte) { + if len(src)%ecb.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + ecb.b.Decrypt(dst, src[:ecb.blockSize]) + src = src[ecb.blockSize:] + dst = dst[ecb.blockSize:] + } +} diff --git a/pkg/crypto/base58/base58.go b/pkg/crypto/base58/base58.go new file mode 100755 index 000000000..a62181a72 --- /dev/null +++ b/pkg/crypto/base58/base58.go @@ -0,0 +1,78 @@ +package base58 + +import ( + "fmt" + "math/big" +) + +const prefix rune = '1' + +var decodeMap = map[rune]int64{ + '1': 0, '2': 1, '3': 2, '4': 3, '5': 4, + '6': 5, '7': 6, '8': 7, '9': 8, 'A': 9, + 'B': 10, 'C': 11, 'D': 12, 'E': 13, 'F': 14, + 'G': 15, 'H': 16, 'J': 17, 'K': 18, 'L': 19, + 'M': 20, 'N': 21, 'P': 22, 'Q': 23, 'R': 24, + 'S': 25, 'T': 26, 'U': 27, 'V': 28, 'W': 29, + 'X': 30, 'Y': 31, 'Z': 32, 'a': 33, 'b': 34, + 'c': 35, 'd': 36, 'e': 37, 'f': 38, 'g': 39, + 'h': 40, 'i': 41, 'j': 42, 'k': 43, 'm': 44, + 'n': 45, 'o': 46, 'p': 47, 'q': 48, 'r': 49, + 's': 50, 't': 51, 'u': 52, 'v': 53, 'w': 54, + 'x': 55, 'y': 56, 'z': 57, +} + +// Base58Decode decodes the base58 encoded string. +func Decode(s string) ([]byte, error) { + var ( + startIndex = 0 + zero = 0 + ) + for i, c := range s { + if c == prefix { + zero++ + } else { + startIndex = i + break + } + } + + var ( + n = big.NewInt(0) + div = big.NewInt(58) + ) + for _, c := range s[startIndex:] { + charIndex, ok := decodeMap[c] + if !ok { + return nil, fmt.Errorf( + "invalid character '%c' when decoding this base58 string: '%s'", c, s, + ) + } + n.Add(n.Mul(n, div), big.NewInt(charIndex)) + } + + out := n.Bytes() + buf := make([]byte, (zero + len(out))) + copy(buf[zero:], out[:]) + + return buf, nil +} + +// Base58Encode encodes a byte slice to be a base58 encoded string. +func Encode(bytes []byte) string { + var ( + lookupTable = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + x = new(big.Int).SetBytes(bytes) + r = new(big.Int) + m = big.NewInt(58) + zero = big.NewInt(0) + encoded string + ) + + for x.Cmp(zero) > 0 { + x.QuoRem(x, m, r) + encoded = string(lookupTable[r.Int64()]) + encoded + } + + return encoded +} diff --git a/pkg/crypto/base58/base58_test.go b/pkg/crypto/base58/base58_test.go new file mode 100755 index 000000000..524f0e55e --- /dev/null +++ b/pkg/crypto/base58/base58_test.go @@ -0,0 +1,32 @@ +package base58 + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecode(t *testing.T) { + input := "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" + + data, err := Decode(input) + if err != nil { + t.Fatal(err) + } + + expected := "0099bc78ba577a95a11f1a344d4d2ae55f2f857b989ea5e5e2" + actual := hex.EncodeToString(data) + assert.Equal(t, expected, actual) +} +func TestEncode(t *testing.T) { + input := "0099bc78ba577a95a11f1a344d4d2ae55f2f857b989ea5e5e2" + + inputBytes, _ := hex.DecodeString(input) + + data := Encode(inputBytes) + + expected := "F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" // Removed the 1 as it is not checkEncoding + actual := data + assert.Equal(t, expected, actual) +} diff --git a/pkg/crypto/elliptic/Readme.md b/pkg/crypto/elliptic/Readme.md new file mode 100755 index 000000000..ea1033488 --- /dev/null +++ b/pkg/crypto/elliptic/Readme.md @@ -0,0 +1,18 @@ +## Package - Elliptic + +### Why + +The curve and arithmetic functions have been modularised, so that curves can be swapped in and out, without effecting the functionality. + +The modular arithmetic used is not specialised for a specific curve. + +In order to use this package, you must declare an ellipticcurve struct and then set the curve. + +Example: + +` + + curve = NewEllipticCurve(Secp256k1) + +` +If no curve is set, the default curve is the r1 curve used for NEO. The tests are done using the k1 curve, so in the elliptic_test.go file, the curve is changed accordingly. diff --git a/pkg/crypto/elliptic/curves.go b/pkg/crypto/elliptic/curves.go new file mode 100755 index 000000000..33f19540b --- /dev/null +++ b/pkg/crypto/elliptic/curves.go @@ -0,0 +1,58 @@ +package elliptic + +/* + This file was originally made by vsergeev. + + Modifications have been made under the MIT license. + License: MIT + + +*/ + +import ( + "math/big" +) + +var curve Curve + +type curveType string + +const ( + Secp256r1 curveType = "Secp256r1" + Secp256k1 curveType = "Secp256k1" +) + +func (ChosenCurve *Curve) SetCurveSecp256r1() { + ChosenCurve.P, _ = new(big.Int).SetString("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16) //Q + ChosenCurve.A, _ = new(big.Int).SetString("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16) + ChosenCurve.B, _ = new(big.Int).SetString("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16) + ChosenCurve.G.X, _ = new(big.Int).SetString("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16) + ChosenCurve.G.Y, _ = new(big.Int).SetString("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16) + ChosenCurve.N, _ = new(big.Int).SetString("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16) + ChosenCurve.H, _ = new(big.Int).SetString("01", 16) + ChosenCurve.Name = "Secp256r1" +} + +func (ChosenCurve *Curve) SetCurveSecp256k1() { + ChosenCurve.P, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16) + ChosenCurve.A, _ = new(big.Int).SetString("0000000000000000000000000000000000000000000000000000000000000000", 16) + ChosenCurve.B, _ = new(big.Int).SetString("0000000000000000000000000000000000000000000000000000000000000007", 16) + ChosenCurve.G.X, _ = new(big.Int).SetString("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16) + ChosenCurve.G.Y, _ = new(big.Int).SetString("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16) + ChosenCurve.N, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16) + ChosenCurve.H, _ = new(big.Int).SetString("01", 16) + ChosenCurve.Name = "Secp256k1" +} + +func NewEllipticCurve(ct curveType) Curve { + var curve Curve + switch ct { + case Secp256k1: + curve.SetCurveSecp256k1() + case Secp256r1: + curve.SetCurveSecp256r1() + default: + curve.SetCurveSecp256r1() + } + return curve +} diff --git a/pkg/crypto/elliptic/elliptic.go b/pkg/crypto/elliptic/elliptic.go new file mode 100755 index 000000000..26a07388f --- /dev/null +++ b/pkg/crypto/elliptic/elliptic.go @@ -0,0 +1,317 @@ +/* +This file has been modified under the MIT license. +Original: https://github.com/vsergeev/btckeygenie +*/ + +package elliptic + +import ( + nativeelliptic "crypto/elliptic" + "encoding/hex" + "errors" + "fmt" + "math/big" +) + +// Point represents a point on an EllipticCurve. +type Point struct { + X *big.Int + Y *big.Int +} + +/* y**2 = x**3 + a*x + b % p */ +// Curve represents the parameters of a short Weierstrass equation elliptic curve. +type Curve struct { + A *big.Int + B *big.Int + P *big.Int + G Point + N *big.Int + H *big.Int + Name string +} + +// dump dumps the bytes of a point for debugging. +func (p *Point) dump() { + fmt.Print(p.format()) +} + +// format formats the bytes of a point for debugging. +func (p *Point) format() string { + if p.X == nil && p.Y == nil { + return "(inf,inf)" + } + return fmt.Sprintf("(%s,%s)", hex.EncodeToString(p.X.Bytes()), hex.EncodeToString(p.Y.Bytes())) +} + +func (ec Curve) Params() *nativeelliptic.CurveParams { + return &nativeelliptic.CurveParams{ + P: ec.P, + N: ec.N, + B: ec.B, + Gx: ec.G.X, + Gy: ec.G.Y, + BitSize: 256, + Name: ec.Name, + } +} + +/*** Modular Arithmetic ***/ + +/* NOTE: Returning a new z each time below is very space inefficient, but the + * alternate accumulator based design makes the point arithmetic functions look + * absolutely hideous. I may still change this in the future. */ + +// addMod computes z = (x + y) % p. +func addMod(x *big.Int, y *big.Int, p *big.Int) (z *big.Int) { + z = new(big.Int).Add(x, y) + z.Mod(z, p) + return z +} + +// subMod computes z = (x - y) % p. +func subMod(x *big.Int, y *big.Int, p *big.Int) (z *big.Int) { + z = new(big.Int).Sub(x, y) + z.Mod(z, p) + return z +} + +// mulMod computes z = (x * y) % p. +func mulMod(x *big.Int, y *big.Int, p *big.Int) (z *big.Int) { + n := new(big.Int).Set(x) + z = big.NewInt(0) + + for i := 0; i < y.BitLen(); i++ { + if y.Bit(i) == 1 { + z = addMod(z, n, p) + } + n = addMod(n, n, p) + } + + return z +} + +// invMod computes z = (1/x) % p. +func invMod(x *big.Int, p *big.Int) (z *big.Int) { + z = new(big.Int).ModInverse(x, p) + return z +} + +// expMod computes z = (x^e) % p. +func expMod(x *big.Int, y *big.Int, p *big.Int) (z *big.Int) { + z = new(big.Int).Exp(x, y, p) + return z +} + +// sqrtMod computes z = sqrt(x) % p. +func sqrtMod(x *big.Int, p *big.Int) (z *big.Int) { + /* assert that p % 4 == 3 */ + if new(big.Int).Mod(p, big.NewInt(4)).Cmp(big.NewInt(3)) != 0 { + panic("p is not equal to 3 mod 4!") + } + + /* z = sqrt(x) % p = x^((p+1)/4) % p */ + + /* e = (p+1)/4 */ + e := new(big.Int).Add(p, big.NewInt(1)) + e = e.Rsh(e, 2) + + z = expMod(x, e, p) + return z +} + +/*** Point Arithmetic on Curve ***/ + +// IsInfinity checks if point P is infinity on EllipticCurve ec. +func (ec *Curve) IsInfinity(P Point) bool { + /* We use (nil,nil) to represent O, the point at infinity. */ + + if P.X == nil && P.Y == nil { + return true + } + + return false +} + +// IsOnCurve checks if point P is on EllipticCurve ec. +func (ec Curve) IsOnCurve(P1, P2 *big.Int) bool { + P := Point{P1, P2} + if ec.IsInfinity(P) { + return false + } + + /* y**2 = x**3 + a*x + b % p */ + lhs := mulMod(P.Y, P.Y, ec.P) + rhs := addMod( + addMod( + expMod(P.X, big.NewInt(3), ec.P), + mulMod(ec.A, P.X, ec.P), ec.P), + ec.B, ec.P) + + if lhs.Cmp(rhs) == 0 { + return true + } + + return false +} + +// Add computes R = P + Q on EllipticCurve ec. +func (ec Curve) Add(P1, P2, Q1, Q2 *big.Int) (R1 *big.Int, R2 *big.Int) { + /* See rules 1-5 on SEC1 pg.7 http://www.secg.org/collateral/sec1_final.pdf */ + P := Point{P1, P2} + Q := Point{Q1, Q2} + R := Point{} + if ec.IsInfinity(P) && ec.IsInfinity(Q) { + /* Rule #1 Identity */ + /* R = O + O = O */ + + R.X = nil + R.Y = nil + + } else if ec.IsInfinity(P) { + /* Rule #2 Identity */ + /* R = O + Q = Q */ + + R.X = new(big.Int).Set(Q.X) + R.Y = new(big.Int).Set(Q.Y) + + } else if ec.IsInfinity(Q) { + /* Rule #2 Identity */ + /* R = P + O = P */ + + R.X = new(big.Int).Set(P.X) + R.Y = new(big.Int).Set(P.Y) + + } else if P.X.Cmp(Q.X) == 0 && addMod(P.Y, Q.Y, ec.P).Sign() == 0 { + /* Rule #3 Identity */ + /* R = (x,y) + (x,-y) = O */ + + R.X = nil + R.Y = nil + + } else if P.X.Cmp(Q.X) == 0 && P.Y.Cmp(Q.Y) == 0 && P.Y.Sign() != 0 { + /* Rule #5 Point doubling */ + /* R = P + P */ + + /* Lambda = (3*P.X*P.X + a) / (2*P.Y) */ + num := addMod( + mulMod(big.NewInt(3), + mulMod(P.X, P.X, ec.P), ec.P), + ec.A, ec.P) + den := invMod(mulMod(big.NewInt(2), P.Y, ec.P), ec.P) + lambda := mulMod(num, den, ec.P) + + /* R.X = lambda*lambda - 2*P.X */ + R.X = subMod( + mulMod(lambda, lambda, ec.P), + mulMod(big.NewInt(2), P.X, ec.P), + ec.P) + /* R.Y = lambda*(P.X - R.X) - P.Y */ + R.Y = subMod( + mulMod(lambda, subMod(P.X, R.X, ec.P), ec.P), + P.Y, ec.P) + + } else if P.X.Cmp(Q.X) != 0 { + /* Rule #4 Point addition */ + /* R = P + Q */ + + /* Lambda = (Q.Y - P.Y) / (Q.X - P.X) */ + num := subMod(Q.Y, P.Y, ec.P) + den := invMod(subMod(Q.X, P.X, ec.P), ec.P) + lambda := mulMod(num, den, ec.P) + + /* R.X = lambda*lambda - P.X - Q.X */ + R.X = subMod( + subMod( + mulMod(lambda, lambda, ec.P), + P.X, ec.P), + Q.X, ec.P) + + /* R.Y = lambda*(P.X - R.X) - P.Y */ + R.Y = subMod( + mulMod(lambda, + subMod(P.X, R.X, ec.P), ec.P), + P.Y, ec.P) + } else { + panic(fmt.Sprintf("Unsupported point addition: %v + %v", P.format(), Q.format())) + } + + return R.X, R.Y +} + +// ScalarMult computes Q = k * P on EllipticCurve ec. +func (ec Curve) ScalarMult(P1, P2 *big.Int, l []byte) (Q1, Q2 *big.Int) { + /* Note: this function is not constant time, due to the branching nature of + * the underlying point Add() function. */ + + /* Montgomery Ladder Point Multiplication + * + * Implementation based on pseudocode here: + * See https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder */ + + P := Point{P1, P2} + k := big.Int{} + k.SetBytes(l) + + var R0 Point + var R1 Point + + R0.X = nil + R0.Y = nil + R1.X = new(big.Int).Set(P.X) + R1.Y = new(big.Int).Set(P.Y) + + for i := ec.N.BitLen() - 1; i >= 0; i-- { + if k.Bit(i) == 0 { + R1.X, R1.Y = ec.Add(R0.X, R0.Y, R1.X, R1.Y) + R0.X, R0.Y = ec.Add(R0.X, R0.Y, R0.X, R0.Y) + } else { + R0.X, R0.Y = ec.Add(R0.X, R0.Y, R1.X, R1.Y) + R1.X, R1.Y = ec.Add(R1.X, R1.Y, R1.X, R1.Y) + } + } + + return R0.X, R0.Y +} + +// ScalarBaseMult computes Q = k * G on EllipticCurve ec. +func (ec Curve) ScalarBaseMult(k []byte) (Q1, Q2 *big.Int) { + + return ec.ScalarMult(ec.G.X, ec.G.Y, k) +} + +// Decompress decompresses coordinate x and ylsb (y's least significant bit) into a Point P on EllipticCurve ec. +func (ec *Curve) Decompress(x *big.Int, ylsb uint) (P Point, err error) { + /* y**2 = x**3 + a*x + b % p */ + rhs := addMod( + addMod( + expMod(x, big.NewInt(3), ec.P), + mulMod(ec.A, x, ec.P), + ec.P), + ec.B, ec.P) + + /* y = sqrt(rhs) % p */ + y := sqrtMod(rhs, ec.P) + + /* Use -y if opposite lsb is required */ + if y.Bit(0) != (ylsb & 0x1) { + y = subMod(big.NewInt(0), y, ec.P) + } + + P.X = x + P.Y = y + + if !ec.IsOnCurve(P.X, P.Y) { + return P, errors.New("Compressed (x, ylsb) not on curve.") + } + + return P, nil +} + +func (ec Curve) Double(x1, y1 *big.Int) (x, y *big.Int) { + x = &big.Int{} + x.SetBytes([]byte{0x00}) + y = &big.Int{} + y.SetBytes([]byte{0x00}) + return x, y +} diff --git a/pkg/crypto/elliptic/elliptic_test.go b/pkg/crypto/elliptic/elliptic_test.go new file mode 100755 index 000000000..fa83dd034 --- /dev/null +++ b/pkg/crypto/elliptic/elliptic_test.go @@ -0,0 +1,231 @@ +/* btckeygenie v1.0.0 + * https://github.com/vsergeev/btckeygenie + * License: MIT + */ + +package elliptic + +import ( + "encoding/hex" + "math/big" + "testing" +) + +func init() { + + curve = NewEllipticCurve(Secp256k1) +} + +func hex2int(hexstring string) (v *big.Int) { + v, _ = new(big.Int).SetString(hexstring, 16) + return v +} + +func TestOnCurve(t *testing.T) { + if !curve.IsOnCurve(curve.G.X, curve.G.Y) { + t.Fatal("failure G on curve") + } + + t.Log("G on curve") +} + +func TestInfinity(t *testing.T) { + O := Point{nil, nil} + + /* O not on curve */ + if curve.IsOnCurve(O.X, O.Y) { + t.Fatal("failure O on curve") + } + + /* O is infinity */ + if !curve.IsInfinity(O) { + t.Fatal("failure O not infinity on curve") + } + + t.Log("O is not on curve and is infinity") +} + +func TestPointAdd(t *testing.T) { + X := "50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352" + Y := "2cd470243453a299fa9e77237716103abc11a1df38855ed6f2ee187e9c582ba6" + + P := Point{hex2int(X), hex2int(Y)} + O := Point{nil, nil} + + /* R = O + O = O */ + { + R1, R2 := curve.Add(O.X, O.Y, O.X, O.Y) + R := Point{R1, R2} + if !curve.IsInfinity(R) { + t.Fatal("failure O + O = O") + } + t.Log("success O + O = O") + } + + /* R = P + O = P */ + { + R1, R2 := curve.Add(P.X, P.Y, O.X, O.Y) + R := Point{R1, R2} + if R.X.Cmp(P.X) != 0 || R.Y.Cmp(P.Y) != 0 { + t.Fatal("failure P + O = P") + } + t.Log("success P + O = P") + } + + /* R = O + Q = Q */ + { + R1, R2 := curve.Add(O.X, O.Y, P.X, P.Y) + R := Point{R1, R2} + if R.X.Cmp(P.X) != 0 || R.Y.Cmp(P.Y) != 0 { + t.Fatal("failure O + Q = Q") + } + t.Log("success O + Q = Q") + } + + /* R = (x,y) + (x,-y) = O */ + { + Q := Point{P.X, subMod(big.NewInt(0), P.Y, curve.P)} + + R1, R2 := curve.Add(P.X, P.Y, Q.X, Q.Y) + R := Point{R1, R2} + if !curve.IsInfinity(R) { + t.Fatal("failure (x,y) + (x,-y) = O") + } + t.Log("success (x,y) + (x,-y) = O") + } + + /* R = P + P */ + { + PP := Point{hex2int("5dbcd5dfea550eb4fd3b5333f533f086bb5267c776e2a1a9d8e84c16a6743d82"), hex2int("8dde3986b6cbe395da64b6e95fb81f8af73f6e0cf1100555005bb4ba2a6a4a07")} + + R1, R2 := curve.Add(P.X, P.Y, P.X, P.Y) + R := Point{R1, R2} + if R.X.Cmp(PP.X) != 0 || R.Y.Cmp(PP.Y) != 0 { + t.Fatal("failure P + P") + } + t.Log("success P + P") + } + + Q := Point{hex2int("a83b8de893467d3a88d959c0eb4032d9ce3bf80f175d4d9e75892a3ebb8ab7e5"), hex2int("370f723328c24b7a97fe34063ba68f253fb08f8645d7c8b9a4ff98e3c29e7f0d")} + PQ := Point{hex2int("fe7d540002e4355eb0ec36c217b4735495de7bd8634055ded3683b0e9da70ef1"), hex2int("fc033c1d74cb34e087a3495e505c0fc0e9e3e8297994878d89d882254ce8a9ef")} + + /* R = P + Q */ + { + R1, R2 := curve.Add(P.X, P.Y, Q.X, Q.Y) + R := Point{R1, R2} + if R.X.Cmp(PQ.X) != 0 || R.Y.Cmp(PQ.Y) != 0 { + t.Fatal("failure P + Q") + } + t.Log("success P + Q") + } + + /* R = Q + P */ + { + R1, R2 := curve.Add(Q.X, Q.Y, P.X, P.Y) + R := Point{R1, R2} + if R.X.Cmp(PQ.X) != 0 || R.Y.Cmp(PQ.Y) != 0 { + t.Fatal("failure Q + P") + } + t.Log("success Q + P") + } +} + +func TestPointScalarMult(t *testing.T) { + X := "50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352" + Y := "2cd470243453a299fa9e77237716103abc11a1df38855ed6f2ee187e9c582ba6" + P := Point{hex2int(X), hex2int(Y)} + + /* Q = k*P */ + { + T := Point{hex2int("87d592bfdd24adb52147fea343db93e10d0585bc66d91e365c359973c0dc7067"), hex2int("a374e206cb7c8cd1074bdf9bf6ddea135f983aaa6475c9ab3bb4c38a0046541b")} + input, _ := hex.DecodeString("14eb373700c3836404acd0820d9fa8dfa098d26177ca6e18b1c7f70c6af8fc18") + + Q1, Q2 := curve.ScalarMult(P.X, P.Y, input) + Q := Point{Q1, Q2} + if Q.X.Cmp(T.X) != 0 || Q.Y.Cmp(T.Y) != 0 { + t.Fatal("failure k*P") + } + t.Log("success k*P") + } + + /* Q = n*G = O */ + { + Q1, Q2 := curve.ScalarMult(curve.G.X, curve.G.Y, curve.N.Bytes()) + Q := Point{Q1, Q2} + if !curve.IsInfinity(Q) { + t.Fatal("failure n*G = O") + } + t.Log("success n*G = O") + } +} + +func TestPointScalarBaseMult(t *testing.T) { + /* Sample Private Key */ + D := "18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725" + /* Sample Corresponding Public Key */ + X := "50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352" + Y := "2cd470243453a299fa9e77237716103abc11a1df38855ed6f2ee187e9c582ba6" + + P := Point{hex2int(X), hex2int(Y)} + + /* Q = d*G = P */ + + Q1, Q2 := curve.ScalarBaseMult(hex2int(D).Bytes()) + Q := Point{Q1, Q2} + if P.X.Cmp(Q.X) != 0 || P.Y.Cmp(Q.Y) != 0 { + t.Fatal("failure Q = d*G") + } + t.Log("success Q = d*G") + + /* Q on curve */ + if !curve.IsOnCurve(Q.X, Q.Y) { + t.Fatal("failure Q on curve") + } + t.Log("success Q on curve") + + /* R = 0*G = O */ + R1, R2 := curve.ScalarBaseMult(big.NewInt(0).Bytes()) + R := Point{R1, R2} + if !curve.IsInfinity(R) { + t.Fatal("failure 0*G = O") + } + t.Log("success 0*G = O") +} + +func TestPointDecompress(t *testing.T) { + /* Valid points */ + var validDecompressVectors = []Point{ + {hex2int("50863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352"), hex2int("2cd470243453a299fa9e77237716103abc11a1df38855ed6f2ee187e9c582ba6")}, + {hex2int("a83b8de893467d3a88d959c0eb4032d9ce3bf80f175d4d9e75892a3ebb8ab7e5"), hex2int("370f723328c24b7a97fe34063ba68f253fb08f8645d7c8b9a4ff98e3c29e7f0d")}, + {hex2int("f680556678e25084a82fa39e1b1dfd0944f7e69fddaa4e03ce934bd6b291dca0"), hex2int("52c10b721d34447e173721fb0151c68de1106badb089fb661523b8302a9097f5")}, + {hex2int("241febb8e23cbd77d664a18f66ad6240aaec6ecdc813b088d5b901b2e285131f"), hex2int("513378d9ff94f8d3d6c420bd13981df8cd50fd0fbd0cb5afabb3e66f2750026d")}, + } + + for i := 0; i < len(validDecompressVectors); i++ { + P, err := curve.Decompress(validDecompressVectors[i].X, validDecompressVectors[i].Y.Bit(0)) + if err != nil { + t.Fatalf("failure decompress P, got error %v on index %d", err, i) + } + if P.X.Cmp(validDecompressVectors[i].X) != 0 || P.Y.Cmp(validDecompressVectors[i].Y) != 0 { + t.Fatalf("failure decompress P, got mismatch on index %d", i) + } + } + t.Log("success Decompress() on valid vectors") + + /* Invalid points */ + var invalidDecompressVectors = []struct { + X *big.Int + YLsb uint + }{ + {hex2int("c8e337cee51ae9af3c0ef923705a0cb1b76f7e8463b3d3060a1c8d795f9630fd"), 0}, + {hex2int("c8e337cee51ae9af3c0ef923705a0cb1b76f7e8463b3d3060a1c8d795f9630fd"), 1}, + } + + for i := 0; i < len(invalidDecompressVectors); i++ { + _, err := curve.Decompress(invalidDecompressVectors[i].X, invalidDecompressVectors[i].YLsb) + if err == nil { + t.Fatalf("failure decompress invalid P, got decompressed point on index %d", i) + } + } + t.Log("success Decompress() on invalid vectors") +} diff --git a/pkg/crypto/hash/hash.go b/pkg/crypto/hash/hash.go new file mode 100755 index 000000000..22ab1c6d6 --- /dev/null +++ b/pkg/crypto/hash/hash.go @@ -0,0 +1,77 @@ +package hash + +import ( + "crypto/sha256" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/util" + "golang.org/x/crypto/ripemd160" +) + +func Sha256(data []byte) (util.Uint256, error) { + var hash util.Uint256 + hasher := sha256.New() + hasher.Reset() + _, err := hasher.Write(data) + + hash, err = util.Uint256DecodeBytes(hasher.Sum(nil)) + if err != nil { + return hash, err + } + return hash, nil +} + +func DoubleSha256(data []byte) (util.Uint256, error) { + var hash util.Uint256 + + h1, err := Sha256(data) + if err != nil { + return hash, err + } + + hash, err = Sha256(h1.Bytes()) + if err != nil { + return hash, err + } + return hash, nil +} + +func RipeMD160(data []byte) (util.Uint160, error) { + var hash util.Uint160 + hasher := ripemd160.New() + hasher.Reset() + _, err := io.WriteString(hasher, string(data)) + + hash, err = util.Uint160DecodeBytes(hasher.Sum(nil)) + if err != nil { + return hash, err + } + return hash, nil +} + +func Hash160(data []byte) (util.Uint160, error) { + var hash util.Uint160 + h1, err := Sha256(data) + + h2, err := RipeMD160(h1.Bytes()) + + hash, err = util.Uint160DecodeBytes(h2.Bytes()) + + if err != nil { + return hash, err + } + return hash, nil +} + +func Checksum(data []byte) ([]byte, error) { + hash, err := Sum(data) + if err != nil { + return nil, err + } + return hash[:4], nil +} + +func Sum(b []byte) (util.Uint256, error) { + hash, err := DoubleSha256((b)) + return hash, err +} diff --git a/pkg/crypto/hash/hash_test.go b/pkg/crypto/hash/hash_test.go new file mode 100755 index 000000000..aa23718f6 --- /dev/null +++ b/pkg/crypto/hash/hash_test.go @@ -0,0 +1,62 @@ +package hash + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSha256(t *testing.T) { + input := []byte("hello") + data, err := Sha256(input) + + if err != nil { + t.Fatal(err) + } + expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + actual := hex.EncodeToString(data.Bytes()) // MARK: In the DecodeBytes function, there is a bytes reverse, not sure why? + + assert.Equal(t, expected, actual) +} + +func TestHashDoubleSha256(t *testing.T) { + input := []byte("hello") + data, err := DoubleSha256(input) + + if err != nil { + t.Fatal(err) + } + + firstSha, _ := Sha256(input) + doubleSha, _ := Sha256(firstSha.Bytes()) + expected := hex.EncodeToString(doubleSha.Bytes()) + + actual := hex.EncodeToString(data.Bytes()) + assert.Equal(t, expected, actual) +} + +func TestHashRipeMD160(t *testing.T) { + input := []byte("hello") + data, err := RipeMD160(input) + + if err != nil { + t.Fatal(err) + } + expected := "108f07b8382412612c048d07d13f814118445acd" + actual := hex.EncodeToString(data.Bytes()) + assert.Equal(t, expected, actual) +} + +func TestHash160(t *testing.T) { + input := "02cccafb41b220cab63fd77108d2d1ebcffa32be26da29a04dca4996afce5f75db" + publicKeyBytes, _ := hex.DecodeString(input) + data, err := Hash160(publicKeyBytes) + + if err != nil { + t.Fatal(err) + } + expected := "c8e2b685cc70ec96743b55beb9449782f8f775d8" + actual := hex.EncodeToString(data.Bytes()) + assert.Equal(t, expected, actual) +} diff --git a/pkg/crypto/privatekey/privatekey.go b/pkg/crypto/privatekey/privatekey.go new file mode 100755 index 000000000..513d9366f --- /dev/null +++ b/pkg/crypto/privatekey/privatekey.go @@ -0,0 +1,117 @@ +package privatekey + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "math/big" + + "github.com/CityOfZion/neo-go/pkg/crypto/publickey" + + "github.com/CityOfZion/neo-go/pkg/crypto/base58" + "github.com/CityOfZion/neo-go/pkg/crypto/elliptic" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/crypto/rfc6979" +) + +// PrivateKey represents a NEO private key. +type PrivateKey struct { + b []byte +} + +func NewPrivateKey() (*PrivateKey, error) { + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + b := make([]byte, curve.N.BitLen()/8+8) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return nil, err + } + + d := new(big.Int).SetBytes(b) + d.Mod(d, new(big.Int).Sub(curve.N, big.NewInt(1))) + d.Add(d, big.NewInt(1)) + + p := &PrivateKey{b: d.Bytes()} + return p, nil +} + +func NewPrivateKeyFromHex(str string) (*PrivateKey, error) { + b, err := hex.DecodeString(str) + if err != nil { + return nil, err + } + return NewPrivateKeyFromBytes(b) +} + +// NewPrivateKeyFromBytes returns a NEO PrivateKey from the given byte slice. +func NewPrivateKeyFromBytes(b []byte) (*PrivateKey, error) { + if len(b) != 32 { + return nil, fmt.Errorf( + "invalid byte length: expected %d bytes got %d", 32, len(b), + ) + } + return &PrivateKey{b}, nil +} + +func (p *PrivateKey) PublicKey() (*publickey.PublicKey, error) { + var ( + c = elliptic.NewEllipticCurve(elliptic.Secp256r1) + q = new(big.Int).SetBytes(p.b) + ) + + p1, p2 := c.ScalarBaseMult(q.Bytes()) + point := elliptic.Point{ + X: p1, + Y: p2, + } + if !c.IsOnCurve(p1, p2) { + return nil, errors.New("failed to derive public key using elliptic curve") + } + + return &publickey.PublicKey{ + Curve: c, + Point: point, + }, nil + +} + +func WIFEncode(key []byte) (s string) { + if len(key) != 32 { + return "invalid private key length" + } + + buf := new(bytes.Buffer) + buf.WriteByte(0x80) + buf.Write(key) + + buf.WriteByte(0x01) + + checksum, _ := hash.Checksum(buf.Bytes()) + + buf.Write(checksum) + + WIF := base58.Encode(buf.Bytes()) + return WIF +} + +func (p *PrivateKey) Sign(data []byte) ([]byte, error) { + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + key := p.b + digest, _ := hash.Sha256(data) + + r, s, err := rfc6979.SignECDSA(curve, key, digest[:], sha256.New) + if err != nil { + return nil, err + } + + curveOrderByteSize := curve.P.BitLen() / 8 + rBytes, sBytes := r.Bytes(), s.Bytes() + signature := make([]byte, curveOrderByteSize*2) + copy(signature[curveOrderByteSize-len(rBytes):], rBytes) + copy(signature[curveOrderByteSize*2-len(sBytes):], sBytes) + + return signature, nil +} diff --git a/pkg/crypto/privatekey/privatekey_test.go b/pkg/crypto/privatekey/privatekey_test.go new file mode 100755 index 000000000..7c46737b6 --- /dev/null +++ b/pkg/crypto/privatekey/privatekey_test.go @@ -0,0 +1,48 @@ +package privatekey + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPrivateKeyToPublicKey(t *testing.T) { + input := "495d528227c7dcc234c690af1222e67cde916dac1652cad97e0263825a8268a6" + + privateKey, err := NewPrivateKeyFromHex(input) + if err != nil { + t.Fatal(err) + } + pubKey, _ := privateKey.PublicKey() + pubKeyBytes := pubKey.Bytes() + actual := hex.EncodeToString(pubKeyBytes) + expected := "03cd4c4ee9c8e1fae9d12ecf7c96cb3a057b550393f9e82182c4dae1139871682e" + assert.Equal(t, expected, actual) +} +func TestWIFEncode(t *testing.T) { + input := "29bbf53185a973d2e3803cb92908fd08117486d1f2e7bab73ed0d00255511637" + inputBytes, _ := hex.DecodeString(input) + + actual := WIFEncode(inputBytes) + expected := "KxcqV28rGDcpVR3fYg7R9vricLpyZ8oZhopyFLAWuRv7Y8TE9WhW" + assert.Equal(t, expected, actual) +} + +func TestSigning(t *testing.T) { + // These were taken from the rfcPage:https://tools.ietf.org/html/rfc6979#page-33 + // public key: U = xG + //Ux = 60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6 + //Uy = 7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299 + PrivateKey, _ := NewPrivateKeyFromHex("C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721") + + data, err := PrivateKey.Sign([]byte("sample")) + if err != nil { + t.Fatal(err) + } + + r := "EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716" + s := "F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8" + assert.Equal(t, strings.ToLower(r+s), hex.EncodeToString(data)) +} diff --git a/pkg/crypto/publickey/TestHelper/helper.go b/pkg/crypto/publickey/TestHelper/helper.go new file mode 100755 index 000000000..23089ac0e --- /dev/null +++ b/pkg/crypto/publickey/TestHelper/helper.go @@ -0,0 +1,20 @@ +package pubkeytesthelper + +import ( + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/crypto/privatekey" +) + +func SignDataWithRandomPrivateKey(data []byte) (bool, error) { + + hashedData, _ := hash.Sha256(data) + + privKey, _ := privatekey.NewPrivateKey() + signedData, err := privKey.Sign(data) + pubKey, _ := privKey.PublicKey() + result := pubKey.Verify(signedData, hashedData.Bytes()) + if err != nil { + return false, err + } + return result, nil +} diff --git a/pkg/crypto/publickey/TestHelper/helper_test.go b/pkg/crypto/publickey/TestHelper/helper_test.go new file mode 100755 index 000000000..68f8c5047 --- /dev/null +++ b/pkg/crypto/publickey/TestHelper/helper_test.go @@ -0,0 +1,34 @@ +package pubkeytesthelper + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/crypto/privatekey" + "github.com/stretchr/testify/assert" +) + +func TestPubKeyVerify(t *testing.T) { + actual, err := SignDataWithRandomPrivateKey([]byte("sample")) + + if err != nil { + t.Fatal(err) + } + expected := true + + assert.Equal(t, expected, actual) +} + +func TestWrongPubKey(t *testing.T) { + privKey, _ := privatekey.NewPrivateKey() + sample := []byte("sample") + hashedData, _ := hash.Sha256(sample) + signedData, _ := privKey.Sign(sample) + + secondPrivKey, _ := privatekey.NewPrivateKey() + wrongPubKey, _ := secondPrivKey.PublicKey() + + actual := wrongPubKey.Verify(signedData, hashedData.Bytes()) + expcted := false + assert.Equal(t, expcted, actual) +} diff --git a/pkg/crypto/publickey/publickey.go b/pkg/crypto/publickey/publickey.go new file mode 100755 index 000000000..ac1b15b03 --- /dev/null +++ b/pkg/crypto/publickey/publickey.go @@ -0,0 +1,160 @@ +package publickey + +import ( + "bytes" + "crypto/ecdsa" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "math/big" + + "github.com/CityOfZion/neo-go/pkg/crypto/base58" + "github.com/CityOfZion/neo-go/pkg/crypto/elliptic" + "github.com/CityOfZion/neo-go/pkg/crypto/hash" +) + +// PublicKeys is a list of public keys. +type PublicKeys []*PublicKey + +func (keys PublicKeys) Len() int { return len(keys) } +func (keys PublicKeys) Swap(i, j int) { keys[i], keys[j] = keys[j], keys[i] } +func (keys PublicKeys) Less(i, j int) bool { + + if keys[i].X.Cmp(keys[j].X) == -1 { + return true + } + if keys[i].X.Cmp(keys[j].X) == 1 { + return false + } + if keys[i].X.Cmp(keys[j].X) == 0 { + return false + } + + return keys[i].Y.Cmp(keys[j].Y) == -1 +} + +// PublicKey represents a public key and provides a high level +// API around the ECPoint. +type PublicKey struct { + Curve elliptic.Curve + elliptic.Point +} + +// NewPublicKeyFromString return a public key created from the +// given hex string. +func NewPublicKeyFromString(s string) (*PublicKey, error) { + b, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + + pubKey := &PublicKey{curve, elliptic.Point{}} + + if err := pubKey.DecodeBinary(bytes.NewReader(b)); err != nil { + return nil, err + } + + return pubKey, nil +} + +// Bytes returns the byte array representation of the public key. +func (p *PublicKey) Bytes() []byte { + if p.Curve.IsInfinity(p.Point) { + return []byte{0x00} + } + + var ( + x = p.X.Bytes() + paddedX = append(bytes.Repeat([]byte{0x00}, 32-len(x)), x...) + prefix = byte(0x03) + ) + + if p.Y.Bit(0) == 0 { + prefix = byte(0x02) + } + + return append([]byte{prefix}, paddedX...) +} +func (p *PublicKey) ToAddress() string { + + publicKeyBytes := p.Bytes() + + publicKeyBytes = append([]byte{0x21}, publicKeyBytes...) // 0x21 = length of pubKey + publicKeyBytes = append(publicKeyBytes, 0xAC) // 0xAC = CheckSig + + hash160PubKey, _ := hash.Hash160(publicKeyBytes) + + versionHash160PubKey := append([]byte{0x17}, hash160PubKey.Bytes()...) + + checksum, _ := hash.Checksum(versionHash160PubKey) + + checkVersionHash160 := append(versionHash160PubKey, checksum...) + + address := base58.Encode(checkVersionHash160) + + return address +} + +// DecodeBinary decodes a PublicKey from the given io.Reader. +func (p *PublicKey) DecodeBinary(r io.Reader) error { + + var prefix uint8 + if err := binary.Read(r, binary.LittleEndian, &prefix); err != nil { + return err + } + + // Infinity + if prefix == 0x00 { + p.Point = elliptic.Point{} + return nil + } + + // Compressed public keys. + if prefix == 0x02 || prefix == 0x03 { + + b := make([]byte, 32) + if err := binary.Read(r, binary.LittleEndian, b); err != nil { + return err + } + + var err error + + p.Point, err = p.Curve.Decompress(new(big.Int).SetBytes(b), uint(prefix&0x1)) + if err != nil { + return err + } + + } else if prefix == 0x04 { + buf := make([]byte, 65) + if err := binary.Read(r, binary.LittleEndian, buf); err != nil { + return err + } + p.X = new(big.Int).SetBytes(buf[1:33]) + p.Y = new(big.Int).SetBytes(buf[33:65]) + } else { + return fmt.Errorf("invalid prefix %d", prefix) + } + + return nil +} + +// EncodeBinary encodes a PublicKey to the given io.Writer. +func (p *PublicKey) EncodeBinary(w io.Writer) error { + return binary.Write(w, binary.LittleEndian, p.Bytes()) +} + +func (p *PublicKey) Verify(signature []byte, hash []byte) bool { + + publicKey := &ecdsa.PublicKey{} + publicKey.Curve = p.Curve + publicKey.X = p.X + publicKey.Y = p.Y + if p.X == nil || p.Y == nil { + return false + } + rBytes := new(big.Int).SetBytes(signature[0:32]) + sBytes := new(big.Int).SetBytes(signature[32:64]) + return ecdsa.Verify(publicKey, hash, rBytes, sBytes) +} diff --git a/pkg/crypto/publickey/publickey_test.go b/pkg/crypto/publickey/publickey_test.go new file mode 100755 index 000000000..0f93adb0c --- /dev/null +++ b/pkg/crypto/publickey/publickey_test.go @@ -0,0 +1,81 @@ +package publickey + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "io" + "math/big" + "testing" + + "github.com/CityOfZion/neo-go/pkg/crypto/elliptic" + "github.com/stretchr/testify/assert" +) + +func TestDecodeFromString(t *testing.T) { + str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" + pubKey, err := NewPublicKeyFromString(str) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, str, hex.EncodeToString(pubKey.Bytes())) +} + +func TestEncodeDecodeInfinity(t *testing.T) { + + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + + key := &PublicKey{curve, elliptic.Point{}} + buf := new(bytes.Buffer) + assert.Nil(t, key.EncodeBinary(buf)) + assert.Equal(t, 1, buf.Len()) + + keyDecode := &PublicKey{} + assert.Nil(t, keyDecode.DecodeBinary(buf)) + assert.Equal(t, []byte{0x00}, keyDecode.Bytes()) +} + +func TestEncodeDecodePublicKey(t *testing.T) { + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + + for i := 0; i < 4; i++ { + p := &PublicKey{curve, randomECPoint()} + buf := new(bytes.Buffer) + assert.Nil(t, p.EncodeBinary(buf)) + + pDecode := &PublicKey{curve, elliptic.Point{}} + assert.Nil(t, pDecode.DecodeBinary(buf)) + assert.Equal(t, p.X, pDecode.X) + } +} + +func TestPubkeyToAddress(t *testing.T) { + + pubKey, err := NewPublicKeyFromString("031ee4e73a17d8f76dc02532e2620bcb12425b33c0c9f9694cc2caa8226b68cad4") + if err != nil { + t.Fatal(err) + } + + actual := pubKey.ToAddress() + expected := "AUpGsNCHzSimeMRVPQfhwrVdiUp8Q2N2Qx" + assert.Equal(t, expected, actual) +} + +func randomECPoint() elliptic.Point { + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + b := make([]byte, curve.N.BitLen()/8+8) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return elliptic.Point{} + } + + d := new(big.Int).SetBytes(b) + d.Mod(d, new(big.Int).Sub(curve.N, big.NewInt(1))) + d.Add(d, big.NewInt(1)) + + q := new(big.Int).SetBytes(d.Bytes()) + P1, P2 := curve.ScalarBaseMult(q.Bytes()) + return elliptic.Point{ + X: P1, + Y: P2, + } +} diff --git a/pkg/crypto/rfc6979/LICENSE b/pkg/crypto/rfc6979/LICENSE new file mode 100755 index 000000000..f9835c241 --- /dev/null +++ b/pkg/crypto/rfc6979/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Coda Hale + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pkg/crypto/rfc6979/dsa.go b/pkg/crypto/rfc6979/dsa.go new file mode 100755 index 000000000..f15b3a73a --- /dev/null +++ b/pkg/crypto/rfc6979/dsa.go @@ -0,0 +1,45 @@ +package rfc6979 + +import ( + "crypto/dsa" + "hash" + "math/big" +) + +// SignDSA signs an arbitrary length hash (which should be the result of hashing +// a larger message) using the private key, priv. It returns the signature as a +// pair of integers. +// +// Note that FIPS 186-3 section 4.6 specifies that the hash should be truncated +// to the byte-length of the subgroup. This function does not perform that +// truncation itself. +func SignDSA(priv *dsa.PrivateKey, hash []byte, alg func() hash.Hash) (r, s *big.Int, err error) { + n := priv.Q.BitLen() + if n&7 != 0 { + err = dsa.ErrInvalidPublicKey + return + } + n >>= 3 + + generateSecret(priv.Q, priv.X, alg, hash, func(k *big.Int) bool { + inv := new(big.Int).ModInverse(k, priv.Q) + r = new(big.Int).Exp(priv.G, k, priv.P) + r.Mod(r, priv.Q) + + if r.Sign() == 0 { + return false + } + + z := new(big.Int).SetBytes(hash) + + s = new(big.Int).Mul(priv.X, r) + s.Add(s, z) + s.Mod(s, priv.Q) + s.Mul(s, inv) + s.Mod(s, priv.Q) + + return s.Sign() != 0 + }) + + return +} diff --git a/pkg/crypto/rfc6979/dsa_test.go b/pkg/crypto/rfc6979/dsa_test.go new file mode 100755 index 000000000..6b0f4e829 --- /dev/null +++ b/pkg/crypto/rfc6979/dsa_test.go @@ -0,0 +1,270 @@ +package rfc6979_test + +import ( + "crypto/dsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "hash" + "math/big" + "testing" + + "github.com/o3labs/neo-utils/neoutils/rfc6979" +) + +type dsaFixture struct { + name string + key *dsaKey + alg func() hash.Hash + message string + r, s string +} + +type dsaKey struct { + key *dsa.PrivateKey + subgroup int +} + +var dsa1024 = &dsaKey{ + key: &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: dsaLoadInt("86F5CA03DCFEB225063FF830A0C769B9DD9D6153AD91D7CE27F787C43278B447E6533B86B18BED6E8A48B784A14C252C5BE0DBF60B86D6385BD2F12FB763ED8873ABFD3F5BA2E0A8C0A59082EAC056935E529DAF7C610467899C77ADEDFC846C881870B7B19B2B58F9BE0521A17002E3BDD6B86685EE90B3D9A1B02B782B1779"), + Q: dsaLoadInt("996F967F6C8E388D9E28D01E205FBA957A5698B1"), + G: dsaLoadInt("07B0F92546150B62514BB771E2A0C0CE387F03BDA6C56B505209FF25FD3C133D89BBCD97E904E09114D9A7DEFDEADFC9078EA544D2E401AEECC40BB9FBBF78FD87995A10A1C27CB7789B594BA7EFB5C4326A9FE59A070E136DB77175464ADCA417BE5DCE2F40D10A46A3A3943F26AB7FD9C0398FF8C76EE0A56826A8A88F1DBD"), + }, + Y: dsaLoadInt("5DF5E01DED31D0297E274E1691C192FE5868FEF9E19A84776454B100CF16F65392195A38B90523E2542EE61871C0440CB87C322FC4B4D2EC5E1E7EC766E1BE8D4CE935437DC11C3C8FD426338933EBFE739CB3465F4D3668C5E473508253B1E682F65CBDC4FAE93C2EA212390E54905A86E2223170B44EAA7DA5DD9FFCFB7F3B"), + }, + X: dsaLoadInt("411602CB19A6CCC34494D79D98EF1E7ED5AF25F7"), + }, + subgroup: 160, +} + +var dsa2048 = &dsaKey{ + key: &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: dsaLoadInt("9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642F0B5C48C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757264E5A1A44FFE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F9716BFE6117C6B5B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091EB51743BF33050C38DE235567E1B34C3D6A5C0CEAA1A0F368213C3D19843D0B4B09DCB9FC72D39C8DE41F1BF14D4BB4563CA28371621CAD3324B6A2D392145BEBFAC748805236F5CA2FE92B871CD8F9C36D3292B5509CA8CAA77A2ADFC7BFD77DDA6F71125A7456FEA153E433256A2261C6A06ED3693797E7995FAD5AABBCFBE3EDA2741E375404AE25B"), + Q: dsaLoadInt("F2C3119374CE76C9356990B465374A17F23F9ED35089BD969F61C6DDE9998C1F"), + G: dsaLoadInt("5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E24809670716C613D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D1AA58C4328A06C46A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A338661D10461C0D135472085057F3494309FFA73C611F78B32ADBB5740C361C9F35BE90997DB2014E2EF5AA61782F52ABEB8BD6432C4DD097BC5423B285DAFB60DC364E8161F4A2A35ACA3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7"), + }, + Y: dsaLoadInt("667098C654426C78D7F8201EAC6C203EF030D43605032C2F1FA937E5237DBD949F34A0A2564FE126DC8B715C5141802CE0979C8246463C40E6B6BDAA2513FA611728716C2E4FD53BC95B89E69949D96512E873B9C8F8DFD499CC312882561ADECB31F658E934C0C197F2C4D96B05CBAD67381E7B768891E4DA3843D24D94CDFB5126E9B8BF21E8358EE0E0A30EF13FD6A664C0DCE3731F7FB49A4845A4FD8254687972A2D382599C9BAC4E0ED7998193078913032558134976410B89D2C171D123AC35FD977219597AA7D15C1A9A428E59194F75C721EBCBCFAE44696A499AFA74E04299F132026601638CB87AB79190D4A0986315DA8EEC6561C938996BEADF"), + }, + X: dsaLoadInt("69C7548C21D0DFEA6B9A51C9EAD4E27C33D3B3F180316E5BCAB92C933F0E4DBC"), + }, + subgroup: 256, +} + +var dsaFixtures = []dsaFixture{ + // DSA, 1024 Bits + // https://tools.ietf.org/html/rfc6979#appendix-A.2.1 + dsaFixture{ + name: "1024/SHA-1 #1", + key: dsa1024, + alg: sha1.New, + message: "sample", + r: "2E1A0C2562B2912CAAF89186FB0F42001585DA55", + s: "29EFB6B0AFF2D7A68EB70CA313022253B9A88DF5", + }, + dsaFixture{ + name: "1024/SHA-224 #1", + key: dsa1024, + alg: sha256.New224, + message: "sample", + r: "4BC3B686AEA70145856814A6F1BB53346F02101E", + s: "410697B92295D994D21EDD2F4ADA85566F6F94C1", + }, + dsaFixture{ + name: "1024/SHA-256 #1", + key: dsa1024, + alg: sha256.New, + message: "sample", + r: "81F2F5850BE5BC123C43F71A3033E9384611C545", + s: "4CDD914B65EB6C66A8AAAD27299BEE6B035F5E89", + }, + dsaFixture{ + name: "1024/SHA-384 #1", + key: dsa1024, + alg: sha512.New384, + message: "sample", + r: "07F2108557EE0E3921BC1774F1CA9B410B4CE65A", + s: "54DF70456C86FAC10FAB47C1949AB83F2C6F7595", + }, + dsaFixture{ + name: "1024/SHA-512 #1", + key: dsa1024, + alg: sha512.New, + message: "sample", + r: "16C3491F9B8C3FBBDD5E7A7B667057F0D8EE8E1B", + s: "02C36A127A7B89EDBB72E4FFBC71DABC7D4FC69C", + }, + dsaFixture{ + name: "1024/SHA-1 #2", + key: dsa1024, + alg: sha1.New, + message: "test", + r: "42AB2052FD43E123F0607F115052A67DCD9C5C77", + s: "183916B0230D45B9931491D4C6B0BD2FB4AAF088", + }, + dsaFixture{ + name: "1024/SHA-224 #2", + key: dsa1024, + alg: sha256.New224, + message: "test", + r: "6868E9964E36C1689F6037F91F28D5F2C30610F2", + s: "49CEC3ACDC83018C5BD2674ECAAD35B8CD22940F", + }, + dsaFixture{ + name: "1024/SHA-256 #2", + key: dsa1024, + alg: sha256.New, + message: "test", + r: "22518C127299B0F6FDC9872B282B9E70D0790812", + s: "6837EC18F150D55DE95B5E29BE7AF5D01E4FE160", + }, + dsaFixture{ + name: "1024/SHA-384 #2", + key: dsa1024, + alg: sha512.New384, + message: "test", + r: "854CF929B58D73C3CBFDC421E8D5430CD6DB5E66", + s: "91D0E0F53E22F898D158380676A871A157CDA622", + }, + dsaFixture{ + name: "1024/SHA-512 #2", + key: dsa1024, + alg: sha512.New, + message: "test", + r: "8EA47E475BA8AC6F2D821DA3BD212D11A3DEB9A0", + s: "7C670C7AD72B6C050C109E1790008097125433E8", + }, + + // DSA, 2048 Bits + // https://tools.ietf.org/html/rfc6979#appendix-A.2.2 + dsaFixture{ + name: "2048/SHA-1 #1", + key: dsa2048, + alg: sha1.New, + message: "sample", + r: "3A1B2DBD7489D6ED7E608FD036C83AF396E290DBD602408E8677DAABD6E7445A", + s: "D26FCBA19FA3E3058FFC02CA1596CDBB6E0D20CB37B06054F7E36DED0CDBBCCF", + }, + dsaFixture{ + name: "2048/SHA-224 #1", + key: dsa2048, + alg: sha256.New224, + message: "sample", + r: "DC9F4DEADA8D8FF588E98FED0AB690FFCE858DC8C79376450EB6B76C24537E2C", + s: "A65A9C3BC7BABE286B195D5DA68616DA8D47FA0097F36DD19F517327DC848CEC", + }, + dsaFixture{ + name: "2048/SHA-256 #1", + key: dsa2048, + alg: sha256.New, + message: "sample", + r: "EACE8BDBBE353C432A795D9EC556C6D021F7A03F42C36E9BC87E4AC7932CC809", + s: "7081E175455F9247B812B74583E9E94F9EA79BD640DC962533B0680793A38D53", + }, + dsaFixture{ + name: "2048/SHA-384 #1", + key: dsa2048, + alg: sha512.New384, + message: "sample", + r: "B2DA945E91858834FD9BF616EBAC151EDBC4B45D27D0DD4A7F6A22739F45C00B", + s: "19048B63D9FD6BCA1D9BAE3664E1BCB97F7276C306130969F63F38FA8319021B", + }, + dsaFixture{ + name: "2048/SHA-512 #1", + key: dsa2048, + alg: sha512.New, + message: "sample", + r: "2016ED092DC5FB669B8EFB3D1F31A91EECB199879BE0CF78F02BA062CB4C942E", + s: "D0C76F84B5F091E141572A639A4FB8C230807EEA7D55C8A154A224400AFF2351", + }, + dsaFixture{ + name: "2048/SHA-1 #2", + key: dsa2048, + alg: sha1.New, + message: "test", + r: "C18270A93CFC6063F57A4DFA86024F700D980E4CF4E2CB65A504397273D98EA0", + s: "414F22E5F31A8B6D33295C7539C1C1BA3A6160D7D68D50AC0D3A5BEAC2884FAA", + }, + dsaFixture{ + name: "2048/SHA-224 #2", + key: dsa2048, + alg: sha256.New224, + message: "test", + r: "272ABA31572F6CC55E30BF616B7A265312018DD325BE031BE0CC82AA17870EA3", + s: "E9CC286A52CCE201586722D36D1E917EB96A4EBDB47932F9576AC645B3A60806", + }, + dsaFixture{ + name: "2048/SHA-256 #2", + key: dsa2048, + alg: sha256.New, + message: "test", + r: "8190012A1969F9957D56FCCAAD223186F423398D58EF5B3CEFD5A4146A4476F0", + s: "7452A53F7075D417B4B013B278D1BB8BBD21863F5E7B1CEE679CF2188E1AB19E", + }, + dsaFixture{ + name: "2048/SHA-384 #2", + key: dsa2048, + alg: sha512.New384, + message: "test", + r: "239E66DDBE8F8C230A3D071D601B6FFBDFB5901F94D444C6AF56F732BEB954BE", + s: "6BD737513D5E72FE85D1C750E0F73921FE299B945AAD1C802F15C26A43D34961", + }, + dsaFixture{ + name: "2048/SHA-512 #2", + key: dsa2048, + alg: sha512.New, + message: "test", + r: "89EC4BB1400ECCFF8E7D9AA515CD1DE7803F2DAFF09693EE7FD1353E90A68307", + s: "C9F0BDABCC0D880BB137A994CC7F3980CE91CC10FAF529FC46565B15CEA854E1", + }, +} + +func TestDSASignatures(t *testing.T) { + for _, f := range dsaFixtures { + testDsaFixture(&f, t) + } +} + +func testDsaFixture(f *dsaFixture, t *testing.T) { + t.Logf("Testing %s", f.name) + + h := f.alg() + h.Write([]byte(f.message)) + digest := h.Sum(nil) + + g := f.key.subgroup / 8 + if len(digest) > g { + digest = digest[0:g] + } + + r, s, err := rfc6979.SignDSA(f.key.key, digest, f.alg) + if err != nil { + t.Error(err) + return + } + + expectedR := dsaLoadInt(f.r) + expectedS := dsaLoadInt(f.s) + + if r.Cmp(expectedR) != 0 { + t.Errorf("%s: Expected R of %X, got %X", f.name, expectedR, r) + } + + if s.Cmp(expectedS) != 0 { + t.Errorf("%s: Expected S of %X, got %X", f.name, expectedS, s) + } +} + +func dsaLoadInt(s string) *big.Int { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + + return new(big.Int).SetBytes(b) +} diff --git a/pkg/crypto/rfc6979/ecdsa.go b/pkg/crypto/rfc6979/ecdsa.go new file mode 100755 index 000000000..8cf10a79b --- /dev/null +++ b/pkg/crypto/rfc6979/ecdsa.go @@ -0,0 +1,59 @@ +package rfc6979 + +import ( + "hash" + "math/big" + + "github.com/CityOfZion/neo-go/pkg/crypto/elliptic" +) + +// SignECDSA signs an arbitrary length hash (which should be the result of +// hashing a larger message) using the private key, priv. It returns the +// signature as a pair of integers. +// +// Note that FIPS 186-3 section 4.6 specifies that the hash should be truncated +// to the byte-length of the subgroup. This function does not perform that +// truncation itself. +func SignECDSA(curve elliptic.Curve, priv []byte, hash []byte, alg func() hash.Hash) (r, s *big.Int, err error) { + c := curve + N := c.N + D := new(big.Int) + D.SetBytes(priv) + generateSecret(N, D, alg, hash, func(k *big.Int) bool { + + inv := new(big.Int).ModInverse(k, N) + + r, _ = curve.ScalarBaseMult(k.Bytes()) + r.Mod(r, N) + + if r.Sign() == 0 { + return false + } + + e := hashToInt(hash, c) + s = new(big.Int).Mul(D, r) + s.Add(s, e) + s.Mul(s, inv) + s.Mod(s, N) + + return s.Sign() != 0 + }) + + return +} + +// copied from crypto/ecdsa +func hashToInt(hash []byte, c elliptic.Curve) *big.Int { + orderBits := c.N.BitLen() + orderBytes := (orderBits + 7) / 8 + if len(hash) > orderBytes { + hash = hash[:orderBytes] + } + + ret := new(big.Int).SetBytes(hash) + excess := len(hash)*8 - orderBits + if excess > 0 { + ret.Rsh(ret, uint(excess)) + } + return ret +} diff --git a/pkg/crypto/rfc6979/ecdsa_test.go b/pkg/crypto/rfc6979/ecdsa_test.go new file mode 100755 index 000000000..f6ef52420 --- /dev/null +++ b/pkg/crypto/rfc6979/ecdsa_test.go @@ -0,0 +1,447 @@ +package rfc6979_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "hash" + "math/big" + "testing" + + "github.com/o3labs/neo-utils/neoutils/rfc6979" +) + +type ecdsaFixture struct { + name string + key *ecdsaKey + alg func() hash.Hash + message string + r, s string +} + +type ecdsaKey struct { + key *ecdsa.PrivateKey + subgroup int +} + +var p224 = &ecdsaKey{ + key: &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P224(), + X: ecdsaLoadInt("00CF08DA5AD719E42707FA431292DEA11244D64FC51610D94B130D6C"), + Y: ecdsaLoadInt("EEAB6F3DEBE455E3DBF85416F7030CBD94F34F2D6F232C69F3C1385A"), + }, + D: ecdsaLoadInt("F220266E1105BFE3083E03EC7A3A654651F45E37167E88600BF257C1"), + }, + subgroup: 224, +} + +var p256 = &ecdsaKey{ + key: &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: ecdsaLoadInt("60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6"), + Y: ecdsaLoadInt("7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299"), + }, + D: ecdsaLoadInt("C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721"), + }, + subgroup: 256, +} + +var p384 = &ecdsaKey{ + key: &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P384(), + X: ecdsaLoadInt("EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64DEF8F0EA9055866064A254515480BC13"), + Y: ecdsaLoadInt("8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1288B231C3AE0D4FE7344FD2533264720"), + }, + D: ecdsaLoadInt("6B9D3DAD2E1B8C1C05B19875B6659F4DE23C3B667BF297BA9AA47740787137D896D5724E4C70A825F872C9EA60D2EDF5"), + }, + subgroup: 384, +} + +var p521 = &ecdsaKey{ + key: &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P521(), + X: ecdsaLoadInt("1894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD371123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F5023A4"), + Y: ecdsaLoadInt("0493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A28A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDFCF5"), + }, + D: ecdsaLoadInt("0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538"), + }, + subgroup: 521, +} + +var fixtures = []ecdsaFixture{ + // ECDSA, 224 Bits (Prime Field) + // https://tools.ietf.org/html/rfc6979#appendix-A.2.4 + ecdsaFixture{ + name: "P224/SHA-1 #1", + key: p224, + alg: sha1.New, + message: "sample", + r: "22226F9D40A96E19C4A301CE5B74B115303C0F3A4FD30FC257FB57AC", + s: "66D1CDD83E3AF75605DD6E2FEFF196D30AA7ED7A2EDF7AF475403D69", + }, + ecdsaFixture{ + name: "P224/SHA-224 #1", + key: p224, + alg: sha256.New224, + message: "sample", + r: "1CDFE6662DDE1E4A1EC4CDEDF6A1F5A2FB7FBD9145C12113E6ABFD3E", + s: "A6694FD7718A21053F225D3F46197CA699D45006C06F871808F43EBC", + }, + ecdsaFixture{ + name: "P224/SHA-256 #1", + key: p224, + alg: sha256.New, + message: "sample", + r: "61AA3DA010E8E8406C656BC477A7A7189895E7E840CDFE8FF42307BA", + s: "BC814050DAB5D23770879494F9E0A680DC1AF7161991BDE692B10101", + }, + ecdsaFixture{ + name: "P224/SHA-384 #1", + key: p224, + alg: sha512.New384, + message: "sample", + r: "0B115E5E36F0F9EC81F1325A5952878D745E19D7BB3EABFABA77E953", + s: "830F34CCDFE826CCFDC81EB4129772E20E122348A2BBD889A1B1AF1D", + }, + ecdsaFixture{ + name: "P224/SHA-512 #1", + key: p224, + alg: sha512.New, + message: "sample", + r: "074BD1D979D5F32BF958DDC61E4FB4872ADCAFEB2256497CDAC30397", + s: "A4CECA196C3D5A1FF31027B33185DC8EE43F288B21AB342E5D8EB084", + }, + ecdsaFixture{ + name: "P224/SHA-1 #2", + key: p224, + alg: sha1.New, + message: "test", + r: "DEAA646EC2AF2EA8AD53ED66B2E2DDAA49A12EFD8356561451F3E21C", + s: "95987796F6CF2062AB8135271DE56AE55366C045F6D9593F53787BD2", + }, + ecdsaFixture{ + name: "P224/SHA-224 #2", + key: p224, + alg: sha256.New224, + message: "test", + r: "C441CE8E261DED634E4CF84910E4C5D1D22C5CF3B732BB204DBEF019", + s: "902F42847A63BDC5F6046ADA114953120F99442D76510150F372A3F4", + }, + ecdsaFixture{ + name: "P224/SHA-256 #2", + key: p224, + alg: sha256.New, + message: "test", + r: "AD04DDE87B84747A243A631EA47A1BA6D1FAA059149AD2440DE6FBA6", + s: "178D49B1AE90E3D8B629BE3DB5683915F4E8C99FDF6E666CF37ADCFD", + }, + ecdsaFixture{ + name: "P224/SHA-384 #2", + key: p224, + alg: sha512.New384, + message: "test", + r: "389B92682E399B26518A95506B52C03BC9379A9DADF3391A21FB0EA4", + s: "414A718ED3249FF6DBC5B50C27F71F01F070944DA22AB1F78F559AAB", + }, + ecdsaFixture{ + name: "P224/SHA-512 #2", + key: p224, + alg: sha512.New, + message: "test", + r: "049F050477C5ADD858CAC56208394B5A55BAEBBE887FDF765047C17C", + s: "077EB13E7005929CEFA3CD0403C7CDCC077ADF4E44F3C41B2F60ECFF", + }, + // ECDSA, 256 Bits (Prime Field) + // https://tools.ietf.org/html/rfc6979#appendix-A.2.5 + ecdsaFixture{ + name: "P256/SHA-1 #1", + key: p256, + alg: sha1.New, + message: "sample", + r: "61340C88C3AAEBEB4F6D667F672CA9759A6CCAA9FA8811313039EE4A35471D32", + s: "6D7F147DAC089441BB2E2FE8F7A3FA264B9C475098FDCF6E00D7C996E1B8B7EB", + }, + ecdsaFixture{ + name: "P256/SHA-224 #1", + key: p256, + alg: sha256.New224, + message: "sample", + r: "53B2FFF5D1752B2C689DF257C04C40A587FABABB3F6FC2702F1343AF7CA9AA3F", + s: "B9AFB64FDC03DC1A131C7D2386D11E349F070AA432A4ACC918BEA988BF75C74C", + }, + ecdsaFixture{ + name: "P256/SHA-256 #1", + key: p256, + alg: sha256.New, + message: "sample", + r: "EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716", + s: "F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8", + }, + ecdsaFixture{ + name: "P256/SHA-384 #1", + key: p256, + alg: sha512.New384, + message: "sample", + r: "0EAFEA039B20E9B42309FB1D89E213057CBF973DC0CFC8F129EDDDC800EF7719", + s: "4861F0491E6998B9455193E34E7B0D284DDD7149A74B95B9261F13ABDE940954", + }, + ecdsaFixture{ + name: "P256/SHA-512 #1", + key: p256, + alg: sha512.New, + message: "sample", + r: "8496A60B5E9B47C825488827E0495B0E3FA109EC4568FD3F8D1097678EB97F00", + s: "2362AB1ADBE2B8ADF9CB9EDAB740EA6049C028114F2460F96554F61FAE3302FE", + }, + ecdsaFixture{ + name: "P256/SHA-1 #2", + key: p256, + alg: sha1.New, + message: "test", + r: "0CBCC86FD6ABD1D99E703E1EC50069EE5C0B4BA4B9AC60E409E8EC5910D81A89", + s: "01B9D7B73DFAA60D5651EC4591A0136F87653E0FD780C3B1BC872FFDEAE479B1", + }, + ecdsaFixture{ + name: "P256/SHA-224 #2", + key: p256, + alg: sha256.New224, + message: "test", + r: "C37EDB6F0AE79D47C3C27E962FA269BB4F441770357E114EE511F662EC34A692", + s: "C820053A05791E521FCAAD6042D40AEA1D6B1A540138558F47D0719800E18F2D", + }, + ecdsaFixture{ + name: "P256/SHA-256 #2", + key: p256, + alg: sha256.New, + message: "test", + r: "F1ABB023518351CD71D881567B1EA663ED3EFCF6C5132B354F28D3B0B7D38367", + s: "019F4113742A2B14BD25926B49C649155F267E60D3814B4C0CC84250E46F0083", + }, + ecdsaFixture{ + name: "P256/SHA-384 #2", + key: p256, + alg: sha512.New384, + message: "test", + r: "83910E8B48BB0C74244EBDF7F07A1C5413D61472BD941EF3920E623FBCCEBEB6", + s: "8DDBEC54CF8CD5874883841D712142A56A8D0F218F5003CB0296B6B509619F2C", + }, + ecdsaFixture{ + name: "P256/SHA-512 #2", + key: p256, + alg: sha512.New, + message: "test", + r: "461D93F31B6540894788FD206C07CFA0CC35F46FA3C91816FFF1040AD1581A04", + s: "39AF9F15DE0DB8D97E72719C74820D304CE5226E32DEDAE67519E840D1194E55", + }, + // ECDSA, 384 Bits (Prime Field) + // https://tools.ietf.org/html/rfc6979#appendix-A.2.6 + ecdsaFixture{ + name: "P384/SHA-1 #1", + key: p384, + alg: sha1.New, + message: "sample", + r: "EC748D839243D6FBEF4FC5C4859A7DFFD7F3ABDDF72014540C16D73309834FA37B9BA002899F6FDA3A4A9386790D4EB2", + s: "A3BCFA947BEEF4732BF247AC17F71676CB31A847B9FF0CBC9C9ED4C1A5B3FACF26F49CA031D4857570CCB5CA4424A443", + }, + ecdsaFixture{ + name: "P384/SHA-224 #1", + key: p384, + alg: sha256.New224, + message: "sample", + r: "42356E76B55A6D9B4631C865445DBE54E056D3B3431766D0509244793C3F9366450F76EE3DE43F5A125333A6BE060122", + s: "9DA0C81787064021E78DF658F2FBB0B042BF304665DB721F077A4298B095E4834C082C03D83028EFBF93A3C23940CA8D", + }, + ecdsaFixture{ + name: "P384/SHA-256 #1", + key: p384, + alg: sha256.New, + message: "sample", + r: "21B13D1E013C7FA1392D03C5F99AF8B30C570C6F98D4EA8E354B63A21D3DAA33BDE1E888E63355D92FA2B3C36D8FB2CD", + s: "F3AA443FB107745BF4BD77CB3891674632068A10CA67E3D45DB2266FA7D1FEEBEFDC63ECCD1AC42EC0CB8668A4FA0AB0", + }, + ecdsaFixture{ + name: "P384/SHA-384 #1", + key: p384, + alg: sha512.New384, + message: "sample", + r: "94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA73D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46", + s: "99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8", + }, + ecdsaFixture{ + name: "P384/SHA-512 #1", + key: p384, + alg: sha512.New, + message: "sample", + r: "ED0959D5880AB2D869AE7F6C2915C6D60F96507F9CB3E047C0046861DA4A799CFE30F35CC900056D7C99CD7882433709", + s: "512C8CCEEE3890A84058CE1E22DBC2198F42323CE8ACA9135329F03C068E5112DC7CC3EF3446DEFCEB01A45C2667FDD5", + }, + ecdsaFixture{ + name: "P384/SHA-1 #2", + key: p384, + alg: sha1.New, + message: "test", + r: "4BC35D3A50EF4E30576F58CD96CE6BF638025EE624004A1F7789A8B8E43D0678ACD9D29876DAF46638645F7F404B11C7", + s: "D5A6326C494ED3FF614703878961C0FDE7B2C278F9A65FD8C4B7186201A2991695BA1C84541327E966FA7B50F7382282", + }, + ecdsaFixture{ + name: "P384/SHA-224 #2", + key: p384, + alg: sha256.New224, + message: "test", + r: "E8C9D0B6EA72A0E7837FEA1D14A1A9557F29FAA45D3E7EE888FC5BF954B5E62464A9A817C47FF78B8C11066B24080E72", + s: "07041D4A7A0379AC7232FF72E6F77B6DDB8F09B16CCE0EC3286B2BD43FA8C6141C53EA5ABEF0D8231077A04540A96B66", + }, + ecdsaFixture{ + name: "P384/SHA-256 #2", + key: p384, + alg: sha256.New, + message: "test", + r: "6D6DEFAC9AB64DABAFE36C6BF510352A4CC27001263638E5B16D9BB51D451559F918EEDAF2293BE5B475CC8F0188636B", + s: "2D46F3BECBCC523D5F1A1256BF0C9B024D879BA9E838144C8BA6BAEB4B53B47D51AB373F9845C0514EEFB14024787265", + }, + ecdsaFixture{ + name: "P384/SHA-384 #2", + key: p384, + alg: sha512.New384, + message: "test", + r: "8203B63D3C853E8D77227FB377BCF7B7B772E97892A80F36AB775D509D7A5FEB0542A7F0812998DA8F1DD3CA3CF023DB", + s: "DDD0760448D42D8A43AF45AF836FCE4DE8BE06B485E9B61B827C2F13173923E06A739F040649A667BF3B828246BAA5A5", + }, + ecdsaFixture{ + name: "P384/SHA-512 #2", + key: p384, + alg: sha512.New, + message: "test", + r: "A0D5D090C9980FAF3C2CE57B7AE951D31977DD11C775D314AF55F76C676447D06FB6495CD21B4B6E340FC236584FB277", + s: "976984E59B4C77B0E8E4460DCA3D9F20E07B9BB1F63BEEFAF576F6B2E8B224634A2092CD3792E0159AD9CEE37659C736", + }, + // ECDSA, 521 Bits (Prime Field) + // https://tools.ietf.org/html/rfc6979#appendix-A.2.7 + ecdsaFixture{ + name: "P521/SHA-1 #1", + key: p521, + alg: sha1.New, + message: "sample", + r: "0343B6EC45728975EA5CBA6659BBB6062A5FF89EEA58BE3C80B619F322C87910FE092F7D45BB0F8EEE01ED3F20BABEC079D202AE677B243AB40B5431D497C55D75D", + s: "0E7B0E675A9B24413D448B8CC119D2BF7B2D2DF032741C096634D6D65D0DBE3D5694625FB9E8104D3B842C1B0E2D0B98BEA19341E8676AEF66AE4EBA3D5475D5D16", + }, + ecdsaFixture{ + name: "P521/SHA-224 #1", + key: p521, + alg: sha256.New224, + message: "sample", + r: "1776331CFCDF927D666E032E00CF776187BC9FDD8E69D0DABB4109FFE1B5E2A30715F4CC923A4A5E94D2503E9ACFED92857B7F31D7152E0F8C00C15FF3D87E2ED2E", + s: "050CB5265417FE2320BBB5A122B8E1A32BD699089851128E360E620A30C7E17BA41A666AF126CE100E5799B153B60528D5300D08489CA9178FB610A2006C254B41F", + }, + ecdsaFixture{ + name: "P521/SHA-256 #1", + key: p521, + alg: sha256.New, + message: "sample", + r: "1511BB4D675114FE266FC4372B87682BAECC01D3CC62CF2303C92B3526012659D16876E25C7C1E57648F23B73564D67F61C6F14D527D54972810421E7D87589E1A7", + s: "04A171143A83163D6DF460AAF61522695F207A58B95C0644D87E52AA1A347916E4F7A72930B1BC06DBE22CE3F58264AFD23704CBB63B29B931F7DE6C9D949A7ECFC", + }, + ecdsaFixture{ + name: "P521/SHA-384 #1", + key: p521, + alg: sha512.New384, + message: "sample", + r: "1EA842A0E17D2DE4F92C15315C63DDF72685C18195C2BB95E572B9C5136CA4B4B576AD712A52BE9730627D16054BA40CC0B8D3FF035B12AE75168397F5D50C67451", + s: "1F21A3CEE066E1961025FB048BD5FE2B7924D0CD797BABE0A83B66F1E35EEAF5FDE143FA85DC394A7DEE766523393784484BDF3E00114A1C857CDE1AA203DB65D61", + }, + ecdsaFixture{ + name: "P521/SHA-512 #1", + key: p521, + alg: sha512.New, + message: "sample", + r: "0C328FAFCBD79DD77850370C46325D987CB525569FB63C5D3BC53950E6D4C5F174E25A1EE9017B5D450606ADD152B534931D7D4E8455CC91F9B15BF05EC36E377FA", + s: "0617CCE7CF5064806C467F678D3B4080D6F1CC50AF26CA209417308281B68AF282623EAA63E5B5C0723D8B8C37FF0777B1A20F8CCB1DCCC43997F1EE0E44DA4A67A", + }, + ecdsaFixture{ + name: "P521/SHA-1 #2", + key: p521, + alg: sha1.New, + message: "test", + r: "13BAD9F29ABE20DE37EBEB823C252CA0F63361284015A3BF430A46AAA80B87B0693F0694BD88AFE4E661FC33B094CD3B7963BED5A727ED8BD6A3A202ABE009D0367", + s: "1E9BB81FF7944CA409AD138DBBEE228E1AFCC0C890FC78EC8604639CB0DBDC90F717A99EAD9D272855D00162EE9527567DD6A92CBD629805C0445282BBC916797FF", + }, + ecdsaFixture{ + name: "P521/SHA-224 #2", + key: p521, + alg: sha256.New224, + message: "test", + r: "1C7ED902E123E6815546065A2C4AF977B22AA8EADDB68B2C1110E7EA44D42086BFE4A34B67DDC0E17E96536E358219B23A706C6A6E16BA77B65E1C595D43CAE17FB", + s: "177336676304FCB343CE028B38E7B4FBA76C1C1B277DA18CAD2A8478B2A9A9F5BEC0F3BA04F35DB3E4263569EC6AADE8C92746E4C82F8299AE1B8F1739F8FD519A4", + }, + ecdsaFixture{ + name: "P521/SHA-256 #2", + key: p521, + alg: sha256.New, + message: "test", + r: "00E871C4A14F993C6C7369501900C4BC1E9C7B0B4BA44E04868B30B41D8071042EB28C4C250411D0CE08CD197E4188EA4876F279F90B3D8D74A3C76E6F1E4656AA8", + s: "0CD52DBAA33B063C3A6CD8058A1FB0A46A4754B034FCC644766CA14DA8CA5CA9FDE00E88C1AD60CCBA759025299079D7A427EC3CC5B619BFBC828E7769BCD694E86", + }, + ecdsaFixture{ + name: "P521/SHA-384 #2", + key: p521, + alg: sha512.New384, + message: "test", + r: "14BEE21A18B6D8B3C93FAB08D43E739707953244FDBE924FA926D76669E7AC8C89DF62ED8975C2D8397A65A49DCC09F6B0AC62272741924D479354D74FF6075578C", + s: "133330865C067A0EAF72362A65E2D7BC4E461E8C8995C3B6226A21BD1AA78F0ED94FE536A0DCA35534F0CD1510C41525D163FE9D74D134881E35141ED5E8E95B979", + }, + ecdsaFixture{ + name: "P521/SHA-512 #2", + key: p521, + alg: sha512.New, + message: "test", + r: "13E99020ABF5CEE7525D16B69B229652AB6BDF2AFFCAEF38773B4B7D08725F10CDB93482FDCC54EDCEE91ECA4166B2A7C6265EF0CE2BD7051B7CEF945BABD47EE6D", + s: "1FBD0013C674AA79CB39849527916CE301C66EA7CE8B80682786AD60F98F7E78A19CA69EFF5C57400E3B3A0AD66CE0978214D13BAF4E9AC60752F7B155E2DE4DCE3", + }, +} + +func TestECDSA(t *testing.T) { + for _, f := range fixtures { + testEcsaFixture(&f, t) + } +} + +func ecdsaLoadInt(s string) (n *big.Int) { + n, _ = new(big.Int).SetString(s, 16) + return +} + +func testEcsaFixture(f *ecdsaFixture, t *testing.T) { + t.Logf("Testing %s", f.name) + + h := f.alg() + h.Write([]byte(f.message)) + digest := h.Sum(nil) + + g := f.key.subgroup / 8 + if len(digest) > g { + digest = digest[0:g] + } + + r, s, err := rfc6979.SignECDSA(f.key.key, digest, f.alg) + if err != nil { + t.Error(err) + return + } + + expectedR := ecdsaLoadInt(f.r) + expectedS := ecdsaLoadInt(f.s) + + if r.Cmp(expectedR) != 0 { + t.Errorf("%s: Expected R of %X, got %X", f.name, expectedR, r) + } + + if s.Cmp(expectedS) != 0 { + t.Errorf("%s: Expected S of %X, got %X", f.name, expectedS, s) + } +} diff --git a/pkg/crypto/rfc6979/example_test.go b/pkg/crypto/rfc6979/example_test.go new file mode 100755 index 000000000..29693c272 --- /dev/null +++ b/pkg/crypto/rfc6979/example_test.go @@ -0,0 +1,76 @@ +package rfc6979 + +import ( + "crypto/dsa" + "crypto/ecdsa" + + "crypto/rand" + "crypto/sha1" + "crypto/sha512" + "fmt" + + "github.com/CityOfZion/neo-go/pkg/crypto/elliptic" +) + +// Generates a 521-bit ECDSA key, uses SHA-512 to sign a message, then verifies +// it. +func ExampleSignECDSA() { + // Generate a key pair. + // You need a high-quality PRNG for this. + curve := elliptic.NewEllipticCurve(elliptic.Secp256r1) + k, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + fmt.Println(err) + return + } + + // Hash a message. + alg := sha512.New() + alg.Write([]byte("I am a potato.")) + hash := alg.Sum(nil) + + // Sign the message. You don't need a PRNG for this. + + r, s, err := SignECDSA(curve, k.D.Bytes(), hash, sha512.New) + if err != nil { + fmt.Println(err) + return + } + + if !ecdsa.Verify(&k.PublicKey, hash, r, s) { + fmt.Println("Invalid signature!") + } +} + +// Generates a 1024-bit DSA key, uses SHA-1 to sign a message, then verifies it. +func ExampleSignDSA() { + // Here I'm generating some DSA params, but you should really pre-generate + // these and re-use them, since this takes a long time and isn't necessary. + k := new(dsa.PrivateKey) + dsa.GenerateParameters(&k.Parameters, rand.Reader, dsa.L1024N160) + + // Generate a key pair. + // You need a high-quality PRNG for this. + err := dsa.GenerateKey(k, rand.Reader) + if err != nil { + fmt.Println(err) + return + } + + // Hash a message. + alg := sha1.New() + alg.Write([]byte("I am a potato.")) + hash := alg.Sum(nil) + + // Sign the message. You don't need a PRNG for this. + r, s, err := SignDSA(k, hash, sha1.New) + if err != nil { + fmt.Println(err) + return + } + + if !dsa.Verify(&k.PublicKey, hash, r, s) { + fmt.Println("Invalid signature!") + } + +} diff --git a/pkg/crypto/rfc6979/rfc6979.go b/pkg/crypto/rfc6979/rfc6979.go new file mode 100755 index 000000000..cb7b4b0d8 --- /dev/null +++ b/pkg/crypto/rfc6979/rfc6979.go @@ -0,0 +1,119 @@ +/* +Package rfc6979 is an implementation of RFC 6979's deterministic DSA. + + Such signatures are compatible with standard Digital Signature Algorithm + (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA) digital + signatures and can be processed with unmodified verifiers, which need not be + aware of the procedure described therein. Deterministic signatures retain + the cryptographic security features associated with digital signatures but + can be more easily implemented in various environments, since they do not + need access to a source of high-quality randomness. + +(https://tools.ietf.org/html/rfc6979) + +Provides functions similar to crypto/dsa and crypto/ecdsa. +*/ +package rfc6979 + +import ( + "bytes" + "crypto/hmac" + "hash" + "math/big" +) + +// mac returns an HMAC of the given key and message. +func mac(alg func() hash.Hash, k, m, buf []byte) []byte { + h := hmac.New(alg, k) + h.Write(m) + return h.Sum(buf[:0]) +} + +// https://tools.ietf.org/html/rfc6979#section-2.3.2 +func bits2int(in []byte, qlen int) *big.Int { + vlen := len(in) * 8 + v := new(big.Int).SetBytes(in) + if vlen > qlen { + v = new(big.Int).Rsh(v, uint(vlen-qlen)) + } + return v +} + +// https://tools.ietf.org/html/rfc6979#section-2.3.3 +func int2octets(v *big.Int, rolen int) []byte { + out := v.Bytes() + + // pad with zeros if it's too short + if len(out) < rolen { + out2 := make([]byte, rolen) + copy(out2[rolen-len(out):], out) + return out2 + } + + // drop most significant bytes if it's too long + if len(out) > rolen { + out2 := make([]byte, rolen) + copy(out2, out[len(out)-rolen:]) + return out2 + } + + return out +} + +// https://tools.ietf.org/html/rfc6979#section-2.3.4 +func bits2octets(in []byte, q *big.Int, qlen, rolen int) []byte { + z1 := bits2int(in, qlen) + z2 := new(big.Int).Sub(z1, q) + if z2.Sign() < 0 { + return int2octets(z1, rolen) + } + return int2octets(z2, rolen) +} + +var one = big.NewInt(1) + +// https://tools.ietf.org/html/rfc6979#section-3.2 +func generateSecret(q, x *big.Int, alg func() hash.Hash, hash []byte, test func(*big.Int) bool) { + qlen := q.BitLen() + holen := alg().Size() + rolen := (qlen + 7) >> 3 + bx := append(int2octets(x, rolen), bits2octets(hash, q, qlen, rolen)...) + + // Step B + v := bytes.Repeat([]byte{0x01}, holen) + + // Step C + k := bytes.Repeat([]byte{0x00}, holen) + + // Step D + k = mac(alg, k, append(append(v, 0x00), bx...), k) + + // Step E + v = mac(alg, k, v, v) + + // Step F + k = mac(alg, k, append(append(v, 0x01), bx...), k) + + // Step G + v = mac(alg, k, v, v) + + // Step H + for { + // Step H1 + var t []byte + + // Step H2 + for len(t) < qlen/8 { + v = mac(alg, k, v, v) + t = append(t, v...) + } + + // Step H3 + secret := bits2int(t, qlen) + if secret.Cmp(one) >= 0 && secret.Cmp(q) < 0 && test(secret) { + return + } + k = mac(alg, k, append(v, 0x00), k) + v = mac(alg, k, v, v) + } +} diff --git a/pkg/crypto/rfc6979/rfc6979_test.go b/pkg/crypto/rfc6979/rfc6979_test.go new file mode 100755 index 000000000..cc9138653 --- /dev/null +++ b/pkg/crypto/rfc6979/rfc6979_test.go @@ -0,0 +1,28 @@ +package rfc6979 + +import ( + "crypto/sha256" + "encoding/hex" + "math/big" + "testing" +) + +// https://tools.ietf.org/html/rfc6979#appendix-A.1 +func TestGenerateSecret(t *testing.T) { + q, _ := new(big.Int).SetString("4000000000000000000020108A2E0CC0D99F8A5EF", 16) + + x, _ := new(big.Int).SetString("09A4D6792295A7F730FC3F2B49CBC0F62E862272F", 16) + + hash, _ := hex.DecodeString("AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF") + + expected, _ := new(big.Int).SetString("23AF4074C90A02B3FE61D286D5C87F425E6BDD81B", 16) + var actual *big.Int + generateSecret(q, x, sha256.New, hash, func(k *big.Int) bool { + actual = k + return true + }) + + if actual.Cmp(expected) != 0 { + t.Errorf("Expected %x, got %x", expected, actual) + } +} diff --git a/pkg/database/leveldb.go b/pkg/database/leveldb.go new file mode 100644 index 000000000..c079fa6a0 --- /dev/null +++ b/pkg/database/leveldb.go @@ -0,0 +1,143 @@ +package database + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/errors" +) + +type LDB struct { + db *leveldb.DB + path string +} + +type Database interface { + Has(key []byte) (bool, error) + Put(key []byte, value []byte) error + Get(key []byte) ([]byte, error) + Delete(key []byte) error + Close() error +} + +var ( + // TX, HEADER AND UTXO are the prefixes for the db + TX = []byte("TX") + HEADER = []byte("HEADER") + LATESTHEADER = []byte("LH") + UTXO = []byte("UTXO") +) + +func New(path string) *LDB { + db, err := leveldb.OpenFile(path, nil) + + if _, corrupted := err.(*errors.ErrCorrupted); corrupted { + db, err = leveldb.RecoverFile(path, nil) + } + + if err != nil { + return nil + } + + return &LDB{ + db, + path, + } +} + +func (l *LDB) Has(key []byte) (bool, error) { + return l.db.Has(key, nil) +} + +func (l *LDB) Put(key []byte, value []byte) error { + return l.db.Put(key, value, nil) +} +func (l *LDB) Get(key []byte) ([]byte, error) { + return l.db.Get(key, nil) +} +func (l *LDB) Delete(key []byte) error { + return l.db.Delete(key, nil) +} +func (l *LDB) Close() error { + return l.db.Close() +} + +func (l *LDB) AddHeader(header *payload.BlockBase) error { + + table := NewTable(l, HEADER) + + byt, err := header.Bytes() + if err != nil { + fmt.Println("Could not Get bytes from decoded BlockBase") + return nil + } + + fmt.Println("Adding Header, This should be batched!!!!") + + // This is the main mapping + //Key: HEADER+BLOCKHASH Value: contents of blockhash + key := header.Hash.Bytes() + err = table.Put(key, byt) + if err != nil { + fmt.Println("Error trying to add the original mapping into the DB for Header. Mapping is [Header]+[Hash]") + return err + } + + // This is the secondary mapping + // Key: HEADER + BLOCKHEIGHT Value: blockhash + + bh := uint32ToBytes(header.Index) + key = []byte(bh) + err = table.Put(key, header.Hash.Bytes()) + if err != nil { + return err + } + // This is the third mapping + // WARNING: This assumes that headers are adding in order. + return table.Put(LATESTHEADER, header.Hash.Bytes()) +} + +func (l *LDB) AddTransactions(blockhash util.Uint256, txs []transaction.Transactioner) error { + + // SHOULD BE DONE IN BATCH!!!! + for i, tx := range txs { + buf := new(bytes.Buffer) + fmt.Println(tx.ID()) + tx.Encode(buf) + txByt := buf.Bytes() + txhash, err := tx.ID() + if err != nil { + fmt.Println("Error adding transaction with bytes", txByt) + return err + } + // This is the original mapping + // Key: [TX] + TXHASH + key := append(TX, txhash.Bytes()...) + l.Put(key, txByt) + + // This is the index + // Key: [TX] + BLOCKHASH + I <- i is the incrementer from the for loop + //Value : TXHASH + key = append(TX, blockhash.Bytes()...) + key = append(key, uint32ToBytes(uint32(i))...) + + err = l.Put(key, txhash.Bytes()) + if err != nil { + fmt.Println("Error could not add tx index into db") + return err + } + } + return nil +} + +// BigEndian +func uint32ToBytes(h uint32) []byte { + a := make([]byte, 4) + binary.BigEndian.PutUint32(a, h) + return a +} diff --git a/pkg/database/leveldb_test.go b/pkg/database/leveldb_test.go new file mode 100644 index 000000000..0991682ee --- /dev/null +++ b/pkg/database/leveldb_test.go @@ -0,0 +1,80 @@ +package database_test + +import ( + "os" + "testing" + + "github.com/CityOfZion/neo-go/pkg/database" + "github.com/stretchr/testify/assert" + "github.com/syndtr/goleveldb/leveldb/errors" +) + +const path = "temp" + +func cleanup(db *database.LDB) { + db.Close() + os.RemoveAll(path) +} +func TestDBCreate(t *testing.T) { + db := database.New(path) + assert.NotEqual(t, nil, db) + cleanup(db) +} +func TestPutGet(t *testing.T) { + db := database.New(path) + + key := []byte("Hello") + value := []byte("World") + + err := db.Put(key, value) + assert.Equal(t, nil, err) + + res, err := db.Get(key) + assert.Equal(t, nil, err) + assert.Equal(t, value, res) + cleanup(db) +} +func TestPutDelete(t *testing.T) { + + db := database.New(path) + + key := []byte("Hello") + value := []byte("World") + + err := db.Put(key, value) + + err = db.Delete(key) + assert.Equal(t, nil, err) + + res, err := db.Get(key) + + assert.Equal(t, errors.ErrNotFound, err) + assert.Equal(t, res, []byte{}) + cleanup(db) +} + +func TestHas(t *testing.T) { + db := database.New("temp") + + res, err := db.Has([]byte("NotExist")) + assert.Equal(t, res, false) + assert.Equal(t, err, nil) + + key := []byte("Hello") + value := []byte("World") + + err = db.Put(key, value) + assert.Equal(t, nil, err) + + res, err = db.Has(key) + assert.Equal(t, res, true) + assert.Equal(t, err, nil) + cleanup(db) + +} +func TestDBClose(t *testing.T) { + db := database.New("temp") + err := db.Close() + assert.Equal(t, nil, err) + cleanup(db) +} diff --git a/pkg/database/table.go b/pkg/database/table.go new file mode 100644 index 000000000..549e50229 --- /dev/null +++ b/pkg/database/table.go @@ -0,0 +1,36 @@ +package database + +//Table is an abstract datastructure built on +// top of a db +type Table struct { + prefix []byte + db Database +} + +func NewTable(db Database, prefix []byte) *Table { + return &Table{ + prefix, + db, + } +} + +func (t *Table) Has(key []byte) (bool, error) { + key = append(t.prefix, key...) + return t.db.Has(key) +} + +func (t *Table) Put(key []byte, value []byte) error { + key = append(t.prefix, key...) + return t.db.Put(key, value) +} +func (t *Table) Get(key []byte) ([]byte, error) { + key = append(t.prefix, key...) + return t.db.Get(key) +} +func (t *Table) Delete(key []byte) error { + key = append(t.prefix, key...) + return t.db.Delete(key) +} +func (t *Table) Close() error { + return nil +} diff --git a/pkg/mempool/config.go b/pkg/mempool/config.go new file mode 100644 index 000000000..19f529d0e --- /dev/null +++ b/pkg/mempool/config.go @@ -0,0 +1,31 @@ +package mempool + +import "time" + +type Config struct { + + // This is the maximum amount + // of transactions that we will allow in the mempool + MaxNumOfTX uint64 + + // FreeTX defines the maximum amount of free txs that can be in the mempool at one time + // Default is 20 + FreeTX uint32 + + // MinTXFee is a number in Fixed8 format. If set at 1GAS, minTXFee would equal 1e8 + // The mineTXFee is used to set the floor, it defaults to zero meaning we will allow all transactions + // with a fee of 0 or more + MinTXFee uint64 + + // MaxTXSize is the maximum number of bytes a tx can have to be entered into the pool + MaxTXSize uint64 + + // TXTTL is the duration to which we should keep an item in the mempool before removing it + // HMM: Should this be amount of blocks instead? For when blocks take time a long time + // to process? + TXTTL time.Duration + + // SigLimit is the maximum amount of signatures + // that we will allow a tx to have, default will be 20 + SigLimit uint8 +} diff --git a/pkg/mempool/mempool.go b/pkg/mempool/mempool.go new file mode 100644 index 000000000..07c1adb9c --- /dev/null +++ b/pkg/mempool/mempool.go @@ -0,0 +1,138 @@ +package mempool + +import ( + "errors" + "fmt" + "sync" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +var ( + ErrMemPoolFull = errors.New("mempool is currently full") + ErrMempoolEmpty = errors.New("There are no TXs in the mempool") + ErrTXTooBig = errors.New("TX has exceed the maximum threshold") + ErrTXTooManyWitnesses = errors.New("Too many witness scripts") + ErrFeeTooLow = errors.New("Fee for transaction too low") + ErrDuplicateTX = errors.New("TX Already in pool") +) + +type Mempool struct { + mtx sync.RWMutex + pool map[util.Uint256]*TX + + cfg Config +} + +func New(cfg Config) *Mempool { + mem := &Mempool{ + sync.RWMutex{}, + make(map[util.Uint256]*TX, 200), + cfg, + } + + return mem +} +func (m *Mempool) AddTransaction(trans transaction.Transactioner) error { + + hash, err := trans.ID() + if err != nil { + return err + } + + // check if tx already in pool + if m.Exists(hash) { + return ErrDuplicateTX + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + if m.cfg.MaxNumOfTX == uint64(len(m.pool)) { + return ErrMemPoolFull + } + + // TODO:Check for double spend from blockchain itself + + // create tx descriptor + tx := Descriptor(trans) + + // check TX size + if tx.Size > m.cfg.MaxTXSize { + return ErrTXTooBig + } + + // check witness length + if tx.NumWitness > m.cfg.SigLimit { + return ErrTXTooManyWitnesses + } + + // TODO: check witness data is good -- Add method to take the Witness and tx return true or false.(blockchain) + + //check fee is over minimum cnfigured + if tx.Fee < m.cfg.MinTXFee { + return ErrFeeTooLow + } + + // Add into pool + m.pool[hash] = tx + + return nil +} + +// RemoveTransaction will remove a transaction from the nodes mempool +func (m *Mempool) RemoveTransaction(hash util.Uint256) error { + m.mtx.Lock() + defer m.mtx.Unlock() + if len(m.pool) == 0 { + return ErrMempoolEmpty + } + // deletes regardless of whether key is there or not. So do not check for existence before delete. + // Use Exists() for this. + delete(m.pool, hash) + + return nil +} + +// Size returns the size of the mempool +func (m *Mempool) Size() uint64 { + m.mtx.RLock() + len := uint64(len(m.pool)) + m.mtx.RUnlock() + + return len +} + +// ReturnAllTransactions will return all transactions in the +// mempool, will be mostly used by the RPC server +func (m *Mempool) ReturnAllTransactions() ([]transaction.Transactioner, error) { + transactions := make([]transaction.Transactioner, 0) + + m.mtx.RLock() + defer m.mtx.RUnlock() + if len(m.pool) == 0 { + return nil, ErrMempoolEmpty + } + + for _, t := range m.pool { + + if t.ParentTX == nil { + fmt.Println(t, "NILNIL") + } + transactions = append(transactions, *t.ParentTX) + fmt.Println(transactions) + } + + return transactions, nil + +} + +// Exists check whether the transaction exists in the mempool +func (m *Mempool) Exists(hash util.Uint256) bool { + m.mtx.RLock() + _, ok := m.pool[hash] + m.mtx.RUnlock() + + return ok +} diff --git a/pkg/mempool/mempool_test.go b/pkg/mempool/mempool_test.go new file mode 100644 index 000000000..7d75a03d6 --- /dev/null +++ b/pkg/mempool/mempool_test.go @@ -0,0 +1,211 @@ +package mempool_test + +import ( + "testing" + "time" + + "github.com/CityOfZion/neo-go/pkg/mempool" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/stretchr/testify/assert" +) + +func TestMempoolExists(t *testing.T) { + cfg := mempool.Config{ + MaxNumOfTX: 100, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: 10000, + TXTTL: 10 * time.Minute, + SigLimit: 20, + } + mem := mempool.New(cfg) + + trans := transaction.NewContract(0) + + assert.Equal(t, false, mem.Exists(trans.Hash)) + + err := mem.AddTransaction(trans) + assert.Equal(t, nil, err) + + assert.Equal(t, true, mem.Exists(trans.Hash)) +} +func TestMempoolFullPool(t *testing.T) { + + maxTx := uint64(100) + cfg := mempool.Config{ + MaxNumOfTX: maxTx, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: 10000, + TXTTL: 10 * time.Minute, + SigLimit: 20, + } + mem := mempool.New(cfg) + + for i := uint64(1); i <= maxTx; i++ { + trans := transaction.NewContract(0) + attr := &transaction.Attribute{ + Usage: transaction.Remark, + Data: []byte{byte(i)}, + } + trans.AddAttribute(attr) + err := mem.AddTransaction(trans) + assert.Equal(t, nil, err) + } + trans := transaction.NewContract(0) + err := mem.AddTransaction(trans) + assert.NotEqual(t, nil, err) + + assert.Equal(t, mempool.ErrMemPoolFull, err) +} +func TestMempoolLargeTX(t *testing.T) { + + maxTxSize := uint64(100) + cfg := mempool.Config{ + MaxNumOfTX: 100, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: maxTxSize, + TXTTL: 10 * time.Minute, + SigLimit: 20, + } + mem := mempool.New(cfg) + + trans := transaction.NewContract(0) + for i := uint64(1); i <= 100; i++ { // 100 attributes will be over 100 bytes + attr := &transaction.Attribute{ + Usage: transaction.Remark, + Data: []byte{byte(i)}, + } + trans.AddAttribute(attr) + } + + err := mem.AddTransaction(trans) + assert.NotEqual(t, nil, err) + assert.Equal(t, mempool.ErrTXTooBig, err) +} +func TestMempoolTooManyWitness(t *testing.T) { + + maxWitness := uint8(3) + cfg := mempool.Config{ + MaxNumOfTX: 100, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: 10000, + TXTTL: 10 * time.Minute, + SigLimit: maxWitness, + } + mem := mempool.New(cfg) + + trans := transaction.NewContract(0) + for i := uint8(1); i <= maxWitness; i++ { // 100 attributes will be over 100 bytes + wit := &transaction.Witness{ + InvocationScript: []byte{byte(i)}, + VerificationScript: []byte{byte(i)}, + } + trans.AddWitness(wit) + } + + trans.AddWitness(&transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: []byte{}, + }) + + err := mem.AddTransaction(trans) + assert.NotEqual(t, nil, err) + assert.Equal(t, mempool.ErrTXTooManyWitnesses, err) +} +func TestMempoolDuplicate(t *testing.T) { + + cfg := mempool.Config{ + MaxNumOfTX: 100, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: 10000, + TXTTL: 10 * time.Minute, + SigLimit: 1, + } + mem := mempool.New(cfg) + + trans := transaction.NewContract(0) + + err := mem.AddTransaction(trans) + assert.Equal(t, nil, err) + + err = mem.AddTransaction(trans) + assert.NotEqual(t, nil, err) + assert.Equal(t, mempool.ErrDuplicateTX, err) +} +func TestMempoolReturnAll(t *testing.T) { + + cfg := mempool.Config{ + MaxNumOfTX: 100, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: 10000, + TXTTL: 10 * time.Minute, + SigLimit: 1, + } + mem := mempool.New(cfg) + + numTx := uint64(10) + + for i := uint64(1); i <= numTx; i++ { + trans := transaction.NewContract(0) + attr := &transaction.Attribute{ + Usage: transaction.Remark, + Data: []byte{byte(i)}, + } + trans.AddAttribute(attr) + err := mem.AddTransaction(trans) + assert.Equal(t, nil, err) + } + + AllTrans, err := mem.ReturnAllTransactions() + assert.Equal(t, nil, err) + + assert.Equal(t, numTx, uint64(len(AllTrans))) + +} +func TestMempoolRemove(t *testing.T) { + + cfg := mempool.Config{ + MaxNumOfTX: 100, + FreeTX: 20, + MinTXFee: 0, + MaxTXSize: 10000, + TXTTL: 3 * time.Minute, + SigLimit: 1, + } + mem := mempool.New(cfg) + + // Remove a transaction when mempool is empty + trans := transaction.NewContract(0) + hash, _ := trans.ID() + err := mem.RemoveTransaction(hash) + assert.Equal(t, mempool.ErrMempoolEmpty, err) + + // Add tx1 into mempool + err = mem.AddTransaction(trans) + assert.Equal(t, nil, err) + + diffTrans := transaction.NewContract(0) // TX2 + + diffTrans.AddAttribute( + &transaction.Attribute{ + Usage: transaction.Remark, + Data: []byte{}, + }) + + diffHash, _ := diffTrans.ID() + + // Try removing TX2, when only TX1 is in mempool + err = mem.RemoveTransaction(diffHash) + assert.Equal(t, nil, err) + assert.Equal(t, uint64(1), mem.Size()) + // Remove hash that is in mempool + err = mem.RemoveTransaction(hash) + assert.Equal(t, nil, err) + assert.Equal(t, uint64(0), mem.Size()) + +} diff --git a/pkg/mempool/tx.go b/pkg/mempool/tx.go new file mode 100644 index 000000000..ac793af5f --- /dev/null +++ b/pkg/mempool/tx.go @@ -0,0 +1,45 @@ +package mempool + +import ( + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" +) + +// TX is a wrapper struct around a normal tx +// which includes extra information about the TX +// +type TX struct { + ParentTX *transaction.Transactioner + Added time.Time + Fee uint64 + Size uint64 + NumWitness uint8 + Free bool +} + +// Descriptor takes a transaction and puts it into a new TX struct along with metadata +func Descriptor(trans transaction.Transactioner) *TX { + + var desc TX + desc.ParentTX = &trans + desc.Fee = getFee(trans.TXOs(), trans.UTXOs()) + desc.Free = desc.Fee != 0 + desc.Added = time.Now() + desc.Size = uint64(len(trans.Bytes())) + + numWit := len(trans.Witness()) + if numWit > 255 || numWit < 0 { // < 0 should not happen + numWit = 255 + } + desc.NumWitness = uint8(numWit) + + return &desc +} + +// TODO: need blockchain package complete for fee calculation +// HMM:Could also put the function in the config +func getFee(in []*transaction.Input, out []*transaction.Output) uint64 { + // query utxo set for inputs, then subtract from out to get fee + return 0 +} diff --git a/pkg/peer/config.go b/pkg/peer/config.go new file mode 100644 index 000000000..0614e5952 --- /dev/null +++ b/pkg/peer/config.go @@ -0,0 +1,48 @@ +package peer + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" +) + +// LocalConfig specifies the properties that should be available for each remote peer + +type LocalConfig struct { + Net protocol.Magic + UserAgent string + Services protocol.ServiceFlag + Nonce uint32 + ProtocolVer protocol.Version + Relay bool + Port uint16 + // pointer to config will keep the startheight updated for each version + //Message we plan to send + StartHeight func() uint32 + OnHeader func(*Peer, *payload.HeadersMessage) + OnGetHeaders func(msg *payload.GetHeadersMessage) // returns HeaderMessage + OnAddr func(*Peer, *payload.AddrMessage) + OnGetAddr func(*Peer, *payload.GetAddrMessage) + OnInv func(*Peer, *payload.InvMessage) + OnGetData func(msg *payload.GetDataMessage) + OnBlock func(*Peer, *payload.BlockMessage) + OnGetBlocks func(msg *payload.GetBlocksMessage) +} + +// func DefaultConfig() LocalConfig { +// return LocalConfig{ +// Net: protocol.MainNet, +// UserAgent: "NEO-GO-Default", +// Services: protocol.NodePeerService, +// Nonce: 1200, +// ProtocolVer: 0, +// Relay: false, +// Port: 10332, +// // pointer to config will keep the startheight updated for each version +// //Message we plan to send +// StartHeight: DefaultHeight, +// } +// } + +// func DefaultHeight() uint32 { +// return 10 +// } diff --git a/pkg/peer/peer.go b/pkg/peer/peer.go new file mode 100644 index 000000000..11668f478 --- /dev/null +++ b/pkg/peer/peer.go @@ -0,0 +1,433 @@ +// This impl uses channels to simulate the queue handler with the actor model. +// A suitable number k ,should be set for channel size, because if #numOfMsg > k, +// we lose determinism. k chosen should be large enough that when filled, it shall indicate that +// the peer has stopped responding, since we do not have a pingMSG, we will need another way to shut down +// peers + +package peer + +import ( + "errors" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + + "github.com/CityOfZion/neo-go/pkg/peer/stall" + "github.com/CityOfZion/neo-go/pkg/wire" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +const ( + maxOutboundConnections = 100 + protocolVer = protocol.DefaultVersion + handshakeTimeout = 30 * time.Second + idleTimeout = 5 * time.Minute // If no message received after idleTimeout, then peer disconnects + + // nodes will have `responseTime` seconds to reply with a response + responseTime = 120 * time.Second + + // the stall detector will check every `tickerInterval` to see if messages + // are overdue. Should be less than `responseTime` + tickerInterval = 30 * time.Second + + // The input buffer size is the amount of mesages that + // can be buffered into the channel to receive at once before + // blocking, and before determinism is broken + inputBufferSize = 100 + + // The output buffer size is the amount of messages that + // can be buffered into the channel to send at once before + // blocking, and before determinism is broken. + outputBufferSize = 100 + + // pingInterval = 20 * time.Second //Not implemented in neo clients +) + +var ( + errHandShakeTimeout = errors.New("Handshake timed out, peers have " + string(handshakeTimeout) + " Seconds to Complete the handshake") +) + +type Peer struct { + config LocalConfig + conn net.Conn + + // atomic vals + disconnected int32 + + //unchangeable state: concurrent safe + addr string + protoVer protocol.Version + port uint16 + inbound bool + userAgent string + services protocol.ServiceFlag + createdAt time.Time + relay bool + + statemutex sync.Mutex + verackReceived bool + versionKnown bool + + *stall.Detector + + inch chan func() // will handle all incoming connections from peer + outch chan func() // will handle all outcoming connections from peer + quitch chan struct{} +} + +func NewPeer(con net.Conn, inbound bool, cfg LocalConfig) *Peer { + p := Peer{} + p.inch = make(chan func(), inputBufferSize) + p.outch = make(chan func(), outputBufferSize) + p.quitch = make(chan struct{}, 1) + p.inbound = inbound + p.config = cfg + p.conn = con + p.createdAt = time.Now() + p.addr = p.conn.RemoteAddr().String() + + p.Detector = stall.NewDetector(responseTime, tickerInterval) + + // TODO: set the unchangeable states + return &p +} + +// Write to a peer +func (p *Peer) Write(msg wire.Messager) error { + return wire.WriteMessage(p.conn, p.config.Net, msg) +} + +// Read to a peer +func (p *Peer) Read() (wire.Messager, error) { + return wire.ReadMessage(p.conn, p.config.Net) +} + +// Disconnects from a peer +func (p *Peer) Disconnect() { + + // return if already disconnected + if atomic.LoadInt32(&p.disconnected) != 0 { + return + } + + atomic.AddInt32(&p.disconnected, 1) + + p.Detector.Quit() + close(p.quitch) + p.conn.Close() + + fmt.Println("Disconnected Peer with address", p.RemoteAddr().String()) + +} + +// Exposed API functions below +func (p *Peer) Port() uint16 { + return p.port +} +func (p *Peer) CreatedAt() time.Time { + return p.createdAt +} +func (p *Peer) CanRelay() bool { + return p.relay +} +func (p *Peer) LocalAddr() net.Addr { + return p.conn.LocalAddr() +} +func (p *Peer) RemoteAddr() net.Addr { + return p.conn.RemoteAddr() +} +func (p *Peer) Services() protocol.ServiceFlag { + return p.config.Services +} +func (p *Peer) Inbound() bool { + return p.inbound +} +func (p *Peer) UserAgent() string { + return p.config.UserAgent +} +func (p *Peer) IsVerackReceived() bool { + return p.verackReceived +} +func (p *Peer) NotifyDisconnect() bool { + fmt.Println("Peer has not disconnected yet") + <-p.quitch + fmt.Println("Peer has just disconnected") + return true +} + +//End of Exposed API functions// + +// Ping not impl. in neo yet, adding it now +// will cause this client to disconnect from all other implementations +func (p *Peer) PingLoop() { /*not implemented in other neo clients*/ } + +// Run is used to start communicating with the peer +// completes the handshake and starts observing +// for messages coming in +func (p *Peer) Run() error { + + err := p.Handshake() + + go p.StartProtocol() + go p.ReadLoop() + go p.WriteLoop() + + //go p.PingLoop() // since it is not implemented. It will disconnect all other impls. + return err + +} + +// run as a go-routine, will act as our queue for messages +// should be ran after handshake +func (p *Peer) StartProtocol() { +loop: + for atomic.LoadInt32(&p.disconnected) == 0 { + select { + case f := <-p.inch: + f() + case <-p.quitch: + break loop + case <-p.Detector.Quitch: + fmt.Println("Peer stalled, disconnecting") + break loop + } + } + p.Disconnect() +} + +// Should only be called after handshake is complete +// on a seperate go-routine. +// ReadLoop Will block on the read until a message is +// read + +func (p *Peer) ReadLoop() { + + idleTimer := time.AfterFunc(idleTimeout, func() { + fmt.Println("Timing out peer") + p.Disconnect() + }) + +loop: + for atomic.LoadInt32(&p.disconnected) == 0 { + + idleTimer.Reset(idleTimeout) // reset timer on each loop + + readmsg, err := p.Read() + + // Message read; stop Timer + idleTimer.Stop() + + if err != nil { + fmt.Println("Err on read", err) // This will also happen if Peer is disconnected + break loop + } + + // Remove message as pending from the stall detector + p.Detector.RemoveMessage(readmsg.Command()) + + switch msg := readmsg.(type) { + + case *payload.VersionMessage: + fmt.Println("Already received a Version, disconnecting. " + p.RemoteAddr().String()) + break loop // We have already done the handshake, break loop and disconnect + case *payload.VerackMessage: + if p.verackReceived { + fmt.Println("Already received a Verack, disconnecting. " + p.RemoteAddr().String()) + break loop + } + p.statemutex.Lock() // This should not happen, however if it does, then we should set it. + p.verackReceived = true + p.statemutex.Unlock() + case *payload.AddrMessage: + p.OnAddr(msg) + case *payload.GetAddrMessage: + p.OnGetAddr(msg) + case *payload.GetBlocksMessage: + p.OnGetBlocks(msg) + case *payload.BlockMessage: + p.OnBlocks(msg) + case *payload.HeadersMessage: + p.OnHeaders(msg) + case *payload.GetHeadersMessage: + p.OnGetHeaders(msg) + case *payload.InvMessage: + p.OnInv(msg) + case *payload.GetDataMessage: + p.OnGetData(msg) + case *payload.TXMessage: + p.OnTX(msg) + default: + fmt.Println("Cannot recognise message", msg.Command()) //Do not disconnect peer, just Log Message + } + } + + idleTimer.Stop() + p.Disconnect() +} + +// WriteLoop will Queue all messages to be written to +// the peer. +func (p *Peer) WriteLoop() { + for atomic.LoadInt32(&p.disconnected) == 0 { + select { + case f := <-p.outch: + f() + case <-p.Detector.Quitch: // if the detector quits, disconnect peer + p.Disconnect() + } + } +} + +func (p *Peer) OnGetData(msg *payload.GetDataMessage) { + + p.inch <- func() { + // fmt.Println(msg.Hashes) + fmt.Println("That was an getdata Message please pass func down through config", msg.Command()) + } +} +func (p *Peer) OnTX(msg *payload.TXMessage) { + + p.inch <- func() { + // fmt.Println(msg.Hashes) + getdata, err := payload.NewGetDataMessage(payload.InvTypeTx) + if err != nil { + fmt.Println("Eor", err) + } + id, err := msg.Tx.ID() + getdata.AddHash(id) + p.Write(getdata) + fmt.Println("That was an tx Message please pass func down through config", msg.Command()) + } +} + +func (p *Peer) OnInv(msg *payload.InvMessage) { + + p.inch <- func() { + if p.config.OnInv != nil { + p.config.OnInv(p, msg) + } + fmt.Println("That was an inv Message please pass func down through config", msg.Command()) + } +} + +// OnGetHeaders Listener, outside of the anonymous func will be extra functionality +// like timing +func (p *Peer) OnGetHeaders(msg *payload.GetHeadersMessage) { + p.inch <- func() { + if p.config.OnGetHeaders != nil { + p.config.OnGetHeaders(msg) + } + fmt.Println("That was a getheaders message, please pass func down through config", msg.Command()) + + } +} + +// OnAddr Listener +func (p *Peer) OnAddr(msg *payload.AddrMessage) { + p.inch <- func() { + if p.config.OnAddr != nil { + p.config.OnAddr(p, msg) + } + fmt.Println("That was a addr message, please pass func down through config", msg.Command()) + + } +} + +// OnGetAddr Listener +func (p *Peer) OnGetAddr(msg *payload.GetAddrMessage) { + p.inch <- func() { + if p.config.OnGetAddr != nil { + p.config.OnGetAddr(p, msg) + } + fmt.Println("That was a getaddr message, please pass func down through config", msg.Command()) + + } +} + +// OnGetBlocks Listener +func (p *Peer) OnGetBlocks(msg *payload.GetBlocksMessage) { + p.inch <- func() { + if p.config.OnGetBlocks != nil { + p.config.OnGetBlocks(msg) + } + fmt.Println("That was a getblocks message, please pass func down through config", msg.Command()) + } +} + +// OnBlocks Listener +func (p *Peer) OnBlocks(msg *payload.BlockMessage) { + p.inch <- func() { + if p.config.OnBlock != nil { + p.config.OnBlock(p, msg) + } + } +} + +// OnVersion Listener will be called +// during the handshake, any error checking should be done here for the versionMessage. +// This should only ever be called during the handshake. Any other place and the peer will disconnect. +func (p *Peer) OnVersion(msg *payload.VersionMessage) error { + if msg.Nonce == p.config.Nonce { + p.conn.Close() + return errors.New("Self connection, disconnecting Peer") + } + p.versionKnown = true + p.port = msg.Port + p.services = msg.Services + p.userAgent = string(msg.UserAgent) + p.createdAt = time.Now() + p.relay = msg.Relay + return nil +} + +// OnHeaders Listener +func (p *Peer) OnHeaders(msg *payload.HeadersMessage) { + fmt.Println("We have received the headers") + p.inch <- func() { + if p.config.OnHeader != nil { + p.config.OnHeader(p, msg) + } + } +} + +// RequestHeaders will write a getheaders to peer +func (p *Peer) RequestHeaders(hash util.Uint256) error { + fmt.Println("Sending header request") + c := make(chan error, 0) + p.outch <- func() { + p.Detector.AddMessage(command.GetHeaders) + getHeaders, err := payload.NewGetHeadersMessage([]util.Uint256{hash}, util.Uint256{}) + err = p.Write(getHeaders) + c <- err + } + + return <-c + +} + +// RequestBlocks will ask a peer for a block +func (p *Peer) RequestBlocks(hashes []util.Uint256) error { + fmt.Println("Requesting block from peer") + c := make(chan error, 0) + + p.outch <- func() { + p.Detector.AddMessage(command.GetData) + getdata, err := payload.NewGetDataMessage(payload.InvTypeBlock) + err = getdata.AddHashes(hashes) + if err != nil { + c <- err + return + } + err = p.Write(getdata) + c <- err + } + + return <-c + +} diff --git a/pkg/peer/peer_test.go b/pkg/peer/peer_test.go new file mode 100644 index 000000000..adcb4d342 --- /dev/null +++ b/pkg/peer/peer_test.go @@ -0,0 +1,209 @@ +package peer_test + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/CityOfZion/neo-go/pkg/peer" + "github.com/CityOfZion/neo-go/pkg/wire" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/stretchr/testify/assert" +) + +func returnConfig() peer.LocalConfig { + + DefaultHeight := func() uint32 { + return 10 + } + + OnAddr := func(p *peer.Peer, msg *payload.AddrMessage) {} + OnHeader := func(p *peer.Peer, msg *payload.HeadersMessage) {} + OnGetHeaders := func(msg *payload.GetHeadersMessage) {} + OnInv := func(p *peer.Peer, msg *payload.InvMessage) {} + OnGetData := func(msg *payload.GetDataMessage) {} + OnBlock := func(p *peer.Peer, msg *payload.BlockMessage) {} + OnGetBlocks := func(msg *payload.GetBlocksMessage) {} + + return peer.LocalConfig{ + Net: protocol.MainNet, + UserAgent: "NEO-GO-Default", + Services: protocol.NodePeerService, + Nonce: 1200, + ProtocolVer: 0, + Relay: false, + Port: 10332, + // pointer to config will keep the startheight updated for each version + //Message we plan to send + StartHeight: DefaultHeight, + OnHeader: OnHeader, + OnAddr: OnAddr, + OnGetHeaders: OnGetHeaders, + OnInv: OnInv, + OnGetData: OnGetData, + OnBlock: OnBlock, + OnGetBlocks: OnGetBlocks, + } +} + +func TestHandshake(t *testing.T) { + address := ":20338" + go func() { + + conn, err := net.DialTimeout("tcp", address, 2*time.Second) + if err != nil { + t.Fatal(err) + } + p := peer.NewPeer(conn, true, returnConfig()) + err = p.Run() + verack, err := payload.NewVerackMessage() + if err != nil { + t.Fail() + } + if err := p.Write(verack); err != nil { + t.Fatal(err) + } + + assert.Equal(t, true, p.IsVerackReceived()) + + }() + + listener, err := net.Listen("tcp", address) + if err != nil { + t.Fatal(err) + return + } + + defer func() { + listener.Close() + }() + + for { + + conn, err := listener.Accept() + if err != nil { + t.Fatal(err) + } + + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP("82.2.97.142"), Port: 20338} + nonce := uint32(100) + messageVer, err := payload.NewVersionMessage(tcpAddrMe, 2595770, false, protocol.DefaultVersion, protocol.UserAgent, nonce, protocol.NodePeerService) + + if err != nil { + t.Fatal(err) + } + + if err := wire.WriteMessage(conn, protocol.MainNet, messageVer); err != nil { + t.Fatal(err) + return + } + + readmsg, err := wire.ReadMessage(conn, protocol.MainNet) + if err != nil { + t.Fatal(err) + } + version, ok := readmsg.(*payload.VersionMessage) + if !ok { + t.Fatal(err) + } + + assert.NotEqual(t, nil, version) + + messageVrck, err := payload.NewVerackMessage() + if err != nil { + t.Fatal(err) + } + + assert.NotEqual(t, nil, messageVrck) + + if err := wire.WriteMessage(conn, protocol.MainNet, messageVrck); err != nil { + t.Fatal(err) + } + + readmsg, err = wire.ReadMessage(conn, protocol.MainNet) + if err != nil { + t.Fatal(err) + } + + assert.NotEqual(t, nil, readmsg) + + verk, ok := readmsg.(*payload.VerackMessage) + if !ok { + t.Fatal(err) + } + assert.NotEqual(t, nil, verk) + + return + } + +} + +func TestConfigurations(t *testing.T) { + _, conn := net.Pipe() + + inbound := true + + config := returnConfig() + + p := peer.NewPeer(conn, inbound, config) + + // test inbound + assert.Equal(t, inbound, p.Inbound()) + + // handshake not done, should be false + assert.Equal(t, false, p.IsVerackReceived()) + + assert.Equal(t, config.Services, p.Services()) + + assert.Equal(t, config.UserAgent, p.UserAgent()) + + assert.Equal(t, config.Relay, p.CanRelay()) + + assert.WithinDuration(t, time.Now(), p.CreatedAt(), 1*time.Second) + +} + +func TestHandshakeCancelled(t *testing.T) { + // These are the conditions which should invalidate the handshake. + // Make sure peer is disconnected. +} + +func TestPeerDisconnect(t *testing.T) { + // Make sure everything is shutdown + // Make sure timer is shutdown in stall detector too. Should maybe put this part of test into stall detector. + + _, conn := net.Pipe() + inbound := true + config := returnConfig() + p := peer.NewPeer(conn, inbound, config) + fmt.Println("Calling disconnect") + p.Disconnect() + fmt.Println("Disconnect finished calling") + verack, _ := payload.NewVerackMessage() + + fmt.Println(" We good here") + + err := p.Write(verack) + + assert.NotEqual(t, err, nil) + + // Check if Stall detector is still running + _, ok := <-p.Detector.Quitch + assert.Equal(t, ok, false) + +} + +func TestNotifyDisconnect(t *testing.T) { + + _, conn := net.Pipe() + inbound := true + config := returnConfig() + p := peer.NewPeer(conn, inbound, config) + + p.Disconnect() + p.NotifyDisconnect() + // TestNotify uses default test timeout as the passing condition + // Failure condition can be seen when you comment out p.Disconnect() +} diff --git a/pkg/peer/peerhandshake.go b/pkg/peer/peerhandshake.go new file mode 100644 index 000000000..2cad19c5d --- /dev/null +++ b/pkg/peer/peerhandshake.go @@ -0,0 +1,131 @@ +package peer + +import ( + "fmt" + "net" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/util/ip" +) + +func (p *Peer) Handshake() error { + + handshakeErr := make(chan error, 1) + go func() { + if p.inbound { + handshakeErr <- p.inboundHandShake() + } else { + handshakeErr <- p.outboundHandShake() + } + }() + + select { + case err := <-handshakeErr: + if err != nil { + return err + } + case <-time.After(handshakeTimeout): + return errHandShakeTimeout + } + + // This is purely here for Logs + if p.inbound { + fmt.Println("inbound handshake with", p.RemoteAddr().String(), "successful") + } else { + + fmt.Println("outbound handshake with", p.RemoteAddr().String(), "successful") + } + return nil +} + +// If this peer has an inbound conn (conn that is going into another peer) +// then he has dialed and so, we must read the version message +func (p *Peer) inboundHandShake() error { + var err error + if err := p.writeLocalVersionMSG(); err != nil { + return err + } + if err := p.readRemoteVersionMSG(); err != nil { + return err + } + verack, err := payload.NewVerackMessage() + if err != nil { + return err + } + err = p.Write(verack) + return p.readVerack() +} +func (p *Peer) outboundHandShake() error { + var err error + err = p.readRemoteVersionMSG() + if err != nil { + return err + } + + err = p.writeLocalVersionMSG() + if err != nil { + return err + } + + err = p.readVerack() + if err != nil { + return err + } + verack, err := payload.NewVerackMessage() + if err != nil { + return err + } + return p.Write(verack) +} +func (p *Peer) writeLocalVersionMSG() error { + + nonce := p.config.Nonce + relay := p.config.Relay + port := int(p.config.Port) + ua := p.config.UserAgent + sh := p.config.StartHeight() + services := p.config.Services + proto := p.config.ProtocolVer + ip := iputils.GetLocalIP() + tcpAddrMe := &net.TCPAddr{IP: ip, Port: port} + + messageVer, err := payload.NewVersionMessage(tcpAddrMe, sh, relay, proto, ua, nonce, services) + + if err != nil { + return err + } + return p.Write(messageVer) +} + +func (p *Peer) readRemoteVersionMSG() error { + readmsg, err := wire.ReadMessage(p.conn, p.config.Net) + if err != nil { + return err + } + + version, ok := readmsg.(*payload.VersionMessage) + if !ok { + return err + } + return p.OnVersion(version) +} + +func (p *Peer) readVerack() error { + readmsg, err := wire.ReadMessage(p.conn, p.config.Net) + + if err != nil { + return err + } + + _, ok := readmsg.(*payload.VerackMessage) + + if !ok { + return err + } + // should only be accessed on one go-routine + p.verackReceived = true + + return nil +} diff --git a/pkg/peer/readme.md b/pkg/peer/readme.md new file mode 100644 index 000000000..ea13a62a5 --- /dev/null +++ b/pkg/peer/readme.md @@ -0,0 +1,67 @@ +# Package - Peer + + + +## Responsibility + +Once a connection has been made. The connection will represent a established peer to the localNode. Since a connection and the `Wire` is a golang primitive, that we cannot do much with. The peer package will encapsulate both, while adding extra functionality. + + +## Features + +- The handshake protocol is automatically executed and handled by the peer package. If a Version/Verack is received twice, the peer will be disconnected. + +- IdleTimeouts: If a Message is not received from the peer within a set period of time, the peer will be disconnected. + +- StallTimeouts: For Example, If a GetHeaders, is sent to the Peer and a Headers Response is not received within a certain period of time, then the peer is disconnected. + +- Concurrency Model: The concurrency model used is similar to Actor model, with a few changes. Messages can be sent to a peer asynchronously or synchronously. An example of an synchornous message send is the `RequestHeaders` method, where the channel blocks until an error value is received. The `OnHeaders` message is however asynchronously called. Furthermore, all methods passed through the config, are wrapped inside of an additional `Peers` method, this is to lay the ground work to capturing statistics regarding a specific command. These are also used so that we can pass behaviour to be executed down the channel. + +- Configuration: Each Peer will have a config struct passed to it, with information about the Local Peer and functions that will encapsulate the behaviour of what the peer should do, given a request. This way, the peer is not dependent on any other package. + +## Usage + + conn, err := net.Dial("tcp", "seed2.neo.org:10333") + if err != nil { + fmt.Println("Error dialing connection", err.Error()) + return + } + + config := peer.LocalConfig{ + Net: protocol.MainNet, + UserAgent: "NEO-G", + Services: protocol.NodePeerService, + Nonce: 1200, + ProtocolVer: 0, + Relay: false, + Port: 10332, + StartHeight: LocalHeight, + OnHeader: OnHeader, + } + + p := peer.NewPeer(conn, false, config) + err = p.Run() + + hash, err := util.Uint256DecodeString(chainparams.GenesisHash) + // hash2, err := util.Uint256DecodeString("ff8fe95efc5d1cc3a22b17503aecaf289cef68f94b79ddad6f613569ca2342d8") + err = p.RequestHeaders(hash) + + func OnHeader(peer *peer.Peer, msg *payload.HeadersMessage) { + // This function is passed to peer + // and the peer will execute it on receiving a header + } + + func LocalHeight() uint32 { + // This will be a function from the object that handles the block heights + return 10 + } + + +### Notes + + +Should we follow the actor model for Peers? Each Peer will have a ID, which we can take as the PID or if +we launch a go-routine for each peer, then we can use that as an implicit PID. + +Peer information should be stored into a database, if no db exists, we should get it from an initial peers file. +We can use this to periodically store information about a peer. \ No newline at end of file diff --git a/pkg/peer/stall/stall.go b/pkg/peer/stall/stall.go new file mode 100644 index 000000000..85bf11858 --- /dev/null +++ b/pkg/peer/stall/stall.go @@ -0,0 +1,174 @@ +package stall + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +// stall detector will keep track of all pendingMessages +// If any message takes too long to reply +// the detector will disconnect the peer + +type Detector struct { + responseTime time.Duration + tickInterval time.Duration + + lock sync.Mutex + responses map[command.Type]time.Time + + // The detector is embedded into a peer and the peer watches this quit chan + // If this chan is closed, the peer disconnects + Quitch chan struct{} + + // atomic vals + disconnected int32 +} + +// rT is the responseTime and signals how long +// a peer has to reply back to a sent message +// tickerInterval is how often the detector wil check for stalled messages +func NewDetector(rTime time.Duration, tickerInterval time.Duration) *Detector { + d := &Detector{ + responseTime: rTime, + tickInterval: tickerInterval, + lock: sync.Mutex{}, + responses: map[command.Type]time.Time{}, + Quitch: make(chan struct{}), + } + go d.loop() + return d +} + +func (d *Detector) loop() { + ticker := time.NewTicker(d.tickInterval) + +loop: + for { + select { + case <-ticker.C: + now := time.Now() + for _, deadline := range d.responses { + if now.After(deadline) { + fmt.Println("Deadline passed") + ticker.Stop() + break loop + } + } + + } + } + d.Quit() + d.DeleteAll() + ticker.Stop() +} + +// Quit is a concurrent safe way to call the Quit channel +// Without blocking +func (d *Detector) Quit() { + // return if already disconnected + if atomic.LoadInt32(&d.disconnected) != 0 { + return + } + + atomic.AddInt32(&d.disconnected, 1) + close(d.Quitch) +} + +// Call this function when we send a message to a peer +// The command passed through is the command that we sent +// and not the command we expect to receive +func (d *Detector) AddMessage(cmd command.Type) { + cmds := d.addMessage(cmd) + d.lock.Lock() + for _, cmd := range cmds { + d.responses[cmd] = time.Now().Add(d.responseTime) + } + d.lock.Unlock() +} + +// Call this function when we receive a message from +// peer. This will remove the pendingresponse message from the map. +// The command passed through is the command we received +func (d *Detector) RemoveMessage(cmd command.Type) { + cmds := d.addMessage(cmd) + d.lock.Lock() + for _, cmd := range cmds { + delete(d.responses, cmd) + } + d.lock.Unlock() +} + +// DeleteAll empties the map of all contents and +// is called when the detector is being shut down +func (d *Detector) DeleteAll() { + d.lock.Lock() + d.responses = make(map[command.Type]time.Time) + d.lock.Unlock() +} + +// GetMessages Will return a map of all of the pendingResponses +// and their deadlines +func (d *Detector) GetMessages() map[command.Type]time.Time { + var resp map[command.Type]time.Time + d.lock.Lock() + resp = d.responses + d.lock.Unlock() + return resp +} + +// when a message is added, we will add a deadline for +// expected response +func (d *Detector) addMessage(cmd command.Type) []command.Type { + + cmds := []command.Type{} + + switch cmd { + case command.GetHeaders: + // We now will expect a Headers Message + cmds = append(cmds, command.Headers) + case command.GetAddr: + // We now will expect a Headers Message + cmds = append(cmds, command.Addr) + + case command.GetData: + // We will now expect a block/tx message + // We can optimise this by including the exact inventory type, however it is not needed + cmds = append(cmds, command.Block) + cmds = append(cmds, command.TX) + case command.GetBlocks: + // we will now expect a inv message + cmds = append(cmds, command.Inv) + case command.Version: + // We will now expect a verack + cmds = append(cmds, command.Verack) + } + return cmds +} + +// if receive a message, we will delete it from pending +func (d *Detector) removeMessage(cmd command.Type) []command.Type { + + cmds := []command.Type{} + + switch cmd { + case command.Block: + // We will now expect a block/tx message + cmds = append(cmds, command.Block) + cmds = append(cmds, command.TX) + case command.TX: + // We will now expect a block/tx message + cmds = append(cmds, command.Block) + cmds = append(cmds, command.TX) + case command.GetBlocks: + // we will now expect a inv message + cmds = append(cmds, command.Inv) + default: + // We will now expect a verack + cmds = append(cmds, cmd) + } + return cmds +} diff --git a/pkg/peer/stall/stall_test.go b/pkg/peer/stall/stall_test.go new file mode 100644 index 000000000..5b6a2b755 --- /dev/null +++ b/pkg/peer/stall/stall_test.go @@ -0,0 +1,75 @@ +package stall + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +func TestAddRemoveMessage(t *testing.T) { + + responseTime := 2 * time.Second + tickerInterval := 1 * time.Second + + d := NewDetector(responseTime, tickerInterval) + d.AddMessage(command.GetAddr) + mp := d.GetMessages() + + assert.Equal(t, 1, len(mp)) + assert.IsType(t, time.Time{}, mp[command.GetAddr]) + + d.RemoveMessage(command.GetAddr) + mp = d.GetMessages() + + assert.Equal(t, 0, len(mp)) + assert.Empty(t, mp[command.GetAddr]) +} + +type mockPeer struct { + online bool + detector *Detector +} + +func (mp *mockPeer) loop() { +loop: + for { + select { + case <-mp.detector.Quitch: + + break loop + } + } + // cleanup + mp.online = false +} +func TestDeadlineWorks(t *testing.T) { + + responseTime := 2 * time.Second + tickerInterval := 1 * time.Second + + d := NewDetector(responseTime, tickerInterval) + mp := mockPeer{online: true, detector: d} + go mp.loop() + + d.AddMessage(command.GetAddr) + time.Sleep(responseTime + 1*time.Second) + + k := make(map[command.Type]time.Time) + assert.Equal(t, k, d.responses) + assert.Equal(t, false, mp.online) + +} +func TestDeadlineShouldNotBeEmpty(t *testing.T) { + responseTime := 10 * time.Second + tickerInterval := 1 * time.Second + + d := NewDetector(responseTime, tickerInterval) + d.AddMessage(command.GetAddr) + time.Sleep(1 * time.Second) + + k := make(map[command.Type]time.Time) + assert.NotEqual(t, k, d.responses) +} diff --git a/pkg/peermanager/peermgr.go b/pkg/peermanager/peermgr.go new file mode 100644 index 000000000..9083b135d --- /dev/null +++ b/pkg/peermanager/peermgr.go @@ -0,0 +1,67 @@ +package peermanager + +import ( + "errors" + "fmt" + + "github.com/CityOfZion/neo-go/pkg/peer" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// NOTE: This package may be removed in the future +// and so full functionality is not yet implemented, see Issue #33 for more details. + +//PeerMgr will act as a convenience Mgr +// It will be notified of added Peers +// It will take care of sending messages to the right peers. In this way, it acts as a load balancer +// If we send a getdata to one peer, it will be smart and send it to another peer who is not as busy +// Using subscription model, we can have the syncmanager/other modules notify the peermgr when they have received data +type PeerMgr struct { + peers []*peer.Peer +} + +// New will create a new peer manager +// As of now it just returns a peerMgr struct and so +// the New method is redundant. A config file will be passed as a parameter, +// if it is decided that we will use this. +func New() *PeerMgr { + return &PeerMgr{} +} + +// Disconnect will close the connection on a peer and +// remove it from the list +// TODO: remove from list once disconnected +func (pm *PeerMgr) Disconnect(p *peer.Peer) { + p.Disconnect() + // Once disconnected, we remove it from the list + // and look for more peers to connect to +} + +// RequestHeaders will request the headers from the most available peer +// As of now, it requests from the first peer in the list, TODO(Kev) +func (pm *PeerMgr) RequestHeaders(hash util.Uint256) (*peer.Peer, error) { + + if len(pm.peers) == 0 { + return nil, errors.New("Peer manager currently has no peers") + } + + return pm.peers[0], pm.peers[0].RequestHeaders(hash) +} + +// RequestBlocks will request blocks from the most available peer +// As of now, it requests from the first peer in the list, TODO(Kev) +func (pm *PeerMgr) RequestBlocks(hash []util.Uint256) (*peer.Peer, error) { + + if len(pm.peers) == 0 { + return nil, errors.New("Peer manager currently has no peers") + } + + return pm.peers[0], pm.peers[0].RequestBlocks(hash) +} + +// AddPeer will add a new peer for the PeerManager to use +func (pm *PeerMgr) AddPeer(p *peer.Peer) error { + pm.peers = append(pm.peers, p) + fmt.Println("Adding peers into the peermanager") + return nil +} diff --git a/pkg/pubsub/event.go b/pkg/pubsub/event.go new file mode 100644 index 000000000..c24893a40 --- /dev/null +++ b/pkg/pubsub/event.go @@ -0,0 +1,14 @@ +package pubsub + +type EventType int + +const ( + NewBlock EventType = iota // When blockchain connects a new block, it will emit an NewBlock Event + BadBlock // When blockchain declines a block, it will emit a new block event + BadHeader // When blockchain rejects a Header, it will emit this event +) + +type Event struct { + Type EventType // E.g. event.NewBlock + data []byte // Raw information +} diff --git a/pkg/pubsub/pub.go b/pkg/pubsub/pub.go new file mode 100644 index 000000000..a0eacb182 --- /dev/null +++ b/pkg/pubsub/pub.go @@ -0,0 +1,20 @@ +package pubsub + +type Publisher struct { + subs []Subscriber +} + +// Send iterates over each subscriber and checks +// if they are interested in the Event +// By looking at their topics, if they are then +// the event is emitted to them +func (p *Publisher) Send(e Event) error { + for _, sub := range p.subs { + for _, topic := range sub.Topics() { + if e.Type == topic { + sub.Emit(e) + } + } + } + return nil +} diff --git a/pkg/pubsub/sub.go b/pkg/pubsub/sub.go new file mode 100644 index 000000000..21a51eb30 --- /dev/null +++ b/pkg/pubsub/sub.go @@ -0,0 +1,6 @@ +package pubsub + +type Subscriber interface { + Topics() []EventType + Emit(Event) +} diff --git a/pkg/syncmanager/config.go b/pkg/syncmanager/config.go new file mode 100644 index 000000000..9b8cbbf4a --- /dev/null +++ b/pkg/syncmanager/config.go @@ -0,0 +1,11 @@ +package syncmanager + +import ( + "github.com/CityOfZion/neo-go/pkg/blockchain" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Config struct { + Chain *blockchain.Chain + BestHash util.Uint256 +} diff --git a/pkg/syncmanager/syncman.go b/pkg/syncmanager/syncman.go new file mode 100644 index 000000000..2c6d863d1 --- /dev/null +++ b/pkg/syncmanager/syncman.go @@ -0,0 +1,152 @@ +// The syncmanager will use a modified verison of the initial block download in bitcoin +// Seen here: https://en.bitcoinwiki.org/wiki/Bitcoin_Core_0.11_(ch_5):_Initial_Block_Download +// MovingWindow is a desired featured from the original codebase + +package syncmanager + +import ( + "fmt" + + "github.com/CityOfZion/neo-go/pkg/peermanager" + + "github.com/CityOfZion/neo-go/pkg/blockchain" + "github.com/CityOfZion/neo-go/pkg/peer" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +var ( + // This is the maximum amount of inflight objects that we would like to have + // Number taken from original codebase + maxBlockRequest = 1024 + + // This is the maximum amount of blocks that we will ask for from a single peer + // Number taken from original codebase + maxBlockRequestPerPeer = 16 +) + +type Syncmanager struct { + pmgr *peermanager.PeerMgr + Mode int // 1 = headersFirst, 2 = Blocks, 3 = Maintain + chain *blockchain.Chain + headers []util.Uint256 + inflightBlockReqs map[util.Uint256]*peer.Peer // when we send a req for block, we will put hash in here, along with peer who we requested it from +} + +// New will setup the syncmanager with the required +// parameters +func New(cfg Config) *Syncmanager { + return &Syncmanager{ + peermanager.New(), + 1, + cfg.Chain, + []util.Uint256{}, + make(map[util.Uint256]*peer.Peer, 2000), + } +} + +func (s *Syncmanager) AddPeer(peer *peer.Peer) error { + return s.pmgr.AddPeer(peer) +} + +func (s *Syncmanager) OnHeaders(p *peer.Peer, msg *payload.HeadersMessage) { + fmt.Println("Sync manager On Headers called") + // On receipt of Headers + // check what mode we are in + // HeadersMode, we check if there is 2k. If so call again. If not then change mode into BlocksOnly + if s.Mode == 1 { + err := s.HeadersFirstMode(p, msg) + if err != nil { + fmt.Println("Error re blocks", err) + return // We should custom name error so, that we can do something on WrongHash Error, Peer disconnect error + } + return + } +} + +func (s *Syncmanager) HeadersFirstMode(p *peer.Peer, msg *payload.HeadersMessage) error { + + fmt.Println("Headers first mode") + + // Validate Headers + err := s.chain.ValidateHeaders(msg) + + if err != nil { + // Re-request headers from a different peer + s.pmgr.Disconnect(p) + fmt.Println("Error Validating headers", err) + return err + } + + // Add Headers into db + err = s.chain.AddHeaders(msg) + if err != nil { + // Try addding them into the db again? + // Since this is simply a db insert, any problems here means trouble + //TODO(KEV) : Should we Switch off system or warn the user that the system is corrupted? + fmt.Println("Error Adding headers", err) + + //TODO: Batching is not yet implemented, + // So here we would need to remove headers which have been added + // from the slice + return err + } + + // Add header hashes into slice + // Requets first batch of blocks here + var hashes []util.Uint256 + for _, header := range msg.Headers { + hashes = append(hashes, header.Hash) + } + s.headers = append(s.headers, hashes...) + + if len(msg.Headers) == 2*1e3 { // should be less than 2000, leave it as this for tests + fmt.Println("Switching to BlocksOnly Mode") + s.Mode = 2 // switch to BlocksOnly. XXX: because HeadersFirst is not in parallel, no race condition here. + return s.RequestMoreBlocks() + } + lastHeader := msg.Headers[len(msg.Headers)-1] + _, err = s.pmgr.RequestHeaders(lastHeader.Hash) + return err +} + +func (s *Syncmanager) RequestMoreBlocks() error { + + var blockReq []util.Uint256 + + var reqAmount int + + if len(s.headers) >= maxBlockRequestPerPeer { + reqAmount = maxBlockRequestPerPeer + blockReq = s.headers[:reqAmount] + } else { + reqAmount = len(s.headers) + blockReq = s.headers[:reqAmount] + } + peer, err := s.pmgr.RequestBlocks(blockReq) + if err != nil { // This could happen if the peermanager has no valid peers to connect to. We should wait a bit and re-request + return err // alternatively we could make RequestBlocks blocking, then make sure it is not triggered when a block is received + } + + //XXX: Possible race condition, between us requesting the block and adding it to + // the inflight block map? Give that node a medal. + + for _, hash := range s.headers { + s.inflightBlockReqs[hash] = peer + } + s.headers = s.headers[reqAmount:] + // NONONO: Here we do not pass all of the hashes to peermanager because + // it is not the peermanagers responsibility to mange inflight blocks + return err +} + +// OnBlock receives a block from a peer, then passes it to the blockchain to process. +// For now we will only use this simple setup, to allow us to test the other parts of the system. +// See Issue #24 +func (s *Syncmanager) OnBlock(p *peer.Peer, msg *payload.BlockMessage) { + err := s.chain.AddBlock(msg) + if err != nil { + // Put headers back in front of queue to fetch block for. + fmt.Println("Block had an error", err) + } +} diff --git a/pkg/wire/Readme.md b/pkg/wire/Readme.md new file mode 100644 index 000000000..e121047bd --- /dev/null +++ b/pkg/wire/Readme.md @@ -0,0 +1,62 @@ +# Package - Wire + + +The neo wire package will implement the network protocol displayed here: http://docs.neo.org/en-us/network/network-protocol.html + +This package will act as a standalone package. + +# Responsibility + +This package will solely be responsible for Encoding and decoding a Message. +It will return a Messager interface, which means that the caller of the package will need to type assert it to the appropriate type. + +# Usage + +## Write Message + + expectedIP := "127.0.0.1" + expectedPort := 8333 + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP(expectedIP), Port: expectedPort} + message, err := NewVersionMessage(tcpAddrMe, 0, true, defaultVersion) + + conn := new(bytes.Buffer) + if err := WriteMessage(con, Production, message); err != nil { + // handle Error + } + +## Read Message + + readmsg, err := ReadMessage(conn, Production) + + if err != nil { + // Log error + } + version := readmsg.(*VersionMessage) + +## RoadMap + +These below commands are left to implement. + + [ x ] CMDVersion (Tests added) + [ x ] CMDVerack (Tests Added) + [ x ] CMDGetAddr(Tests Added) + [ x ] CMDAddr (Tests Added) + [ x ] CMDGetHeaders (Tests Added) + [ x ] CMDHeaders (Tests Added) + [ x ] CMDGetBlocks (Tests Added) + [ x ] CMDInv (Tests Added) + [ x ] CMDGetData (Tests Added) + [ x ] CMDBlock (Tests Added) + [ x ] CMDTX // Each tx implments the messager interface + [ ] CMDConsensus + +## Notes + +Please not that this package will do sanity checks on the fields, however it will not verify if any of the items are valid for the current state of the system. Please see `Responbilities`. + +The difference between Encode/Decode and EncodePayload/DecodePayload, is the parameter type. +In most cases, Encode/Decode is just a convenience method. +# Contributors + +When modifying this package, please ensure that it does not depend on any other package and that it conforms to the Single Responsibility Principle. If you see somewhere in the current implementation that does not do this, then please tell me. + diff --git a/pkg/wire/base.go b/pkg/wire/base.go new file mode 100644 index 000000000..1d778f45e --- /dev/null +++ b/pkg/wire/base.go @@ -0,0 +1,44 @@ +package wire + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// Base is everything in the message except the payload + +type Base struct { + Magic uint32 + CMD command.Type + PayloadLength uint32 + Checksum uint32 +} + +// Note, That there is no EncodeBase +// As the header is implicitly inferred from +// the message on Encode To send +func (h *Base) DecodeBase(r io.Reader) (io.Reader, error) { + br := &util.BinReader{R: r} + + br.Read(&h.Magic) + + var cmd [12]byte + br.Read(&cmd) + h.CMD = command.Type(cmdByteArrayToString(cmd)) + + br.Read(&h.PayloadLength) + br.Read(&h.Checksum) + return br.R, br.Err +} + +func cmdByteArrayToString(cmd [command.Size]byte) string { + buf := []byte{} + for i := 0; i < command.Size; i++ { + if cmd[i] != 0 { + buf = append(buf, cmd[i]) + } + } + return string(buf) +} diff --git a/pkg/wire/command/command.go b/pkg/wire/command/command.go new file mode 100644 index 000000000..3d9b749fe --- /dev/null +++ b/pkg/wire/command/command.go @@ -0,0 +1,30 @@ +package command + +// Size of command field in bytes +const ( + Size = 12 +) + +// CommandType represents the type of a message command. +type Type string + +// Valid protocol commands used to send between nodes. +// use this to get +const ( + Version Type = "version" + Mempool Type = "mempool" + Ping Type = "ping" + Pong Type = "pong" + Verack Type = "verack" + GetAddr Type = "getaddr" + Addr Type = "addr" + GetHeaders Type = "getheaders" + Headers Type = "headers" + GetBlocks Type = "getblocks" + Inv Type = "inv" + GetData Type = "getdata" + Block Type = "block" + TX Type = "tx" + Consensus Type = "consensus" + Unknown Type = "unknown" +) diff --git a/pkg/wire/message.go b/pkg/wire/message.go new file mode 100644 index 000000000..d02c5ce92 --- /dev/null +++ b/pkg/wire/message.go @@ -0,0 +1,148 @@ +package wire + +import ( + "bufio" + "bytes" + "errors" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + checksum "github.com/CityOfZion/neo-go/pkg/wire/util/Checksum" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Messager interface { + EncodePayload(w io.Writer) error + DecodePayload(r io.Reader) error + Command() command.Type +} + +const ( + // Magic + cmd + length + checksum + minMsgSize = 4 + 12 + 4 + 4 +) + +var ( + errChecksumMismatch = errors.New("checksum mismatch") +) + +func WriteMessage(w io.Writer, magic protocol.Magic, message Messager) error { + bw := &util.BinWriter{W: w} + bw.Write(magic) + bw.Write(cmdToByteArray(message.Command())) + + buf := new(bytes.Buffer) + if err := message.EncodePayload(buf); err != nil { + return err + } + + payloadLen := util.BufferLength(buf) + checksum := checksum.FromBytes(buf.Bytes()) + + bw.Write(payloadLen) + bw.Write(checksum) + + bw.WriteBigEnd(buf.Bytes()) + + return bw.Err +} + +func ReadMessage(r io.Reader, magic protocol.Magic) (Messager, error) { + + byt := make([]byte, minMsgSize) + + if _, err := io.ReadFull(r, byt); err != nil { + return nil, err + } + + reader := bytes.NewReader(byt) + + var header Base + _, err := header.DecodeBase(reader) + + if err != nil { + return nil, errors.New("Error decoding into the header base") + } + + buf := new(bytes.Buffer) + + _, err = io.CopyN(buf, r, int64(header.PayloadLength)) + if err != nil { + return nil, err + } + + // Compare the checksum of the payload. + if !checksum.Compare(header.Checksum, buf.Bytes()) { + return nil, errChecksumMismatch + } + switch header.CMD { + case command.Version: + v := &payload.VersionMessage{} + err := v.DecodePayload(buf) + return v, err + case command.Verack: + v, err := payload.NewVerackMessage() + err = v.DecodePayload(buf) + return v, err + case command.Inv: + v, err := payload.NewInvMessage(0) + err = v.DecodePayload(buf) + return v, err + case command.GetAddr: + v, err := payload.NewGetAddrMessage() + err = v.DecodePayload(buf) + return v, err + case command.Addr: + v, err := payload.NewAddrMessage() + err = v.DecodePayload(buf) + return v, err + case command.Block: + v, err := payload.NewBlockMessage() + err = v.DecodePayload(buf) + return v, err + case command.GetBlocks: + v, err := payload.NewGetBlocksMessage([]util.Uint256{}, util.Uint256{}) + err = v.DecodePayload(buf) + return v, err + case command.GetData: + v, err := payload.NewGetDataMessage(payload.InvTypeTx) + err = v.DecodePayload(buf) + return v, err + case command.GetHeaders: + v, err := payload.NewGetHeadersMessage([]util.Uint256{}, util.Uint256{}) + err = v.DecodePayload(buf) + return v, err + case command.Headers: + v, err := payload.NewHeadersMessage() + err = v.DecodePayload(buf) + return v, err + case command.TX: + reader := bufio.NewReader(buf) + tx, err := transaction.FromBytes(reader) + if err != nil { + return nil, err + } + return payload.NewTXMessage(tx) + } + return nil, errors.New("Unknown Message found") + +} + +func cmdToByteArray(cmd command.Type) [command.Size]byte { + cmdLen := len(cmd) + if cmdLen > command.Size { + panic("exceeded command max length of size 12") + } + + // The command can have max 12 bytes, rest is filled with 0. + b := [command.Size]byte{} + for i := 0; i < cmdLen; i++ { + b[i] = cmd[i] + } + + return b +} diff --git a/pkg/wire/message_test.go b/pkg/wire/message_test.go new file mode 100644 index 000000000..320653801 --- /dev/null +++ b/pkg/wire/message_test.go @@ -0,0 +1,55 @@ +package wire + +import ( + "bytes" + "net" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/stretchr/testify/assert" +) + +// This is quite hard to test because the message uses time.Now() +// TODO: Test each field expect time.Now(), just make sure it is a uint32 +func TestWriteMessageLen(t *testing.T) { + + expectedIP := "127.0.0.1" + expectedPort := 8333 + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP(expectedIP), Port: expectedPort} + + message, err := payload.NewVersionMessage(tcpAddrMe, 0, true, protocol.DefaultVersion, protocol.UserAgent, 100, protocol.NodePeerService) + if err != nil { + assert.Fail(t, err.Error()) + } + + buf := new(bytes.Buffer) + if err := WriteMessage(buf, protocol.MainNet, message); err != nil { + assert.Fail(t, err.Error()) + } + assert.Equal(t, 60, len(buf.Bytes())) +} +func TestReadMessage(t *testing.T) { + + expectedIP := "127.0.0.1" + expectedPort := 8333 + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP(expectedIP), Port: expectedPort} + + message, err := payload.NewVersionMessage(tcpAddrMe, 23, true, protocol.DefaultVersion, protocol.UserAgent, 100, protocol.NodePeerService) + if err != nil { + assert.Fail(t, err.Error()) + } + buf := new(bytes.Buffer) + if err := WriteMessage(buf, protocol.MainNet, message); err != nil { + assert.Fail(t, err.Error()) + } + + readmsg, err := ReadMessage(buf, protocol.MainNet) + + if err != nil { + assert.Fail(t, err.Error()) + } + version := readmsg.(*payload.VersionMessage) + assert.Equal(t, 23, int(version.StartHeight)) + // If MessageReading was unsuccessfull it will return a nil object +} diff --git a/pkg/wire/payload/block.go b/pkg/wire/payload/block.go new file mode 100644 index 000000000..7dc4e000e --- /dev/null +++ b/pkg/wire/payload/block.go @@ -0,0 +1,59 @@ +package payload + +import ( + "bufio" + "bytes" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Block struct { + BlockBase + Txs []transaction.Transactioner +} + +func (b *Block) Decode(r io.Reader) error { + br := &util.BinReader{R: r} + b.DecodePayload(br) + return br.Err +} +func (b *Block) Encode(w io.Writer) error { + bw := &util.BinWriter{W: w} + b.EncodePayload(bw) + return bw.Err +} +func (b *Block) EncodePayload(bw *util.BinWriter) { + b.BlockBase.EncodePayload(bw) + bw.VarUint(uint64(len(b.Txs))) + for _, tx := range b.Txs { + tx.Encode(bw.W) + } +} + +func (b *Block) DecodePayload(br *util.BinReader) error { + + b.BlockBase.DecodePayload(br) + lenTXs := br.VarUint() + + b.Txs = make([]transaction.Transactioner, lenTXs) + + reader := bufio.NewReader(br.R) + for i := 0; i < int(lenTXs); i++ { + + tx, err := transaction.FromBytes(reader) + if err != nil { + return err + } + b.Txs[i] = tx + + } + return nil +} + +func (b *Block) Bytes() ([]byte, error) { + buf := new(bytes.Buffer) + err := b.Encode(buf) + return buf.Bytes(), err +} diff --git a/pkg/wire/payload/block_test.go b/pkg/wire/payload/block_test.go new file mode 100644 index 000000000..2de7cdb4e --- /dev/null +++ b/pkg/wire/payload/block_test.go @@ -0,0 +1,80 @@ +package payload + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/stretchr/testify/assert" +) + +func TestBlockDecodeEncode(t *testing.T) { + // transaction taken from mainnet: 2000000 + rawtx := "00000000e5a49e24ee36e972e1bbee16c6897b88050e95e40db157d901cbb68de5243dc93482b51e7ce810eca512afe201768668de5910d4373db067418ad1cf95cd291de424a15a80841e00ec3bd62b5562099d59e75d652b5d3827bf04c165bbe9ef95cca4bf5501fd450140f9ef37e9a31614d0c42aca576d11fcd2ca4cade56143e725ab45e2c7372601e5322a89e0585b44f6f436147be6dc6513ebe781c358abadb1336cadc8f1fdf2e4407fedf529ec4b16ada7fec16efcb377e9c0ea515b12b98a8bed01c385999f8f6121dd5fad32abe4d95dc0c11e9a3a6ce093a7f550b96b779c45f584022bb8a93640d266010bee43509f70c9e7d86cd5037214718de5682abeb42141d1691a1595e5ee188393c26b9ca9f31e4db2d87c3c76869c4b02d081672909268e4d53bcc850401866a84eafd9003c17f1469f1830c5c5f2976da54991f7a1ed292a8af0de2ce202d8f15cb0f362f0ae0ee8bf43886785db45fed0d77b5254503ac105e694a7ac40bfc7166d3495ad4ab540e287ec51afc0569f292e106055b13765d6dacc1ed14807eb63cfeb04b50977c2a64735a4d7496c95f361b773dc58ae29a11b8183f717f1552102486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a7021024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d2102aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e2103b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c2103b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a2102ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba5542102df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e89509357ae120000ec3bd62b0000000080000001e91406db8ce12ff273d9c04cb0f224870d43eeb53c7522a6ad47b0c2ad9614cc0000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5005ed0b200000000606fe152cafd010c1911b9c7932b77562d13b02f01414054f533865c49ae27ee9898f18c3ad18e2bb27928af9dbfdca44713b666f343e06416fe04869c3b5355934b101ac40caa4910cebdcfc3fec7322d12b0aa4d5bd7232103dfe98cbad29e3116324a5125a32b36250679190f74dca7425c40fff589cc530cac800000012ae528ca6f1c1a740a716603aac1e167cad482d49c41e4a356565cefa50b5dbd0000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5008d190207000000be87c8c3cb58ca27e5235c5d451474435b895386014140fe20765111b9845d7d842cf09205931ea35e0de5d7078c3831e2b36e41e2298a1cf3d8bc697676ae48a577d70757ecf262085ed314c2d077e0e7a11830dc1b7d232103f2ad7aebe26d46541cb99deb3ff06fd24f0b87efa7e52a1aba6a2d3167be6044acd1015b0600a0724e1809146063795d3b9b3cd55aef026eae992b91063db0db1458f310994f5561f8759ebbdab2e87a05fa4d256553c1087472616e7366657267f91d6b7085db7c5aaf09f19eeec1ca3c0db2c6ecf166aacd14a39b8747f90000000000000000012058f310994f5561f8759ebbdab2e87a05fa4d25650000014140664803b370fb1125fccb44b470a9062d2f1108f4b9c43b7aa35fd56eb52c0564c72e649eefacf82bfb25d252aef5e677307bd0142cecc60e369265b3d053a8d9232103a95af1ee45fe9a2cbad6358e3357da7a13e994361b385c72141ca5a47ef993f6ac800000028190ef017f831707830dd8dc2867572136e7d73bdc13a7421433072e36830c6c00008190ef017f831707830dd8dc2867572136e7d73bdc13a7421433072e36830c6c010001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c600300000000000000494a6c348bdcbf192fb1a88ee41883fcdd11ef37014140a2dc5f161bcb696deeae6f078702627280cfc360d5bf0cae13a417212da2bc87c207d58cca83deb3e6fd76c05bc135367eb520e0138fbfc25e05985ddfa514352321039beeb554e21dfdd414fd07844460d2d1117d26e6b6d18314c535ef25511cee16ac8000000144a1e8637a27d17be06f4d5e4483841c5db27420757350833e970861cf0f6c650500019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500c817a80400000034a3393450b5217977ba2efebad0dd6c37bb0a690141409f6eec2a9983ae782e3d7bdf8ce02f2c9b6db4660ee93197ed8ae85ae9ef424d421ba35a3b70738618b255ab387fe32a0dee1ccd6c0bb8d1e306e3947c383557232103994c84c3cbbc3a75be1045a2ce795d2d5733bb8425f2580e19ff7f179bb8719bac80000001e04c80df4a53b8ba9dca435630f1170d3cc6f15f508e98006ef44bf9f38de82d0000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500bca06501000000d32f28fd47cc176e3d2bf6ac30eb87dfb838ef99014140b8c50990b976e837033eb09da821dae396782bd0df404e1e0675f2db9b7fc6b681b1cb6cb717a546268363f63ae65f89a18769d201b531fee4afd78b2fffb9aa232103ef4c441d2c4cd1a2e3f255366d149553a7f8f799edf1a49a0bc1af0081cc65beac80000001a6f0562becc89fd0ab4c36e2bcc4eedeb6de9fa6bb398be22dafc4e0512b55bc0000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc50001b2c4000000001decb55a65ad7d23a4d474a1e75ff24c9a71dcc4014140175ea6f8e03e7fbf4254ba16ccdd06a6d6c71b7b4d3965bc0913cf02f16ce3180dc278cde426f493225c4e51db28d934da120f5ef2b723585c5aafc8493071e4232103f3215374a4384eb8e2a60635f1e8394ccbe1e21a7b90e0063f5d5ac1836402ceacd100530800fee9e85100000014f9dfa019739920e7ab28544e979c0e6613f54536140a551a5ab4acf6a31dc6fc2a91d05684424a620e53c1087472616e7366657267fb1c540417067c270dee32f21023aa8b9b71abce0001243726662530e8d5576aced9b76cc57b078a9e2857e1ae1bd04869a608657f00000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6001000000000000000a551a5ab4acf6a31dc6fc2a91d05684424a620e014140fd662c8eeaeb3fdc5e9906277c79e31e521b2aa503f0e0684ce876283b27949b2366cd5034a74d7b0b0c47e0648b57a2d569378ee0f82d2bdb3717c4cb3d5c4c2321032204ffdc2173ee04c61f3dbe1ba1ca743106f7cccc7cf6fff86be5dcd6ac63d6ac02000134c42a75a09535438eda8df28e58f8fdc7c5d3329ed6f1d9a845627f7beb53410000000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60429415000000000046a030db679a5ce51ac083c73b44f0ff6729561c01414065eb2a2a52e6c34ce3a2925ec03b958eeda5cc310468f198ac9c7c111f9a62778214e9b8234c92aef5dba75a29536d6b58b8a074eeb547c53a0e26dc0eaadb86232102670e8d99a3c28d947ddfaa76416a4f53dbcde3ee3d271b1bc78941d5f6d7b829acd1015b0600a0724e1809146063795d3b9b3cd55aef026eae992b91063db0db1458f310994f5561f8759ebbdab2e87a05fa4d256553c1087472616e7366657267f91d6b7085db7c5aaf09f19eeec1ca3c0db2c6ecf1669fb73cc8cde6c72f0000000000000000012058f310994f5561f8759ebbdab2e87a05fa4d25650000014140a1cfd844d2623cfb2b867ec8bf24d3951bd7fcdd5de1e9d59a97b3e240f80020cc7612a3a7dfa32696b7837a1a38f85d27227f76a45b0e6c956abb8b68183177232103a95af1ee45fe9a2cbad6358e3357da7a13e994361b385c72141ca5a47ef993f6ac800000016ab96ecf9f3a32fd8e209d2b5650fb2d12c39bc89493421bdd290d69bbe74d540000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500cf14130400000088eb48d2d1c288c0982ae188b13d5a2a994ab254014140e3269b08dfc3409031a24b7493376a9597eca37a6d6da868fda33005b596accb6e2b7ad4f137d597420d29f080ee9ba545935b47e6358f3f8c1f9118b4e4a085232102e93db927a1f516b37962c4262e78ad56b20e4d349cb1908ee612526ccd0d1118acd10153088005a34c17000000147f808fa952008067570e27dfe0fdc97f647964a51439de68a3e72a015de6ea5543dc896d8609c1c45153c1087472616e7366657267fb1c540417067c270dee32f21023aa8b9b71abce00000000000000000001cd018dd92f5209d47590d74fdd3f982e1ac86d202c12bc948a2890cdb857cff1000002e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60010000000000000039de68a3e72a015de6ea5543dc896d8609c1c451e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60a9301b000000000039de68a3e72a015de6ea5543dc896d8609c1c4510141407fad0f5501e9ce3fa9bef9d378257ce233f41d5fd4955e69367ad57392f9feac7608d90553e3e285785cf87fe01b8ab1266d9c73de1e33ceff1c51133735e39b2321028ad2a0b73a8624cc027d80b6ef4af9de5167740aede9cb6401ccc0faef7244bbac0200012d5a5b03a06bbcb891054ae72e64e2d9a3e46077fecf8d2d6af4496771671d910000000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60a0490100000000008e7778b7ff900a0b82779d0d768cb63b11dcd685014140c54d9d6148503a45de3c037aa6c08ccede953246bdac680a9e9f4ca2414cb02ee130c9a246771191df6bda8796f01f432bfaa464f331137d75865d0c3310f4dc2321036f97d99a1fa298e82ab6ccdedd6febd72fa5b3fa415e09f47354f3cecec51f07acd101530800ba1dd20500000014e31c39cf87027016bf0b925cb6db0699e41d235514bd12389c3eede155f28d6f72c44cb70756aff3f253c1087472616e7366657267fb1c540417067c270dee32f21023aa8b9b71abce0000000000000000000191c12f3ad7a6dc3b8c9cfb6e47d5adb94ef31e76b8dbac538a20a103ba11931f000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c600100000000000000bd12389c3eede155f28d6f72c44cb70756aff3f201414026034a105f0a694f0320b617f112eea7b21a268c54d383e8964e9045e970b8bcc020c3c12e99d560657f2f5874d0a508748bf362bccb0d15f0c9a2c22641ceba2321036200e224b07b5f88b934e36933e5c31826d3b2dc9a5a0a72a555cd5fd70feda5acd1015308803b081117000000141be8765dcb5600ac58a6cc9be39f8d5734c4f1d3148cba604700afc229870ec93ae4f2c25e78f95c4f53c1087472616e7366657267fb1c540417067c270dee32f21023aa8b9b71abce00000000000000000001d4d309fddde1101278ec53443cabb902cbbb2330ce4a16b0876c7f2c411990b5000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6001000000000000008cba604700afc229870ec93ae4f2c25e78f95c4f01414057755879205922936aef9d641f7ac2fd59a9605c01ae5e2cf730ededcc59ef60eaf5d1a5f150c5bd33c3c78b86e441d4d464856bf30ca7b9d89f5299c86d3d7e23210237965fbbff6457d897a9763d6bf97c8f700735e938fa21236aa23e648e641494ac02000109992fadaed0cf0609e42d5fea6fc5057a340893b756c19d73bd0ed92109a0a00000000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c606027000000000000fa1f3b71b6332c5880e1b2808f86e3bf730fbdec0141409ee7a22eee8a502ea3bdd264ba81f36cf7360332456fae92f50d4d89c06cbeda083c61562af8c279e91e490bbabc2e4bee481d85887ff5a36f8dbb26d1308fa22321036980dc27f23a7e3c0ea0fe50f66f6b37a7d21ffaa66d5571ce6d4d317e22a5e7ac80000001cba3499f1640f91f7074593e632d18224e5ca94054c63a72cc3c208fe76a65960100019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500aaa0680b0000007c2f0bc324085be8b5dcf68bc8ed142620b18d850141406020880ded8ae65e6b8af680882f4c3e702cb41f9dcd8a898ea69a32077d05a0503df9d70ff1e71a400dd9a7a2222df9ab348e6cd501473b05add04fb028f960232102f4830c08c254e6e81839396c44c039639f1e30b6e53878269b567d9253590aacac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + b := Block{} + + r := bytes.NewReader(rawtxBytes) + b.Decode(r) + + expected := map[string]bool{ // 18 trans + + "009f61f481f47eb7478e887871e4e744669d461b13d68e04250035260171d706": false, + "3a62e473c1d67ac561b98e8131f7f7ceded4cd250edb78a6814ec9915930ad93": false, + "d56a545d2f9400c09d5aa4e8cc37cc994d5a6892f9c30de95ff69a3b647b27a2": false, + "57f6baa9cb23ce9117d66aee7c31ba6d1e82e140a805db1c5992ada849f6a7c7": false, + "f700be9c723ed44900ac9d70874b9d8601033bb78883c0a43ab38b5d96c09c11": false, + "31674ac8553b371ddf06db6a3aef09b8d6f37da03a8cc2868b71044c54ef0034": false, + "44858de48ec97cea2f823128e9d58981dde11f28a6ebf0a2cb745ea13223dd71": false, + "317f3ff3768b2aebe3d4866f6e0e8b875cc7937a1b8b5f91be066dc51ed61be2": false, + "8c24f44f1533567c71e722f49bc7a4d9b323a09e2950fd975291817578119508": false, + "55a7a738aaee8f7e6d7bcd4c8a38813e57763bff8bfb296418b6cac6d5bfb89a": false, + "dfa5f84366cf0b48f1b1e9b24a73557e657f6ac21b676528401f5a630aece571": false, + "5839fbcbbca68aef41dfa9a371222565519626affad6be0977d38a82259480a5": false, + "6873568cae35e4ce0a7d07ef080ef6eb699b2b9dcbc419fad1c4f645ff8579fc": false, + "dbb3c0688003bede7e7bc56d2c9d6362b594512ac686820739d963ef91e2eb9e": false, + "3d12353cb8bae8be928131580e960a82f37ca3ad6957ad22c8cadc1b21b2dd1a": false, + "8ca87fd5843f000939244151ce027bad5c1f30f1867c7054918b7f9a66b949e8": false, + "ad088940e45a73e00a3cdb7f3248c67a3f6e5d1f05d4cfd44c4e1f4d26cfef87": false, + "908a398dd65dfd2aad6c06090c5a71d5e5280746577a6ddd5a1f2c1453f71ead": false, + } + + hashes := []string{} + + for _, tx := range b.Txs { + switch t := tx.(type) { + case *transaction.Contract: + hashes = append(hashes, t.Hash.String()) + case *transaction.Miner: + hashes = append(hashes, t.Hash.String()) + case *transaction.Claim: + hashes = append(hashes, t.Hash.String()) + case *transaction.Invocation: + hashes = append(hashes, t.Hash.String()) + } + } + + assert.Equal(t, len(expected), len(hashes)) + + // changes value in map to true, if hash found + for _, hash := range hashes { + expected[hash] = true + } + + // iterate map; all vlaues should be true + val := true + for _, v := range expected { + if v == false { + val = false + } + } + assert.Equal(t, true, val) + + buf := new(bytes.Buffer) + + b.Encode(buf) + + assert.Equal(t, rawtx, hex.EncodeToString(buf.Bytes())) +} diff --git a/pkg/wire/payload/blockbase.go b/pkg/wire/payload/blockbase.go new file mode 100644 index 000000000..2feec8ec0 --- /dev/null +++ b/pkg/wire/payload/blockbase.go @@ -0,0 +1,124 @@ +package payload + +import ( + "bytes" + "errors" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +var ( + ErrPadding = errors.New("There is a padding mismatch") +) + +type BlockBase struct { + // Version of the block. + Version uint32 `json:"version"` + + // hash of the previous block. + PrevHash util.Uint256 `json:"previousblockhash"` + + // Root hash of a transaction list. + MerkleRoot util.Uint256 `json:"merkleroot"` + + // The time stamp of each block must be later than previous block's time stamp. + // Generally the difference of two block's time stamp is about 15 seconds and imprecision is allowed. + // The height of the block must be exactly equal to the height of the previous block plus 1. + Timestamp uint32 `json:"time"` + + // index/height of the block + Index uint32 `json:"height"` + + // Random number also called nonce + ConsensusData uint64 `json:"nonce"` + + // Contract addresss of the next miner + NextConsensus util.Uint160 `json:"next_consensus"` + + // Padding that is fixed to 1 + _ uint8 + + // Script used to validate the block + Witness transaction.Witness `json:"script"` + + // hash of this block, created when binary encoded. + Hash util.Uint256 +} + +func (b *BlockBase) EncodePayload(bw *util.BinWriter) error { + + b.encodeHashableFields(bw) + + bw.Write(uint8(1)) + b.Witness.Encode(bw) + + return bw.Err +} + +func (b *BlockBase) Decode(r io.Reader) error { + br := &util.BinReader{R: r} + b.DecodePayload(br) + return br.Err +} +func (b *BlockBase) Encode(w io.Writer) error { + bw := &util.BinWriter{W: w} + b.EncodePayload(bw) + return bw.Err +} + +func (b *BlockBase) encodeHashableFields(bw *util.BinWriter) { + bw.Write(b.Version) + bw.Write(b.PrevHash) + bw.Write(b.MerkleRoot) + bw.Write(b.Timestamp) + bw.Write(b.Index) + bw.Write(b.ConsensusData) + bw.Write(b.NextConsensus) +} + +func (b *BlockBase) DecodePayload(br *util.BinReader) error { + + b.decodeHashableFields(br) + + var padding uint8 + br.Read(&padding) + if padding != 1 { + return ErrPadding + } + + b.Witness = transaction.Witness{} + b.Witness.Decode(br) + + err := b.createHash() + + if err != nil { + return err + } + + return br.Err +} + +func (b *BlockBase) decodeHashableFields(br *util.BinReader) { + br.Read(&b.Version) + br.Read(&b.PrevHash) + br.Read(&b.MerkleRoot) + br.Read(&b.Timestamp) + br.Read(&b.Index) + br.Read(&b.ConsensusData) + br.Read(&b.NextConsensus) +} + +func (b *BlockBase) createHash() error { + + hash, err := util.CalculateHash(b.encodeHashableFields) + b.Hash = hash + return err +} + +func (b *BlockBase) Bytes() ([]byte, error) { + buf := new(bytes.Buffer) + err := b.Encode(buf) + return buf.Bytes(), err +} diff --git a/pkg/wire/payload/blockbase_test.go b/pkg/wire/payload/blockbase_test.go new file mode 100644 index 000000000..09241399f --- /dev/null +++ b/pkg/wire/payload/blockbase_test.go @@ -0,0 +1,8 @@ +package payload + +import "testing" + +func Test(t *testing.T) { + //tests for this have been included in the mheaders_test file + +} diff --git a/pkg/wire/payload/maddr.go b/pkg/wire/payload/maddr.go new file mode 100644 index 000000000..674013dfa --- /dev/null +++ b/pkg/wire/payload/maddr.go @@ -0,0 +1,61 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type AddrMessage struct { + AddrList []*Net_addr +} + +func NewAddrMessage() (*AddrMessage, error) { + addrMess := &AddrMessage{ + nil, + } + return addrMess, nil +} + +func (a *AddrMessage) AddNetAddr(n *Net_addr) error { + a.AddrList = append(a.AddrList, n) + // TODO:check if max reached, if so return err. What is max? + + return nil +} + +// Implements Messager interface +func (a *AddrMessage) DecodePayload(r io.Reader) error { + + br := &util.BinReader{R: r} + listLen := br.VarUint() + + a.AddrList = make([]*Net_addr, listLen) + for i := 0; i < int(listLen); i++ { + a.AddrList[i] = &Net_addr{} + a.AddrList[i].DecodePayload(br) + if br.Err != nil { + return br.Err + } + } + return br.Err +} + +// Implements messager interface +func (v *AddrMessage) EncodePayload(w io.Writer) error { + bw := &util.BinWriter{W: w} + + listLen := uint64(len(v.AddrList)) + bw.VarUint(listLen) + + for _, addr := range v.AddrList { + addr.EncodePayload(bw) + } + return bw.Err +} + +// Implements messager interface +func (v *AddrMessage) Command() command.Type { + return command.Addr +} diff --git a/pkg/wire/payload/maddr_test.go b/pkg/wire/payload/maddr_test.go new file mode 100644 index 000000000..100972f6b --- /dev/null +++ b/pkg/wire/payload/maddr_test.go @@ -0,0 +1,40 @@ +package payload + +import ( + "bytes" + "net" + "testing" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/util/Checksum" + + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/stretchr/testify/assert" +) + +func TestAddrMessageEncodeDecode(t *testing.T) { + + ip := []byte(net.ParseIP("127.0.0.1").To16()) + + var ipByte [16]byte + copy(ipByte[:], ip) + + netaddr, err := NewNetAddr(uint32(time.Now().Unix()), ipByte, 8080, protocol.NodePeerService) + addrmsg, err := NewAddrMessage() + addrmsg.AddNetAddr(netaddr) + + buf := new(bytes.Buffer) + err = addrmsg.EncodePayload(buf) + expected := checksum.FromBuf(buf) + + addrmsgDec, err := NewAddrMessage() + r := bytes.NewReader(buf.Bytes()) + err = addrmsgDec.DecodePayload(r) + + buf = new(bytes.Buffer) + err = addrmsgDec.EncodePayload(buf) + have := checksum.FromBuf(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, expected, have) +} diff --git a/pkg/wire/payload/mblock.go b/pkg/wire/payload/mblock.go new file mode 100644 index 000000000..399d28965 --- /dev/null +++ b/pkg/wire/payload/mblock.go @@ -0,0 +1,36 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/util" + + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +type BlockMessage struct { + Block +} + +func NewBlockMessage() (*BlockMessage, error) { + return &BlockMessage{}, nil +} + +// Implements Messager interface +func (b *BlockMessage) DecodePayload(r io.Reader) error { + br := &util.BinReader{R: r} + b.Block.DecodePayload(br) + return br.Err +} + +// Implements messager interface +func (b *BlockMessage) EncodePayload(w io.Writer) error { + bw := &util.BinWriter{W: w} + b.Block.EncodePayload(bw) + return bw.Err +} + +// Implements messager interface +func (v *BlockMessage) Command() command.Type { + return command.Block +} diff --git a/pkg/wire/payload/mgetaddr.go b/pkg/wire/payload/mgetaddr.go new file mode 100644 index 000000000..02473ba12 --- /dev/null +++ b/pkg/wire/payload/mgetaddr.go @@ -0,0 +1,29 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +// No payload +type GetAddrMessage struct{} + +func NewGetAddrMessage() (*GetAddrMessage, error) { + return &GetAddrMessage{}, nil +} + +// Implements Messager interface +func (v *GetAddrMessage) DecodePayload(r io.Reader) error { + return nil +} + +// Implements messager interface +func (v *GetAddrMessage) EncodePayload(w io.Writer) error { + return nil +} + +// Implements messager interface +func (v *GetAddrMessage) Command() command.Type { + return command.GetAddr +} diff --git a/pkg/wire/payload/mgetaddr_test.go b/pkg/wire/payload/mgetaddr_test.go new file mode 100644 index 000000000..db6491d5a --- /dev/null +++ b/pkg/wire/payload/mgetaddr_test.go @@ -0,0 +1,24 @@ +package payload + +import ( + "bytes" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/util/Checksum" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/stretchr/testify/assert" +) + +func TestNewGetAddr(t *testing.T) { + + getAddrMessage, err := NewGetAddrMessage() + assert.Equal(t, nil, err) + + assert.Equal(t, command.GetAddr, getAddrMessage.Command()) + + buf := new(bytes.Buffer) + + assert.Equal(t, int(3806393949), int(checksum.FromBuf(buf))) + assert.Equal(t, int(0), len(buf.Bytes())) +} diff --git a/pkg/wire/payload/mgetblocks.go b/pkg/wire/payload/mgetblocks.go new file mode 100644 index 000000000..660e17d96 --- /dev/null +++ b/pkg/wire/payload/mgetblocks.go @@ -0,0 +1,22 @@ +package payload + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type GetBlocksMessage struct { + *GetHeadersMessage +} + +func NewGetBlocksMessage(start []util.Uint256, stop util.Uint256) (*GetBlocksMessage, error) { + GetHeaders, err := newAbstractGetHeaders(start, stop, command.GetBlocks) + + if err != nil { + return nil, err + } + return &GetBlocksMessage{ + GetHeaders, + }, nil + +} diff --git a/pkg/wire/payload/mgetblocks_test.go b/pkg/wire/payload/mgetblocks_test.go new file mode 100644 index 000000000..d413bfc9a --- /dev/null +++ b/pkg/wire/payload/mgetblocks_test.go @@ -0,0 +1,27 @@ +package payload + +import ( + "crypto/sha256" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/stretchr/testify/assert" +) + +func TestGetBlocksCommandType(t *testing.T) { + var ( + start = []util.Uint256{ + sha256.Sum256([]byte("a")), + sha256.Sum256([]byte("b")), + sha256.Sum256([]byte("c")), + sha256.Sum256([]byte("d")), + } + stop = sha256.Sum256([]byte("e")) + ) + + getBlocks, err := NewGetBlocksMessage(start, stop) + + assert.Equal(t, err, nil) + assert.Equal(t, command.GetBlocks, getBlocks.Command()) +} diff --git a/pkg/wire/payload/mgetdata.go b/pkg/wire/payload/mgetdata.go new file mode 100644 index 000000000..d13fbab30 --- /dev/null +++ b/pkg/wire/payload/mgetdata.go @@ -0,0 +1,16 @@ +package payload + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +type GetDataMessage struct { + *InvMessage +} + +func NewGetDataMessage(typ InvType) (*GetDataMessage, error) { + getData, err := newAbstractInv(typ, command.GetData) + return &GetDataMessage{ + getData, + }, err +} diff --git a/pkg/wire/payload/mgetdata_test.go b/pkg/wire/payload/mgetdata_test.go new file mode 100644 index 000000000..44c4918b4 --- /dev/null +++ b/pkg/wire/payload/mgetdata_test.go @@ -0,0 +1,19 @@ +package payload + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/stretchr/testify/assert" +) + +func TestGetDataCommandType(t *testing.T) { + getData, err := NewGetDataMessage(InvTypeBlock) + + assert.Equal(t, err, nil) + assert.Equal(t, command.GetData, getData.Command()) +} + +func TestOtherFunctions(t *testing.T) { + // Other capabilities are tested in the inherited struct +} diff --git a/pkg/wire/payload/mgetheaders.go b/pkg/wire/payload/mgetheaders.go new file mode 100644 index 000000000..09d91c5e0 --- /dev/null +++ b/pkg/wire/payload/mgetheaders.go @@ -0,0 +1,59 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type GetHeadersMessage struct { + cmd command.Type + hashStart []util.Uint256 + hashStop util.Uint256 +} + +// Start contains the list of all headers you want to fetch +// End contains the list of the highest header hash you would like to fetch +func NewGetHeadersMessage(start []util.Uint256, stop util.Uint256) (*GetHeadersMessage, error) { + getHeaders := &GetHeadersMessage{command.GetHeaders, start, stop} + + return getHeaders, nil + +} + +func newAbstractGetHeaders(start []util.Uint256, stop util.Uint256, cmd command.Type) (*GetHeadersMessage, error) { + getHeaders, err := NewGetHeadersMessage(start, stop) + + if err != nil { + return nil, err + } + getHeaders.cmd = cmd + return getHeaders, nil +} + +// Implements Messager interface +func (v *GetHeadersMessage) DecodePayload(r io.Reader) error { + + br := util.BinReader{R: r} + lenStart := br.VarUint() + v.hashStart = make([]util.Uint256, lenStart) + br.Read(&v.hashStart) + br.Read(&v.hashStop) + + return br.Err +} + +// Implements messager interface +func (v *GetHeadersMessage) EncodePayload(w io.Writer) error { + bw := &util.BinWriter{W: w} + bw.VarUint(uint64(len(v.hashStart))) + bw.Write(v.hashStart) + bw.Write(v.hashStop) + return bw.Err +} + +// Implements messager interface +func (v *GetHeadersMessage) Command() command.Type { + return v.cmd +} diff --git a/pkg/wire/payload/mgetheaders_test.go b/pkg/wire/payload/mgetheaders_test.go new file mode 100644 index 000000000..7ba0d6a10 --- /dev/null +++ b/pkg/wire/payload/mgetheaders_test.go @@ -0,0 +1,47 @@ +package payload + +import ( + "bytes" + "crypto/sha256" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/util/Checksum" + + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/stretchr/testify/assert" +) + +// Test taken from neo-go v1 +func TestGetHeadersEncodeDecode(t *testing.T) { + + var ( + start = []util.Uint256{ + sha256.Sum256([]byte("a")), + sha256.Sum256([]byte("b")), + sha256.Sum256([]byte("c")), + sha256.Sum256([]byte("d")), + } + stop = sha256.Sum256([]byte("e")) + ) + msgGetHeaders, err := NewGetHeadersMessage(start, stop) + assert.Equal(t, nil, err) + + buf := new(bytes.Buffer) + + err = msgGetHeaders.EncodePayload(buf) + assert.Equal(t, nil, err) + expected := checksum.FromBuf(buf) + + msgGetHeadersDec, err := NewGetHeadersMessage([]util.Uint256{}, util.Uint256{}) + assert.Equal(t, nil, err) + + r := bytes.NewReader(buf.Bytes()) + err = msgGetHeadersDec.DecodePayload(r) + assert.Equal(t, nil, err) + + buf = new(bytes.Buffer) + err = msgGetHeadersDec.EncodePayload(buf) + have := checksum.FromBuf(buf) + + assert.Equal(t, expected, have) +} diff --git a/pkg/wire/payload/mheaders.go b/pkg/wire/payload/mheaders.go new file mode 100644 index 000000000..b7d008d3d --- /dev/null +++ b/pkg/wire/payload/mheaders.go @@ -0,0 +1,78 @@ +package payload + +import ( + "errors" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type HeadersMessage struct { + Headers []*BlockBase + + // Padding that is fixed to 0 + _ uint8 +} + +// Users can at most request 2k header +const ( + maxHeadersAllowed = 2000 +) + +var ( + ErrMaxHeaders = errors.New("Maximum amount of headers allowed is 2000") +) + +func NewHeadersMessage() (*HeadersMessage, error) { + + headers := &HeadersMessage{nil, 0} + return headers, nil +} + +func (h *HeadersMessage) AddHeader(head *BlockBase) error { + if len(h.Headers)+1 > maxHeadersAllowed { + return ErrMaxHeaders + } + h.Headers = append(h.Headers, head) + + return nil +} + +// Implements Messager interface +func (v *HeadersMessage) DecodePayload(r io.Reader) error { + + br := &util.BinReader{R: r} + + lenHeaders := br.VarUint() + v.Headers = make([]*BlockBase, lenHeaders) + + for i := 0; i < int(lenHeaders); i++ { + header := &BlockBase{} + header.DecodePayload(br) + var padding uint8 + br.Read(&padding) + if padding != 0 { + return ErrPadding + } + v.Headers[i] = header + } + + return br.Err +} + +// Implements messager interface +func (v *HeadersMessage) EncodePayload(w io.Writer) error { + bw := &util.BinWriter{W: w} + bw.VarUint(uint64(len(v.Headers))) + for _, header := range v.Headers { + header.EncodePayload(bw) + bw.Write(uint8(0)) + } + return bw.Err +} + +// Implements messager interface +func (v *HeadersMessage) Command() command.Type { + return command.Headers +} diff --git a/pkg/wire/payload/mheaders_test.go b/pkg/wire/payload/mheaders_test.go new file mode 100644 index 000000000..a03f43486 --- /dev/null +++ b/pkg/wire/payload/mheaders_test.go @@ -0,0 +1,82 @@ +package payload + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/CityOfZion/neo-go/pkg/wire/util/address" + "github.com/stretchr/testify/assert" +) + +func TestNewHeaderMessage(t *testing.T) { + msgHeaders, err := NewHeadersMessage() + + assert.Equal(t, nil, err) + assert.Equal(t, 0, len(msgHeaders.Headers)) + +} + +func TestAddAndEncodeHeaders(t *testing.T) { + + // uses block10 from mainnet + + msgHeaders, _ := NewHeadersMessage() + + prevH, _ := util.Uint256DecodeString("005fb74a6de169ce5daf59a114405e5b27238b2489690e3b2a60c14ddfc3b326") + merkleRoot, _ := util.Uint256DecodeString("ca6d58bcb837472c2f77877e68495b83fd5b714dfe0c8230a525f4511a3239f4") + invocationScript, _ := hex.DecodeString("4036fdd23248880c1c311bcd97df04fe6d740dc1bf340c26915f0466e31e81c039012eca7a760270389e04b58b99820fe49cf8c24c9afc65d696b4d3f406a1e6b5405172a9b461e68dd399c8716de11d31f7dd2ec3be327c636b024562db6ac5df1cffdbee74c994736fd49803234d2baffbc0054f28ba5ec76494a467b4106955bb4084af7746d269241628c667003e9d39288b190ad5cef218ada625cbba8be411bb153828d8d3634e8f586638e2448425bc5b671be69800392ccbdebc945a5099c7406f6a11824105ecad345e525957053e77fbc0119d6b3fa7f854527e816cfce0d95dac66888e07e8990c95103d8e46124aac16f152e088520d7ec8325e3a2456f840e5b77ef0e3c410b347ccaf8a87516d10b88d436563c80712153273993afc320ec49b638225f58de464a1345e62a564b398939f96f6f4b7cf21b583609f85495a") + verificationScript, _ := hex.DecodeString("552102486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a7021024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d2102aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e2103b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c2103b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a2102ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba5542102df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e89509357ae") + nextCon, _ := util.Uint160DecodeString(address.ToScriptHash("APyEx5f4Zm4oCHwFWiSTaph1fPBxZacYVR")) + + msgHeaders.AddHeader(&BlockBase{ + Version: 0, + Index: 10, + PrevHash: prevH.Reverse(), + MerkleRoot: merkleRoot.Reverse(), + Timestamp: 1476647551, + ConsensusData: 0xc0f0280216ff14bf, + NextConsensus: nextCon, + Witness: transaction.Witness{ + InvocationScript: invocationScript, + VerificationScript: verificationScript, + }, + }) + + assert.Equal(t, 1, len(msgHeaders.Headers)) + + err := msgHeaders.Headers[0].createHash() + assert.Equal(t, nil, err) + // Hash being correct, automatically verifies that the fields are encoded properly + assert.Equal(t, "f3c4ec44c07eccbda974f1ee34bc6654ab6d3f22cd89c2e5c593a16d6cc7e6e8", msgHeaders.Headers[0].Hash.String()) + +} + +func TestEncodeDecode(t *testing.T) { + rawBlockHeaders := "010000000026b3c3df4dc1602a3b0e6989248b23275b5e4014a159af5dce69e16d4ab75f00f439321a51f425a530820cfe4d715bfd835b49687e87772f2c4737b8bc586dca7fda03580a000000bf14ff160228f0c059e75d652b5d3827bf04c165bbe9ef95cca4bf5501fd45014036fdd23248880c1c311bcd97df04fe6d740dc1bf340c26915f0466e31e81c039012eca7a760270389e04b58b99820fe49cf8c24c9afc65d696b4d3f406a1e6b5405172a9b461e68dd399c8716de11d31f7dd2ec3be327c636b024562db6ac5df1cffdbee74c994736fd49803234d2baffbc0054f28ba5ec76494a467b4106955bb4084af7746d269241628c667003e9d39288b190ad5cef218ada625cbba8be411bb153828d8d3634e8f586638e2448425bc5b671be69800392ccbdebc945a5099c7406f6a11824105ecad345e525957053e77fbc0119d6b3fa7f854527e816cfce0d95dac66888e07e8990c95103d8e46124aac16f152e088520d7ec8325e3a2456f840e5b77ef0e3c410b347ccaf8a87516d10b88d436563c80712153273993afc320ec49b638225f58de464a1345e62a564b398939f96f6f4b7cf21b583609f85495af1552102486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a7021024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d2102aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e2103b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c2103b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a2102ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba5542102df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e89509357ae00" + + var headerMsg HeadersMessage + + rawBlockBytes, _ := hex.DecodeString(rawBlockHeaders) + + r := bytes.NewReader(rawBlockBytes) + + err := headerMsg.DecodePayload(r) + + assert.Equal(t, 1, len(headerMsg.Headers)) + + header := headerMsg.Headers[0] + err = header.createHash() + + assert.Equal(t, "f3c4ec44c07eccbda974f1ee34bc6654ab6d3f22cd89c2e5c593a16d6cc7e6e8", header.Hash.String()) + + buf := new(bytes.Buffer) + + err = headerMsg.EncodePayload(buf) + + assert.Equal(t, nil, err) + + assert.Equal(t, hex.EncodeToString(rawBlockBytes), hex.EncodeToString(buf.Bytes())) +} diff --git a/pkg/wire/payload/minventory.go b/pkg/wire/payload/minventory.go new file mode 100644 index 000000000..d2f4207dd --- /dev/null +++ b/pkg/wire/payload/minventory.go @@ -0,0 +1,110 @@ +package payload + +import ( + "errors" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type InvType uint8 + +//Inventory types +const ( + InvTypeTx InvType = 0x01 + InvTypeBlock InvType = 0x02 + InvTypeConsensus InvType = 0xe0 +) + +const ( + maxHashes = 0x10000000 +) + +var ( + MaxHashError = errors.New("Max size For Hashes reached") +) + +type InvMessage struct { + cmd command.Type + Type InvType + Hashes []util.Uint256 +} + +func NewInvMessage(typ InvType) (*InvMessage, error) { + + inv := &InvMessage{ + command.Inv, + typ, + nil, + } + return inv, nil +} + +func newAbstractInv(typ InvType, cmd command.Type) (*InvMessage, error) { + inv, err := NewInvMessage(typ) + + if err != nil { + return nil, err + } + inv.cmd = cmd + + return inv, nil + +} + +func (i *InvMessage) AddHash(h util.Uint256) error { + if len(i.Hashes)+1 > maxHashes { + return MaxHashError + } + i.Hashes = append(i.Hashes, h) + return nil +} +func (i *InvMessage) AddHashes(hashes []util.Uint256) error { + var err error + for _, hash := range hashes { + err = i.AddHash(hash) + if err != nil { + break + } + } + return err +} + +// Implements Messager interface +func (v *InvMessage) DecodePayload(r io.Reader) error { + br := &util.BinReader{R: r} + + br.Read(&v.Type) + + listLen := br.VarUint() + v.Hashes = make([]util.Uint256, listLen) + + for i := 0; i < int(listLen); i++ { + br.Read(&v.Hashes[i]) + } + return nil +} + +// Implements messager interface +func (v *InvMessage) EncodePayload(w io.Writer) error { + + bw := &util.BinWriter{W: w} + bw.Write(v.Type) + + lenhashes := len(v.Hashes) + bw.VarUint(uint64(lenhashes)) + + for _, hash := range v.Hashes { + + bw.Write(hash) + + } + + return bw.Err +} + +// Implements messager interface +func (v *InvMessage) Command() command.Type { + return v.cmd +} diff --git a/pkg/wire/payload/minventory_test.go b/pkg/wire/payload/minventory_test.go new file mode 100644 index 000000000..9e9c82dc9 --- /dev/null +++ b/pkg/wire/payload/minventory_test.go @@ -0,0 +1,78 @@ +package payload + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/util" + + "github.com/stretchr/testify/assert" +) + +func TestNewInventory(t *testing.T) { + msgInv, err := NewInvMessage(InvTypeBlock) + + assert.Equal(t, nil, err) + assert.Equal(t, command.Inv, msgInv.Command()) + + hash, _ := util.Uint256DecodeBytes([]byte("hello")) + err = msgInv.AddHash(hash) + assert.Equal(t, nil, err) +} + +// Adjust test time or it will timeout +// func TestMaxHashes(t *testing.T) { +// msgInv, err := NewInvMessage(InvTypeBlock) +// assert.Equal(t, nil, err) + +// hash, _ := util.Uint256DecodeBytes([]byte("hello")) + +// for i := 0; i <= maxHashes+1; i++ { +// err = msgInv.AddHash(hash) +// } +// if err == nil { +// assert.Fail(t, "Max Hashes Exceeded, only allowed %v but have %v", maxHashes, len(msgInv.Hashes)) +// } else if err != MaxHashError { +// assert.Fail(t, "Expected a MaxHashError, however we got %s", err.Error()) +// } +// } +func TestEncodeDecodePayload(t *testing.T) { + msgInv, err := NewInvMessage(InvTypeBlock) + assert.Equal(t, nil, err) + + blockOneHash := "d782db8a38b0eea0d7394e0f007c61c71798867578c77c387c08113903946cc9" + hash, _ := util.Uint256DecodeString(blockOneHash) + + err = msgInv.AddHash(hash) + assert.Equal(t, nil, err) + + buf := new(bytes.Buffer) + err = msgInv.EncodePayload(buf) + assert.Equal(t, nil, err) + + numOfHashes := []byte{1} + expected := append([]byte{uint8(InvTypeBlock)}, numOfHashes...) + expected = append(expected, hash.Bytes()...) + + assert.Equal(t, hex.EncodeToString(expected), hex.EncodeToString(buf.Bytes())) + + var InvDec InvMessage + r := bytes.NewReader(buf.Bytes()) + err = InvDec.DecodePayload(r) + assert.Equal(t, nil, err) + + assert.Equal(t, 1, len(InvDec.Hashes)) + assert.Equal(t, blockOneHash, hex.EncodeToString(InvDec.Hashes[0].Bytes())) + +} +func TestEmptyInv(t *testing.T) { + msgInv, err := NewInvMessage(InvTypeBlock) + assert.Equal(t, nil, err) + + buf := new(bytes.Buffer) + msgInv.EncodePayload(buf) + assert.Equal(t, []byte{byte(InvTypeBlock), 0}, buf.Bytes()) + assert.Equal(t, 0, len(msgInv.Hashes)) +} diff --git a/pkg/wire/payload/mmempool.go b/pkg/wire/payload/mmempool.go new file mode 100644 index 000000000..5408c826b --- /dev/null +++ b/pkg/wire/payload/mmempool.go @@ -0,0 +1,29 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +// No payload +type GetMempool struct{} + +func NewGetMempool() (*GetMempool, error) { + return &GetMempool{}, nil +} + +// Implements Messager interface +func (v *GetMempool) DecodePayload(r io.Reader) error { + return nil +} + +// Implements messager interface +func (v *GetMempool) EncodePayload(w io.Writer) error { + return nil +} + +// Implements messager interface +func (v *GetMempool) Command() command.Type { + return command.Mempool +} diff --git a/pkg/wire/payload/mtx.go b/pkg/wire/payload/mtx.go new file mode 100644 index 000000000..8b6c08b28 --- /dev/null +++ b/pkg/wire/payload/mtx.go @@ -0,0 +1,34 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction" +) + +type TXMessage struct { + // w *bytes.Buffer + Tx transaction.Transactioner +} + +func NewTXMessage(tx transaction.Transactioner) (*TXMessage, error) { + + Tx := &TXMessage{tx} + return Tx, nil +} + +// Implements Messager interface +func (t *TXMessage) DecodePayload(r io.Reader) error { + return t.Tx.Decode(r) +} + +// Implements messager interface +func (t *TXMessage) EncodePayload(w io.Writer) error { + return t.Tx.Encode(w) +} + +// Implements messager interface +func (v *TXMessage) Command() command.Type { + return command.TX +} diff --git a/pkg/wire/payload/mverack.go b/pkg/wire/payload/mverack.go new file mode 100644 index 000000000..249579415 --- /dev/null +++ b/pkg/wire/payload/mverack.go @@ -0,0 +1,29 @@ +package payload + +import ( + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/command" +) + +// No payload +type VerackMessage struct{} + +func NewVerackMessage() (*VerackMessage, error) { + return &VerackMessage{}, nil +} + +// Implements Messager interface +func (v *VerackMessage) DecodePayload(r io.Reader) error { + return nil +} + +// Implements messager interface +func (v *VerackMessage) EncodePayload(w io.Writer) error { + return nil +} + +// Implements messager interface +func (v *VerackMessage) Command() command.Type { + return command.Verack +} diff --git a/pkg/wire/payload/mverack_test.go b/pkg/wire/payload/mverack_test.go new file mode 100644 index 000000000..5c4832e73 --- /dev/null +++ b/pkg/wire/payload/mverack_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + + "github.com/stretchr/testify/assert" +) + +func TestNewVerack(t *testing.T) { + + verackMessage, err := NewVerackMessage() + + assert.Equal(t, nil, err) + assert.Equal(t, command.Verack, verackMessage.Command()) +} diff --git a/pkg/wire/payload/mversion.go b/pkg/wire/payload/mversion.go new file mode 100644 index 000000000..1a7cff06e --- /dev/null +++ b/pkg/wire/payload/mversion.go @@ -0,0 +1,97 @@ +// Copied and Modified for NEO from: https://github.com/decred/dcrd/blob/master/wire/VersionMessage.go + +package payload + +import ( + "errors" + "io" + "net" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/command" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +const ( + minMsgVersionSize = 28 +) + +// TODO: Refactor to pull out the useragent out of initialiser +// and have a seperate method to add it + +type VersionMessage struct { + // w *bytes.Buffer + Version protocol.Version + Timestamp uint32 + Services protocol.ServiceFlag + IP net.IP + Port uint16 + Nonce uint32 + UserAgent []byte + StartHeight uint32 + Relay bool +} + +var ErrInvalidNetAddr = errors.New("provided net.Addr is not a net.TCPAddr") + +func NewVersionMessage(addr net.Addr, startHeight uint32, relay bool, pver protocol.Version, userAgent string, nonce uint32, services protocol.ServiceFlag) (*VersionMessage, error) { + + tcpAddr, ok := addr.(*net.TCPAddr) + if !ok { + return nil, ErrInvalidNetAddr + } + + version := &VersionMessage{ + pver, + uint32(time.Now().Unix()), + services, + tcpAddr.IP, + uint16(tcpAddr.Port), + nonce, + []byte(userAgent), + startHeight, + relay, + } + return version, nil +} + +// Implements Messager interface +func (v *VersionMessage) DecodePayload(r io.Reader) error { + br := &util.BinReader{R: r} + br.Read(&v.Version) + br.Read(&v.Services) + br.Read(&v.Timestamp) + br.Read(&v.Port) // Port is not BigEndian + br.Read(&v.Nonce) + + var lenUA uint8 + br.Read(&lenUA) + + v.UserAgent = make([]byte, lenUA) + br.Read(&v.UserAgent) + br.Read(&v.StartHeight) + br.Read(&v.Relay) + return br.Err +} + +// Implements messager interface +func (v *VersionMessage) EncodePayload(w io.Writer) error { + bw := &util.BinWriter{W: w} + + bw.Write(v.Version) + bw.Write(v.Services) + bw.Write(v.Timestamp) + bw.Write(v.Port) // Not big End + bw.Write(v.Nonce) + bw.Write(uint8(len(v.UserAgent))) + bw.Write(v.UserAgent) + bw.Write(v.StartHeight) + bw.Write(v.Relay) + return bw.Err +} + +// Implements messager interface +func (v *VersionMessage) Command() command.Type { + return command.Version +} diff --git a/pkg/wire/payload/mversion_test.go b/pkg/wire/payload/mversion_test.go new file mode 100644 index 000000000..9f7b1c129 --- /dev/null +++ b/pkg/wire/payload/mversion_test.go @@ -0,0 +1,59 @@ +package payload + +import ( + "bytes" + "math/rand" + "net" + "testing" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/stretchr/testify/assert" +) + +func TestValidNewVersionMessage(t *testing.T) { + + expectedIP := "127.0.0.1" + expectedPort := 8333 + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP(expectedIP), Port: expectedPort} + nonce := randRange(12949672, 42949672) + message, err := NewVersionMessage(tcpAddrMe, 0, true, protocol.DefaultVersion, protocol.UserAgent, nonce, protocol.NodePeerService) + + assert.Equal(t, nil, err) + assert.Equal(t, expectedIP, message.IP.String()) + assert.Equal(t, uint16(expectedPort), message.Port) + assert.Equal(t, protocol.DefaultVersion, message.Version) +} +func TestEncode(t *testing.T) { + + expectedIP := "127.0.0.1" + expectedPort := 8333 + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP(expectedIP), Port: expectedPort} + nonce := randRange(12949672, 42949672) + message, err := NewVersionMessage(tcpAddrMe, 0, true, protocol.DefaultVersion, protocol.UserAgent, nonce, protocol.NodePeerService) + + buf := new(bytes.Buffer) + err = message.EncodePayload(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, len(message.UserAgent)+minMsgVersionSize, int(buf.Len())) +} +func TestLenIsCorrect(t *testing.T) { + + expectedIP := "127.0.0.1" + expectedPort := 8333 + tcpAddrMe := &net.TCPAddr{IP: net.ParseIP(expectedIP), Port: expectedPort} + nonce := randRange(12949672, 42949672) + message, err := NewVersionMessage(tcpAddrMe, 0, true, protocol.DefaultVersion, protocol.UserAgent, nonce, protocol.NodePeerService) + + buf := new(bytes.Buffer) + err = message.EncodePayload(buf) + assert.Equal(t, nil, err) + + assert.Equal(t, len(message.UserAgent)+minMsgVersionSize, len(buf.Bytes())) +} + +func randRange(min, max int) uint32 { + rand.Seed(time.Now().Unix() + int64(rand.Uint64())) + return uint32(rand.Intn(max-min) + min) +} diff --git a/pkg/wire/payload/net_addr.go b/pkg/wire/payload/net_addr.go new file mode 100644 index 000000000..5e1c528d0 --- /dev/null +++ b/pkg/wire/payload/net_addr.go @@ -0,0 +1,54 @@ +package payload + +import ( + "net" + "strconv" + "time" + + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// Once a VersionMessage is received, we can then store it inside of AddrMessage struct +// TODO: store this inside version message and have a bool to indicate whether to encode ip +// VersionMessage does not encodeIP +type Net_addr struct { + Timestamp uint32 + IP [16]byte + Port uint16 + Service protocol.ServiceFlag +} + +func NewNetAddr(time uint32, ip [16]byte, port uint16, service protocol.ServiceFlag) (*Net_addr, error) { + return &Net_addr{time, ip, port, service}, nil +} + +func NewAddrFromVersionMessage(version VersionMessage) (*Net_addr, error) { + + var ip [16]byte + + copy(ip[:], []byte(version.IP)[:16]) + + return NewNetAddr(version.Timestamp, ip, version.Port, version.Services) +} + +func (n *Net_addr) EncodePayload(bw *util.BinWriter) { + + bw.Write(uint32(time.Now().Unix())) + bw.Write(protocol.NodePeerService) + bw.WriteBigEnd(n.IP) + bw.WriteBigEnd(n.Port) +} +func (n *Net_addr) DecodePayload(br *util.BinReader) { + + br.Read(&n.Timestamp) + br.Read(&n.Service) + br.ReadBigEnd(&n.IP) + br.ReadBigEnd(&n.Port) +} +func (n *Net_addr) IPPort() string { + ip := net.IP(n.IP[:]).String() + port := strconv.Itoa(int(n.Port)) + ipport := ip + ":" + port + return ipport +} diff --git a/pkg/wire/payload/transaction/Attribute.go b/pkg/wire/payload/transaction/Attribute.go new file mode 100644 index 000000000..64e98df34 --- /dev/null +++ b/pkg/wire/payload/transaction/Attribute.go @@ -0,0 +1,65 @@ +package transaction + +import ( + "errors" + + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// Attribute represents a Transaction attribute. +type Attribute struct { + Usage AttrUsage + Data []byte +} + +var ( + ErrMaxData = errors.New("Max Size of Attribute reached") +) + +const ( + maxAttrSize = 65535 +) + +func (a *Attribute) Encode(bw *util.BinWriter) { + if len(a.Data) > maxAttrSize { + bw.Err = ErrMaxData + return + } + bw.Write(uint8(a.Usage)) + + if a.Usage == DescriptionURL || a.Usage == Vote || (a.Usage >= Hash1 && a.Usage <= Hash15) { + bw.Write(a.Data[:32]) + + } else if a.Usage == Script { + bw.Write(a.Data[:20]) + } else if a.Usage == ECDH02 || a.Usage == ECDH03 { + bw.Write(a.Data[1:33]) + } else if a.Usage == CertURL || a.Usage == DescriptionURL || a.Usage == Description || a.Usage >= Remark { + bw.VarUint(uint64(len(a.Data))) + bw.Write(a.Data) + } else { + bw.Write(a.Data) + } + +} + +func (a *Attribute) Decode(br *util.BinReader) { + br.Read(&a.Usage) + if a.Usage == DescriptionURL || a.Usage == Vote || a.Usage >= Hash1 && a.Usage <= Hash15 { + a.Data = make([]byte, 32) + br.Read(&a.Data) + + } else if a.Usage == Script { + a.Data = make([]byte, 20) + br.Read(&a.Data) + } else if a.Usage == ECDH02 || a.Usage == ECDH03 { + a.Data = make([]byte, 32) + br.Read(&a.Data) + } else if a.Usage == CertURL || a.Usage == DescriptionURL || a.Usage == Description || a.Usage >= Remark { + lenData := br.VarUint() + a.Data = make([]byte, lenData) + br.Read(&a.Data) + } else { + br.Read(&a.Data) + } +} diff --git a/pkg/wire/payload/transaction/Input.go b/pkg/wire/payload/transaction/Input.go new file mode 100644 index 000000000..788e47960 --- /dev/null +++ b/pkg/wire/payload/transaction/Input.go @@ -0,0 +1,28 @@ +package transaction + +import "github.com/CityOfZion/neo-go/pkg/wire/util" + +// Input represents a Transaction input. +type Input struct { + // The hash of the previous transaction. + PrevHash util.Uint256 + + // The index of the previous transaction. + PrevIndex uint16 +} + +func NewInput(prevHash util.Uint256, prevIndex uint16) *Input { + return &Input{ + prevHash, + prevIndex, + } +} +func (i *Input) Encode(bw *util.BinWriter) { + bw.Write(i.PrevHash) + bw.Write(i.PrevIndex) +} + +func (i *Input) Decode(br *util.BinReader) { + br.Read(&i.PrevHash) + br.Read(&i.PrevIndex) +} diff --git a/pkg/wire/payload/transaction/Output.go b/pkg/wire/payload/transaction/Output.go new file mode 100644 index 000000000..9af346dc3 --- /dev/null +++ b/pkg/wire/payload/transaction/Output.go @@ -0,0 +1,34 @@ +package transaction + +import "github.com/CityOfZion/neo-go/pkg/wire/util" + +type Output struct { + // The NEO asset id used in the transaction. + AssetID util.Uint256 + + // Amount of AssetType send or received. + Amount int64 + + // The address of the remittee. + ScriptHash util.Uint160 +} + +func NewOutput(assetID util.Uint256, Amount int64, ScriptHash util.Uint160) *Output { + return &Output{ + assetID, + Amount, + ScriptHash, + } +} + +func (o *Output) Encode(bw *util.BinWriter) { + bw.Write(o.AssetID) + bw.Write(o.Amount) + bw.Write(o.ScriptHash) +} + +func (o *Output) Decode(br *util.BinReader) { + br.Read(&o.AssetID) + br.Read(&o.Amount) + br.Read(&o.ScriptHash) +} diff --git a/pkg/wire/payload/transaction/Witness.go b/pkg/wire/payload/transaction/Witness.go new file mode 100644 index 000000000..8b0d2fa0b --- /dev/null +++ b/pkg/wire/payload/transaction/Witness.go @@ -0,0 +1,34 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Witness struct { + InvocationScript []byte + VerificationScript []byte +} + +func (s *Witness) Encode(bw *util.BinWriter) error { + + bw.VarUint(uint64(len(s.InvocationScript))) + bw.Write(s.InvocationScript) + + bw.VarUint(uint64(len(s.VerificationScript))) + bw.Write(s.VerificationScript) + + return bw.Err +} + +func (s *Witness) Decode(br *util.BinReader) error { + + lenb := br.VarUint() + s.InvocationScript = make([]byte, lenb) + br.Read(s.InvocationScript) + + lenb = br.VarUint() + s.VerificationScript = make([]byte, lenb) + br.Read(s.VerificationScript) + + return br.Err +} diff --git a/pkg/wire/payload/transaction/assettype.go b/pkg/wire/payload/transaction/assettype.go new file mode 100644 index 000000000..82fc4913a --- /dev/null +++ b/pkg/wire/payload/transaction/assettype.go @@ -0,0 +1,16 @@ +package transaction + +// AssetType represent a NEO asset type +type AssetType uint8 + +// Valid asset types. +const ( + CreditFlag AssetType = 0x40 + DutyFlag AssetType = 0x80 + GoverningToken AssetType = 0x00 + UtilityToken AssetType = 0x01 + Currency AssetType = 0x08 + Share AssetType = DutyFlag | 0x10 + Invoice AssetType = DutyFlag | 0x18 + Token AssetType = CreditFlag | 0x20 +) diff --git a/pkg/wire/payload/transaction/attr_usage.go b/pkg/wire/payload/transaction/attr_usage.go new file mode 100644 index 000000000..b8f3a3469 --- /dev/null +++ b/pkg/wire/payload/transaction/attr_usage.go @@ -0,0 +1,48 @@ +package transaction + +type AttrUsage uint8 + +// List of valid attribute usages. +const ( + ContractHash AttrUsage = 0x00 + ECDH02 AttrUsage = 0x02 + ECDH03 AttrUsage = 0x03 + Script AttrUsage = 0x20 + Vote AttrUsage = 0x30 + CertURL AttrUsage = 0x80 + DescriptionURL AttrUsage = 0x81 + Description AttrUsage = 0x90 + + Hash1 AttrUsage = 0xa1 + Hash2 AttrUsage = 0xa2 + Hash3 AttrUsage = 0xa3 + Hash4 AttrUsage = 0xa4 + Hash5 AttrUsage = 0xa5 + Hash6 AttrUsage = 0xa6 + Hash7 AttrUsage = 0xa7 + Hash8 AttrUsage = 0xa8 + Hash9 AttrUsage = 0xa9 + Hash10 AttrUsage = 0xaa + Hash11 AttrUsage = 0xab + Hash12 AttrUsage = 0xac + Hash13 AttrUsage = 0xad + Hash14 AttrUsage = 0xae + Hash15 AttrUsage = 0xaf + + Remark AttrUsage = 0xf0 + Remark1 AttrUsage = 0xf1 + Remark2 AttrUsage = 0xf2 + Remark3 AttrUsage = 0xf3 + Remark4 AttrUsage = 0xf4 + Remark5 AttrUsage = 0xf5 + Remark6 AttrUsage = 0xf6 + Remark7 AttrUsage = 0xf7 + Remark8 AttrUsage = 0xf8 + Remark9 AttrUsage = 0xf9 + Remark10 AttrUsage = 0xfa + Remark11 AttrUsage = 0xfb + Remark12 AttrUsage = 0xfc + Remark13 AttrUsage = 0xfd + Remark14 AttrUsage = 0xfe + Remark15 AttrUsage = 0xff +) diff --git a/pkg/wire/payload/transaction/base.go b/pkg/wire/payload/transaction/base.go new file mode 100644 index 000000000..9770e3bd1 --- /dev/null +++ b/pkg/wire/payload/transaction/base.go @@ -0,0 +1,204 @@ +package transaction + +import ( + "bytes" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type encodeExclusiveFields func(bw *util.BinWriter) +type decodeExclusiveFields func(br *util.BinReader) + +// Transactioner is the interface that will unite the +// transaction types. Each transaction will implement this interface +// and so wil be a transactioner +type Transactioner interface { + Encode(w io.Writer) error + Decode(r io.Reader) error + ID() (util.Uint256, error) + Bytes() []byte + UTXOs() []*Output + TXOs() []*Input + Witness() []*Witness +} + +// Base transaction is the template for all other transactions +// It contains all of the shared fields between transactions and +// the additional encodeExclusive and decodeExclusive methods, which +// can be overwitten in the other transactions to encode the non shared fields +type Base struct { + Type types.TX + Version version.TX + Inputs []*Input + Outputs []*Output + Attributes []*Attribute + Witnesses []*Witness + Hash util.Uint256 + encodeExclusive encodeExclusiveFields + decodeExclusive decodeExclusiveFields +} + +func createBaseTransaction(typ types.TX, ver version.TX) *Base { + return &Base{ + Type: typ, + Version: ver, + Inputs: []*Input{}, + Outputs: []*Output{}, + Attributes: []*Attribute{}, + Witnesses: []*Witness{}, + } + +} + +func (b *Base) Decode(r io.Reader) error { + br := &util.BinReader{R: r} + return b.DecodePayload(br) +} +func (b *Base) Encode(w io.Writer) error { + bw := &util.BinWriter{W: w} + b.EncodePayload(bw) + return bw.Err +} + +func (b *Base) EncodePayload(bw *util.BinWriter) { + b.encodeHashableFields(bw) + + lenWitnesses := uint64(len(b.Witnesses)) + bw.VarUint(lenWitnesses) + + for _, witness := range b.Witnesses { + witness.Encode(bw) + } +} + +func (b *Base) DecodePayload(br *util.BinReader) error { + b.decodeHashableFields(br) + + lenWitnesses := br.VarUint() + + b.Witnesses = make([]*Witness, lenWitnesses) + for i := 0; i < int(lenWitnesses); i++ { + b.Witnesses[i] = &Witness{} + b.Witnesses[i].Decode(br) + } + + if br.Err != nil { + return br.Err + } + + return b.createHash() +} + +func (b *Base) encodeHashableFields(bw *util.BinWriter) { + b.Type.Encode(bw) + b.Version.Encode(bw) + + b.encodeExclusive(bw) + + lenAttrs := uint64(len(b.Attributes)) + lenInputs := uint64(len(b.Inputs)) + lenOutputs := uint64(len(b.Outputs)) + + bw.VarUint(lenAttrs) + for _, attr := range b.Attributes { + attr.Encode(bw) + } + + bw.VarUint(lenInputs) + for _, input := range b.Inputs { + input.Encode(bw) + } + + bw.VarUint(lenOutputs) + for _, output := range b.Outputs { + output.Encode(bw) + } +} + +// created for consistency +func (b *Base) decodeHashableFields(br *util.BinReader) { + b.Type.Decode(br) + + b.Version.Decode(br) + + b.decodeExclusive(br) + + lenAttrs := br.VarUint() + b.Attributes = make([]*Attribute, lenAttrs) + for i := 0; i < int(lenAttrs); i++ { + + b.Attributes[i] = &Attribute{} + b.Attributes[i].Decode(br) + } + + lenInputs := br.VarUint() + + b.Inputs = make([]*Input, lenInputs) + for i := 0; i < int(lenInputs); i++ { + b.Inputs[i] = &Input{} + b.Inputs[i].Decode(br) + } + + lenOutputs := br.VarUint() + b.Outputs = make([]*Output, lenOutputs) + for i := 0; i < int(lenOutputs); i++ { + b.Outputs[i] = &Output{} + b.Outputs[i].Decode(br) + } + +} + +func (b *Base) AddInput(i *Input) { + b.Inputs = append(b.Inputs, i) +} +func (b *Base) AddOutput(o *Output) { + b.Outputs = append(b.Outputs, o) +} +func (b *Base) AddAttribute(a *Attribute) { + b.Attributes = append(b.Attributes, a) +} +func (b *Base) AddWitness(w *Witness) { + b.Witnesses = append(b.Witnesses, w) +} + +func (b *Base) createHash() error { + + hash, err := util.CalculateHash(b.encodeHashableFields) + b.Hash = hash + return err +} + +// ID returns the TXID of the transaction +func (b *Base) ID() (util.Uint256, error) { + var emptyHash util.Uint256 + var err error + if b.Hash == emptyHash { + err = b.createHash() + } + return b.Hash, err +} + +// Bytes returns the raw bytes of the tx +func (b *Base) Bytes() []byte { + buf := new(bytes.Buffer) + b.Encode(buf) + return buf.Bytes() +} + +// UTXOs returns the outputs in the tx +func (b *Base) UTXOs() []*Output { + return b.Outputs +} + +// TXOs returns the inputs in the tx +func (b *Base) TXOs() []*Input { + return b.Inputs +} + +// Witness returns the witnesses in the tx +func (b *Base) Witness() []*Witness { + return b.Witnesses +} diff --git a/pkg/wire/payload/transaction/claim.go b/pkg/wire/payload/transaction/claim.go new file mode 100644 index 000000000..d93e4263c --- /dev/null +++ b/pkg/wire/payload/transaction/claim.go @@ -0,0 +1,43 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Claim struct { + *Base + Claims []*Input +} + +func NewClaim(ver version.TX) *Claim { + basicTrans := createBaseTransaction(types.Contract, ver) + + claim := &Claim{} + claim.Base = basicTrans + claim.encodeExclusive = claim.encodeExcl + claim.decodeExclusive = claim.decodeExcl + return claim +} + +func (c *Claim) encodeExcl(bw *util.BinWriter) { + + bw.VarUint(uint64(len(c.Claims))) + for _, claim := range c.Claims { + claim.Encode(bw) + } +} + +func (c *Claim) decodeExcl(br *util.BinReader) { + lenClaims := br.VarUint() + + c.Claims = make([]*Input, lenClaims) + for i := 0; i < int(lenClaims); i++ { + c.Claims[i] = &Input{} + c.Claims[i].Decode(br) + } + +} + +// use encode and decode exclusive to make the interface diff --git a/pkg/wire/payload/transaction/claim_test.go b/pkg/wire/payload/transaction/claim_test.go new file mode 100644 index 000000000..fb049fb58 --- /dev/null +++ b/pkg/wire/payload/transaction/claim_test.go @@ -0,0 +1,38 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeClaim(t *testing.T) { + + // test taken from mainnet: abf142faf539c340e42722b5b34b505cf4fd73185fed775784e37c2c5ef1b866 + rawtx := "020001af1b3a0f3729572893ce4e82f2113d18ec9a5e9d6fe02117eaa9e0c5a43770490000000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6048ae5801000000001123b6b74273562540479eea5cd0139f88ac7dd301414085f6d9edc24ab68c5d15c8e164de6702106c53bc15fa2c45b575bd3543c19132de61dd1922407be56affbcea73e5f8878811549340fd3c951e8593d51f3c8a962321028cf5e5a4d430db0202755c2cf1b3c99efcb4da4e41e182450dc5e1ddffb54bbfac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + c := NewClaim(0) + + r := bytes.NewReader(rawtxBytes) + err := c.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Claim, c.Type) + assert.Equal(t, 0, int(c.Version)) + assert.Equal(t, 1, int(len(c.Claims))) + + claim := c.Claims[0] + assert.Equal(t, "497037a4c5e0a9ea1721e06f9d5e9aec183d11f2824ece93285729370f3a1baf", claim.PrevHash.String()) + assert.Equal(t, uint16(0), claim.PrevIndex) + assert.Equal(t, "abf142faf539c340e42722b5b34b505cf4fd73185fed775784e37c2c5ef1b866", c.Hash.String()) + + // Encode + buf := new(bytes.Buffer) + err = c.Encode(buf) + assert.Equal(t, nil, err) + assert.Equal(t, rawtx, hex.EncodeToString(buf.Bytes())) +} diff --git a/pkg/wire/payload/transaction/contract.go b/pkg/wire/payload/transaction/contract.go new file mode 100644 index 000000000..dc93ef0ef --- /dev/null +++ b/pkg/wire/payload/transaction/contract.go @@ -0,0 +1,30 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Contract struct { + *Base +} + +func NewContract(ver version.TX) *Contract { + basicTrans := createBaseTransaction(types.Contract, ver) + + contract := &Contract{ + basicTrans, + } + contract.encodeExclusive = contract.encodeExcl + contract.decodeExclusive = contract.decodeExcl + return contract +} + +func (c *Contract) encodeExcl(bw *util.BinWriter) { + return +} + +func (c *Contract) decodeExcl(br *util.BinReader) { + return +} diff --git a/pkg/wire/payload/transaction/contract_test.go b/pkg/wire/payload/transaction/contract_test.go new file mode 100644 index 000000000..ea0393301 --- /dev/null +++ b/pkg/wire/payload/transaction/contract_test.go @@ -0,0 +1,45 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeContract(t *testing.T) { + + // mainnet transaction: bdf6cc3b9af12a7565bda80933a75ee8cef1bc771d0d58effc08e4c8b436da79 + rawtx := "80000001888da99f8f497fd65c4325786a09511159c279af4e7eb532e9edd628c87cc1ee0000019b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc50082167010000000a8666b4830229d6a1a9b80f6088059191c122d2b0141409e79e132290c82916a88f1a3db5cf9f3248b780cfece938ab0f0812d0e188f3a489c7d1a23def86bd69d863ae67de753b2c2392e9497eadc8eb9fc43aa52c645232103e2f6a334e05002624cf616f01a62cff2844c34a3b08ca16048c259097e315078ac" + rawtxBytes, _ := hex.DecodeString(rawtx) + c := NewContract(30) + + r := bytes.NewReader(rawtxBytes) + err := c.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Contract, c.Type) + assert.Equal(t, 0, int(c.Version)) + assert.Equal(t, 1, int(len(c.Inputs))) + + input := c.Inputs[0] + + assert.Equal(t, "eec17cc828d6ede932b57e4eaf79c2591151096a7825435cd67f498f9fa98d88", input.PrevHash.String()) + assert.Equal(t, 0, int(input.PrevIndex)) + assert.Equal(t, int64(70600000000), c.Outputs[0].Amount) + assert.Equal(t, "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", c.Outputs[0].AssetID.String()) + assert.Equal(t, "a8666b4830229d6a1a9b80f6088059191c122d2b", c.Outputs[0].ScriptHash.String()) + assert.Equal(t, "bdf6cc3b9af12a7565bda80933a75ee8cef1bc771d0d58effc08e4c8b436da79", c.Hash.String()) + + // Encode + buf := new(bytes.Buffer) + + err = c.Encode(buf) + assert.Equal(t, nil, err) + + assert.Equal(t, rawtxBytes, buf.Bytes()) + +} diff --git a/pkg/wire/payload/transaction/enrollment.go b/pkg/wire/payload/transaction/enrollment.go new file mode 100644 index 000000000..317124783 --- /dev/null +++ b/pkg/wire/payload/transaction/enrollment.go @@ -0,0 +1,31 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Enrollment struct { + *Base + Key PublicKey +} + +func NewEnrollment(ver version.TX) *Enrollment { + basicTrans := createBaseTransaction(types.Enrollment, ver) + + Enrollment := &Enrollment{} + Enrollment.Base = basicTrans + Enrollment.encodeExclusive = Enrollment.encodeExcl + Enrollment.decodeExclusive = Enrollment.decodeExcl + return Enrollment +} + +func (e *Enrollment) encodeExcl(bw *util.BinWriter) { + e.Key.Encode(bw) + +} + +func (e *Enrollment) decodeExcl(br *util.BinReader) { + e.Key.Decode(br) +} diff --git a/pkg/wire/payload/transaction/enrollment_test.go b/pkg/wire/payload/transaction/enrollment_test.go new file mode 100644 index 000000000..cf76af8e7 --- /dev/null +++ b/pkg/wire/payload/transaction/enrollment_test.go @@ -0,0 +1,30 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeEnrollment(t *testing.T) { + rawtx := "200002ff8ac54687f36bbc31a91b730cc385da8af0b581f2d59d82b5cfef824fd271f60001d3d3b7028d61fea3b7803fda3d7f0a1f7262d38e5e1c8987b0313e0a94574151000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60005441d11600000050ac4949596f5b62fef7be4d1c3e494e6048ed4a01414079d78189d591097b17657a62240c93595e8233dc81157ea2cd477813f09a11fd72845e6bd97c5a3dda125985ea3d5feca387e9933649a9a671a69ab3f6301df6232102ff8ac54687f36bbc31a91b730cc385da8af0b581f2d59d82b5cfef824fd271f6ac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + enroll := NewEnrollment(30) + + r := bytes.NewReader(rawtxBytes) + err := enroll.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Enrollment, enroll.Type) + + buf := new(bytes.Buffer) + err = enroll.Encode(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, rawtx, hex.EncodeToString(buf.Bytes())) + assert.Equal(t, "988832f693785dcbcb8d5a0e9d5d22002adcbfb1eb6bbeebf8c494fff580e147", enroll.Hash.String()) +} diff --git a/pkg/wire/payload/transaction/invocation.go b/pkg/wire/payload/transaction/invocation.go new file mode 100644 index 000000000..04fe85a89 --- /dev/null +++ b/pkg/wire/payload/transaction/invocation.go @@ -0,0 +1,59 @@ +package transaction + +import ( + "errors" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/CityOfZion/neo-go/pkg/wire/util/fixed8" +) + +type Invocation struct { + *Base + Script []byte + Gas fixed8.Fixed8 +} + +func NewInvocation(ver version.TX) *Invocation { + basicTrans := createBaseTransaction(types.Invocation, ver) + + invocation := &Invocation{} + invocation.Base = basicTrans + invocation.encodeExclusive = invocation.encodeExcl + invocation.decodeExclusive = invocation.decodeExcl + return invocation +} + +func (c *Invocation) encodeExcl(bw *util.BinWriter) { + bw.VarUint(uint64(len(c.Script))) + bw.Write(c.Script) + + switch c.Version { + case 0: + c.Gas = fixed8.Fixed8(0) + case 1: + bw.Write(&c.Gas) + default: + bw.Write(&c.Gas) + } + + return +} + +func (c *Invocation) decodeExcl(br *util.BinReader) { + + lenScript := br.VarUint() + c.Script = make([]byte, lenScript) + br.Read(&c.Script) + + switch c.Version { + case 0: + c.Gas = fixed8.Fixed8(0) + case 1: + br.Read(&c.Gas) + default: + br.Err = errors.New("Invalid Version Number for Invocation Transaction") + } + return +} diff --git a/pkg/wire/payload/transaction/invocation_test.go b/pkg/wire/payload/transaction/invocation_test.go new file mode 100644 index 000000000..82ce2fcb6 --- /dev/null +++ b/pkg/wire/payload/transaction/invocation_test.go @@ -0,0 +1,44 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeInvoc(t *testing.T) { + // taken from mainnet b2a22cd9dd7636ae23e25576866cd1d9e2f3d85a85e80874441f085cd60006d1 + + rawtx := "d10151050034e23004141ad842821c7341d5a32b17d7177a1750d30014ca14628c9e5bc6a9346ca6bcdf050ceabdeb2bdc774953c1087472616e736665726703e1df72015bdef1a1b9567d4700635f23b1f406f100000000000000000220628c9e5bc6a9346ca6bcdf050ceabdeb2bdc7749f02f31363a30373a3032203a2030333366616431392d643638322d343035382d626437662d31356339333132343433653800000141403ced56c16f933e0a0a7d37470e114f6a4216ef9b834d61db67b74b9bd117370d10870857c0ee8adcf9956bc9fc92c5158de0c2db34ef459c17de042f20ad8fe92321027392870a5994b090d1750dda173a54df8dad324ed6d9ed25290d17c59059a112ac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + i := NewInvocation(30) + + r := bytes.NewReader(rawtxBytes) + err := i.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Invocation, i.Type) + + assert.Equal(t, 2, len(i.Attributes)) + + attr1 := i.Attributes[0] + assert.Equal(t, Script, attr1.Usage) + assert.Equal(t, "628c9e5bc6a9346ca6bcdf050ceabdeb2bdc7749", hex.EncodeToString(attr1.Data)) + + attr2 := i.Attributes[1] + assert.Equal(t, Remark, attr2.Usage) + assert.Equal(t, "31363a30373a3032203a2030333366616431392d643638322d343035382d626437662d313563393331323434336538", hex.EncodeToString(attr2.Data)) + + assert.Equal(t, "050034e23004141ad842821c7341d5a32b17d7177a1750d30014ca14628c9e5bc6a9346ca6bcdf050ceabdeb2bdc774953c1087472616e736665726703e1df72015bdef1a1b9567d4700635f23b1f406f1", hex.EncodeToString(i.Script)) + assert.Equal(t, "b2a22cd9dd7636ae23e25576866cd1d9e2f3d85a85e80874441f085cd60006d1", i.Hash.String()) + + // Encode + buf := new(bytes.Buffer) + err = i.Encode(buf) + assert.Equal(t, nil, err) + assert.Equal(t, rawtxBytes, buf.Bytes()) +} diff --git a/pkg/wire/payload/transaction/issue.go b/pkg/wire/payload/transaction/issue.go new file mode 100644 index 000000000..c64f7bfb8 --- /dev/null +++ b/pkg/wire/payload/transaction/issue.go @@ -0,0 +1,35 @@ +package transaction + +import ( + "errors" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Issue struct { + *Base +} + +func NewIssue(ver version.TX) *Issue { + basicTrans := createBaseTransaction(types.Issue, ver) + + Issue := &Issue{ + basicTrans, + } + Issue.encodeExclusive = Issue.encodeExcl + Issue.decodeExclusive = Issue.decodeExcl + return Issue +} + +func (c *Issue) encodeExcl(bw *util.BinWriter) { + if c.Version > 1 { + bw.Err = errors.New("Version Number Invalid, Issue cannot be more than 0") + } + return +} + +func (c *Issue) decodeExcl(br *util.BinReader) { + return +} diff --git a/pkg/wire/payload/transaction/issue_test.go b/pkg/wire/payload/transaction/issue_test.go new file mode 100644 index 000000000..4cbfd1697 --- /dev/null +++ b/pkg/wire/payload/transaction/issue_test.go @@ -0,0 +1,8 @@ +package transaction + +import "testing" + +func TestEncodeDecodeIssue(t *testing.T) { + + // This is the same as Contract, as it has no special fields. +} diff --git a/pkg/wire/payload/transaction/miner.go b/pkg/wire/payload/transaction/miner.go new file mode 100644 index 000000000..24fdaea5b --- /dev/null +++ b/pkg/wire/payload/transaction/miner.go @@ -0,0 +1,34 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Miner struct { + *Base + Nonce uint32 +} + +func NewMiner(ver version.TX) *Miner { + basicTrans := createBaseTransaction(types.Miner, ver) + + Miner := &Miner{} + Miner.Base = basicTrans + Miner.encodeExclusive = Miner.encodeExcl + Miner.decodeExclusive = Miner.decodeExcl + return Miner +} + +func (c *Miner) encodeExcl(bw *util.BinWriter) { + + bw.Write(c.Nonce) + return +} + +func (c *Miner) decodeExcl(br *util.BinReader) { + + br.Read(&c.Nonce) + +} diff --git a/pkg/wire/payload/transaction/miner_test.go b/pkg/wire/payload/transaction/miner_test.go new file mode 100644 index 000000000..d053ce414 --- /dev/null +++ b/pkg/wire/payload/transaction/miner_test.go @@ -0,0 +1,37 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeMiner(t *testing.T) { + // transaction from mainnet a1f219dc6be4c35eca172e65e02d4591045220221b1543f1a4b67b9e9442c264 + + rawtx := "0000fcd30e22000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60c8000000000000001f72e68b4e39602912106d53b229378a082784b200" + rawtxBytes, _ := hex.DecodeString(rawtx) + + m := NewMiner(0) + + r := bytes.NewReader(rawtxBytes) + err := m.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Miner, m.Type) + assert.Equal(t, uint32(571397116), m.Nonce) + + assert.Equal(t, "a1f219dc6be4c35eca172e65e02d4591045220221b1543f1a4b67b9e9442c264", m.Hash.String()) + + // Encode + buf := new(bytes.Buffer) + + err = m.Encode(buf) + assert.Equal(t, nil, err) + + assert.Equal(t, rawtxBytes, buf.Bytes()) + +} diff --git a/pkg/wire/payload/transaction/paramtype.go b/pkg/wire/payload/transaction/paramtype.go new file mode 100644 index 000000000..5f65e4906 --- /dev/null +++ b/pkg/wire/payload/transaction/paramtype.go @@ -0,0 +1,17 @@ +package transaction + +// ParamType represent the Type of the contract parameter +type ParamType uint8 + +// A list of supported smart contract parameter types. +const ( + SignatureType ParamType = iota + BoolType + IntegerType + Hash160Type + Hash256Type + ByteArrayType + PublicKeyType + StringType + ArrayType +) diff --git a/pkg/wire/payload/transaction/publickey.go b/pkg/wire/payload/transaction/publickey.go new file mode 100644 index 000000000..b5ea2c72e --- /dev/null +++ b/pkg/wire/payload/transaction/publickey.go @@ -0,0 +1,35 @@ +package transaction + +import ( + "errors" + + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type PublicKey struct { + Key []byte +} + +func (p *PublicKey) Encode(bw *util.BinWriter) { + bw.Write(p.Key) +} +func (p *PublicKey) Decode(br *util.BinReader) { + var prefix uint8 + br.Read(&prefix) + + // Compressed public keys. + if prefix == 0x02 || prefix == 0x03 { + p.Key = make([]byte, 32) + br.Read(p.Key) + } else if prefix == 0x04 { + p.Key = make([]byte, 65) + br.Read(p.Key) + } else if prefix == 0x00 { + // do nothing, For infinity, the p.Key == 0x00, included in the prefix + } else { + br.Err = errors.New("Prefix not recognised for public key") + return + } + + p.Key = append([]byte{prefix}, p.Key...) +} diff --git a/pkg/wire/payload/transaction/publish.go b/pkg/wire/payload/transaction/publish.go new file mode 100644 index 000000000..07988b364 --- /dev/null +++ b/pkg/wire/payload/transaction/publish.go @@ -0,0 +1,88 @@ +package transaction + +import ( + "errors" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type Publish struct { + *Base + Script []byte + ParamList []ParamType + ReturnType ParamType + NeedStorage byte + Name string + CodeVersion string + Author string + Email string + Description string +} + +func NewPublish(ver version.TX) *Publish { + basicTrans := createBaseTransaction(types.Publish, ver) + + Publish := &Publish{} + Publish.Base = basicTrans + Publish.encodeExclusive = Publish.encodeExcl + Publish.decodeExclusive = Publish.decodeExcl + return Publish +} + +func (p *Publish) encodeExcl(bw *util.BinWriter) { + bw.VarBytes(p.Script) + bw.VarUint(uint64(len(p.ParamList))) + for _, param := range p.ParamList { + bw.Write(param) + } + + bw.Write(p.ReturnType) + switch p.Version { + case 0: + p.NeedStorage = byte(0) + case 1: + bw.Write(p.NeedStorage) + default: + bw.Err = errors.New("Version Number unknown for Publish Transaction") + } + + bw.VarString(p.Name) + bw.VarString(p.CodeVersion) + bw.VarString(p.Author) + bw.VarString(p.Email) + bw.VarString(p.Description) + +} + +func (p *Publish) decodeExcl(br *util.BinReader) { + p.Script = br.VarBytes() + + lenParams := br.VarUint() + p.ParamList = make([]ParamType, lenParams) + for i := 0; i < int(lenParams); i++ { + var ptype uint8 + br.Read(&ptype) + p.ParamList[i] = ParamType(ptype) + } + + var rtype uint8 + br.Read(&rtype) + p.ReturnType = ParamType(rtype) + + switch p.Version { + case 0: + p.NeedStorage = byte(0) + case 1: + br.Read(&p.NeedStorage) + default: + br.Err = errors.New("Version Number unknown for Publish Transaction") + } + + p.Name = br.VarString() + p.CodeVersion = br.VarString() + p.Author = br.VarString() + p.Email = br.VarString() + p.Description = br.VarString() +} diff --git a/pkg/wire/payload/transaction/publish_test.go b/pkg/wire/payload/transaction/publish_test.go new file mode 100644 index 000000000..1c9bb1de1 --- /dev/null +++ b/pkg/wire/payload/transaction/publish_test.go @@ -0,0 +1,33 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodePublish(t *testing.T) { + ///transaction taken from neo-python; can be found on testnet 5467a1fc8723ceffa8e5ee59399b02eea1df6fbaa53768c6704b90b960d223fa + // taken from neo-python; + rawtx := "d000fd3f01746b4c04000000004c04000000004c040000000061681e416e745368617265732e426c6f636b636861696e2e476574486569676874681d416e745368617265732e426c6f636b636861696e2e476574426c6f636b744c0400000000948c6c766b947275744c0402000000936c766b9479744c0400000000948c6c766b9479681d416e745368617265732e4865616465722e47657454696d657374616d70a0744c0401000000948c6c766b947275744c0401000000948c6c766b9479641b004c0400000000744c0402000000948c6c766b947275623000744c0401000000936c766b9479744c0400000000936c766b9479ac744c0402000000948c6c766b947275620300744c0402000000948c6c766b947961748c6c766b946d748c6c766b946d748c6c766b946d746c768c6b946d746c768c6b946d746c768c6b946d6c75660302050001044c6f636b0c312e302d70726576696577310a4572696b205a68616e67126572696b40616e747368617265732e6f7267234c6f636b20796f75722061737365747320756e74696c20612074696d657374616d702e00014e23ac4c4851f93407d4c59e1673171f39859db9e7cac72540cd3cc1ae0cca87000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6000ebcaaa0d00000067f97110a66136d38badc7b9f88eab013027ce49014140c298da9f06d5687a0bb87ea3bba188b7dcc91b9667ea5cb71f6fdefe388f42611df29be9b2d6288655b9f2188f46796886afc3b37d8b817599365d9e161ecfb62321034b44ed9c8a88fb2497b6b57206cc08edd42c5614bd1fee790e5b795dee0f4e11ac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + publ := NewPublish(30) + + r := bytes.NewReader(rawtxBytes) + err := publ.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Publish, publ.Type) + + buf := new(bytes.Buffer) + err = publ.Encode(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, rawtx, hex.EncodeToString(buf.Bytes())) + assert.Equal(t, "5467a1fc8723ceffa8e5ee59399b02eea1df6fbaa53768c6704b90b960d223fa", publ.Hash.String()) + +} diff --git a/pkg/wire/payload/transaction/register.go b/pkg/wire/payload/transaction/register.go new file mode 100644 index 000000000..36bc41772 --- /dev/null +++ b/pkg/wire/payload/transaction/register.go @@ -0,0 +1,56 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/CityOfZion/neo-go/pkg/wire/util/fixed8" +) + +type Register struct { + *Base + // The type of the asset being registered. + AssetType AssetType + + // Name of the asset being registered. + Name string + + // Amount registered + // Unlimited mode -0.00000001 + Amount fixed8.Fixed8 + + // Decimals + Precision uint8 + + // Public key of the owner + Owner PublicKey + + Admin util.Uint160 +} + +func NewRegister(ver version.TX) *Register { + basicTrans := createBaseTransaction(types.Register, ver) + Register := &Register{} + Register.Base = basicTrans + Register.encodeExclusive = Register.encodeExcl + Register.decodeExclusive = Register.decodeExcl + return Register +} + +func (r *Register) encodeExcl(bw *util.BinWriter) { + bw.Write(r.AssetType) + bw.VarString(r.Name) + bw.Write(r.Amount) + bw.Write(r.Precision) + r.Owner.Encode(bw) + bw.Write(r.Admin) +} + +func (r *Register) decodeExcl(br *util.BinReader) { + br.Read(&r.AssetType) + r.Name = br.VarString() + br.Read(&r.Amount) + br.Read(&r.Precision) + r.Owner.Decode(br) + br.Read(&r.Admin) +} diff --git a/pkg/wire/payload/transaction/register_test.go b/pkg/wire/payload/transaction/register_test.go new file mode 100644 index 000000000..d5492a6d8 --- /dev/null +++ b/pkg/wire/payload/transaction/register_test.go @@ -0,0 +1,54 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeRegister(t *testing.T) { + // transaction taken from neo-python; can be found on testnet 0c092117b4ba47b81001712425e6e7f760a637695eaf23741ba335925b195ecd + + rawtx := "400060245b7b226c616e67223a227a682d434e222c226e616d65223a2254657374436f696e227d5dffffffffffffffff08034b44ed9c8a88fb2497b6b57206cc08edd42c5614bd1fee790e5b795dee0f4e1167f97110a66136d38badc7b9f88eab013027ce4900014423a26aeca49cdeeb9522c720e1ae3a93bbe27d53662839b16a438305c20906010001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c60001e1a210b00000067f97110a66136d38badc7b9f88eab013027ce490141405d8223ec807e3416a220a75ef9805dfa2e36bd4f6dcc7372373aa45f15c7fadfc96a8642e52acf56c2c66d549be4ba820484873d5cada00b9c1ce9674fbf96382321034b44ed9c8a88fb2497b6b57206cc08edd42c5614bd1fee790e5b795dee0f4e11ac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + reg := NewRegister(0) + + r := bytes.NewReader(rawtxBytes) + err := reg.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Register, reg.Type) + + buf := new(bytes.Buffer) + err = reg.Encode(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, rawtx, hex.EncodeToString(buf.Bytes())) + assert.Equal(t, "0c092117b4ba47b81001712425e6e7f760a637695eaf23741ba335925b195ecd", reg.Hash.String()) +} +func TestEncodeDecodeGenesisRegister(t *testing.T) { + + // genesis transaction taken from mainnet; can be found on mainnet(Block 0) : c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b + + rawtx := "400000455b7b226c616e67223a227a682d434e222c226e616d65223a22e5b08fe89a81e882a1227d2c7b226c616e67223a22656e222c226e616d65223a22416e745368617265227d5d0000c16ff28623000000da1745e9b549bd0bfa1a569971c77eba30cd5a4b00000000" + rawtxBytes, _ := hex.DecodeString(rawtx) + + reg := NewRegister(0) + + r := bytes.NewReader(rawtxBytes) + err := reg.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.Register, reg.Type) + + buf := new(bytes.Buffer) + err = reg.Encode(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, rawtx, hex.EncodeToString(buf.Bytes())) + assert.Equal(t, "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", reg.Hash.String()) +} diff --git a/pkg/wire/payload/transaction/state.go b/pkg/wire/payload/transaction/state.go new file mode 100644 index 000000000..2dcbbeebd --- /dev/null +++ b/pkg/wire/payload/transaction/state.go @@ -0,0 +1,40 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/version" + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type StateTX struct { + *Base + Descriptors []*StateDescriptor +} + +func NewStateTX(ver version.TX) *StateTX { + basicTrans := createBaseTransaction(types.State, ver) + + StateTX := &StateTX{} + StateTX.Base = basicTrans + StateTX.encodeExclusive = StateTX.encodeExcl + StateTX.decodeExclusive = StateTX.decodeExcl + return StateTX +} + +func (s *StateTX) encodeExcl(bw *util.BinWriter) { + + bw.VarUint(uint64(len(s.Descriptors))) + for _, desc := range s.Descriptors { + desc.Encode(bw) + } +} + +func (s *StateTX) decodeExcl(br *util.BinReader) { + lenDesc := br.VarUint() + + s.Descriptors = make([]*StateDescriptor, lenDesc) + for i := 0; i < int(lenDesc); i++ { + s.Descriptors[i] = &StateDescriptor{} + s.Descriptors[i].Decode(br) + } +} diff --git a/pkg/wire/payload/transaction/state_test.go b/pkg/wire/payload/transaction/state_test.go new file mode 100644 index 000000000..209d9b9c0 --- /dev/null +++ b/pkg/wire/payload/transaction/state_test.go @@ -0,0 +1,47 @@ +package transaction + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeState(t *testing.T) { + + // transaction taken from testnet 8abf5ebdb9a8223b12109513647f45bd3c0a6cf1a6346d56684cff71ba308724 + rawtx := "900001482103c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c10a5265676973746572656401010001cb4184f0a96e72656c1fbdd4f75cca567519e909fd43cefcec13d6c6abcb92a1000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6000b8fb050109000071f9cf7f0ec74ec0b0f28a92b12e1081574c0af00141408780d7b3c0aadc5398153df5e2f1cf159db21b8b0f34d3994d865433f79fafac41683783c48aef510b67660e3157b701b9ca4dd9946a385d578fba7dd26f4849232103c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1ac" + rawtxBytes, _ := hex.DecodeString(rawtx) + + s := NewStateTX(0) + + r := bytes.NewReader(rawtxBytes) + err := s.Decode(r) + assert.Equal(t, nil, err) + + assert.Equal(t, types.State, s.Type) + + assert.Equal(t, 1, len(s.Inputs)) + input := s.Inputs[0] + assert.Equal(t, "a192cbabc6d613ecfcce43fd09e9197556ca5cf7d4bd1f6c65726ea9f08441cb", input.PrevHash.String()) + assert.Equal(t, uint16(0), input.PrevIndex) + + assert.Equal(t, 1, len(s.Descriptors)) + descriptor := s.Descriptors[0] + assert.Equal(t, "03c089d7122b840a4935234e82e26ae5efd0c2acb627239dc9f207311337b6f2c1", hex.EncodeToString(descriptor.Key)) + assert.Equal(t, "52656769737465726564", hex.EncodeToString(descriptor.Value)) + assert.Equal(t, "\x01", descriptor.Field) + assert.Equal(t, Validator, descriptor.Type) + + // Encode + + buf := new(bytes.Buffer) + + err = s.Encode(buf) + + assert.Equal(t, nil, err) + assert.Equal(t, rawtxBytes, buf.Bytes()) + assert.Equal(t, "8abf5ebdb9a8223b12109513647f45bd3c0a6cf1a6346d56684cff71ba308724", s.Hash.String()) +} diff --git a/pkg/wire/payload/transaction/statedescriptor.go b/pkg/wire/payload/transaction/statedescriptor.go new file mode 100644 index 000000000..17e09a457 --- /dev/null +++ b/pkg/wire/payload/transaction/statedescriptor.go @@ -0,0 +1,52 @@ +package transaction + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// DescStateType represents the type of StateDescriptor. +type DescStateType uint8 + +// Valid DescStateType constants. +const ( + Account DescStateType = 0x40 + Validator DescStateType = 0x48 +) + +// StateDescriptor .. +type StateDescriptor struct { + Type DescStateType + Key []byte + Value []byte + Field string +} + +func (s *StateDescriptor) Decode(br *util.BinReader) { + br.Read(&s.Type) + + keyLen := br.VarUint() + s.Key = make([]byte, keyLen) + br.Read(s.Key) + + valLen := br.VarUint() + s.Value = make([]byte, valLen) + br.Read(s.Value) + + fieldLen := br.VarUint() + field := make([]byte, fieldLen) + br.Read(field) + + s.Field = string(field) +} +func (s *StateDescriptor) Encode(bw *util.BinWriter) { + bw.Write(s.Type) + + bw.VarUint(uint64(len(s.Key))) + bw.Write(s.Key) + + bw.VarUint(uint64(len(s.Value))) + bw.Write(s.Value) + + bw.VarString(s.Field) + +} diff --git a/pkg/wire/payload/transaction/types/types.go b/pkg/wire/payload/transaction/types/types.go new file mode 100644 index 000000000..e39138600 --- /dev/null +++ b/pkg/wire/payload/transaction/types/types.go @@ -0,0 +1,59 @@ +package types + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +// TX is the type of a transaction. +type TX uint8 + +const ( + Miner TX = 0x00 + Issue TX = 0x01 + Claim TX = 0x02 + Enrollment TX = 0x20 + Voting TX = 0x24 + Register TX = 0x40 + Contract TX = 0x80 + State TX = 0x90 + Agency TX = 0xb0 + Publish TX = 0xd0 + Invocation TX = 0xd1 +) + +func (t *TX) Encode(bw *util.BinWriter) { + bw.Write(t) +} +func (t *TX) Decode(br *util.BinReader) { + br.Read(t) +} + +// String implements the stringer interface. +func (t TX) String() string { + switch t { + case Miner: + return "MinerTransaction" + case Issue: + return "IssueTransaction" + case Claim: + return "ClaimTransaction" + case Enrollment: + return "EnrollmentTransaction" + case Voting: + return "VotingTransaction" + case Register: + return "RegisterTransaction" + case Contract: + return "ContractTransaction" + case State: + return "StateTransaction" + case Agency: + return "AgencyTransaction" + case Publish: + return "PublishTransaction" + case Invocation: + return "InvocationTransaction" + default: + return "UnkownTransaction" + } +} diff --git a/pkg/wire/payload/transaction/util.go b/pkg/wire/payload/transaction/util.go new file mode 100644 index 000000000..63fdd458c --- /dev/null +++ b/pkg/wire/payload/transaction/util.go @@ -0,0 +1,61 @@ +package transaction + +import ( + "bufio" + "encoding/hex" + "errors" + + "github.com/CityOfZion/neo-go/pkg/wire/payload/transaction/types" +) + +func FromBytes(reader *bufio.Reader) (Transactioner, error) { + + t, err := reader.Peek(1) + + typ := types.TX(t[0]) + var trans Transactioner + + switch typ { + case types.Miner: + miner := NewMiner(0) + err = miner.Decode(reader) + trans = miner + case types.Contract: + contract := NewContract(0) + err = contract.Decode(reader) + trans = contract + case types.Invocation: + invoc := NewInvocation(0) + err = invoc.Decode(reader) + trans = invoc + case types.Claim: + claim := NewClaim(0) + err = claim.Decode(reader) + trans = claim + case types.Register: + reg := NewRegister(0) + err = reg.Decode(reader) + trans = reg + case types.Issue: + iss := NewIssue(0) + err = iss.Decode(reader) + trans = iss + case types.Publish: + pub := NewPublish(0) + err = pub.Decode(reader) + trans = pub + case types.State: + sta := NewStateTX(0) + err = sta.Decode(reader) + trans = sta + case types.Enrollment: + enr := NewEnrollment(0) + err = enr.Decode(reader) + trans = enr + case types.Agency: + err = errors.New("Unsupported transaction type: Agency") + default: + err = errors.New("Unsupported transaction with byte type " + hex.EncodeToString([]byte{t[0]})) + } + return trans, err +} diff --git a/pkg/wire/payload/transaction/version/version.go b/pkg/wire/payload/transaction/version/version.go new file mode 100644 index 000000000..9b9e9ce57 --- /dev/null +++ b/pkg/wire/payload/transaction/version/version.go @@ -0,0 +1,19 @@ +package version + +import ( + "github.com/CityOfZion/neo-go/pkg/wire/util" +) + +type TX uint8 + +const ( + Contract TX = 0 +) + +func (v *TX) Encode(bw *util.BinWriter) { + bw.Write(v) +} + +func (v *TX) Decode(br *util.BinReader) { + br.Read(v) +} diff --git a/pkg/wire/protocol/protocol.go b/pkg/wire/protocol/protocol.go new file mode 100644 index 000000000..3f9493f4e --- /dev/null +++ b/pkg/wire/protocol/protocol.go @@ -0,0 +1,27 @@ +package protocol + +type Version uint32 + +const ( + DefaultVersion Version = 0 + UserAgent = "/NEO-GO/" // TODO: This may be relocated to a config file +) + +// ServiceFlag indicates the services provided by the node. 1 = P2P Full Node +type ServiceFlag uint64 + +const ( + NodePeerService ServiceFlag = 1 + // BloomFilerService ServiceFlag = 2 // Not implemented + // PrunedNode ServiceFlag = 3 // Not implemented + // LightNode ServiceFlag = 4 // Not implemented + +) + +// Magic is the network that NEO is running on +type Magic uint32 + +const ( + MainNet Magic = 7630401 + TestNet Magic = 0x74746e41 +) diff --git a/pkg/wire/util/Checksum/checksum.go b/pkg/wire/util/Checksum/checksum.go new file mode 100644 index 000000000..092ea177b --- /dev/null +++ b/pkg/wire/util/Checksum/checksum.go @@ -0,0 +1,29 @@ +package checksum + +import ( + "bytes" + "encoding/binary" + + "github.com/CityOfZion/neo-go/pkg/crypto/hash" +) + +func Compare(have uint32, b []byte) bool { + want := FromBytes(b) + return have == want +} + +func FromBuf(buf *bytes.Buffer) uint32 { + + return FromBytes(buf.Bytes()) +} + +func FromBytes(buf []byte) uint32 { + b, err := hash.DoubleSha256(buf) + + if err != nil { + return 0 + } + + // checksum := SumSHA256(SumSHA256(buf.Bytes())) + return binary.LittleEndian.Uint32(b.Bytes()[:4]) +} diff --git a/pkg/wire/util/address/address.go b/pkg/wire/util/address/address.go new file mode 100644 index 000000000..6a9baeb8b --- /dev/null +++ b/pkg/wire/util/address/address.go @@ -0,0 +1,18 @@ +package address + +import ( + "encoding/hex" + + "github.com/CityOfZion/neo-go/pkg/crypto/base58" +) + +func ToScriptHash(address string) string { + + decodedAddressAsBytes, err := base58.Decode(address) + if err != nil { + return "" + } + decodedAddressAsHex := hex.EncodeToString(decodedAddressAsBytes) + scriptHash := (decodedAddressAsHex[2:42]) + return scriptHash +} diff --git a/pkg/wire/util/binaryReader.go b/pkg/wire/util/binaryReader.go new file mode 100644 index 000000000..ce2c0219b --- /dev/null +++ b/pkg/wire/util/binaryReader.go @@ -0,0 +1,59 @@ +package util + +import ( + "encoding/binary" + "io" +) + +type BinReader struct { + R io.Reader + Err error +} + +func (r *BinReader) Read(v interface{}) { + if r.Err != nil { + return + } + r.Err = binary.Read(r.R, binary.LittleEndian, v) +} +func (r *BinReader) ReadBigEnd(v interface{}) { + if r.Err != nil { + return + } + r.Err = binary.Read(r.R, binary.BigEndian, v) +} + +func (r *BinReader) VarUint() uint64 { + var b uint8 + r.Err = binary.Read(r.R, binary.LittleEndian, &b) + + if b == 0xfd { + var v uint16 + r.Err = binary.Read(r.R, binary.LittleEndian, &v) + return uint64(v) + } + if b == 0xfe { + var v uint32 + r.Err = binary.Read(r.R, binary.LittleEndian, &v) + return uint64(v) + } + if b == 0xff { + var v uint64 + r.Err = binary.Read(r.R, binary.LittleEndian, &v) + return v + } + + return uint64(b) +} + +func (r *BinReader) VarBytes() []byte { + n := r.VarUint() + b := make([]byte, n) + r.Read(b) + return b +} + +func (r *BinReader) VarString() string { + b := r.VarBytes() + return string(b) +} diff --git a/pkg/wire/util/binaryWriter.go b/pkg/wire/util/binaryWriter.go new file mode 100644 index 000000000..076e78dea --- /dev/null +++ b/pkg/wire/util/binaryWriter.go @@ -0,0 +1,68 @@ +package util + +import ( + "encoding/binary" + "errors" + "io" +) + +type BinWriter struct { + W io.Writer + Err error +} + +func (w *BinWriter) Write(v interface{}) { + if w.Err != nil { + return + } + w.Err = binary.Write(w.W, binary.LittleEndian, v) +} + +// Only used for IP and PORT. Additional method makes the default LittleEndian case clean +func (w *BinWriter) WriteBigEnd(v interface{}) { + if w.Err != nil { + return + } + w.Err = binary.Write(w.W, binary.BigEndian, v) +} + +func (w *BinWriter) VarString(s string) { + w.VarBytes([]byte(s)) +} + +func (w *BinWriter) VarUint(val uint64) { + if val < 0 { + w.Err = errors.New("value out of range") + return + } + + if w.Err != nil { + return + } + + if val < 0xfd { + w.Err = binary.Write(w.W, binary.LittleEndian, uint8(val)) + return + } + if val < 0xFFFF { + w.Err = binary.Write(w.W, binary.LittleEndian, byte(0xfd)) + w.Err = binary.Write(w.W, binary.LittleEndian, uint16(val)) + return + } + if val < 0xFFFFFFFF { + w.Err = binary.Write(w.W, binary.LittleEndian, byte(0xfe)) + w.Err = binary.Write(w.W, binary.LittleEndian, uint32(val)) + return + + } + + w.Err = binary.Write(w.W, binary.LittleEndian, byte(0xff)) + w.Err = binary.Write(w.W, binary.LittleEndian, val) + +} + +// WriteVarBytes writes a variable length byte array. +func (w *BinWriter) VarBytes(b []byte) { + w.VarUint(uint64(len(b))) + w.Write(b) +} diff --git a/pkg/wire/util/crypto/base58/base58.go b/pkg/wire/util/crypto/base58/base58.go new file mode 100644 index 000000000..b0a67a912 --- /dev/null +++ b/pkg/wire/util/crypto/base58/base58.go @@ -0,0 +1,123 @@ +package base58 + +import ( + "fmt" + "math/big" +) + +const prefix rune = '1' + +var decodeMap = map[rune]int64{ + '1': 0, '2': 1, '3': 2, '4': 3, '5': 4, + '6': 5, '7': 6, '8': 7, '9': 8, 'A': 9, + 'B': 10, 'C': 11, 'D': 12, 'E': 13, 'F': 14, + 'G': 15, 'H': 16, 'J': 17, 'K': 18, 'L': 19, + 'M': 20, 'N': 21, 'P': 22, 'Q': 23, 'R': 24, + 'S': 25, 'T': 26, 'U': 27, 'V': 28, 'W': 29, + 'X': 30, 'Y': 31, 'Z': 32, 'a': 33, 'b': 34, + 'c': 35, 'd': 36, 'e': 37, 'f': 38, 'g': 39, + 'h': 40, 'i': 41, 'j': 42, 'k': 43, 'm': 44, + 'n': 45, 'o': 46, 'p': 47, 'q': 48, 'r': 49, + 's': 50, 't': 51, 'u': 52, 'v': 53, 'w': 54, + 'x': 55, 'y': 56, 'z': 57, +} + +// Base58Decode decodes the base58 encoded string. +func Decode(s string) ([]byte, error) { + var ( + startIndex = 0 + zero = 0 + ) + for i, c := range s { + if c == prefix { + zero++ + } else { + startIndex = i + break + } + } + + var ( + n = big.NewInt(0) + div = big.NewInt(58) + ) + for _, c := range s[startIndex:] { + charIndex, ok := decodeMap[c] + if !ok { + return nil, fmt.Errorf( + "invalid character '%c' when decoding this base58 string: '%s'", c, s, + ) + } + n.Add(n.Mul(n, div), big.NewInt(charIndex)) + } + + out := n.Bytes() + buf := make([]byte, (zero + len(out))) + copy(buf[zero:], out[:]) + + return buf, nil +} + +// Base58Encode encodes a byte slice to be a base58 encoded string. +func Encode(bytes []byte) string { + var ( + lookupTable = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + x = new(big.Int).SetBytes(bytes) + r = new(big.Int) + m = big.NewInt(58) + zero = big.NewInt(0) + encoded string + ) + + for x.Cmp(zero) > 0 { + x.QuoRem(x, m, r) + encoded = string(lookupTable[r.Int64()]) + encoded + } + + return encoded +} + +// Base58CheckDecode decodes the given string. +// func CheckDecode(s string) (b []byte, err error) { +// b, err = Decode(s) +// if err != nil { +// return nil, err +// } + +// for i := 0; i < len(s); i++ { +// if s[i] != '1' { +// break +// } +// b = append([]byte{0x00}, b...) +// } + +// if len(b) < 5 { +// return nil, fmt.Errorf("Invalid base-58 check string: missing checksum. -1") +// } + +// hash, err := hash.DoubleSha256(b[:len(b)-4]) + +// if err != nil { +// return nil, fmt.Errorf("Could not double sha256 data") +// } + +// if bytes.Compare(hash[0:4], b[len(b)-4:]) != 0 { +// return nil, fmt.Errorf("Invalid base-58 check string: invalid checksum. -2") +// } + +// // Strip the 4 byte long hash. +// b = b[:len(b)-4] + +// return b, nil +// } + +// // Base58checkEncode encodes b into a base-58 check encoded string. +// func CheckEncode(b []byte) (string, error) { +// hash, err := hash.DoubleSha256(b) +// if err != nil { +// return "", fmt.Errorf("Could not double sha256 data") +// } +// b = append(b, hash[0:4]...) + +// return Encode(b), nil +// } diff --git a/pkg/wire/util/crypto/base58/base58_test.go b/pkg/wire/util/crypto/base58/base58_test.go new file mode 100644 index 000000000..524f0e55e --- /dev/null +++ b/pkg/wire/util/crypto/base58/base58_test.go @@ -0,0 +1,32 @@ +package base58 + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecode(t *testing.T) { + input := "1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" + + data, err := Decode(input) + if err != nil { + t.Fatal(err) + } + + expected := "0099bc78ba577a95a11f1a344d4d2ae55f2f857b989ea5e5e2" + actual := hex.EncodeToString(data) + assert.Equal(t, expected, actual) +} +func TestEncode(t *testing.T) { + input := "0099bc78ba577a95a11f1a344d4d2ae55f2f857b989ea5e5e2" + + inputBytes, _ := hex.DecodeString(input) + + data := Encode(inputBytes) + + expected := "F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX" // Removed the 1 as it is not checkEncoding + actual := data + assert.Equal(t, expected, actual) +} diff --git a/pkg/wire/util/crypto/hash/hash.go b/pkg/wire/util/crypto/hash/hash.go new file mode 100644 index 000000000..22ab1c6d6 --- /dev/null +++ b/pkg/wire/util/crypto/hash/hash.go @@ -0,0 +1,77 @@ +package hash + +import ( + "crypto/sha256" + "io" + + "github.com/CityOfZion/neo-go/pkg/wire/util" + "golang.org/x/crypto/ripemd160" +) + +func Sha256(data []byte) (util.Uint256, error) { + var hash util.Uint256 + hasher := sha256.New() + hasher.Reset() + _, err := hasher.Write(data) + + hash, err = util.Uint256DecodeBytes(hasher.Sum(nil)) + if err != nil { + return hash, err + } + return hash, nil +} + +func DoubleSha256(data []byte) (util.Uint256, error) { + var hash util.Uint256 + + h1, err := Sha256(data) + if err != nil { + return hash, err + } + + hash, err = Sha256(h1.Bytes()) + if err != nil { + return hash, err + } + return hash, nil +} + +func RipeMD160(data []byte) (util.Uint160, error) { + var hash util.Uint160 + hasher := ripemd160.New() + hasher.Reset() + _, err := io.WriteString(hasher, string(data)) + + hash, err = util.Uint160DecodeBytes(hasher.Sum(nil)) + if err != nil { + return hash, err + } + return hash, nil +} + +func Hash160(data []byte) (util.Uint160, error) { + var hash util.Uint160 + h1, err := Sha256(data) + + h2, err := RipeMD160(h1.Bytes()) + + hash, err = util.Uint160DecodeBytes(h2.Bytes()) + + if err != nil { + return hash, err + } + return hash, nil +} + +func Checksum(data []byte) ([]byte, error) { + hash, err := Sum(data) + if err != nil { + return nil, err + } + return hash[:4], nil +} + +func Sum(b []byte) (util.Uint256, error) { + hash, err := DoubleSha256((b)) + return hash, err +} diff --git a/pkg/wire/util/crypto/hash/hash_test.go b/pkg/wire/util/crypto/hash/hash_test.go new file mode 100644 index 000000000..aa23718f6 --- /dev/null +++ b/pkg/wire/util/crypto/hash/hash_test.go @@ -0,0 +1,62 @@ +package hash + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSha256(t *testing.T) { + input := []byte("hello") + data, err := Sha256(input) + + if err != nil { + t.Fatal(err) + } + expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + actual := hex.EncodeToString(data.Bytes()) // MARK: In the DecodeBytes function, there is a bytes reverse, not sure why? + + assert.Equal(t, expected, actual) +} + +func TestHashDoubleSha256(t *testing.T) { + input := []byte("hello") + data, err := DoubleSha256(input) + + if err != nil { + t.Fatal(err) + } + + firstSha, _ := Sha256(input) + doubleSha, _ := Sha256(firstSha.Bytes()) + expected := hex.EncodeToString(doubleSha.Bytes()) + + actual := hex.EncodeToString(data.Bytes()) + assert.Equal(t, expected, actual) +} + +func TestHashRipeMD160(t *testing.T) { + input := []byte("hello") + data, err := RipeMD160(input) + + if err != nil { + t.Fatal(err) + } + expected := "108f07b8382412612c048d07d13f814118445acd" + actual := hex.EncodeToString(data.Bytes()) + assert.Equal(t, expected, actual) +} + +func TestHash160(t *testing.T) { + input := "02cccafb41b220cab63fd77108d2d1ebcffa32be26da29a04dca4996afce5f75db" + publicKeyBytes, _ := hex.DecodeString(input) + data, err := Hash160(publicKeyBytes) + + if err != nil { + t.Fatal(err) + } + expected := "c8e2b685cc70ec96743b55beb9449782f8f775d8" + actual := hex.EncodeToString(data.Bytes()) + assert.Equal(t, expected, actual) +} diff --git a/pkg/wire/util/fixed8/fixed8.go b/pkg/wire/util/fixed8/fixed8.go new file mode 100644 index 000000000..9a8119ae7 --- /dev/null +++ b/pkg/wire/util/fixed8/fixed8.go @@ -0,0 +1,55 @@ +package fixed8 + +import ( + "errors" + "fmt" + "strconv" +) + +const ( + decimals = 100000000 +) + +var errInvalidString = errors.New("Fixed8 must satisfy following regex \\d+(\\.\\d{1,8})?") + +// Fixed8 represents a fixed-point number with precision 10^-8. +type Fixed8 int64 + +// String implements the Stringer interface. +func (f Fixed8) String() string { + val := f.Value() + return strconv.FormatFloat(val, 'f', -1, 64) +} + +// Value returns the original value representing the Fixed8. +func (f Fixed8) Value() float64 { + return float64(f) / float64(decimals) +} +func (f Fixed8) Add(val Fixed8) Fixed8 { + a := int64(f.Value()) + b := int64(val.Value()) + c := a + b + return FromInt(c) +} +func (f Fixed8) Sub(val Fixed8) Fixed8 { + a := int64(f.Value()) + b := int64(val.Value()) + c := a - b + return FromInt(c) +} + +func FromInt(val int64) Fixed8 { + return Fixed8(val * decimals) +} + +func FromFloat(val float64) Fixed8 { + return Fixed8(val * decimals) +} + +func FromString(val string) (Fixed8, error) { + res, err := strconv.ParseFloat(val, 64) + if err != nil { + return 0, fmt.Errorf("Failed at parsing string %s", val) + } + return FromFloat(res), nil +} diff --git a/pkg/wire/util/fixed8/fixed8_test.go b/pkg/wire/util/fixed8/fixed8_test.go new file mode 100644 index 000000000..8ee5ab37f --- /dev/null +++ b/pkg/wire/util/fixed8/fixed8_test.go @@ -0,0 +1,85 @@ +package fixed8 + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFixed8Value(t *testing.T) { + + input := int64(12) + assert.Equal(t, float64(input), FromInt(input).Value()) + +} +func TestFixed8Add(t *testing.T) { + + a := FromInt(1) + b := FromInt(2) + + c := a.Add(b) + expected := float64(3) + assert.Equal(t, expected, c.Value()) + +} +func TestFixed8AddRecursive(t *testing.T) { + + a := FromInt(1) + sum := int64(1) + + for i := int64(2); i <= 10; i++ { + + sum += i + b := FromInt(i) + c := a.Add(b) + a = c // 1 + 2 + 3 ... + 10 + } + assert.Equal(t, float64(sum), a.Value()) + +} + +func TestFromInt(t *testing.T) { + + inputs := []int64{12, 23, 100, 456789} + + for _, val := range inputs { + assert.Equal(t, Fixed8(val*decimals), FromInt(val)) + assert.Equal(t, float64(val), FromInt(val).Value()) + } + + for _, val := range inputs { + valString := strconv.FormatInt(val, 10) + assert.Equal(t, valString, FromInt(val).String()) + } + +} +func TestFromFloat(t *testing.T) { + inputs := []float64{12.98, 23.87654333, 100.654322, 456789.12345665} + + for _, val := range inputs { + assert.Equal(t, Fixed8(val*decimals), FromFloat(val)) + assert.Equal(t, float64(val), FromFloat(val).Value()) + } +} +func TestFromString(t *testing.T) { + inputs := []string{"9000", "100000000", "5", "10945", "20.45", "0.00000001"} + + for _, val := range inputs { + + n, err := FromString(val) + assert.Nil(t, err) + assert.Equal(t, val, n.String()) + + } + + val := "123456789.12345678" + n, err := FromString(val) + assert.Nil(t, err) + assert.Equal(t, Fixed8(12345678912345678), n) + + val = "901.2341" + n, err = FromString(val) + assert.Nil(t, err) + assert.Equal(t, Fixed8(90123410000), n) +} diff --git a/pkg/wire/util/io/io.go b/pkg/wire/util/io/io.go new file mode 100644 index 000000000..0d6ada43c --- /dev/null +++ b/pkg/wire/util/io/io.go @@ -0,0 +1,16 @@ +package fileutils + +import ( + "os" +) + +func UpdateFile(filename string, data []byte) error { + + f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + + dataWNewline := append(data, []byte("\n")...) + + _, err = f.Write(dataWNewline) + err = f.Close() + return err +} diff --git a/pkg/wire/util/ip/ip.go b/pkg/wire/util/ip/ip.go new file mode 100644 index 000000000..7befd8cca --- /dev/null +++ b/pkg/wire/util/ip/ip.go @@ -0,0 +1,19 @@ +package iputils + +import ( + "log" + "net" +) + +// https://stackoverflow.com/a/37382208 +func GetLocalIP() net.IP { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP +} diff --git a/pkg/wire/util/slice/slice.go b/pkg/wire/util/slice/slice.go new file mode 100644 index 000000000..0cf29435f --- /dev/null +++ b/pkg/wire/util/slice/slice.go @@ -0,0 +1,15 @@ +package slice + +// SliceReverse return a reversed version of the given byte slice. +func Reverse(b []byte) []byte { + // Protect from big.Ints that have 1 len bytes. + if len(b) < 2 { + return b + } + + dest := make([]byte, len(b)) + for i, j := 0, len(b)-1; i < j+1; i, j = i+1, j-1 { + dest[i], dest[j] = b[j], b[i] + } + return dest +} diff --git a/pkg/wire/util/slice/slice_test.go b/pkg/wire/util/slice/slice_test.go new file mode 100644 index 000000000..e65c63f65 --- /dev/null +++ b/pkg/wire/util/slice/slice_test.go @@ -0,0 +1,33 @@ +package slice + +import ( + "bytes" + "testing" +) + +func TestSliceReverse(t *testing.T) { + arr := []byte{0x01, 0x02, 0x03, 0x04} + have := Reverse(arr) + want := []byte{0x04, 0x03, 0x02, 0x01} + if bytes.Compare(have, want) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} +func TestSliceReverseOddNumberOfElements(t *testing.T) { + arr := []byte{0x01, 0x02, 0x03, 0x04, 0x05} + have := Reverse(arr) + want := []byte{0x05, 0x04, 0x03, 0x02, 0x01} + if bytes.Compare(have, want) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} + +// This tests a bug that occured with arrays of size 1 +func TestSliceReverseLen2(t *testing.T) { + arr := []byte{0x01} + have := Reverse(arr) + want := []byte{0x01} + if bytes.Compare(have, want) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} diff --git a/pkg/wire/util/uint160.go b/pkg/wire/util/uint160.go new file mode 100644 index 000000000..429fe5d95 --- /dev/null +++ b/pkg/wire/util/uint160.go @@ -0,0 +1,86 @@ +package util + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/CityOfZion/neo-go/pkg/wire/util/slice" + "golang.org/x/crypto/ripemd160" +) + +const uint160Size = 20 + +// Uint160 is a 20 byte long unsigned integer. +type Uint160 [uint160Size]uint8 + +// Uint160DecodeString attempts to decode the given string into an Uint160. +func Uint160DecodeString(s string) (u Uint160, err error) { + if len(s) != uint160Size*2 { + return u, fmt.Errorf("expected string size of %d got %d", uint160Size*2, len(s)) + } + b, err := hex.DecodeString(s) + if err != nil { + return u, err + } + return Uint160DecodeBytes(b) +} + +// Uint160DecodeBytes attempts to decode the given bytes into an Uint160. +func Uint160DecodeBytes(b []byte) (u Uint160, err error) { + if len(b) != uint160Size { + return u, fmt.Errorf("expected byte size of %d got %d", uint160Size, len(b)) + } + for i := 0; i < uint160Size; i++ { + u[i] = b[i] + } + return +} + +// Uint160FromScript returns a Uint160 type from a raw script. +func Uint160FromScript(script []byte) (u Uint160, err error) { + sha := sha256.New() + sha.Write(script) + b := sha.Sum(nil) + ripemd := ripemd160.New() + ripemd.Write(b) + b = ripemd.Sum(nil) + return Uint160DecodeBytes(b) +} + +// Bytes returns the byte slice representation of u. +func (u Uint160) Bytes() []byte { + b := make([]byte, uint160Size) + for i := 0; i < uint160Size; i++ { + b[i] = byte(u[i]) + } + return b +} + +// BytesReverse return a reversed byte representation of u. +func (u Uint160) BytesReverse() []byte { + return slice.Reverse(u.Bytes()) +} + +// String implements the stringer interface. +func (u Uint160) String() string { + return hex.EncodeToString(u.Bytes()) +} + +// Equals returns true if both Uint256 values are the same. +func (u Uint160) Equals(other Uint160) bool { + for i := 0; i < uint160Size; i++ { + if u[i] != other[i] { + return false + } + } + return true +} + +// MarshalJSON implements the json marshaller interface. +func (u Uint160) MarshalJSON() ([]byte, error) { + return json.Marshal( + fmt.Sprintf("0x%s", hex.EncodeToString(slice.Reverse(u.Bytes()))), + ) +} diff --git a/pkg/wire/util/uint160_test.go b/pkg/wire/util/uint160_test.go new file mode 100644 index 000000000..96416653c --- /dev/null +++ b/pkg/wire/util/uint160_test.go @@ -0,0 +1,50 @@ +package util + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUInt160DecodeString(t *testing.T) { + hexStr := "2d3b96ae1bcc5a585e075e3b81920210dec16302" + val, err := Uint160DecodeString(hexStr) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, hexStr, val.String()) +} + +func TestUint160DecodeBytes(t *testing.T) { + hexStr := "2d3b96ae1bcc5a585e075e3b81920210dec16302" + b, err := hex.DecodeString(hexStr) + if err != nil { + t.Fatal(err) + } + val, err := Uint160DecodeBytes(b) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, hexStr, val.String()) +} + +func TestUInt160Equals(t *testing.T) { + a := "2d3b96ae1bcc5a585e075e3b81920210dec16302" + b := "4d3b96ae1bcc5a585e075e3b81920210dec16302" + + ua, err := Uint160DecodeString(a) + if err != nil { + t.Fatal(err) + } + ub, err := Uint160DecodeString(b) + if err != nil { + t.Fatal(err) + } + if ua.Equals(ub) { + t.Fatalf("%s and %s cannot be equal", ua, ub) + } + if !ua.Equals(ua) { + t.Fatalf("%s and %s must be equal", ua, ua) + } +} diff --git a/pkg/wire/util/uint256.go b/pkg/wire/util/uint256.go new file mode 100644 index 000000000..e1a5a914b --- /dev/null +++ b/pkg/wire/util/uint256.go @@ -0,0 +1,70 @@ +package util + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/CityOfZion/neo-go/pkg/wire/util/slice" +) + +const uint256Size = 32 + +// Uint256 is a 32 byte long unsigned integer. +type Uint256 [uint256Size]uint8 + +// Uint256DecodeString attempts to decode the given string into an Uint256. +func Uint256DecodeString(s string) (u Uint256, err error) { + if len(s) != uint256Size*2 { + return u, fmt.Errorf("expected string size of %d got %d", uint256Size*2, len(s)) + } + b, err := hex.DecodeString(s) + if err != nil { + return u, err + } + return Uint256DecodeBytes(b) +} + +// Uint256DecodeBytes attempts to decode the given string into an Uint256. +func Uint256DecodeBytes(b []byte) (u Uint256, err error) { + if len(b) != uint256Size { + return u, fmt.Errorf("expected []byte of size %d got %d", uint256Size, len(b)) + } + for i := 0; i < uint256Size; i++ { + u[i] = b[i] + } + return u, nil +} + +// Bytes returns a byte slice representation of u. +func (u Uint256) Bytes() []byte { + b := make([]byte, uint256Size) + for i := 0; i < uint256Size; i++ { + b[i] = byte(u[i]) + } + return b +} +func (u Uint256) Reverse() (res Uint256) { + res, _ = Uint256DecodeBytes(u.BytesReverse()) + return +} + +// BytesReverse return a reversed byte representation of u. +func (u Uint256) BytesReverse() []byte { + return slice.Reverse(u.Bytes()) +} + +// Equals returns true if both Uint256 values are the same. +func (u Uint256) Equals(other Uint256) bool { + return u.String() == other.String() +} + +// String implements the stringer interface. +func (u Uint256) String() string { + return hex.EncodeToString(slice.Reverse(u.Bytes())) +} + +// MarshalJSON implements the json marshaller interface. +func (u Uint256) MarshalJSON() ([]byte, error) { + return json.Marshal(fmt.Sprintf("0x%s", u.String())) +} diff --git a/pkg/wire/util/uint256_test.go b/pkg/wire/util/uint256_test.go new file mode 100644 index 000000000..120bee380 --- /dev/null +++ b/pkg/wire/util/uint256_test.go @@ -0,0 +1,50 @@ +package util + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUint256DecodeString(t *testing.T) { + hexStr := "f037308fa0ab18155bccfc08485468c112409ea5064595699e98c545f245f32d" + val, err := Uint256DecodeString(hexStr) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, hexStr, val.Reverse().String()) +} + +func TestUint256DecodeBytes(t *testing.T) { + hexStr := "f037308fa0ab18155bccfc08485468c112409ea5064595699e98c545f245f32d" + b, err := hex.DecodeString(hexStr) + if err != nil { + t.Fatal(err) + } + val, err := Uint256DecodeBytes(b) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, hexStr, val.Reverse().String()) +} + +func TestUInt256Equals(t *testing.T) { + a := "f037308fa0ab18155bccfc08485468c112409ea5064595699e98c545f245f32d" + b := "e287c5b29a1b66092be6803c59c765308ac20287e1b4977fd399da5fc8f66ab5" + + ua, err := Uint256DecodeString(a) + if err != nil { + t.Fatal(err) + } + ub, err := Uint256DecodeString(b) + if err != nil { + t.Fatal(err) + } + if ua.Equals(ub) { + t.Fatalf("%s and %s cannot be equal", ua, ub) + } + if !ua.Equals(ua) { + t.Fatalf("%s and %s must be equal", ua, ua) + } +} diff --git a/pkg/wire/util/util.go b/pkg/wire/util/util.go new file mode 100644 index 000000000..65c485617 --- /dev/null +++ b/pkg/wire/util/util.go @@ -0,0 +1,47 @@ +package util + +import ( + "bytes" + "crypto/sha256" + + "io" + "io/ioutil" +) + +// Functions +func BufferLength(buf *bytes.Buffer) uint32 { + + return uint32(buf.Len()) +} + +func SumSHA256(b []byte) []byte { + h := sha256.New() + h.Write(b) + return h.Sum(nil) +} + +func CalculateHash(f func(bw *BinWriter)) (Uint256, error) { + buf := new(bytes.Buffer) + bw := &BinWriter{W: buf} + + f(bw) + + var hash Uint256 + hash = sha256.Sum256(buf.Bytes()) + hash = sha256.Sum256(hash.Bytes()) + return hash, bw.Err + +} + +func ReaderToBuffer(r io.Reader) (buf *bytes.Buffer, err error) { + byt, err := ioutil.ReadAll(r) + + if err != nil { + + return + } + + buf = bytes.NewBuffer(byt) + + return +} diff --git a/server.go b/server.go new file mode 100644 index 000000000..ed87daef7 --- /dev/null +++ b/server.go @@ -0,0 +1,178 @@ +package main + +import ( + "errors" + "fmt" + "net" + + "github.com/CityOfZion/neo-go/pkg/blockchain" + "github.com/CityOfZion/neo-go/pkg/database" + "github.com/CityOfZion/neo-go/pkg/syncmanager" + + "github.com/CityOfZion/neo-go/pkg/connmgr" + "github.com/CityOfZion/neo-go/pkg/peer" + "github.com/CityOfZion/neo-go/pkg/wire/payload" + "github.com/CityOfZion/neo-go/pkg/wire/protocol" + "github.com/CityOfZion/neo-go/pkg/wire/util" + "github.com/CityOfZion/neo-go/pkg/wire/util/io" +) + +// this file will act as a stub server +// Will create a server package + +type Server struct { + chain *blockchain.Chain + db *database.LDB // TODO(Kev) change to database.Database + sm *syncmanager.Syncmanager + cm *connmgr.Connmgr + + peercfg peer.LocalConfig + + latestHash util.Uint256 +} + +func (s *Server) setupConnMgr() error { + // Connection Manager - Integrate + s.cm = connmgr.New(connmgr.Config{ + GetAddress: nil, + OnConnection: s.OnConn, + OnAccept: nil, + Port: "10333", + }) + + return nil +} +func (s *Server) setupDatabase() error { + // Database -- Integrate + s.db = database.New("test") + return nil +} +func (s *Server) setupChain() error { + // Blockchain - Integrate + s.chain = blockchain.New(s.db, protocol.MainNet) + + if s.chain != nil { + table := database.NewTable(s.db, database.HEADER) + resa, err := table.Get(database.LATESTHEADER) + s.latestHash, err = util.Uint256DecodeBytes(resa) + if err != nil { + return errors.New("Failed to get LastHeader " + err.Error()) + } + } else { + return errors.New("Failed to add genesis block") + } + return nil +} +func (s *Server) setupSyncManager() error { + // Sync Manager - Integrate + s.sm = syncmanager.New(syncmanager.Config{ + Chain: s.chain, + BestHash: s.latestHash, + }) + return nil +} +func (s *Server) setupPeerConfig() error { + // Peer config struct - Integrate + s.peercfg = peer.LocalConfig{ + Net: protocol.MainNet, + UserAgent: "DIG", + Services: protocol.NodePeerService, + Nonce: 1200, + ProtocolVer: 0, + Relay: false, + Port: 10332, + StartHeight: LocalHeight, + OnHeader: s.sm.OnHeaders, + OnBlock: s.sm.OnBlock, + } + return nil +} + +func (s *Server) Run() error { + + // Add all other run based methods for modules + + // Connmgr - Run + s.cm.Run() + // Initial hardcoded nodes to connect to + err := s.cm.Connect(&connmgr.Request{ + Addr: "seed1.ngd.network:10333", + }) + return err +} + +func main() { + + setup() +} + +func setup() { + + server := Server{} + fmt.Println(server.sm) + + err := server.setupConnMgr() + err = server.setupDatabase() + err = server.setupChain() + err = server.setupSyncManager() + err = server.setupPeerConfig() + + fmt.Println(server.sm) + + err = server.Run() + if err != nil { + fmt.Println(err) + } + + <-make(chan struct{}) + +} + +func OnHeader(peer *peer.Peer, msg *payload.HeadersMessage) { + + for _, header := range msg.Headers { + if err := fileutils.UpdateFile("headers.txt", []byte(header.Hash.String())); err != nil { + fmt.Println("Error writing headers to file") + break + } + } + if len(msg.Headers) == 2000 { // reached tip + lastHeader := msg.Headers[len(msg.Headers)-1] + + fmt.Println("Latest hash is", lastHeader.Hash.String()) + fmt.Println("Latest Header height is", lastHeader.Index) + + err := peer.RequestHeaders(lastHeader.Hash.Reverse()) + if err != nil { + fmt.Println("Error getting more headers", err) + } + } +} + +func LocalHeight() uint32 { + return 10 +} + +// OnConn is called when a successful connection has been made +func (s *Server) OnConn(conn net.Conn, addr string) { + fmt.Println(conn.RemoteAddr().String()) + fmt.Println(addr) + + p := peer.NewPeer(conn, false, s.peercfg) + err := p.Run() + + if err != nil { + fmt.Println("Error running peer" + err.Error()) + } + + if err == nil { + s.sm.AddPeer(p) + } + + // This is here just to quickly test the system + err = p.RequestHeaders(s.latestHash) + fmt.Println("For tests, we are only fetching first 2k batch") + if err != nil { + fmt.Println(err.Error()) + } +}