diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8981c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +go.sum +wallet_* +simpleBlockchain_*.db +blockchain/ \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..664d83f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,22 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "build", + "command": "go build cmd/cli/main.go && mkdir -p build && mv main build/main", + "problemMatcher": [ + "$go" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "close": true + } + } + ] +} \ No newline at end of file diff --git a/Readme.md b/Readme.md index 9e6ff90..d80dadb 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,4 @@ -Simple Blockchain ------- +# Simple Blockchain This repository contains the golang code of a simple blockchain implementation. @@ -7,16 +6,90 @@ This blockchain consists of three parts: - A simple wallet that you can get address, scan utxos, sign transaction. - A simple blockchain can sync block from other known nodes, mining new block, send transaction and broadcast to other node. - A simple restful server you can query blocks and utxos from blockchain. +- This project only supports two nodes. There are many part are not like real blockchain because it's just simple implementation, still insecure and incomplete. you can learn the basic operation of the blockchain through this project. +## changes from zweieuro (author): +- The peers were saved in an array, but there are only ever 2 + - reduced to a single string, making management easier + - removed file that wrote the peers down, no reason to have this in this case, looks like it has no other function, removed dead code +- Make the peers' and owns' address with respect to port or address:port arguments + - Same functionality as before when only the port is specified + + +### Docker compose users: + +I re-implemented some of the addressing (really it was mostly arg parsing) so that the node and its peer +can have independent base addresses. This means they are now docker container compatible. + +The folder `docker_compose` shows how this might be used in your `docker-compose.yml` file. +The `docker-compose.yml` expects `existingWallets/` to have two files: +- `alice_wallet` +- `bob_wallet` + +WARNING: If the wallet files are missing docker will attempt to bind directories instead! This will also crash. Additionally docker might make them write-protected. Remove them at your own discretion. + + +Of course you can rename the files as you want if you change their occurrences. + +These wallets are copied to the individual container before they start up, they will instantly crash without them. + + +When binding, in order to retain DB information, both DB files are linked back into a folder for each node. This is mostly due to docker volume limitations as dual binding is not really a good idea (binding the same folder of the host to two different sub-containers), especially since we _want_ separate files. + + + +#### Docker compose address resolution +Containers that run in docker and are declared inside a `docker-compose.yml` have a built-in hostname resolution that can make them find other containers in the same service file. They are automatically available to any container. This is what this example files uses to route data between the two node instances. + + +## Running commands without compiling your own version: +Since you already have a node running inside docker containers, it seems a bit backwards to then compile again just to talk to it. +You can either: Start a shell inside your container and run it from there, which i find quite cumbersome. + +alternatively: You can run the commands raw. The commands are specified in `servercmd.go` and their respective endpoint can be found in `conn.go`. +This has the nice effect of letting us craft our own requests. +Here we also see the main security problem, the server is very unprotected, these requests have no kind of confirmation. + +One raw curl request might look like this: +```shell +curl -i -X POST \ + -H "Content-Type:application/json" \ + -d \ + '{ + "To": "1LHroFft5WAxZTqXizQJjBJrPwkVQFAcsa", + "Amount": 200 + }' \ + 'http://localhost:7080/wallet/send' + +``` + +Which read quite easily. Transfer 200 coins to the given public key. + +Mining is now a simple get request: +```shell +curl -i -X GET \ + 'http://localhost:7080/chain/mining' + ``` + + + +# How to run -How to run ------- ## Build +Install deps: + + +```shell script +go mod tidy +``` + +build it: + ```shell script go build ./cmd/cli ``` @@ -28,11 +101,11 @@ go build ./cmd/cli ./cli wallet create -walletname "bob" ``` -### Start two node +### Start two nodes/start server commands ```shell script -./cli server start -nodeport 3000 -apiport 8080 -walletname "alice" -ismining=true -./cli server start -nodeport 3001 -apiport 8081 -walletname "bob" -ismining=true +./cli server start -nodeaddress 3000 -peernodeaddress 3001 -apiport 8080 -walletname "alice" -ismining=true +./cli server start -nodeaddress 3001 -peernodeaddress 3000 -apiport 8081 -walletname "bob" -ismining=true ``` ### Mining empty block to get block reward @@ -45,7 +118,7 @@ go build ./cmd/cli ./cli server sendtransaction --apiport 8080 --to "172wJyiJZxXWyBW7CYSVddsR5e7ZMxtja9" -amount 100000 ``` -threr are still have other blockchain command, you can find out by type `./cli server`. +There are still have other blockchain command, you can find out by type `./cli server`. Example @@ -57,12 +130,6 @@ Example ./cli wallet create -walletname "alice" ``` -### Start blockchain server - - ```shell script - ./cli server start -nodeport 3000 -apiport 8080 -walletname "alice" -ismining=true - ``` - ### Get blocks ```shell script diff --git a/blockchain.go b/blockchain.go index d0e2dc2..960efec 100644 --- a/blockchain.go +++ b/blockchain.go @@ -6,10 +6,12 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/boltdb/bolt" "log" "math/big" + "os" "sync" + + "github.com/boltdb/bolt" ) var genesisBlock = &Block{ @@ -70,9 +72,9 @@ var genesisBlock = &Block{ }, } +var dbFolderName = "blockchain" - -var dbSigName = "simpleBlockchain_%d.db" +var dbSigName = dbFolderName + "/simpleBlockchain_%d.db" type BlockChain struct { db *bolt.DB @@ -128,6 +130,17 @@ func NewBlockChain(address string, port int, isMining bool) *BlockChain { func CreateBlockChain(address string, port int, isMining bool) *BlockChain { dbName := fmt.Sprintf(dbSigName, port) + + // create the folder if it does not exist + stat, err := os.Stat(dbFolderName) + + if err != nil || stat == nil || stat.IsDir() == false { + err := os.Mkdir(dbFolderName, 0755) + if err != nil { + panic(err) + } + } + db, err := bolt.Open(dbName, 0600, nil) if err != nil { panic(err) diff --git a/cmd/flags.go b/cmd/flags.go index ce899b6..6882180 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -5,10 +5,15 @@ import ( ) var ( - nodeportFlag = &cli.IntFlag{ - Name: "nodeport", - Usage: "nodeport", - Value: 3000, + peernodeaddressFlag = &cli.StringFlag{ + Name: "peernodeaddress", + Usage: "partner node address, either addr:port or just port", + Required: true, + } + nodeaddressFlag = &cli.StringFlag{ + Name: "nodeaddress", + Usage: "what address is this node reachable under, addr:port or port (localhost)", + Required: true, } apiportFlag = &cli.IntFlag{ Name: "apiport", @@ -31,7 +36,7 @@ var ( isminingFlag = &cli.BoolFlag{ Name: "ismining", Usage: "ismining", - Required: true, + Value: false, } toFlag = &cli.StringFlag{ Name: "to", diff --git a/cmd/servercmd.go b/cmd/servercmd.go index 109afe1..54b04c1 100644 --- a/cmd/servercmd.go +++ b/cmd/servercmd.go @@ -2,9 +2,11 @@ package cmd import ( "fmt" + "os" + "strconv" + "github.com/tn606024/simpleBlockchain" "github.com/urfave/cli/v2" - "os" ) var ( @@ -12,19 +14,32 @@ var ( Name: "start", Usage: "start blockchain server", Description: "start blockchain server", - ArgsUsage: "", + ArgsUsage: " ", Flags: []cli.Flag{ - nodeportFlag, + nodeaddressFlag, + peernodeaddressFlag, apiportFlag, walletnameFlag, isminingFlag, }, Action: func(c *cli.Context) error { - nodeport := c.Int("nodeport") + // check if peerNodeAddressFlag is just a number (port) + // if so, prepend "localhost:" to it + peerNodeAddress := c.String("peernodeaddress") + if _, err := strconv.Atoi(peerNodeAddress); err == nil { + peerNodeAddress = "localhost:" + peerNodeAddress + } + + nodeaddress := c.String("nodeaddress") + if _, err := strconv.Atoi(nodeaddress); err == nil { + nodeaddress = "localhost:" + nodeaddress + } + + apiport := c.Int("apiport") walletname := c.String("walletname") ismining := c.Bool("ismining") - server := simpleBlockchain.NewServer(nodeport, apiport, walletname, ismining) + server := simpleBlockchain.NewServer(nodeaddress, peerNodeAddress, apiport, walletname, ismining) server.StartServer() return nil }, diff --git a/docker_compose/.gitignore b/docker_compose/.gitignore new file mode 100644 index 0000000..0af9ead --- /dev/null +++ b/docker_compose/.gitignore @@ -0,0 +1 @@ +blockchain_*/ \ No newline at end of file diff --git a/docker_compose/Dockerfile b/docker_compose/Dockerfile new file mode 100644 index 0000000..2d95426 --- /dev/null +++ b/docker_compose/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.23 + +WORKDIR /app + +RUN git clone https://github.com/ZweiEuro/simpleBlockchain + + +WORKDIR /app/simpleBlockchain +# download dependencies + +RUN go mod tidy + +RUN go build ./cmd/cli diff --git a/docker_compose/docker-compose.yml b/docker_compose/docker-compose.yml new file mode 100644 index 0000000..466a7dd --- /dev/null +++ b/docker_compose/docker-compose.yml @@ -0,0 +1,29 @@ +services: + blockchain_alice: + build: + context: ./ + dockerfile: ./Dockerfile + container_name: blockchain_alice + expose: + - "3000" # nodeaddress, only needed between nodes + ports: + - "7080:8080" # apiport, accessed from your command line + volumes: + - ./blockchain_alice/:/app/simpleBlockchain/blockchain/ + - ./existingWallets/wallet_alice:/app/simpleBlockchain/wallet_alice + command: ./cli server start -nodeaddress blockchain_alice:3000 -peernodeaddress blockchain_bob:3000 -walletname alice -ismining=true + blockchain_bob: + build: + context: ./ + dockerfile: ./Dockerfile + container_name: blockchain_bob + expose: + - "3000" # nodeaddress, only needed between nodes + ports: + - "7081:8080" # apiport, accessed from your command line + volumes: + - ./blockchain_bob/:/app/simpleBlockchain/blockchain/ + - ./existingWallets/wallet_bob:/app/simpleBlockchain/wallet_bob + command: ./cli server start -nodeaddress blockchain_bob:3000 -peernodeaddress blockchain_alice:3000 -walletname bob + + diff --git a/docker_compose/existingWallets/.gitkeep b/docker_compose/existingWallets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server.go b/server.go index fcde001..54d06bf 100644 --- a/server.go +++ b/server.go @@ -1,7 +1,6 @@ package simpleBlockchain import ( - "github.com/gin-gonic/gin" "bytes" "encoding/json" "fmt" @@ -9,7 +8,11 @@ import ( "io/ioutil" "net" "net/http" + "strconv" + "strings" "sync" + + "github.com/gin-gonic/gin" ) const ( @@ -17,13 +20,10 @@ const ( protocal = "tcp" ) -var KnownNodes = []string{ - "localhost:3000", - "localhost:3001", -} - var knownNodeName = "knownnodes_%d.txt" +var knnownAddrFileName = "knownAddresses.txt" + type MessageHeader string const ( @@ -143,9 +143,9 @@ type Server struct { node string wallet *Wallet utxos []*UTXO - nodeport int apiport int - knownNodes []string + nodeaddress string + peerNodeAddress string connectMap map[string]bool blockMap map[string]int blockchain *BlockChain @@ -155,82 +155,52 @@ type Server struct { -func NewServer(nodeport int, apiport int, walletName string, isMining bool) *Server{ - node := fmt.Sprintf("localhost:%d",nodeport) +func NewServer(nodeaddress string, peernodeaddress string, apiport int, walletName string, isMining bool) *Server{ wallet, err := GetExistWallet(walletName) - addrs, _:=wallet.getAddresses() - blockchain := NewBlockChain(addrs[0],nodeport, isMining) if err != nil { panic(err) } - knownNodes, err := NewKnownNodes(nodeport) - if err != nil { - panic(err) + + addrs, _ := wallet.getAddresses() + + parts := strings.Split(nodeaddress, ":"); + portNumber := 0 + if len(parts) == 1 { + // convert the port to a number + portNumber, err = strconv.Atoi(parts[0]) + if err != nil { + panic(err) + } + + }else{ + portNumber, err = strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } } + + + blockchain := NewBlockChain(addrs[0], portNumber, isMining) + connectMap := make(map[string]bool,0) blockMap := make(map[string]int,0) utxos := make([]*UTXO,0) s:= &Server{ - node: node, + node: nodeaddress, wallet: wallet, utxos: utxos, - nodeport: nodeport, + nodeaddress: nodeaddress, apiport: apiport, - knownNodes: knownNodes, + peerNodeAddress: peernodeaddress, blockchain: blockchain, connectMap: connectMap, blockMap: blockMap, } + s.ScanWalletUTXOs() return s } -func NewKnownNodes(port int) ([]string, error){ - var knownNodes []string - nodefile := fmt.Sprintf(knownNodeName,port) - if IsFileExists(nodefile) == false { - bkn, _ := json.Marshal(KnownNodes) - err := ioutil.WriteFile(nodefile, bkn, 0644) - if err != nil{ - return nil, err - } - } - b, err := ioutil.ReadFile(fmt.Sprintf(knownNodeName,port)) - if err != nil { - return nil, err - } - err = json.Unmarshal(b, &knownNodes) - if err != nil { - return nil, err - } - return knownNodes, nil -} - -func (s *Server) AddKnownNode(knownNode string) error{ - s.mutex.Lock() - s.knownNodes = append(s.knownNodes, knownNode) - bkn, _ := json.Marshal(s.knownNodes) - s.mutex.Unlock() - nodefile := fmt.Sprintf(knownNodeName, s.nodeport) - err := ioutil.WriteFile(nodefile, bkn, 0644) - if err != nil { - return err - } - return nil -} - -func (s *Server) SearchKnownNode(knownNode string) bool { - s.mutex.Lock() - for _, node := range s.knownNodes{ - if node == knownNode{ - s.mutex.Unlock() - return true - } - } - s.mutex.Unlock() - return false -} - func (s *Server) StartServer() { ln, err := net.Listen(protocal, s.node) if err != nil { @@ -436,28 +406,16 @@ func (s *Server) MiningEmptyBlockAndBroadcast() (*Block,error) { return blk, nil } func (s *Server) broadcastVersion(){ - for _, knownNode := range s.knownNodes { - if knownNode != s.node { - s.sendVersion(knownNode) - } - } + s.sendVersion(s.peerNodeAddress) } func (s *Server) broadcastBlock(block *Block){ - for _, knownNode := range s.knownNodes{ - if knownNode != s.node { - s.sendBlock(knownNode, []*Block{block}) - } - } + s.sendBlock(s.peerNodeAddress, []*Block{block}) } func (s *Server) broadcastTx(tx *Transaction){ - for _, knownNode := range s.knownNodes{ - if knownNode != s.node { - s.sendTx(knownNode, tx) - } - } + s.sendTx(s.peerNodeAddress, tx) } @@ -498,9 +456,6 @@ func (s *Server) handleVersion(payload json.RawMessage){ } logHandleMsg(VersionMsgHeader, &versionMsg) if versionMsg.Version == blockchainVersion { - if !s.SearchKnownNode(versionMsg.AddrFrom) { - s.AddKnownNode(versionMsg.AddrFrom) - } s.mutex.Lock() s.blockMap[versionMsg.AddrFrom] = versionMsg.StartHeight conn, ok := s.connectMap[versionMsg.AddrFrom] @@ -712,12 +667,7 @@ func (s *Server) handleTx(payload json.RawMessage) { Type: "block", Hash: []Hashes{blk.newHash()}, } - for _, knownNode := range s.knownNodes { - if knownNode != s.node { - s.sendInv(knownNode, &invMsg) - } - } - + s.sendInv(s.peerNodeAddress, &invMsg) } func (s *Server) sendVersion(addr string){ @@ -844,7 +794,7 @@ func (s *Server) send(addr string, data []byte) error{ delete(s.blockMap, addr) delete(s.connectMap, addr) //s.deleteKnownNodes(addr) - return fmt.Errorf("%s is not online \n", addr) + return fmt.Errorf("%s is not online ", addr) } defer conn.Close() _, err = io.Copy(conn, bytes.NewReader(data))