├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── blocace-full.png └── blocace-logo.png ├── blockchain ├── account.go ├── block.go ├── blockchain.go ├── merkle_tree.go ├── merkle_tree_test.go ├── search.go ├── transaction.go └── utils.go ├── docs ├── .nojekyll ├── README.md ├── coverpage.md └── index.html ├── go.mod ├── go.sum ├── main.go ├── p2p ├── accounts_p2p.go ├── block_p2p.go ├── blockchain_forest.go ├── challenge_word_p2p.go ├── mappings_p2p.go ├── p2p.go └── request_p2p.go ├── pool ├── queue.go ├── queue_test.go └── receiver.go ├── start_cluster.sh └── webapi ├── auth.go └── http.go /.gitignore: -------------------------------------------------------------------------------- 1 | blocace 2 | /data*/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | - master 6 | 7 | gobuild_args: -ldflags "-s -w -X main.version=0.0.6" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | drawing 2 | 3 |
4 | 5 | [![GoDoc](https://godoc.org/github.com/codingpeasant/blocace?status.svg)](https://godoc.org/github.com/codingpeasant/blocace) [![Go Report Card](https://goreportcard.com/badge/github.com/codingpeasant/blocace)](https://goreportcard.com/report/github.com/codingpeasant/blocace) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | 7 | __Blocace__ is a distributed document database powered by the blockchain technology. 8 | 9 | ## Note to Developers 10 | * This is a prototype. 11 | * The APIs are constantly evolving and designed to demonstrate types of functionality. Expect substantial changes before the release. 12 | 13 | ## Install 14 | 15 | ### Compile on Linux/macOS/Windows 16 | > Prerequisite: Go version: 1.12 or later; GCC 5.1 or later. 17 | > 18 | > Windows may need to install [GCC](http://tdm-gcc.tdragon.net/download) if missing before installing the dependencies. Linux may also need to install gcc using the corresponding package management tool, like `yum install gcc` on RedHat or alike. macOS may need to install [Xcode Command Line Tools](https://www.ics.uci.edu/~pattis/common/handouts/macmingweclipse/allexperimental/macxcodecommandlinetools.html). 19 | 20 | Build and run with Go Modules 21 | ```bash 22 | git clone https://github.com/codingpeasant/blocace.git 23 | cd blocace 24 | export GO111MODULE=on # Go 1.12 and earlier 25 | go get 26 | go build -ldflags="-s -w -X main.version=0.0.6" 27 | ./blocace server 28 | ``` 29 | 30 | ### Download for Linux/macOS/Windows 31 | If you'd like to try Blocace directly, please navigate to [Releases](https://github.com/codingpeasant/blocace/releases) and get the right binary for your OS/ARCH. 32 | 33 | ## Docs 34 | Checkout [Blocace JS Client Example](https://github.com/codingpeasant/blocace-js/blob/master/example.js) and [Blocace JS Client APIs Reference](https://github.com/codingpeasant/blocace-js/blob/master/README.md) 35 | 36 | ## License 37 | Blocace is licensed as [Apache 2.0](https://github.com/codingpeasant/blocace/blob/master/LICENSE). 38 | -------------------------------------------------------------------------------- /assets/blocace-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingpeasant/blocace/504a4871c832ea5a2cdd3ac090137f018f98d013/assets/blocace-full.png -------------------------------------------------------------------------------- /assets/blocace-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingpeasant/blocace/504a4871c832ea5a2cdd3ac090137f018f98d013/assets/blocace-logo.png -------------------------------------------------------------------------------- /blockchain/account.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Account represents an end user's information including the public key. Note that these account fields are just a placeholder for convenience to track identity, which doesn't affect the usage of Blocace. 11 | // TODO: support ldap or other auth protocols 12 | type Account struct { 13 | DateOfBirth string `json:"dateOfBirth" validate:"len=10"` 14 | FirstName string `json:"firstName" validate:"nonzero"` 15 | LastName string `json:"lastName" validate:"nonzero"` 16 | Organization string `json:"organization" validate:"nonzero"` 17 | Position string `json:"position" validate:"nonzero"` 18 | Email string `json:"email" validate:"min=6,max=80"` 19 | Phone string `json:"phone" validate:"min=6,max=40"` 20 | Address string `json:"address" validate:"min=10,max=140"` 21 | PublicKey string `json:"publicKey" validate:"len=128"` 22 | Role `json:"role"` 23 | LastModified int64 `json:"lastModified"` 24 | } 25 | 26 | // Role represents the rights of access to collections and API endpoints 27 | type Role struct { 28 | Name string `json:"name"` 29 | CollectionsWrite []string `json:"collectionsWrite"` 30 | CollectionsReadOverride []string `json:"collectionsReadOverride"` 31 | } 32 | 33 | // Serialize serializes the account 34 | func (a Account) Marshal() []byte { 35 | var result bytes.Buffer 36 | encoder := gob.NewEncoder(&result) 37 | err := encoder.Encode(a) 38 | if err != nil { 39 | log.Error(err) 40 | } 41 | 42 | return result.Bytes() 43 | } 44 | 45 | // UnmarshalAccount deserializes an account for p2p 46 | func UnmarshalAccount(a []byte) (Account, error) { 47 | var account Account 48 | 49 | decoder := gob.NewDecoder(bytes.NewReader(a)) 50 | err := decoder.Decode(&account) 51 | if err != nil { 52 | log.Error(err) 53 | } 54 | 55 | return account, err 56 | } 57 | 58 | // DeserializeAccount deserializes an account 59 | func DeserializeAccount(a []byte) *Account { 60 | var account Account 61 | 62 | decoder := gob.NewDecoder(bytes.NewReader(a)) 63 | err := decoder.Decode(&account) 64 | if err != nil { 65 | log.Error(err) 66 | } 67 | 68 | return &account 69 | } 70 | 71 | // ToMap converts an Account struct to map 72 | func (a Account) ToMap(isAdmin bool) map[string]interface{} { 73 | accountMap := make(map[string]interface{}) 74 | 75 | accountMap["dateOfBirth"] = a.DateOfBirth 76 | accountMap["firstName"] = a.FirstName 77 | accountMap["lastName"] = a.LastName 78 | accountMap["organization"] = a.Organization 79 | accountMap["position"] = a.Position 80 | accountMap["email"] = a.Email 81 | accountMap["phone"] = a.Phone 82 | accountMap["address"] = a.Address 83 | accountMap["publicKey"] = a.PublicKey 84 | 85 | if isAdmin { 86 | accountMap["roleName"] = a.Role.Name 87 | accountMap["collectionsReadOverride"] = a.Role.CollectionsReadOverride 88 | accountMap["collectionsWrite"] = a.Role.CollectionsWrite 89 | } 90 | 91 | return accountMap 92 | } 93 | -------------------------------------------------------------------------------- /blockchain/block.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/gob" 7 | "fmt" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/boltdb/bolt" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // Block keeps block headers 16 | type Block struct { 17 | Timestamp int64 18 | PrevBlockHash []byte 19 | Height uint64 20 | Hash []byte 21 | TotalTransactions int 22 | Transactions []*Transaction 23 | } 24 | 25 | // Serialize serializes the block 26 | func (b *Block) serialize() []byte { 27 | var result bytes.Buffer 28 | encoder := gob.NewEncoder(&result) 29 | transactions := b.Transactions 30 | b.Transactions = nil // don't encode transactions 31 | err := encoder.Encode(b) 32 | if err != nil { 33 | log.Error(err) 34 | } 35 | 36 | b.Transactions = transactions 37 | 38 | return result.Bytes() 39 | } 40 | 41 | // SetHash set the hash of the whole block 42 | func (b *Block) SetHash() []byte { 43 | blockHash := sha256.Sum256(bytes.Join( 44 | [][]byte{ 45 | b.PrevBlockHash, 46 | b.GetMerkleTree().RootNode.Data, 47 | IntToHex(b.Timestamp), 48 | }, 49 | []byte{}, 50 | )) 51 | 52 | return blockHash[:] 53 | } 54 | 55 | // GetMerkleTree builds a merkle tree of all the transactions in the block 56 | func (b *Block) GetMerkleTree() *MerkleTree { 57 | var txHashes [][]byte 58 | 59 | for _, tx := range b.Transactions { 60 | txHashes = append(txHashes, tx.ID) 61 | } 62 | return NewMerkleTree(txHashes) 63 | } 64 | 65 | // Persist stores the block with the transactions to DB 66 | func (b Block) Persist(db *bolt.DB, isTip bool) ([]byte, error) { 67 | var currentTxTotal []byte 68 | var currentTxTotalInt int64 69 | 70 | err := db.View(func(dbtx *bolt.Tx) error { 71 | bBucket := dbtx.Bucket([]byte(BlocksBucket)) 72 | currentTxTotal = bBucket.Get([]byte("t")) 73 | 74 | return nil 75 | }) 76 | 77 | if err != nil { 78 | log.WithFields(log.Fields{ 79 | "method": "Persist()", 80 | }).Panic(err) 81 | } 82 | 83 | if currentTxTotal != nil { 84 | currentTxTotalInt, err = strconv.ParseInt(string(currentTxTotal), 10, 64) 85 | } else { 86 | currentTxTotalInt = 0 87 | } 88 | 89 | encodedBlock := b.serialize() 90 | 91 | // A DB transaction to guarantee the block and [transaction] is an atom operation 92 | err = db.Update(func(dbtx *bolt.Tx) error { 93 | bBucket := dbtx.Bucket([]byte(BlocksBucket)) 94 | txBucket := dbtx.Bucket([]byte(TransactionsBucket)) 95 | 96 | err := bBucket.Put(b.Hash, encodedBlock) 97 | 98 | if err != nil { 99 | log.Panic(err) 100 | } 101 | 102 | for _, tx := range b.Transactions { 103 | // key format: blockHash_transactionId 104 | err := txBucket.Put(append(append(b.Hash, []byte("_")...), tx.ID...), tx.Serialize()) 105 | if err != nil { 106 | log.Panic(err) 107 | } 108 | } 109 | 110 | if isTip { // only update tip and height if this is a tip block (local or peer) 111 | err = bBucket.Put([]byte("l"), b.Hash) 112 | err = bBucket.Put([]byte("b"), []byte(fmt.Sprint(b.Height))) 113 | } 114 | 115 | err = bBucket.Put([]byte("t"), []byte(fmt.Sprint(int64(b.TotalTransactions)+currentTxTotalInt))) 116 | 117 | if err != nil { 118 | log.WithFields(log.Fields{ 119 | "method": "Persist()", 120 | }).Error(err) 121 | } 122 | 123 | return nil 124 | }) 125 | 126 | if err != nil { 127 | log.WithFields(log.Fields{ 128 | "method": "Persist()", 129 | }).Error(err) 130 | } 131 | 132 | return b.Hash, nil 133 | } 134 | 135 | // DeserializeBlock deserializes a block from persistence 136 | func DeserializeBlock(d []byte) *Block { 137 | var block Block 138 | 139 | decoder := gob.NewDecoder(bytes.NewReader(d)) 140 | err := decoder.Decode(&block) 141 | if err != nil { 142 | log.WithFields(log.Fields{ 143 | "method": "DeserializeBlock()", 144 | }).Error(err) 145 | } 146 | 147 | return &block 148 | } 149 | 150 | // NewBlock creates and returns Block 151 | func NewBlock(transactions []*Transaction, prevBlockHash []byte, height uint64) *Block { 152 | block := &Block{time.Now().Unix(), prevBlockHash, height, []byte{}, len(transactions), transactions} 153 | block.Hash = block.SetHash() 154 | for _, tx := range transactions { 155 | tx.BlockHash = block.Hash 156 | } 157 | 158 | return block 159 | } 160 | 161 | // NewGenesisBlock creates and returns genesis block 162 | func NewGenesisBlock(coinbase *Transaction, db *bolt.DB) *Block { 163 | err := db.Update(func(tx *bolt.Tx) error { 164 | _, err := tx.CreateBucket([]byte(TransactionsBucket)) 165 | 166 | if err != nil { 167 | return fmt.Errorf("create bucket: %s", err) 168 | } 169 | 170 | return nil 171 | }) 172 | 173 | if err != nil { 174 | log.WithFields(log.Fields{ 175 | "method": "NewGenesisBlock()", 176 | }).Error(err) 177 | } 178 | // height starts from 0 179 | return NewBlock([]*Transaction{coinbase}, []byte{}, 0) 180 | } 181 | -------------------------------------------------------------------------------- /blockchain/blockchain.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/boltdb/bolt" 10 | "github.com/perlin-network/noise" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Blockchain keeps a sequence of Blocks. Blockchain DB keys: lastHash - l; lastHeight - b; totalTransactions - t; p2pPrivKey; peerId 15 | type Blockchain struct { 16 | Tip []byte 17 | PeerId []byte 18 | Db *bolt.DB 19 | Search *Search 20 | DataDir string 21 | } 22 | 23 | // RegisterAccount persists the account to the storage 24 | func (bc *Blockchain) RegisterAccount(address []byte, account Account) error { 25 | result := account.Marshal() 26 | 27 | err := bc.Db.Update(func(dbtx *bolt.Tx) error { 28 | aBucket, _ := dbtx.CreateBucketIfNotExists([]byte(AccountsBucket)) 29 | err := aBucket.Put(address, result) 30 | if err != nil { 31 | log.Error(err) 32 | } 33 | 34 | return nil 35 | }) 36 | 37 | return err 38 | } 39 | 40 | // AddBlock saves provided data as a block in the blockchain 41 | func (bc *Blockchain) AddBlock(txs []*Transaction) []byte { 42 | var lastHash []byte 43 | var lastHeight []byte 44 | 45 | err := bc.Db.View(func(dbtx *bolt.Tx) error { 46 | bBucket := dbtx.Bucket([]byte(BlocksBucket)) 47 | lastHash = bBucket.Get([]byte("l")) 48 | lastHeight = bBucket.Get([]byte("b")) 49 | 50 | return nil 51 | }) 52 | 53 | lastHeightInt, err := strconv.ParseInt(string(lastHeight), 10, 64) 54 | 55 | newBlock := NewBlock(txs, lastHash, uint64(lastHeightInt+1)) 56 | bc.Tip, err = newBlock.Persist(bc.Db, true) 57 | 58 | if err != nil { 59 | log.Error(err) 60 | } 61 | 62 | start := time.Now().UnixNano() 63 | log.Debug("start indexing the block:" + strconv.FormatInt(start, 10)) 64 | bc.Search.IndexBlock(newBlock, bc.PeerId) 65 | end := time.Now().UnixNano() 66 | log.Debug("end indexing the block:" + strconv.FormatInt(end, 10) + ", duration:" + strconv.FormatInt((end-start)/1000000, 10) + "ms") 67 | 68 | return newBlock.Hash 69 | } 70 | 71 | // IsComplete iterate all the blocks of a blockchain to check its completeness 72 | func (bc *Blockchain) IsComplete() bool { 73 | isComplete := false 74 | err := bc.Db.View(func(dbtx *bolt.Tx) error { 75 | bBucket := dbtx.Bucket([]byte(BlocksBucket)) 76 | 77 | height := bBucket.Get([]byte("b")) 78 | heightInt, err := strconv.Atoi(string(height)) 79 | 80 | if err != nil { 81 | log.Errorf("cannot get blockchain height: %s", err) 82 | return err 83 | } 84 | 85 | var currentBlock *Block 86 | i := 0 87 | currentBlockHash := bc.Tip 88 | for ; i <= heightInt; i++ { 89 | currentBlockBytes := bBucket.Get(currentBlockHash) 90 | if currentBlockBytes != nil { 91 | currentBlock = DeserializeBlock(currentBlockBytes) 92 | } else { 93 | log.Errorf("cannot find block: %x", currentBlockHash) 94 | break 95 | } 96 | currentBlockHash = currentBlock.PrevBlockHash 97 | } 98 | 99 | if i == heightInt+1 && bytes.Compare(currentBlockHash, []byte{}) == 0 { 100 | isComplete = true 101 | } else { 102 | log.Errorf("blockchain height and genesis block don't match - lastHeight: %d; lastHash: %x", i, currentBlockHash) 103 | } 104 | 105 | return nil 106 | }) 107 | 108 | if err != nil { 109 | return false 110 | } 111 | 112 | return isComplete 113 | } 114 | 115 | func DbExists(dbFile string) bool { 116 | if _, err := os.Stat(dbFile); os.IsNotExist(err) { 117 | return false 118 | } 119 | 120 | return true 121 | } 122 | 123 | // NewBlockchain creates a new Blockchain with genesis Block (reading existing DB data and initializing a Blockchain struct) 124 | func NewBlockchain(dbFile string, dataDir string) *Blockchain { 125 | if DbExists(dbFile) == false { 126 | log.Fatal("No existing blockchain found. Create one first.") 127 | } 128 | 129 | var tip []byte 130 | db, err := bolt.Open(dbFile, 0600, nil) 131 | if err != nil { 132 | log.Panic(err) 133 | } 134 | 135 | err = db.View(func(dbtx *bolt.Tx) error { 136 | bBucket := dbtx.Bucket([]byte(BlocksBucket)) 137 | tip = bBucket.Get([]byte("l")) 138 | 139 | return nil 140 | }) 141 | 142 | blockchainSearch, err := NewSearch(db, dataDir) 143 | 144 | if err != nil { 145 | log.Panic(err) 146 | } 147 | 148 | var p2pPrivKey noise.PrivateKey 149 | var p2pPrivKeyBytes []byte 150 | // make sure to reuse the priv key 151 | err = db.View(func(dbtx *bolt.Tx) error { 152 | bBucket := dbtx.Bucket([]byte(BlocksBucket)) 153 | p2pPrivKeyBytes = bBucket.Get([]byte(P2PPrivateKeyKey)) 154 | 155 | return nil 156 | }) 157 | 158 | if err != nil { 159 | log.Panic(err) 160 | } 161 | 162 | copy(p2pPrivKey[:], p2pPrivKeyBytes) 163 | var publicKey = p2pPrivKey.Public() 164 | bc := Blockchain{tip, publicKey[:], db, blockchainSearch, dataDir} 165 | 166 | return &bc 167 | } 168 | 169 | // CreateBlockchain creates a new local blockchain DB 170 | func CreateBlockchain(dbFile string, dataDir string) *Blockchain { 171 | if DbExists(dbFile) { 172 | log.Fatal("Blockchain already exists.") 173 | } 174 | 175 | var tip []byte 176 | db, err := bolt.Open(dbFile, 0600, nil) 177 | if err != nil { 178 | log.Panic(err) 179 | } 180 | 181 | // publicKey is peerId 182 | newPublicKey, newPrivateKey, err := noise.GenerateKeys(nil) 183 | if err != nil { 184 | log.Panic(err) 185 | } 186 | 187 | err = db.Update(func(dbtx *bolt.Tx) error { 188 | bBucket, err := dbtx.CreateBucket([]byte(BlocksBucket)) 189 | if err != nil { 190 | log.Panic(err) 191 | } 192 | 193 | err = bBucket.Put([]byte(P2PPrivateKeyKey), newPrivateKey[:]) 194 | if err != nil { 195 | log.Panic(err) 196 | } 197 | return nil 198 | }) 199 | 200 | cbtx := NewCoinbaseTX(newPublicKey[:]) 201 | genesisBlock := NewGenesisBlock(cbtx, db) 202 | 203 | tip, err = genesisBlock.Persist(db, true) 204 | 205 | if err != nil { 206 | log.Panic(err) 207 | } 208 | 209 | blockchainSearch, err := NewSearch(db, dataDir) 210 | 211 | if err != nil { 212 | log.Panic(err) 213 | } 214 | 215 | bc := Blockchain{tip, newPublicKey[:], db, blockchainSearch, dataDir} 216 | 217 | return &bc 218 | } 219 | -------------------------------------------------------------------------------- /blockchain/merkle_tree.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/ethereum/go-ethereum/crypto" 8 | ) 9 | 10 | // MerkleTree represents a Merkle tree 11 | type MerkleTree struct { 12 | RootNode *MerkleNode 13 | } 14 | 15 | // MerkleNode represents a Merkle tree node 16 | type MerkleNode struct { 17 | Left *MerkleNode 18 | Right *MerkleNode 19 | Data []byte 20 | } 21 | 22 | // NewMerkleTree creates a new Merkle tree from a sequence of data 23 | func NewMerkleTree(txHashes [][]byte) *MerkleTree { 24 | var nodes []MerkleNode 25 | 26 | // sort the slice to make sure creating the unique merkle tree 27 | txHashes = SortByteArrays(txHashes) 28 | 29 | if len(txHashes)%2 != 0 { 30 | txHashes = append(txHashes, txHashes[len(txHashes)-1]) 31 | } 32 | 33 | for _, txHash := range txHashes { 34 | node := NewMerkleNode(nil, nil, txHash) 35 | nodes = append(nodes, *node) 36 | } 37 | 38 | for len(nodes) > 1 { 39 | var newLevel []MerkleNode 40 | 41 | if len(nodes)%2 != 0 { 42 | nodes = append(nodes, nodes[len(nodes)-1]) 43 | } 44 | 45 | for j := 0; j < len(nodes); j += 2 { 46 | node := NewMerkleNode(&nodes[j], &nodes[j+1], nil) 47 | newLevel = append(newLevel, *node) 48 | } 49 | 50 | nodes = newLevel 51 | } 52 | 53 | mTree := MerkleTree{&nodes[0]} 54 | 55 | return &mTree 56 | } 57 | 58 | // NewMerkleNode creates a new Merkle tree node 59 | func NewMerkleNode(left, right *MerkleNode, txHash []byte) *MerkleNode { 60 | mNode := MerkleNode{} 61 | 62 | if left == nil && right == nil { 63 | mNode.Data = txHash 64 | } else { 65 | prevHashes := append(left.Data, right.Data...) 66 | hash := crypto.Keccak256(prevHashes) 67 | mNode.Data = hash[:] 68 | } 69 | 70 | mNode.Left = left 71 | mNode.Right = right 72 | 73 | return &mNode 74 | } 75 | 76 | // GetVerificationPath finds the necessary transaction hashes for clients to verify if a transaction has been included in the block 77 | func (mt MerkleTree) GetVerificationPath(txToVerify []byte) map[int][]byte { 78 | nodes, index := mt.findNodeByData(txToVerify) 79 | 80 | if index <= 0 { 81 | return nil 82 | } 83 | 84 | var hashPath = make(map[int][]byte) 85 | cursor := index 86 | for cursor > 0 { 87 | if cursor%2 != 0 { // left node 88 | hashPath[cursor+1] = nodes[cursor+1].Data 89 | } else { 90 | hashPath[cursor-1] = nodes[cursor-1].Data 91 | } 92 | cursor = (cursor - 1) / 2 93 | } 94 | hashPath[cursor] = nodes[cursor].Data // root node 95 | 96 | return hashPath 97 | } 98 | 99 | // findNodeByData finds the tree node having the transation hash 100 | func (mt MerkleTree) findNodeByData(hash []byte) ([]*MerkleNode, int) { 101 | if mt.RootNode != nil { 102 | var nodeBuffer []*MerkleNode 103 | var nodes []*MerkleNode 104 | var nodeIndex int 105 | 106 | nodeBuffer = append(nodeBuffer, mt.RootNode) 107 | 108 | for len(nodeBuffer) != 0 { 109 | node := nodeBuffer[0] 110 | nodes = append(nodes, node) 111 | nodeBuffer = nodeBuffer[1:] 112 | 113 | if bytes.Equal(node.Data, hash) { 114 | nodeIndex = len(nodes) - 1 // nodeIndex might not be unique - can be overridden after the first one is found 115 | } 116 | 117 | if node.Left != nil { 118 | nodeBuffer = append(nodeBuffer, node.Left) 119 | } 120 | if node.Right != nil { 121 | nodeBuffer = append(nodeBuffer, node.Right) 122 | } 123 | } 124 | 125 | return nodes, nodeIndex 126 | 127 | } 128 | 129 | return nil, -1 130 | } 131 | 132 | // walk traverses the tree in level by level 133 | func (mt MerkleTree) walk() { 134 | if mt.RootNode != nil { 135 | var nodeBuffer []*MerkleNode 136 | nodeBuffer = append(nodeBuffer, mt.RootNode) 137 | 138 | for len(nodeBuffer) != 0 { 139 | node := nodeBuffer[0] 140 | nodeBuffer = nodeBuffer[1:] 141 | 142 | fmt.Printf("\nData: %x\n", node.Data) 143 | 144 | if node.Left != nil { 145 | nodeBuffer = append(nodeBuffer, node.Left) 146 | } 147 | if node.Right != nil { 148 | nodeBuffer = append(nodeBuffer, node.Right) 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /blockchain/merkle_tree_test.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "encoding/hex" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | var hashStr []string = []string{"69292d123e8278e18e040fe7080898b4f6695413bd8890c851251b6646e4be82", "8841661dc86c2fbc2586f3f658b72713e371d89efae562d848f0ef4329a78280", "4da4d28f757484cb26ff94d94df6154d3676d33e00a0afd5dead650abe42c217", "7494edfee13f844b71cea5735f7566c2e01cca3f3be8746dd43551fc1fb67d0b", "a8af696e9eb5d84d5f504b190c7150e1ec1a0306c2453e1151937d9430dc18d9"} 10 | 11 | func TestNewMerkleTree(t *testing.T) { 12 | expectedRootHash := "7e85ea1a1bc07d4a934661d0b78295617316d6e7363bce5a1e8d9e4557859437" 13 | 14 | var txHashes [][]byte 15 | for _, hash := range hashStr { 16 | decodedHash, _ := hex.DecodeString(hash) 17 | txHashes = append(txHashes, decodedHash) 18 | } 19 | 20 | merkleTree := NewMerkleTree(txHashes) 21 | 22 | if hex.EncodeToString(merkleTree.RootNode.Data) != expectedRootHash { 23 | t.Errorf("tree root hash expected: %s, actual: %x", expectedRootHash, merkleTree.RootNode.Data) 24 | } 25 | } 26 | 27 | func TestVerificationPath(t *testing.T) { 28 | txHashToFindStr := "a8af696e9eb5d84d5f504b190c7150e1ec1a0306c2453e1151937d9430dc18d9" 29 | txHashToFind, _ := hex.DecodeString(txHashToFindStr) 30 | 31 | var txHashes [][]byte 32 | for _, hash := range hashStr { 33 | decodedHash, _ := hex.DecodeString(hash) 34 | txHashes = append(txHashes, decodedHash) 35 | } 36 | 37 | merkleTree := NewMerkleTree(txHashes) 38 | _, index := merkleTree.findNodeByData(txHashToFind) 39 | if index != 14 { 40 | t.Errorf("findNodeByData expected: %d, actual: %d", 14, index) 41 | } 42 | 43 | txHashToVerifyStr := "8841661dc86c2fbc2586f3f658b72713e371d89efae562d848f0ef4329a78280" 44 | txHashToVerify, _ := hex.DecodeString(txHashToVerifyStr) 45 | 46 | verificationPath := merkleTree.GetVerificationPath(txHashToVerify) 47 | 48 | indices := make([]int, 0, len(verificationPath)) 49 | for index := range verificationPath { 50 | indices = append(indices, index) 51 | } 52 | 53 | expectedIndices := []int{0, 2, 3, 9} 54 | sort.Ints(indices) 55 | 56 | for i := 0; i < len(expectedIndices); i++ { 57 | if expectedIndices[i] != indices[i] { 58 | t.Errorf("verificationPath expected: %d, actual: %d", expectedIndices, indices) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /blockchain/search.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/blevesearch/bleve" 15 | "github.com/blevesearch/bleve/index/scorch" 16 | "github.com/boltdb/bolt" 17 | 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | const indexDefault = "default" 22 | 23 | // Search encapsulates all the indices with search engine features 24 | type Search struct { 25 | sync.Mutex 26 | db *bolt.DB 27 | indexDirRoot string 28 | BlockchainIndices map[string]bleve.Index 29 | } 30 | 31 | // Document represents a document with metadata in the search result 32 | type Document struct { 33 | ID string `json:"_id"` 34 | BlockID string `json:"_blockId"` 35 | BlockchainId string `json:"_blockchainId"` // peerId 36 | Source string `json:"_source"` 37 | Timestamp string `json:"_timestamp"` 38 | Signature string `json:"_signature"` 39 | Address string `json:"_address"` // Issuer address 40 | } 41 | 42 | // NewSearch create an instance to access the search features 43 | func NewSearch(db *bolt.DB, dataDir string) (*Search, error) { 44 | blockchainIndices := make(map[string]bleve.Index) 45 | 46 | indexDirRoot := dataDir + filepath.Dir("/") + "collections" 47 | defaultIndex, err := bleve.Open(indexDirRoot + "/" + indexDefault) 48 | 49 | if err != nil { 50 | log.Infof("%s: %s. creating the default collection instead...\n", err, indexDirRoot) 51 | 52 | jsonSchema := ` 53 | { 54 | "collection": "default", 55 | "fields": { 56 | "id": {"type": "number"}, 57 | "message": {"type": "text"} 58 | } 59 | } 60 | ` 61 | 62 | search := Search{db: db, indexDirRoot: indexDirRoot, BlockchainIndices: blockchainIndices} 63 | 64 | defaultIndex, err := search.CreateMappingByJson([]byte(jsonSchema)) 65 | 66 | if err != nil { 67 | log.Fatalf("creating default collection err: %s", err) 68 | return nil, err 69 | } 70 | search.BlockchainIndices[indexDefault] = defaultIndex 71 | return &search, nil 72 | } 73 | 74 | log.Info("opening existing collections...") 75 | blockchainIndices["default"] = defaultIndex 76 | 77 | files, err := ioutil.ReadDir(indexDirRoot) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | 82 | // add all other indices than the default 83 | for _, file := range files { 84 | if file.Name() != indexDefault { 85 | bleveIndex, err := bleve.Open(indexDirRoot + filepath.Dir("/") + file.Name()) 86 | 87 | if err != nil { 88 | log.Error("cannot add this index: " + err.Error()) 89 | } else { 90 | blockchainIndices[file.Name()] = bleveIndex 91 | } 92 | } 93 | } 94 | 95 | return &Search{db: db, indexDirRoot: indexDirRoot, BlockchainIndices: blockchainIndices}, nil 96 | } 97 | 98 | // IndexBlock index all the txs in a block 99 | func (s *Search) IndexBlock(block *Block, peerId []byte) { 100 | s.Lock() 101 | defer s.Unlock() 102 | var jsonDoc map[string]interface{} 103 | 104 | // using batch index for better performance 105 | indexBatches := make(map[string]*bleve.Batch) 106 | for collection, index := range s.BlockchainIndices { 107 | indexBatches[collection] = index.NewBatch() 108 | } 109 | 110 | for _, tx := range block.Transactions { 111 | // do not index the doc where there is no index exists for it 112 | if nil == s.BlockchainIndices[tx.Collection] { 113 | //log.Println("The collection " + tx.Collection + " doesn't exist... Skipped the indexing.") 114 | continue 115 | } 116 | 117 | // parse bytes as json 118 | err := json.Unmarshal(tx.RawData, &jsonDoc) 119 | 120 | if err != nil { 121 | log.Errorf("error indexing tx with ID %x: %s", tx.ID, err) 122 | } 123 | 124 | // all searchable system fields 125 | jsonDoc["_type"] = tx.Collection 126 | jsonDoc["_blockId"] = fmt.Sprintf("%x", tx.BlockHash) 127 | jsonDoc["_timestamp"] = time.Unix(0, tx.AcceptedTimestamp*int64(time.Millisecond)).Format(time.RFC3339) 128 | jsonDoc["_publicKey"] = fmt.Sprintf("%x", tx.PubKey) 129 | jsonDoc["_id"] = fmt.Sprintf("%x", tx.ID) 130 | jsonDoc["_peerId"] = fmt.Sprintf("%x", peerId) 131 | jsonDoc["_permittedAddresses"] = tx.PermittedAddresses 132 | 133 | indexBatches[tx.Collection].Index(string(append(append(block.Hash, []byte("_")...), tx.ID...)), jsonDoc) 134 | } 135 | 136 | for collection, batch := range indexBatches { 137 | s.BlockchainIndices[collection].Batch(batch) 138 | } 139 | } 140 | 141 | // DocumentMapping represents the schema of a collection 142 | type DocumentMapping struct { 143 | Collection string `json:"collection"` 144 | Fields map[string]interface{} `json:"fields"` 145 | } 146 | 147 | // Serialize serializes the transaction 148 | func (dm DocumentMapping) Serialize() []byte { 149 | var result bytes.Buffer 150 | 151 | mappingExpression := map[string]interface{}{ 152 | "id": "{\"type\": \"text\"}", 153 | } 154 | gob.Register(mappingExpression) 155 | 156 | encoder := gob.NewEncoder(&result) 157 | err := encoder.Encode(dm) 158 | if err != nil { 159 | log.Error(err) 160 | } 161 | 162 | return result.Bytes() 163 | } 164 | 165 | // DeserializeDocumentMapping deserializes encoded bytes to an DocumentMapping object 166 | func DeserializeDocumentMapping(a []byte) *DocumentMapping { 167 | var dm DocumentMapping 168 | 169 | mappingExpression := map[string]interface{}{ 170 | "id": "{\"type\": \"text\"}", 171 | } 172 | gob.Register(mappingExpression) 173 | 174 | decoder := gob.NewDecoder(bytes.NewReader(a)) 175 | err := decoder.Decode(&dm) 176 | if err != nil { 177 | log.Error(err) 178 | } 179 | 180 | return &dm 181 | } 182 | 183 | // CreateMapping creates the data schema for a specific collection. 184 | func (s *Search) CreateMapping(documentMapping DocumentMapping) (bleve.Index, error) { 185 | // a generic reusable mapping for text 186 | textFieldMapping := bleve.NewTextFieldMapping() 187 | textFieldMapping.Store = false 188 | 189 | // a generic reusable mapping for datetime 190 | dateTimeFieldMapping := bleve.NewDateTimeFieldMapping() 191 | dateTimeFieldMapping.Store = false 192 | 193 | // a generic reusable mapping for number 194 | numericFieldMapping := bleve.NewNumericFieldMapping() 195 | numericFieldMapping.Store = false 196 | 197 | // a generic reusable mapping for boolean 198 | booleanFieldMapping := bleve.NewBooleanFieldMapping() 199 | booleanFieldMapping.Store = false 200 | 201 | // a generic reusable mapping for geopoint 202 | geoPointFieldMapping := bleve.NewGeoPointFieldMapping() 203 | geoPointFieldMapping.Store = false 204 | 205 | collectionSchema := bleve.NewDocumentMapping() 206 | 207 | // iterate all the fields in the payload and create the field mappings for each of them - index name and mapping name is the same 208 | for fieldName, v := range documentMapping.Fields { 209 | if strings.HasPrefix(fieldName, "_") { // _ is for system only fields 210 | return nil, fmt.Errorf("field name: %s cannot start with _", fieldName) 211 | } 212 | 213 | fieldType := v.(map[string]interface{}) 214 | 215 | switch fieldType["type"] { 216 | case "text": 217 | collectionSchema.AddFieldMappingsAt(fieldName, textFieldMapping) 218 | case "number": 219 | collectionSchema.AddFieldMappingsAt(fieldName, numericFieldMapping) 220 | case "datetime": 221 | collectionSchema.AddFieldMappingsAt(fieldName, dateTimeFieldMapping) 222 | case "boolean": 223 | collectionSchema.AddFieldMappingsAt(fieldName, booleanFieldMapping) 224 | case "geopoint": 225 | collectionSchema.AddFieldMappingsAt(fieldName, geoPointFieldMapping) 226 | default: 227 | log.Errorf("The data type: %s for field: %s is not valid.", fieldType["type"], fieldName) 228 | return nil, fmt.Errorf("the data type: %s for field: %s is not valid", fieldType["type"], fieldName) 229 | } 230 | } 231 | 232 | // System fields 233 | collectionSchema.AddFieldMappingsAt("_blockId", textFieldMapping) 234 | collectionSchema.AddFieldMappingsAt("_publicKey", textFieldMapping) 235 | collectionSchema.AddFieldMappingsAt("_timestamp", dateTimeFieldMapping) 236 | collectionSchema.AddFieldMappingsAt("_type", textFieldMapping) 237 | collectionSchema.AddFieldMappingsAt("_id", textFieldMapping) // transaction ID 238 | collectionSchema.AddFieldMappingsAt("_peerId", textFieldMapping) 239 | collectionSchema.AddFieldMappingsAt("_permittedAddresses", textFieldMapping) 240 | 241 | indexMapping := bleve.NewIndexMapping() 242 | indexMapping.AddDocumentMapping(documentMapping.Collection, collectionSchema) 243 | 244 | indexMapping.TypeField = "_type" 245 | indexMapping.DefaultAnalyzer = "en" 246 | indexMapping.StoreDynamic = false 247 | indexMapping.IndexDynamic = false 248 | 249 | collectionIndex, err := bleve.NewUsing(s.indexDirRoot+filepath.Dir("/")+documentMapping.Collection, indexMapping, scorch.Name, scorch.Name, nil) 250 | 251 | if err != nil { 252 | log.WithFields(log.Fields{ 253 | "method": "CreateMapping()", 254 | }).Error(err) 255 | return nil, err 256 | } 257 | 258 | err = s.db.Update(func(dbtx *bolt.Tx) error { 259 | collectionBucket, err := dbtx.CreateBucketIfNotExists([]byte(CollectionsBucket)) 260 | 261 | if err != nil { 262 | return err 263 | } 264 | 265 | err = collectionBucket.Put([]byte(documentMapping.Collection), documentMapping.Serialize()) 266 | if err != nil { 267 | return err 268 | } 269 | 270 | return nil 271 | }) 272 | 273 | if err != nil { 274 | log.WithFields(log.Fields{ 275 | "method": "CreateMapping()", 276 | }).Error(err) 277 | return nil, err 278 | } 279 | 280 | collectionIndex.SetName(documentMapping.Collection) // rewrite the default name 281 | s.BlockchainIndices[documentMapping.Collection] = collectionIndex 282 | return collectionIndex, nil 283 | } 284 | 285 | // CreateMappingByJson creates the data schema for a specific collection, which is defined in JSON 286 | // An example JSON payload: 287 | // { 288 | // "collection": "new_collection", 289 | // "fields": { 290 | // "id": {"type": "text"}, 291 | // "title": {"type": "text"}, 292 | // "age": {"type": "number"}, 293 | // "created": {"type": "datetime"}, 294 | // "isModified": {"type": "boolean"}, 295 | // "location": {"type": "geopoint"} 296 | // } 297 | // } 298 | func (s *Search) CreateMappingByJson(mappingJSON []byte) (bleve.Index, error) { 299 | var documentMapping DocumentMapping 300 | 301 | if err := json.Unmarshal(mappingJSON, &documentMapping); err != nil { 302 | log.Errorf("error parsing the document json mapping json payload: " + err.Error()) 303 | return nil, err 304 | } 305 | 306 | if len(documentMapping.Collection) == 0 || documentMapping.Fields == nil { 307 | log.Errorf("%s is not a valid collection schema definition\n", mappingJSON) 308 | return nil, fmt.Errorf("%s is not a valid collection schema definition", mappingJSON) 309 | } 310 | 311 | if nil != s.BlockchainIndices[documentMapping.Collection] { 312 | log.Warnf("the collection " + documentMapping.Collection + " already exists. Nothing to do.") 313 | return nil, fmt.Errorf("the collection %s already exists. Nothing to do", documentMapping.Collection) 314 | } 315 | 316 | return s.CreateMapping(documentMapping) 317 | 318 | } 319 | -------------------------------------------------------------------------------- /blockchain/transaction.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/sha256" 7 | "encoding/gob" 8 | "time" 9 | 10 | "github.com/ethereum/go-ethereum/crypto" 11 | uuid "github.com/satori/go.uuid" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // Transaction one transaction has on document 16 | type Transaction struct { 17 | ID []byte // hash 18 | BlockHash []byte 19 | PeerId []byte // blockchain ID 20 | RawData []byte 21 | AcceptedTimestamp int64 22 | Collection string 23 | PubKey []byte 24 | Signature []byte 25 | PermittedAddresses []string 26 | } 27 | 28 | // SetID sets ID of a transaction based on the raw data and timestamp 29 | func (tx *Transaction) SetID() { 30 | idHash := sha256.Sum256(uuid.NewV4().Bytes()) 31 | tx.ID = idHash[:] 32 | } 33 | 34 | // Serialize serializes the transaction 35 | func (tx *Transaction) Serialize() []byte { 36 | var result bytes.Buffer 37 | encoder := gob.NewEncoder(&result) 38 | err := encoder.Encode(tx) 39 | if err != nil { 40 | log.Error(err) 41 | } 42 | 43 | return result.Bytes() 44 | } 45 | 46 | // DeserializeTransaction deserializes a transaction 47 | func DeserializeTransaction(d []byte) *Transaction { 48 | var tx Transaction 49 | 50 | decoder := gob.NewDecoder(bytes.NewReader(d)) 51 | err := decoder.Decode(&tx) 52 | if err != nil { 53 | log.Error(err) 54 | } 55 | 56 | return &tx 57 | } 58 | 59 | // Sign signs the digest of rawData 60 | func Sign(privKey ecdsa.PrivateKey, rawDataDigest []byte) []byte { 61 | signature, err := crypto.Sign(rawDataDigest, &privKey) 62 | 63 | if err != nil { 64 | log.Error(err) 65 | } 66 | 67 | return signature 68 | } 69 | 70 | // NewTransaction creates a new transaction 71 | func NewTransaction(peerId []byte, data []byte, collection string, pubKey []byte, signature []byte, permittedAddresses []string) *Transaction { 72 | tx := &Transaction{[]byte{}, []byte{}, peerId, data, time.Now().UnixNano() / 1000000, collection, pubKey, signature, permittedAddresses} 73 | tx.SetID() 74 | 75 | return tx 76 | } 77 | 78 | // NewCoinbaseTX creates a new coinbase transaction 79 | func NewCoinbaseTX(peerId []byte) *Transaction { 80 | return NewTransaction(peerId, []byte(genesisCoinbaseRawData), "default", []byte{}, []byte{}, nil) 81 | } 82 | -------------------------------------------------------------------------------- /blockchain/utils.go: -------------------------------------------------------------------------------- 1 | package blockchain 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "math/rand" 7 | "regexp" 8 | "sort" 9 | "time" 10 | 11 | "github.com/ethereum/go-ethereum/crypto" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | const ( 16 | BlocksBucket = "blocks" 17 | TransactionsBucket = "transactions" 18 | AccountsBucket = "accounts" 19 | CollectionsBucket = "collections" 20 | P2PPrivateKeyKey = "p2pPrivKey" 21 | genesisCoinbaseRawData = `{"isActive":true,"balance":"$1,608.00","picture":"http://placehold.it/32x32","age":37,"eyeColor":"brown","name":"Rosa Sherman","gender":"male","organization":"STELAECOR","email":"rosasherman@stelaecor.com","phone":"+1 (907) 581-2115","address":"546 Meserole Street, Clara, New Jersey, 5471","about":"Reprehenderit eu pariatur proident id voluptate eu pariatur minim ut magna aliquip esse. Eu et quis sint quis et anim duis non tempor esse minim voluptate fugiat. Cillum qui nulla aute ullamco.\r\n","registered":"2018-01-15T05:53:18 +05:00","latitude":-55.183323,"longitude":-63.077504,"tags":["laborum","ex","officia","nisi","adipisicing","commodo","incididunt"],"friends":[{"id":0,"name":"Franks Harper"},{"id":1,"name":"Bettye Nash"},{"id":2,"name":"Mai Buck"}],"greeting":"Hello, Rosa Sherman! You have 3 unread messages.","favoriteFruit":"strawberry"}` 22 | 23 | letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 24 | letterIdxBits = 6 // 6 bits to represent a letter index 25 | letterIdxMask = 1<= 0; { 61 | if remain == 0 { 62 | cache, remain = src.Int63(), letterIdxMax 63 | } 64 | if idx := int(cache & letterIdxMask); idx < l { 65 | b[i] = letterBytes[idx] 66 | i-- 67 | } 68 | cache >>= letterIdxBits 69 | remain-- 70 | } 71 | 72 | return string(b) 73 | } 74 | 75 | // PublicKeyToAddress generates the address of a given publicKey in bytes 76 | func PublicKeyToAddress(publicKeyBytes []byte) (string, error) { 77 | publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) 78 | 79 | if err != nil { 80 | log.WithFields(log.Fields{ 81 | "method": "PublicKeyToAddress()", 82 | }).Error(err) 83 | return "", err 84 | } 85 | return crypto.PubkeyToAddress(*publicKey).String(), nil 86 | } 87 | 88 | // implement `Interface` in sort package. 89 | type sortByteArrays [][]byte 90 | 91 | func (b sortByteArrays) Len() int { 92 | return len(b) 93 | } 94 | 95 | func (b sortByteArrays) Less(i, j int) bool { 96 | return bytes.Compare(b[i], b[j]) < 0 97 | } 98 | 99 | func (b sortByteArrays) Swap(i, j int) { 100 | b[j], b[i] = b[i], b[j] 101 | } 102 | 103 | // SortByteArrays sorts the slice of byte arrays 104 | func SortByteArrays(src [][]byte) [][]byte { 105 | sorted := sortByteArrays(src) 106 | sort.Sort(sorted) 107 | return sorted 108 | } 109 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingpeasant/blocace/504a4871c832ea5a2cdd3ac090137f018f98d013/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Blocace In 10 Minutes 2 | This guide assumes you have an existing basic knowledge of Web API, database and digital signature. 3 | 4 | System prerequisites: 5 | 6 | * (only when you prefer to compile Blocace server) Go version: 1.12 or later; 7 | 8 | * (only when you prefer to compile Blocace server) GCC 5.1 or later. Windows may need to install [GCC](http://tdm-gcc.tdragon.net/download) if missing before installing the dependencies. Linux may also need to install gcc using the corresponding package management tool, like `yum install gcc` on RedHat or alike. macOS may need to install [Xcode Command Line Tools](https://www.ics.uci.edu/~pattis/common/handouts/macmingweclipse/allexperimental/macxcodecommandlinetools.html). 9 | * [Node.js](https://nodejs.org/) 10 | 11 | * JS libraries (install using `npm install` in [blocace-js](https://github.com/codingpeasant/blocace-js)): 12 | 13 | ```javascript 14 | { 15 | "dependencies": { 16 | "ethers": "^4.0.43", 17 | "axios": "^0.19.1" 18 | } 19 | } 20 | 21 | ``` 22 | 23 | ## Step 1: Compile and start Blocace server 24 | 25 | ```bash 26 | $ git clone https://github.com/codingpeasant/blocace.git 27 | $ cd blocace 28 | $ go get 29 | $ go build -ldflags="-s -w -X main.version=0.1.0" 30 | $ ./blocace server # it'd be blocace.exe for Windows 31 | 32 | ____ __ __ ___ __ ___ ____ 33 | ( _ \( ) / \ / __) / _\ / __)( __) 34 | ) _ (/ (_/\( O )( (__ / \( (__ ) _) 35 | (____/\____/ \__/ \___)\_/\_/ \___)(____) 36 | 37 | Community Edition 0.1.0 38 | 39 | INFO[2020-05-31T15:38:27-04:00] configurations: advertiseAddress= bulkLoading=false hostP2p=0.0.0.0 loglevel=debug maxtime=2000 maxtx=2048 path=./data0 peerAddresses= portP2p=6091 porthttp=6899 40 | INFO[2020-05-31T15:38:27-04:00] cannot find the db file. creating new... 41 | INFO[2020-05-31T15:38:27-04:00] cannot open index, path does not exist: ./data0/collections. creating the default collection instead... 42 | INFO[2020-05-31T15:38:27-04:00] the admin account has been created and registered successfully 43 | 44 | #################### 45 | PRIVATE KEY: 1471dc3e2fb9d2d43c2c598ff98f90c147c41a7f8899d490bee9e813ab5bcfe6 46 | WARNING: THIS PRIVATE KEY ONLY SHOWS ONCE. PLEASE SAVE IT NOW AND KEEP IT SAFE. YOU ARE THE ONLY PERSON THAT IS SUPPOSED TO OWN THIS KEY IN THE WORLD. 47 | #################### 48 | 49 | INFO[2020-05-31T15:38:27-04:00] did not find peer db dir ./data0/peers, creating one... 50 | INFO[2020-05-31T15:38:27-04:00] no peer address(es) provided, starting without trying to discover 51 | INFO[2020-05-31T15:38:28-04:00] begin to monitor transactions every 2000 milliseconds... 52 | INFO[2020-05-31T15:38:28-04:00] awaiting signal... 53 | ``` 54 | By default, __Blocace__ creates a `data` directory within the working dir to store the blockchain and DB collections; the time interval to generate a block is 2 seconds; the max number of transactions (about documents) is 2048; it listens on port 6899 for web API calls; advertiseAddress bonds to loopback network interface; the bulk loading API is disabled; peerAddresses is empty (single node mode); the port open for P2P traffic is 6091. Please keep a note of the `root private key` which will be used to make administration API calls to Blocace server (step 2 below). 55 | 56 | > If you are running this in a bash terminal on linux, you can quickly spin up a Blocace cluster by running `./start_cluster.sh 3` to compile and spin up 3 Blocace server nodes on localhost. To terminate the 3 nodes, simply use Ctrl + C in the terminal. 57 | 58 | ## Step 2: Run example.js with the root admin account private key 59 | ```bash 60 | # open a new terminal tab and run 61 | $ git clone https://github.com/codingpeasant/blocace-js.git 62 | $ cd blocace-js 63 | $ npm install 64 | $ node ./example.js 65 | 66 | Private key decrypted: true 67 | 68 | JWT (admin): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlTmFtZSI6ImFkbWluIiwiYWRkcmVzcyI6IjB4MmIyOTNkOUIyM0JiNkZlRTM2OTY4OTlkNmMxQURjQWY1N0ZGQjM3RCIsImF1ZCI6ImJsb2NhY2UgdXNlciIsImV4cCI6MTU5MDk1NzExOSwiaWF0IjoxNTkwOTU2NTE5LCJpc3MiOiJibG9jYWNlIn0.TUiA4G1rp-E55_3cqyHbk01-lIk3bUsho7f9a-LZeR8 69 | 70 | Address of new account: 0xf55486314B0C4F032d603B636327ed5c82218688 71 | 72 | New account Info: {"address":"699 Canton Court, Mulino, South Dakota, 9647","collectionsReadOverride":null,"collectionsWrite":null,"dateOfBirth":"2018-10-01","email":"hoopervincent@mitroc.com","firstName":"Hooper","lastName":"Vincent","organization":"MITROC","phone":"+1 (849) 503-2756","position":"VP of Marketing","publicKey":"04b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa","roleName":"user"} 73 | 74 | Account permission response: {"message":"account permission updated","address":"0xf55486314B0C4F032d603B636327ed5c82218688"} 75 | 76 | Get the update account: {"address":"699 Canton Court, Mulino, South Dakota, 9647","collectionsReadOverride":["default","collection2","new1"],"collectionsWrite":["default","new1"],"dateOfBirth":"2018-10-01","email":"hoopervincent@mitroc.com","firstName":"Hooper","lastName":"Vincent","organization":"MITROC","phone":"+1 (849) 503-2756","position":"VP of Marketing","publicKey":"04b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa","roleName":"user"} 77 | New collection info: {"message":"collection new1 created"} 78 | 79 | JWT (new user): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlTmFtZSI6InVzZXIiLCJhZGRyZXNzIjoiMHhmNTU0ODYzMTRCMEM0RjAzMmQ2MDNCNjM2MzI3ZWQ1YzgyMjE4Njg4IiwiYXVkIjoiYmxvY2FjZSB1c2VyIiwiZXhwIjoxNTkwOTU3MTE5LCJpYXQiOjE1OTA5NTY1MTksImlzcyI6ImJsb2NhY2UifQ.OH1XuB_ECCdwLRgMyp1DiDab6IzDBUX0zpd-0vznsxo 80 | 81 | Update account response: {"message":"account updated","address":"0xf55486314B0C4F032d603B636327ed5c82218688"} 82 | 83 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"69292d123e8278e18e040fe7080898b4f6695413bd8890c851251b6646e4be82"} 84 | 85 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"8841661dc86c2fbc2586f3f658b72713e371d89efae562d848f0ef4329a78280"} 86 | 87 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"4da4d28f757484cb26ff94d94df6154d3676d33e00a0afd5dead650abe42c217"} 88 | 89 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"7494edfee13f844b71cea5735f7566c2e01cca3f3be8746dd43551fc1fb67d0b"} 90 | 91 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"a8af696e9eb5d84d5f504b190c7150e1ec1a0306c2453e1151937d9430dc18d9"} 92 | 93 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"052c7ea57c35174d075af7e67f02094fc9918bcc5d266250cc1e47cb48e6c859"} 94 | 95 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"8b389d537fd3fabb5a8a5fb00337ff30cfea6751f34d85321051be766ac7fd46"} 96 | 97 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"654998deca958eeef0c78e0b048529515a11fb18a73963d67dc2f2cf9baeaced"} 98 | 99 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"d441505afc8d491ba6cb79a6a3f2f28f3bcbc6e16bea14fafcddf373e3dba72b"} 100 | 101 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"a792c9491cb2abe5160d2c73723608bd3b9c87da6624c5b9d36611efd8909eb9"} 102 | 103 | Waiting for the document to be included in a block... 104 | 105 | Query result: {"collection":"new1","status":{"total":1,"failed":0,"successful":1},"total_hits":10,"hits":[{"_id":"8b389d537fd3fabb5a8a5fb00337ff30cfea6751f34d85321051be766ac7fd46","_blockId":"373fb73d7d39436932e7cb969e97c42ae13886b6c1bf64c19d84479859a21192","_blockchainId":"99cd155750514bf8aeb2f87cdf25fab3877301e6cfdeb2ab769fe0a8c7283410","_source":"{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}","_timestamp":"2020-05-31T16:22:00.001-04:00","_signature":"98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5","_address":"0xf55486314B0C4F032d603B636327ed5c82218688"},{"_id":"a8af696e9eb5d84d5f504b190c7150e1ec1a0306c2453e1151937d9430dc18d9","_blockId":"373fb73d7d39436932e7cb969e97c42ae13886b6c1bf64c19d84479859a21192","_blockchainId":"99cd155750514bf8aeb2f87cdf25fab3877301e6cfdeb2ab769fe0a8c7283410","_source":"{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}","_timestamp":"2020-05-31T16:21:59.988-04:00","_signature":"98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5","_address":"0xf55486314B0C4F032d603B636327ed5c82218688"},{"_id":"654998deca958eeef0c78e0b048529515a11fb18a73963d67dc2f2cf9baeaced","_blockId":"373fb73d7d39436932e7cb969e97c42ae13886b6c1bf64c19d84479859a21192","_blockchainId":"99cd155750514bf8aeb2f87cdf25fab3877301e6cfdeb2ab769fe0a8c7283410","_source":"{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}","_timestamp":"2020-05-31T16:22:00.011-04:00","_signature":"98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5","_address":"0xf55486314B0C4F032d603B636327ed5c82218688"}]} 106 | 107 | Document included in the block: true 108 | 109 | The document's integrity check passed: true 110 | 111 | Block information: {"blockchainId":"99cd155750514bf8aeb2f87cdf25fab3877301e6cfdeb2ab769fe0a8c7283410","blockId":"373fb73d7d39436932e7cb969e97c42ae13886b6c1bf64c19d84479859a21192","prevBlockId":"c39e703c750f99f5a774ba52615ff0b55a72edf516a687e6a1e8e5658e083a2b","blockHeight":1,"totalTransactions":10} 112 | 113 | Blockchain information: [{"blockchainId":"99cd155750514bf8aeb2f87cdf25fab3877301e6cfdeb2ab769fe0a8c7283410","tipBlockId":"373fb73d7d39436932e7cb969e97c42ae13886b6c1bf64c19d84479859a21192","lastHeight":1,"totalTransactions":11}] 114 | 115 | All collections in the blockchain: {"message":"ok","collections":["default","new1"]} 116 | 117 | Collection new1 data schema: {"message":"ok","mapping":{"collection":"new1","fields":{"age":{"encrypted":true,"type":"number"},"gender":{"type":"text"},"guid":{"type":"text"},"id":{"type":"text"},"isActive":{"type":"boolean"},"location":{"type":"geopoint"},"name":{"encrypted":true,"type":"text"},"registered":{"type":"datetime"},"tags":{"type":"text"}}}} 118 | 119 | Peers of 99cd155750514bf8aeb2f87cdf25fab3877301e6cfdeb2ab769fe0a8c7283410: [] 120 | ``` 121 | > Troubleshooting: if you see `{ message: 'public key not valid' }` error running `example.js` above, you can switch back to the Blocace server terminal, stop the server by Ctrl + C, remove the `./data*` folder(s), restart the server to generate a new private key. 122 | 123 | That's it. You have successfully built Blocace server and accessed *ALL* its web APIs using the Blocace Javascript client. 124 | 125 | # Step-by-step breakdown of example.js 126 | > If you'd like to know more about the APIs, please continue reading. 127 | 128 | ## Setup root account 129 | ```javascript 130 | var blocace = Blocace.createFromPrivateKey(process.argv[2]) 131 | 132 | // encrypt and decrypt the seed 133 | var encryptPrivKey = blocace.encryptPrivateKey('123456') 134 | var decryptPrivKey = Blocace.decryptPrivateKey(encryptPrivKey, '123456') 135 | 136 | console.log('Private key decrypted: ' + (blocace.wallet.privateKey === decryptPrivKey) + '\n') 137 | ``` 138 | Output 139 | ``` 140 | Private key decrypted: true 141 | ``` 142 | ## Create account using the root account (private key) 143 | ```javascript 144 | // get JWT 145 | const jwt = await blocace.getJWT() 146 | console.log('JWT (admin): ' + jwt + '\n') 147 | 148 | // register a new user account 149 | const accountRes = await Blocace.createAccount(accountPayload, 'http', 'localhost', '6899') 150 | console.log('Address of new account: ' + accountRes.data.address + '\n') 151 | 152 | // get account 153 | const account = await blocace.getAccount(accountRes.data.address) 154 | console.log('New account Info: ' + JSON.stringify(account) + '\n')onsole.log(error); 155 | }); 156 | ``` 157 | Output 158 | ``` 159 | JWT (admin): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlTmFtZSI6ImFkbWluIiwiYWRkcmVzcyI6IjB4RDE2MjFGNzZiMzMzOWIyRUFENTA2ODU5ZGRFRWRhRkZBMWYxOGM1MiIsImF1ZCI6ImJsb2NhY2UgdXNlciIsImV4cCI6MTU4MDM2MTAyOCwiaWF0IjoxNTgwMzYwNDI4LCJpc3MiOiJibG9jYWNlIn0.rKqkdaD-k8HmUW-z0W9WI41SUs7_sqSFdjGePdrYtKQ 160 | 161 | Address of new account: 0xf55486314B0C4F032d603B636327ed5c82218688 162 | 163 | New account Info: {"address":"699 Canton Court, Mulino, South Dakota, 9647","collectionsReadOverride":null,"collectionsWrite":null,"organization":"MITROC","dateOfBirth":"2018-10-01","email":"hoopervincent@mitroc.com","firstName":"Hooper","lastName":"Vincent","phone":"+1 (849) 503-2756","position":"VP of Marketing","publicKey":"04b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa","roleName":"user"} 164 | ``` 165 | First login as the root admin user and obtain a [JSON Web Token](https://jwt.io/) to access Blocace server and create a new user account without read/write permissions. And then get the account information noting that `"collectionsReadOverride":null,"collectionsWrite":null` 166 | 167 | ## Grant collection-level permission to the new user 168 | ```javascript 169 | // set the new account read / write permission 170 | const accountPermissionRes = await blocace.setAccountReadWrite(permission, accountRes.data.address) 171 | console.log('Account permission response: ' + JSON.stringify(accountPermissionRes.data) + '\n') 172 | 173 | // get the user account again 174 | const accountUpdated = await blocace.getAccount(accountRes.data.address) 175 | console.log('Get the update account: ' + JSON.stringify(accountUpdated)) 176 | ``` 177 | Output 178 | ``` 179 | Account permission response: {"message":"account permission updated","address":"0xf55486314B0C4F032d603B636327ed5c82218688"} 180 | 181 | Get the update account: {"address":"699 Canton Court, Mulino, South Dakota, 9647","collectionsReadOverride":["default","collection2","new1"],"collectionsWrite":["default","new1"],"organization":"MITROC","dateOfBirth":"2018-10-01","email":"hoopervincent@mitroc.com","firstName":"Hooper","lastName":"Vincent","phone":"+1 (849) 503-2756","position":"VP of Marketing","publicKey":"04b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa","roleName":"user"} 182 | ``` 183 | Setting the read/write permission to the new user: `'collectionsWrite': ['default', 'new1'],'collectionsReadOverride': ['default', 'collection2', 'new1']`. 184 | 185 | ## Create a new collection as the root admin user 186 | ```javascript 187 | // create collection 188 | const collectionCreationRes = await blocace.createCollection(collectionMappingPaylod) 189 | console.log('New collection info: ' + JSON.stringify(collectionCreationRes) + '\n') 190 | 191 | ``` 192 | Output 193 | ``` 194 | New collection info: {"message":"collection new1 created"} 195 | ``` 196 | Create the collection (or table in SQL databases) with defined schema. 197 | 198 | ## Update the new user account information 199 | ```javascript 200 | // initialize the new user account 201 | var blocaceUser = Blocace.createFromPrivateKey('277d271593d205c6078964c31fb393303efd76d5297906f60d2a7a7d7d12c99a') 202 | // get JWT for the user account 203 | const jwtUser = await blocaceUser.getJWT() 204 | console.log('JWT (new user): ' + jwtUser + '\n') 205 | 206 | // update account 207 | accountPayload.email = 'asd@asd.com' 208 | const accountUpdateRes = await blocaceUser.updateAccount(accountPayload, accountRes.data.address) 209 | console.log('Update account response: ' + JSON.stringify(accountUpdateRes.data) + '\n') 210 | ``` 211 | Output 212 | ``` 213 | JWT (new user): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlTmFtZSI6InVzZXIiLCJhZGRyZXNzIjoiMHhmNTU0ODYzMTRCMEM0RjAzMmQ2MDNCNjM2MzI3ZWQ1YzgyMjE4Njg4IiwiYXVkIjoiYmxvY2FjZSB1c2VyIiwiZXhwIjoxNTgwMzYxMDI4LCJpYXQiOjE1ODAzNjA0MjgsImlzcyI6ImJsb2NhY2UifQ.UBw-D7AL1KNBl-Ww2NHz-HvV92BNrfcmdXyb0HwzjGI 214 | 215 | Update account response: {"message":"account updated","address":"0xf55486314B0C4F032d603B636327ed5c82218688"} 216 | ``` 217 | Login as the new user and update its own account's email address. Note that the account information is just for convenience to track identity and doesn't affect the usage of Blocace 218 | 219 | ## Sign and put documents to Blocace and query them 220 | ```javascript 221 | // put 10 documents 222 | for (let index = 0; index < 10; index++) { 223 | const putDocRes = await blocaceUser.signAndPutDocument(document, 'new1') 224 | console.log('Put document response: ' + JSON.stringify(putDocRes) + '\n') 225 | } 226 | 227 | // wait for block to be generated 228 | await timeout(2000) 229 | console.log('Waiting for the document to be included in a block... \n') 230 | 231 | // query the blockchain 232 | const queryRes = await blocaceUser.query(queryPayload, 'new1') 233 | console.log('Query result: ' + JSON.stringify(queryRes) + '\n') 234 | ``` 235 | Output 236 | ``` 237 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"8a545086ebfac8d7f38c08ceb618f2afe35850e9ba9890784abe89288f42e7bd"} 238 | 239 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"dd6182df7f97a8df1bcbfe9c107e369a002b03a62114f5f7152460ad98194e03"} 240 | 241 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"01fa1686554b585e1436436c2cff40bb7b250eb383699dcebd389ff4af504e50"} 242 | 243 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"e4644e6d64fdc2f45526742e4921010f48b29ae8fe1b8655ef544853b7acd10c"} 244 | 245 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"28728359c7e240dddcb83e15e7f078ba45f329b1202cd0ca0ad9d11ba4945814"} 246 | 247 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"98c41ffa3227d8f8674a3b8865d7d0e4622815e3895acc6e0da8d1b8caf39084"} 248 | 249 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"516ab6ec7db085b0347b7a5f67b36e6654092bc60cc40b2ec3e6370999ef42a3"} 250 | 251 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"8c62c28482b098eba844471289f2ddfad1e1a6748c389d8348e96df017841b33"} 252 | 253 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"f8dde1543a7d644fc1ec6e1765c0e694fc96f51625c4d83926b611959188739d"} 254 | 255 | Put document response: {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"07245c7a01cf7fac705a40b3e1632bcc06754e6ce7f5d01624d66e9b567d91ca"} 256 | 257 | Waiting for the document to be included in a block... 258 | 259 | Query result: 260 | { 261 | "collection": "new1", 262 | "status": { 263 | "total": 1, 264 | "failed": 0, 265 | "successful": 1 266 | }, 267 | "total_hits": 10, 268 | "hits": [{ 269 | "_id": "8a545086ebfac8d7f38c08ceb618f2afe35850e9ba9890784abe89288f42e7bd", 270 | "_blockId": "cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8", 271 | "_source": "{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}", 272 | "_timestamp": "2020-01-30T00:00:28.624-05:00", 273 | "_signature": "98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5", 274 | "_address": "0xf55486314B0C4F032d603B636327ed5c82218688" 275 | }, { 276 | "_id": "f8dde1543a7d644fc1ec6e1765c0e694fc96f51625c4d83926b611959188739d", 277 | "_blockId": "cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8", 278 | "_source": "{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}", 279 | "_timestamp": "2020-01-30T00:00:28.712-05:00", 280 | "_signature": "98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5", 281 | "_address": "0xf55486314B0C4F032d603B636327ed5c82218688" 282 | }, { 283 | "_id": "516ab6ec7db085b0347b7a5f67b36e6654092bc60cc40b2ec3e6370999ef42a3", 284 | "_blockId": "cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8", 285 | "_source": "{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}", 286 | "_timestamp": "2020-01-30T00:00:28.691-05:00", 287 | "_signature": "98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5", 288 | "_address": "0xf55486314B0C4F032d603B636327ed5c82218688" 289 | }] 290 | } 291 | ``` 292 | Sign each of the documents with the new user's private key and send them to Blocace. Blocace server verifies the digital signature of each document and put them to the transaction queue. The goroutine to generate blocks dequeues transactions periodically and append them to the blockchain so all the transactions in each block are indexed to be queried later. The query we use in this example is: 293 | ```javascript 294 | { 295 | 'size': 3, 296 | 'from': 0, 297 | 'query': { 298 | 'match': 'Compton', 299 | 'field': 'name' 300 | } 301 | } 302 | ``` 303 | 304 | ## Verify the integrity of the documents 305 | ```javascript 306 | // verify if the transaction is included in the block (by block merkle tree rebuild) 307 | const verificationPassed = await blocaceUser.verifyTransaction(queryRes.hits[0]._blockchainId, queryRes.hits[0]._blockId, queryRes.hits[0]._id) 308 | console.log('Document included in the block: ' + verificationPassed + '\n') 309 | 310 | // verify signature 311 | console.log('The document\'s integrity check passed: ' + Blocace.verifySignature(queryRes.hits[0]._source, queryRes.hits[0]._signature, blocaceUser.wallet.address) + '\n') 312 | ``` 313 | Output 314 | ``` 315 | Document included in the block: true 316 | 317 | The document's integrity check passed: true 318 | ``` 319 | The Blocace client first verifies that the document has been persisted in the blockchain and that the document is not tempered with. This is the blockchain philosophy: 320 | > Don't Trust. Verify! 321 | 322 | ## Get block, blockchain and collection administration information 323 | ```javascript 324 | // get block information 325 | const blockRes = await blocace.getBlockInfo(queryRes.hits[0]._blockId) 326 | console.log('Block information: ' + JSON.stringify(blockRes) + '\n') 327 | 328 | // get blockchain information 329 | const blockchainRes = await blocace.getBlockchainInfo() 330 | console.log('Blockchain information: ' + JSON.stringify(blockchainRes) + '\n') 331 | 332 | // get all collections 333 | const collectionsRes = await blocace.getCollections() 334 | console.log('All collections in the blockchain: ' + JSON.stringify(collectionsRes) + '\n') 335 | 336 | // get collection data schema 337 | const collectionRes = await blocace.getCollection('new1') 338 | console.log('Collection new1 data schema: ' + JSON.stringify(collectionRes) + '\n') 339 | ``` 340 | Output 341 | ``` 342 | Block information: {"blockId":"cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8","lastBlockId":"47e7023f02c4f762d458e674ce1075666e47cafa93a701b6cb88615c6b4f6dc5","blockHeight":1,"totalTransactions":10} 343 | 344 | Blockchain information: {"newestBlockId":"cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8","lastHeight":1,"totalTransactions":11} 345 | 346 | All collections in the blockchain: {"message":"ok","collections":["default","new1"]} 347 | 348 | Collection new1 data schema: {"message":"ok","mapping":{"collection":"new1","fields":{"age":{"encrypted":true,"type":"number"},"gender":{"type":"text"},"guid":{"type":"text"},"id":{"type":"text"},"isActive":{"type":"boolean"},"location":{"type":"geopoint"},"name":{"encrypted":true,"type":"text"},"registered":{"type":"datetime"},"tags":{"type":"text"}}}} 349 | ``` 350 | Blocace client is able to get adminstration information about a given block. In this example, the `blockHeight` is `1` as this block is the 2nd in the blockchain (after the genesis block). It has `10` transaction documents that we just put; the blockchain has `11` transaction documents: 1 `genesis transactions` + 10 `user transactions`; it also gets the schema of collection `new1`. 351 | 352 | ## Get peer(s) of the current Blocace node 353 | ```javascript 354 | // get peer nodes of a given node 355 | const peerRes = await blocace.getPeers() 356 | console.log('Peers of ' + queryRes.hits[0]._blockchainId + ': ' + JSON.stringify(peerRes) + '\n') 357 | ``` 358 | Output 359 | ``` 360 | Peers of cba1739c9aeabc0698a5da2a701f22c6f2e4c0122b2a44d019bd4b6105a4801c: [{"public_key":"45b3c9f26eef3b0bfbdea99af1731ab7a08a1dfcaf58a7e8abf55db73eda45c9","address":"::","Port":6092,"Address":":6092"},{"public_key":"70a50de06cbc9e36010e9277c6ae74bafce8af4e6c4b168b864a373a3a8d1a1f","address":"::","Port":6093,"Address":":6093"}] 361 | ``` 362 | In this example, the blocace-js client is configured talking to a single server. The `getPeers()` returns the alive peer(s) that are currently known this server. 363 | 364 | > You're all set! Go ahead an build your web DAPP around Blocace! 365 | 366 | # Usage Reference 367 | Note that the APIs are constantly changing in the pre-release phase. 368 | ## Blocace CLI reference 369 | ### Server CLI 370 | The major command to start a Blocace instance. 371 | ``` 372 | $ ./blocace s -h 373 | ____ __ __ ___ __ ___ ____ 374 | ( _ \( ) / \ / __) / _\ / __)( __) 375 | ) _ (/ (_/\( O )( (__ / \( (__ ) _) 376 | (____/\____/ \__/ \___)\_/\_/ \___)(____) 377 | 378 | Community Edition 0.1.0 379 | 380 | NAME: 381 | blocace server - start the major blocace server 382 | 383 | USAGE: 384 | blocace server [command options] [arguments...] 385 | 386 | OPTIONS: 387 | --dir value, -d value the path to the folder of data persistency (default: "data") 388 | --secret value, -s value the password to encrypt data and manage JWT 389 | --maxtx value, -m value the max transactions in a block (default: 2048) 390 | --maxtime value, -t value the time in milliseconds interval to generate a block (default: 2000) 391 | --porthttp value, -o value the port that the web api http server listens on (default: "6899") 392 | --portP2p value, -p value the port that the p2p node listens on (default: 6091) 393 | --hostP2p value, -w value the hostname/ip address that the p2p node binds to (default: "0.0.0.0") 394 | --advertiseAddress value, -a value the public address of this node which is advertised on the ID sent to peers during a handshake protocol (optional) 395 | --peerAddresses value, -e value the comma-separated address:port list of the peers (optional) 396 | --bulkLoading value, -b value enable bulking loading API (default: "false") 397 | --loglevel value, -l value the log levels: panic, fatal, error, warn, info, debug, trace (default: "info") 398 | 399 | ``` 400 | Example: 401 | ``` 402 | $ ./blocace s -l debug 403 | ____ __ __ ___ __ ___ ____ 404 | ( _ \( ) / \ / __) / _\ / __)( __) 405 | ) _ (/ (_/\( O )( (__ / \( (__ ) _) 406 | (____/\____/ \__/ \___)\_/\_/ \___)(____) 407 | 408 | Community Edition 0.1.0 409 | 410 | INFO[2020-05-31T15:38:27-04:00] configurations: advertiseAddress= bulkLoading=false hostP2p=0.0.0.0 loglevel=debug maxtime=2000 maxtx=2048 path=./data0 peerAddresses= portP2p=6091 porthttp=6899 411 | INFO[2020-05-31T15:38:27-04:00] cannot find the db file. creating new... 412 | INFO[2020-05-31T15:38:27-04:00] cannot open index, path does not exist: ./data0/collections. creating the default collection instead... 413 | INFO[2020-05-31T15:38:27-04:00] the admin account has been created and registered successfully 414 | 415 | #################### 416 | PRIVATE KEY: 1471dc3e2fb9d2d43c2c598ff98f90c147c41a7f8899d490bee9e813ab5bcfe6 417 | WARNING: THIS PRIVATE KEY ONLY SHOWS ONCE. PLEASE SAVE IT NOW AND KEEP IT SAFE. YOU ARE THE ONLY PERSON THAT IS SUPPOSED TO OWN THIS KEY IN THE WORLD. 418 | #################### 419 | 420 | INFO[2020-05-31T15:38:27-04:00] did not find peer db dir ./data0/peers, creating one... 421 | INFO[2020-05-31T15:38:27-04:00] no peer address(es) provided, starting without trying to discover 422 | INFO[2020-05-31T15:38:28-04:00] begin to monitor transactions every 2000 milliseconds... 423 | INFO[2020-05-31T15:38:28-04:00] awaiting signal... 424 | ``` 425 | ### Key generation CLI 426 | In case the Blocace administrator lost the root admin account, this command recreates it. 427 | ``` 428 | $ ./blocace k -h 429 | 430 | ____ __ __ ___ __ ___ ____ 431 | ( _ \( ) / \ / __) / _\ / __)( __) 432 | ) _ (/ (_/\( O )( (__ / \( (__ ) _) 433 | (____/\____/ \__/ \___)\_/\_/ \___)(____) 434 | 435 | Community Edition 0.1.0 436 | 437 | NAME: 438 | blocace keygen - generate and register an admin account 439 | 440 | USAGE: 441 | blocace keygen [command options] [arguments...] 442 | 443 | OPTIONS: 444 | --dir value, -d value the path to the folder of data persistency (default: "data") 445 | ``` 446 | Example: 447 | ``` 448 | $ ./blocace k 449 | 450 | ____ __ __ ___ __ ___ ____ 451 | ( _ \( ) / \ / __) / _\ / __)( __) 452 | ) _ (/ (_/\( O )( (__ / \( (__ ) _) 453 | (____/\____/ \__/ \___)\_/\_/ \___)(____) 454 | 455 | Community Edition 0.1.0 456 | 457 | INFO[2020-02-01T12:02:30-05:00] db file exists. generating an admin keypair and registering an account... 458 | INFO[2020-02-01T12:02:30-05:00] the account has been created and registered successfully 459 | 460 | #################### 461 | PRIVATE KEY: 81244df62f43a163a2f4a4894ef531ba1a493b921fb3bbaabdb2222e632f7734 462 | WARNING: THIS PRIVATE KEY ONLY SHOWS ONCE. PLEASE SAVE IT NOW AND KEEP IT SAFE. YOU ARE THE ONLY PERSON THAT IS SUPPOSED TO OWN THIS KEY IN THE WORLD. 463 | #################### 464 | ``` 465 | ## Blocace web API reference 466 | ### `static create(protocol, hostname, port)` 467 | Generate random Blocace client key pair and initialize the client class 468 | 469 | Example: 470 | ``` 471 | var blocace = Blocace.create('http', 'localhost', '6899') 472 | ``` 473 | ### `static createFromPrivateKey(privKey, protocol, hostname, port)` 474 | Use an existing client private key and initialize the client class 475 | 476 | Example: 477 | ``` 478 | var blocace = Blocace.createFromPrivateKey('81244df62f43a163a2f4a4894ef531ba1a493b921fb3bbaabdb2222e632f7734) 479 | ``` 480 | 481 | ### `encryptPrivateKey(password)` 482 | Get the encrypted private key. The return value is a concatenation of the salt, IV and the cipher text of the private key 483 | 484 | Example: 485 | ``` 486 | var encryptPrivKey = blocace.encryptPrivateKey('123456') 487 | ``` 488 | 489 | ### `static decryptPrivateKey(encrypted, password)` 490 | Decrypt the private key from the encryption string, which is a concatenation of the salt, IV and the cipher text of the private key 491 | 492 | Example: 493 | ``` 494 | var decryptPrivKey = Blocace.decryptPrivateKey(encryptPrivKey, '123456') 495 | ``` 496 | 497 | ### `static verifySignature(rawDocument, signature, address)` 498 | Verify if the signature of a document matches the claimed address (aka. public key). This API can be used to verify the integrity of a document 499 | 500 | Example: 501 | ``` 502 | var isValidSignature = Blocace.verifySignature(queryRes.hits[0]._source, queryRes.hits[0]._signature, blocaceUser.wallet.address) 503 | ``` 504 | 505 | ### `getPublicKey()` 506 | Get public key of the wallet 507 | 508 | Example: 509 | ``` 510 | var publicKey = blocace.getPublicKey() 511 | ``` 512 | 513 | ### `static async createAccount(accountPayload)` 514 | Create a new account 515 | 516 | Example: 517 | ``` 518 | const accountPayload = { 519 | 'dateOfBirth': '2018-10-01', 520 | 'firstName': 'Hooper', 521 | 'lastName': 'Vincent', 522 | 'organization': 'MITROC', 523 | 'position': 'VP of Marketing', 524 | 'email': 'hoopervincent@mitroc.com', 525 | 'phone': '+1 (849) 503-2756', 526 | 'address': '699 Canton Court, Mulino, South Dakota, 9647', 527 | 'publicKey': 'b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa' 528 | } 529 | 530 | const accountRes = await Blocace.createAccount(accountPayload, 'http', 'localhost', '6899') 531 | ``` 532 | 533 | ### `async updateAccount(accountPayload, address)` 534 | Update the current account 535 | 536 | Example: 537 | ``` 538 | const accountPayload = { 539 | 'dateOfBirth': '2018-10-01', 540 | 'firstName': 'Hooper', 541 | 'lastName': 'Vincent', 542 | 'organization': 'MITROC', 543 | 'position': 'VP of Marketing', 544 | 'email': 'hoopervincent@mitroc.com', 545 | 'phone': '+1 (849) 503-2756', 546 | 'address': '699 Canton Court, Mulino, South Dakota, 9647', 547 | 'publicKey': 'b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa' 548 | } 549 | 550 | accountPayload.email = 'asd@asd.com' 551 | const accountUpdateRes = await blocaceUser.updateAccount(accountPayload, accountRes.data.address) 552 | ``` 553 | Output: 554 | ``` 555 | {"address":"699 Canton Court, Mulino, South Dakota, 9647","collectionsReadOverride":null,"collectionsWrite":null,"organization":"MITROC","dateOfBirth":"2018-10-01","email":"hoopervincent@mitroc.com","firstName":"Hooper","lastName":"Vincent","phone":"+1 (849) 503-2756","position":"VP of Marketing","publicKey":"04b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa","roleName":"user"} 556 | ``` 557 | ### `async setAccountReadWrite(permissionPayload, address)` 558 | Grand collection level read/write permission 559 | 560 | Example: 561 | ``` 562 | const accountPermissionRes = await blocace.setAccountReadWrite(permission, accountRes.data.address) 563 | ``` 564 | Output: 565 | ``` 566 | {"message":"account permission updated","address":"0xf55486314B0C4F032d603B636327ed5c82218688"} 567 | ``` 568 | ### `async getChallenge()` 569 | A challenge issued from Blocace server for the client to authenticate 570 | 571 | Example: 572 | ``` 573 | const challengeResponse = await this.getChallenge() 574 | ``` 575 | ### `async getJWT()` 576 | Get the challenge, give back the solution and obtain the JWT ([JSON Web Token](https://jwt.io/)) 577 | 578 | Example: 579 | ``` 580 | const jwt = await blocace.getJWT() 581 | ``` 582 | Output: 583 | ``` 584 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlTmFtZSI6ImFkbWluIiwiYWRkcmVzcyI6IjB4RDE2MjFGNzZiMzMzOWIyRUFENTA2ODU5ZGRFRWRhRkZBMWYxOGM1MiIsImF1ZCI6ImJsb2NhY2UgdXNlciIsImV4cCI6MTU4MDM2MTAyOCwiaWF0IjoxNTgwMzYwNDI4LCJpc3MiOiJibG9jYWNlIn0.rKqkdaD-k8HmUW-z0W9WI41SUs7_sqSFdjGePdrYtKQ 585 | ``` 586 | ### `async getAccount(address)` 587 | Get the account's information 588 | 589 | Example: 590 | ``` 591 | const account = await blocace.getAccount(accountRes.data.address) 592 | ``` 593 | Output: 594 | ``` 595 | { 596 | "address": "699 Canton Court, Mulino, South Dakota, 9647", 597 | "collectionsReadOverride": null, 598 | "collectionsWrite": null, 599 | "organization": "MITROC", 600 | "dateOfBirth": "2018-10-01", 601 | "email": "hoopervincent@mitroc.com", 602 | "firstName": "Hooper", 603 | "lastName": "Vincent", 604 | "phone": "+1 (849) 503-2756", 605 | "position": "VP of Marketing", 606 | "publicKey": "04b0a303c71d99ad217c77af1e4d5b85e3ccc3e359d2ac9ff95e042fb0e0016e4d4c25482ba57de472c44c58f6fb124a0ab86613b0dcd1253a23d5ae00180854fa", 607 | "roleName": "user" 608 | } 609 | ``` 610 | ### `async createCollection(collectionPayload)` 611 | Create an new collection with schema 612 | 613 | Example: 614 | ``` 615 | const collectionCreationRes = await blocace.createCollection(collectionMappingPaylod) 616 | ``` 617 | Output: 618 | ``` 619 | {"message":"collection new1 created"} 620 | ``` 621 | ### `async signAndPutDocument(document, collection)` 622 | Write and digitally sign a JSON document to add to a collection 623 | 624 | Example: 625 | ``` 626 | const document = { 627 | 'id': '5bf1d3fdf6fd4a5c4638f64e', 628 | 'guid': 'f51b68c5-f274-4ce1-984f-b4fb4d618ff3', 629 | 'isActive': false, 630 | 'age': 28, 631 | 'name': 'Carly Compton', 632 | 'gender': 'male', 633 | 'registered': '2015-09-18T12:59:51Z', 634 | 'location': { 635 | 'lon': 46.564666, 636 | 'lat': 53.15213 637 | }, 638 | 'tags': [ 639 | 'incididunt', 640 | 'dolore' 641 | ], 642 | 'friends': [ 643 | { 644 | 'id': 0, 645 | 'name': 'Jimenez Byers' 646 | }, 647 | { 648 | 'id': 1, 649 | 'name': 'Gabriela Mayer' 650 | } 651 | ], 652 | 'notExist': 'haha' 653 | } 654 | 655 | const putDocRes = await blocaceUser.signAndPutDocument(document, 'new1') 656 | ``` 657 | Output: 658 | ``` 659 | {"status":"ok","fieldErrors":null,"isValidSignature":true,"transactionID":"8a545086ebfac8d7f38c08ceb618f2afe35850e9ba9890784abe89288f42e7bd"} 660 | ``` 661 | ### `async putDocumentBulk(documents, collection)` 662 | Write a bulk of JSON documents in a single HTTP request to a collection. WARNING: this makes the documents unverifiable 663 | 664 | Example: 665 | ``` 666 | const payload = [ 667 | {...}, 668 | {...}, 669 | {...} 670 | ] 671 | await blocaceUser.putDocumentBulk(payload, 'new2') 672 | ``` 673 | 674 | ### `async query(queryPayload, collection)` 675 | Query the documents from Blocase with a query against a collection. Check out [Blevesearch Query](https://blevesearch.com/docs/Query/) for the query DSL. 676 | 677 | Example: 678 | ``` 679 | const queryPayload = { 680 | 'size': 3, 681 | 'from': 0, 682 | 'query': { 683 | 'match': 'Compton', 684 | 'field': 'name' 685 | } 686 | } 687 | const queryRes = await blocaceUser.query(queryPayload, 'new1') 688 | ``` 689 | Output: 690 | ``` 691 | { 692 | "collection": "new1", 693 | "status": { 694 | "total": 1, 695 | "failed": 0, 696 | "successful": 1 697 | }, 698 | "total_hits": 10, 699 | "hits": [{ 700 | "_id": "8a545086ebfac8d7f38c08ceb618f2afe35850e9ba9890784abe89288f42e7bd", 701 | "_blockId": "cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8", 702 | "_source": "{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}", 703 | "_timestamp": "2020-01-30T00:00:28.624-05:00", 704 | "_signature": "98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5", 705 | "_address": "0xf55486314B0C4F032d603B636327ed5c82218688" 706 | }, { 707 | "_id": "f8dde1543a7d644fc1ec6e1765c0e694fc96f51625c4d83926b611959188739d", 708 | "_blockId": "cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8", 709 | "_source": "{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}", 710 | "_timestamp": "2020-01-30T00:00:28.712-05:00", 711 | "_signature": "98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5", 712 | "_address": "0xf55486314B0C4F032d603B636327ed5c82218688" 713 | }, { 714 | "_id": "516ab6ec7db085b0347b7a5f67b36e6654092bc60cc40b2ec3e6370999ef42a3", 715 | "_blockId": "cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8", 716 | "_source": "{\"id\":\"5bf1d3fdf6fd4a5c4638f64e\",\"guid\":\"f51b68c5-f274-4ce1-984f-b4fb4d618ff3\",\"isActive\":false,\"age\":28,\"name\":\"Carly Compton\",\"gender\":\"male\",\"registered\":\"2015-09-18T12:59:51Z\",\"location\":{\"lon\":46.564666,\"lat\":53.15213},\"tags\":[\"incididunt\",\"dolore\"],\"friends\":[{\"id\":0,\"name\":\"Jimenez Byers\"},{\"id\":1,\"name\":\"Gabriela Mayer\"}],\"notExist\":\"haha\"}", 717 | "_timestamp": "2020-01-30T00:00:28.691-05:00", 718 | "_signature": "98c21b760b61fd4a59af9ea511f75f0338a76881bbd820ed3bb5c14a7dcf3d9847025cdf3aca07e7b448d8a7358d8678298afba8b3d9b16b9bac635457dccde5", 719 | "_address": "0xf55486314B0C4F032d603B636327ed5c82218688" 720 | }] 721 | } 722 | ``` 723 | ### `async verifyTransaction(blockchainId, blockId, transationId)` 724 | Obtain a copy of block [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) and verify if the target document adding transaction has been included in the blockchain 725 | 726 | Example: 727 | ``` 728 | const verificationPassed = await blocaceUser.verifyTransaction(queryRes.hits[0]._blockchainId, queryRes.hits[0]._blockId, queryRes.hits[0]._id) 729 | ``` 730 | 731 | ### `async getBlockInfo(blockchainId, blockId)` 732 | Get the information of a target block 733 | 734 | Example: 735 | ``` 736 | const blockRes = await blocace.getBlockInfo(queryRes.hits[0]._blockchainId, queryRes.hits[0]._blockId) 737 | ``` 738 | Output: 739 | ``` 740 | {"blockId":"cfc01dc667753185a5635b33ebbff42b452476f15a4f63fceb210aad68dac3b8","lastBlockId":"47e7023f02c4f762d458e674ce1075666e47cafa93a701b6cb88615c6b4f6dc5","blockHeight":1,"totalTransactions":10} 741 | ``` 742 | ### `async getBlockchainInfo()` 743 | Get the information of all the chains in the network 744 | 745 | Example: 746 | ``` 747 | const blockchainRes = await blocace.getBlockchainInfo() 748 | ``` 749 | Output: 750 | ``` 751 | [ 752 | { 753 | "blockchainId": "bcfe7d257e28f656937b16b93448ebae57107e1536730c61600655a640194037", 754 | "tipBlockId": "86ccbffe9aac977eead58ff60d65d3d79022e6e191caf6bd3deffa5a1fffee4b", 755 | "lastHeight": 1, 756 | "totalTransactions": 11 757 | }, 758 | { 759 | "blockchainId": "3cb1b5a4e3d6a5482e83381d747104e4e8ad6c78d7202760764079cc82b66bca", 760 | "tipBlockId": "b82ebc9e8e9eda31606c37c8772dd4c8b42c68579c149a34a0676dadce200ca4", 761 | "lastHeight": 5, 762 | "totalTransactions": 3001 763 | }, 764 | { 765 | "blockchainId": "53388a5926ccc8ec4a6a129ea57fe0b86ffee6a1c033e6aca820e26d0e0c418c", 766 | "tipBlockId": "84a558c5a59b16711adc91cd7716775756c39a7475d0a06d1aedf89574eba9cf", 767 | "lastHeight": 0, 768 | "totalTransactions": 1 769 | } 770 | ] 771 | ``` 772 | ### `async getPeers()` 773 | Get the basic information of the alive peers known to node that the client currently talks to 774 | 775 | Example: 776 | ``` 777 | const peerRes = await blocace.getPeers() 778 | ``` 779 | Output: 780 | ``` 781 | [ 782 | { 783 | "public_key": "3cb1b5a4e3d6a5482e83381d747104e4e8ad6c78d7202760764079cc82b66bca", 784 | "address": "::", 785 | "Port": 16091, 786 | "Address": ":16091" 787 | }, 788 | { 789 | "public_key": "53388a5926ccc8ec4a6a129ea57fe0b86ffee6a1c033e6aca820e26d0e0c418c", 790 | "address": "::", 791 | "Port": 26091, 792 | "Address": ":26091" 793 | } 794 | ] 795 | ``` 796 | ### `async getCollections()` 797 | Get all the collections in the blockchain 798 | 799 | Example: 800 | ``` 801 | const collectionsRes = await blocace.getCollections() 802 | ``` 803 | Output: 804 | ``` 805 | {"message":"ok","collections":["default","new1"]} 806 | ``` 807 | ### `async getCollection(collectionName)` 808 | Get the information of a certain collection 809 | 810 | Example: 811 | ``` 812 | const collectionRes = await blocace.getCollection('new1') 813 | ``` 814 | Output: 815 | ``` 816 | { 817 | "message": "ok", 818 | "mapping": { 819 | "collection": "new1", 820 | "fields": { 821 | "age": { 822 | "encrypted": true, 823 | "type": "number" 824 | }, 825 | "gender": { 826 | "type": "text" 827 | }, 828 | "guid": { 829 | "type": "text" 830 | }, 831 | "id": { 832 | "type": "text" 833 | }, 834 | "isActive": { 835 | "type": "boolean" 836 | }, 837 | "location": { 838 | "type": "geopoint" 839 | }, 840 | "name": { 841 | "encrypted": true, 842 | "type": "text" 843 | }, 844 | "registered": { 845 | "type": "datetime" 846 | }, 847 | "tags": { 848 | "type": "text" 849 | } 850 | } 851 | } 852 | } 853 | ``` 854 | 855 | > Check out [example.js](https://github.com/codingpeasant/blocace-js/blob/master/example.js) for the full usage of the client lib. 856 | -------------------------------------------------------------------------------- /docs/coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](https://www.blocace.com/images/blocace-logo.png) 2 | 3 | ## Blocace Community Edition 4 | ### v0.1.0 5 | 6 | > The generic blockchain for all 7 | 8 | [Home Page](https://www.blocace.com) 9 | [Getting Started](#blocace-in-10-minutes) -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blocace Documentation 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codingpeasant/blocace 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/RoaringBitmap/roaring v0.4.21 // indirect 7 | github.com/blevesearch/bleve v0.8.1 8 | github.com/blevesearch/go-porterstemmer v1.0.2 // indirect 9 | github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f // indirect 10 | github.com/boltdb/bolt v1.3.1 11 | github.com/couchbase/vellum v0.0.0-20190829182332-ef2e028c01fd // indirect 12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 13 | github.com/etcd-io/bbolt v1.3.3 // indirect 14 | github.com/ethereum/go-ethereum v1.9.10 15 | github.com/gorilla/mux v1.7.3 16 | github.com/patrickmn/go-cache v2.1.0+incompatible 17 | github.com/perlin-network/noise v1.1.3 18 | github.com/rs/cors v1.7.0 19 | github.com/satori/go.uuid v1.2.0 20 | github.com/sirupsen/logrus v1.4.2 21 | github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 // indirect 22 | github.com/thoas/go-funk v0.5.0 23 | github.com/urfave/cli v1.22.2 24 | gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= 2 | github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= 3 | github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4= 4 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 5 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 6 | github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= 7 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 8 | github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= 9 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 10 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 11 | github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= 12 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 13 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= 18 | github.com/RoaringBitmap/roaring v0.4.18 h1:nh8Ngxctxt5QAoMLuR7MHJe4jEqpn+EnsdgDWPryQWo= 19 | github.com/RoaringBitmap/roaring v0.4.18/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= 20 | github.com/RoaringBitmap/roaring v0.4.21 h1:WJ/zIlNX4wQZ9x8Ey33O1UaD9TCTakYsdLFSBcTwH+8= 21 | github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= 22 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 23 | github.com/VictoriaMetrics/fastcache v1.5.3/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE= 24 | github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8= 25 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 26 | github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= 27 | github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= 28 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 29 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 30 | github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= 31 | github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= 32 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 33 | github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 34 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 35 | github.com/blevesearch/bleve v0.7.1-0.20190709170133-4a7e6fb7547c h1:OBOlhiOlyvIpV7yABMYBVxHT0BkEZlJ0HqPhGnBgZtY= 36 | github.com/blevesearch/bleve v0.7.1-0.20190709170133-4a7e6fb7547c/go.mod h1:Y2lmIkzV6mcNfAnAdOd+ZxHkHchhBfU/xroGIp61wfw= 37 | github.com/blevesearch/bleve v0.8.1 h1:20zBREtGe8dvBxCC+717SaxKcUVQOWk3/Fm75vabKpU= 38 | github.com/blevesearch/bleve v0.8.1/go.mod h1:Y2lmIkzV6mcNfAnAdOd+ZxHkHchhBfU/xroGIp61wfw= 39 | github.com/blevesearch/go-porterstemmer v1.0.2 h1:qe7n69gBd1OLY5sHKnxQHIbzn0LNJA4hpAf+5XDxV2I= 40 | github.com/blevesearch/go-porterstemmer v1.0.2/go.mod h1:haWQqFT3RdOGz7PJuM3or/pWNJS1pKkoZJWCkWu0DVA= 41 | github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f h1:kqbi9lqXLLs+zfWlgo1PIiRQ86n33K1JKotjj4rSYOg= 42 | github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f/go.mod h1:IInt5XRvpiGE09KOk9mmCMLjHhydIhNPKPPFLFBB7L8= 43 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 44 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 45 | github.com/boltdb/bolt v1.3.2-0.20180302180052-fd01fc79c553 h1:JsFGvzmvh7HGD2Q56FkCtowlJyTJcesskDcqWMG0Zho= 46 | github.com/boltdb/bolt v1.3.2-0.20180302180052-fd01fc79c553/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 47 | github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6 h1:Eey/GGQ/E5Xp1P2Lyx1qj007hLZfbi0+CoVeJruGCtI= 48 | github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= 49 | github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= 50 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 51 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 52 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 53 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 54 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 55 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 56 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 57 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 58 | github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= 59 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 60 | github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18/go.mod h1:HD5P3vAIAh+Y2GAxg0PrPN1P8WkepXGpjbUPDHJqqKM= 61 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 62 | github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U= 63 | github.com/codingpeasant/blocace v0.0.0-20200202040511-56f67507d91b h1:/zixfxGvZ2b73FlNHbo/12AXxjsJMc2zCk6dZLomLDY= 64 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 65 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 66 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 67 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 68 | github.com/couchbase/vellum v0.0.0-20190626091642-41f2deade2cf h1:B4yFDSyYolVT4DsKpztKYPeme6/kRdLV7iPZmAv2tEE= 69 | github.com/couchbase/vellum v0.0.0-20190626091642-41f2deade2cf/go.mod h1:prYTC8EgTu3gwbqJihkud9zRXISvyulAplQ6exdCo1g= 70 | github.com/couchbase/vellum v0.0.0-20190829182332-ef2e028c01fd h1:zeuJhcG3f8eePshH3KxkNE+Xtl53pVln9MOUPMyr/1w= 71 | github.com/couchbase/vellum v0.0.0-20190829182332-ef2e028c01fd/go.mod h1:xbc8Ff/oG7h2ejd7AlwOpfd+6QZntc92ygpAOfGwcKY= 72 | github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= 73 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 74 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 75 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 76 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 77 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 78 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 79 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 80 | github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 81 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 82 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 83 | github.com/dgrijalva/jwt-go v3.2.1-0.20190620180102-5e25c22bd5d6+incompatible h1:4jGdduO4ceTJFKf0IhgaB8NJapGqKHwC2b4xQ/cXujM= 84 | github.com/dgrijalva/jwt-go v3.2.1-0.20190620180102-5e25c22bd5d6+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 85 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 86 | github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 87 | github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c h1:JHHhtb9XWJrGNMcrVP6vyzO4dusgi/HnceHTgxSejUM= 88 | github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 89 | github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= 90 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 91 | github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= 92 | github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= 93 | github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 94 | github.com/ethereum/go-ethereum v1.9.0 h1:9Kaf7UfDkV3aIUJlf14hI/GgEgRAUq60u4fBlb9dLWw= 95 | github.com/ethereum/go-ethereum v1.9.0/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= 96 | github.com/ethereum/go-ethereum v1.9.1-0.20190719092543-a1f854926256 h1:PqvO5AyaKTOea0nC5IswAQp7uuR7Pc+SuvhDQKWoeXk= 97 | github.com/ethereum/go-ethereum v1.9.1-0.20190719092543-a1f854926256/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= 98 | github.com/ethereum/go-ethereum v1.9.10 h1:jooX7tWcscpC7ytufk73t9JMCeJQ7aJF2YmZJQEuvFo= 99 | github.com/ethereum/go-ethereum v1.9.10/go.mod h1:lXHkVo/MTvsEXfYsmNzelZ8R1e0DTvdk/wMZJIRpaRw= 100 | github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 101 | github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= 102 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 103 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= 104 | github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4= 105 | github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 106 | github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 107 | github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 108 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 109 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 110 | github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= 111 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 112 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 113 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 114 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 115 | github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c h1:zqAKixg3cTcIasAMJV+EcfVbWwLpOZ7LeoWJvcuD/5Q= 116 | github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 117 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 118 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 119 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 120 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 121 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 122 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 123 | github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 124 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 125 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 126 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 127 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 128 | github.com/gorilla/mux v1.7.4-0.20190720201435-e67b3c02c719 h1:zQ+2G/ywb753CxKkvvZ0O6uu/fk5HRm21wdcVEq1U6E= 129 | github.com/gorilla/mux v1.7.4-0.20190720201435-e67b3c02c719/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 130 | github.com/gorilla/websocket v1.4.1-0.20190629185528-ae1634f6a989/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 131 | github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= 132 | github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 133 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 134 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 135 | github.com/huin/goupnp v0.0.0-20161224104101-679507af18f3/go.mod h1:MZ2ZmwcBpvOoJ22IJsc7va19ZwoheaBk43rKg12SKag= 136 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 137 | github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= 138 | github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 139 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 140 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 141 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 142 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 143 | github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 144 | github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= 145 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 146 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 147 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 148 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 149 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 150 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 151 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 152 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 153 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 154 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 155 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 156 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 157 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 158 | github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= 159 | github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= 160 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 161 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 162 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 163 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 164 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 165 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 166 | github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae h1:VeRdUYdCw49yizlSbMEn2SZ+gT+3IUKx8BqxyQdz+BY= 167 | github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= 168 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= 169 | github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= 170 | github.com/oasislabs/ed25519 v0.0.0-20191122104632-9d9ffc15f526 h1:xKlK+m6tNFucKVOP4V0GDgU4IgaLbS+HRoiVbN3W8Y4= 171 | github.com/oasislabs/ed25519 v0.0.0-20191122104632-9d9ffc15f526/go.mod h1:xIpCyrK2ouGA4QBGbiNbkoONrvJ00u9P3QOkXSOAC0c= 172 | github.com/oasislabs/ed25519 v0.0.0-20200302143042-29f6767a7c3e h1:85L+lUTJHx4O7UP9y/65XV8iq7oaA2Uqe5WiUSB8XE4= 173 | github.com/oasislabs/ed25519 v0.0.0-20200302143042-29f6767a7c3e/go.mod h1:xIpCyrK2ouGA4QBGbiNbkoONrvJ00u9P3QOkXSOAC0c= 174 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 175 | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 176 | github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 177 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 178 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 179 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 180 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 181 | github.com/patrickmn/go-cache v1.0.0 h1:3gD5McaYs9CxjyK5AXGcq8gdeCARtd/9gJDUvVeaZ0Y= 182 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 183 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 184 | github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= 185 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 186 | github.com/perlin-network/noise v1.1.2 h1:qCPhX3vKepLeMC/5xueloUIs8K2cwKv99lwu4CMEaHI= 187 | github.com/perlin-network/noise v1.1.2/go.mod h1:m/4QYe1g6/00ihFiZL2x9HY9ANICE3+Mj5O/5inm1N4= 188 | github.com/perlin-network/noise v1.1.3 h1:x5lfqlvNLxYSzwRu5mjKyWusxOVg1wWEALT9XXVvCDI= 189 | github.com/perlin-network/noise v1.1.3/go.mod h1:Tr+hlttT8o7bobjS0o4GI+ef3MW8Gfk4RNJVolTpIcA= 190 | github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= 191 | github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= 192 | github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 193 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 194 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 195 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 196 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 197 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 198 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 199 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 200 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 201 | github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 202 | github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= 203 | github.com/robertkrimen/otto v0.0.0-20170205013659-6a77b7cbc37d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= 204 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 205 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 h1:8DPul/X0IT/1TNMIxoKLwdemEOBBHDC/K4EB16Cw5WE= 206 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 207 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 208 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 209 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 h1:3hxavr+IHMsQBrYUPQM5v0CgENFktkkbg1sfpgM3h20= 210 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= 211 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 212 | github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8= 213 | github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= 214 | github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 215 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 216 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 217 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 218 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 219 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 220 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 221 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 222 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 223 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 224 | github.com/sirupsen/logrus v1.4.3-0.20190701143506-07a84ee7412e h1:yhvQJ2VMgZVrOdYNPQM0mJSb9JoedMN9YvCQyIjwXjo= 225 | github.com/sirupsen/logrus v1.4.3-0.20190701143506-07a84ee7412e/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 226 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 227 | github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= 228 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 229 | github.com/spaolacci/murmur3 v1.0.1-0.20190317074736-539464a789e9/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 230 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 231 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 232 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 233 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 234 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 235 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 236 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 237 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 238 | github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= 239 | github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw= 240 | github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU= 241 | github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 h1:JNEGSiWg6D3lcBCMCBqN3ELniXujt+0QNHLhNnO0w3s= 242 | github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2/go.mod h1:mjqs7N0Q6m5HpR7QfXVBZXZWSqTjQLeTujjA/xUp2uw= 243 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 244 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 245 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 246 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 247 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 248 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 249 | github.com/syndtr/goleveldb v1.0.1-0.20190625010220-02440ea7a285 h1:uSDYjYejelKyceA6DiCsngFof9jAyeaSyX9XC5a1a7Q= 250 | github.com/syndtr/goleveldb v1.0.1-0.20190625010220-02440ea7a285/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= 251 | github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= 252 | github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= 253 | github.com/thoas/go-funk v0.1.1-0.20190502194129-a0a199e85d6d h1:/immLmJnxDcFXpmx7bobWHX+rqZYgeAstobr3RaVxIE= 254 | github.com/thoas/go-funk v0.1.1-0.20190502194129-a0a199e85d6d/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= 255 | github.com/thoas/go-funk v0.5.0 h1:XXFUVqX6xnIDqXxENFHBFS1X5AoT0EDs7HJq2krRfD8= 256 | github.com/thoas/go-funk v0.5.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 257 | github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= 258 | github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 259 | github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= 260 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 261 | github.com/urfave/cli v1.20.1-0.20190203184040-693af58b4d51 h1:9BPDfnoHp4nfdJvTcgc5nHV8Wh9gRJwH4xNylDIiAbQ= 262 | github.com/urfave/cli v1.20.1-0.20190203184040-693af58b4d51/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 263 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 264 | github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= 265 | github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 266 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 267 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 268 | github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= 269 | github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 270 | github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= 271 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 272 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 273 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 274 | go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM= 275 | go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 276 | go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo= 277 | go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 278 | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= 279 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 280 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 281 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 282 | go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= 283 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 284 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 285 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 286 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 287 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 288 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 289 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 290 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 291 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 292 | golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba h1:9bFeDpN3gTqNanMVqNcoR/pJQuP5uroC3t1D7eXozTE= 293 | golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 294 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 295 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= 296 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 297 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 298 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 299 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 300 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 301 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 302 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 303 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= 304 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 305 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 306 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 307 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 308 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 309 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 310 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 311 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 312 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 313 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 314 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 315 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= 316 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 317 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 318 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 319 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 320 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 321 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 322 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 323 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 324 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 325 | golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 326 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 327 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 328 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 329 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 330 | golang.org/x/tools v0.0.0-20200129045341-207d3de1faaf h1:mFgR10kFfr83r2+nXf0GZC2FKrFhMSs9NdJ0YdEaGiY= 331 | golang.org/x/tools v0.0.0-20200129045341-207d3de1faaf/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 332 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 333 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 334 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 335 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 336 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 337 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 338 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 339 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 340 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 341 | gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190213234257-ec84240a7772/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= 342 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 343 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 344 | gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= 345 | gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= 346 | gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 347 | gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 h1:2QQcyaEBdpfjjYkF0MXc69jZbHb4IOYuXz2UwsmVM8k= 348 | gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 349 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 350 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 351 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 352 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 353 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 354 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 355 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Blocace is a distributed document database powered by the blockchain technology. 2 | // A super light-weight yet powerful document-oriented database backed by blockchain / distributed ledger technology. 3 | // Data immutable and verifiable is all about trust, which creates the most efficient business. 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "path/filepath" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/boltdb/bolt" 18 | "github.com/ethereum/go-ethereum/crypto" 19 | "github.com/gorilla/mux" 20 | "github.com/rs/cors" 21 | log "github.com/sirupsen/logrus" 22 | "github.com/thoas/go-funk" 23 | "github.com/urfave/cli" 24 | 25 | "github.com/codingpeasant/blocace/blockchain" 26 | "github.com/codingpeasant/blocace/p2p" 27 | "github.com/codingpeasant/blocace/pool" 28 | "github.com/codingpeasant/blocace/webapi" 29 | ) 30 | 31 | var secret = "blocace_secret" 32 | var dataDir string 33 | var maxTxsPerBlock int 34 | var maxTimeToGenerateBlock int // milliseconds 35 | var portHttp string 36 | var portP2p int 37 | var hostP2p string 38 | var advertiseAddress string 39 | var peerAddresses string 40 | var peerAddressesArray []string 41 | var bulkLoading string 42 | var loglevel string 43 | var version string // build-time variable 44 | 45 | func init() { 46 | fmt.Printf(` 47 | ____ __ __ ___ __ ___ ____ 48 | ( _ \( ) / \ / __) / _\ / __)( __) 49 | ) _ (/ (_/\( O )( (__ / \( (__ ) _) 50 | (____/\____/ \__/ \___)\_/\_/ \___)(____) 51 | 52 | Community Edition %s 53 | 54 | `, version) 55 | 56 | log.SetFormatter(&log.TextFormatter{ 57 | FullTimestamp: true, 58 | }) 59 | log.SetOutput(os.Stdout) 60 | } 61 | 62 | func main() { 63 | app := cli.NewApp() 64 | app.Name = "Blocace Community Edition" 65 | app.Version = version 66 | app.Copyright = "(c) 2020 Blocace Labs" 67 | app.Usage = "The Generic Blockchain Solution" 68 | app.HelpName = "blocace" 69 | 70 | app.Commands = []cli.Command{ 71 | { 72 | Name: "server", 73 | Aliases: []string{"s"}, 74 | Usage: "start the major blocace server", 75 | HelpName: "blocace server", 76 | Flags: []cli.Flag{ 77 | cli.StringFlag{ 78 | Name: "dir, d", 79 | Value: "data", 80 | Usage: "the path to the folder of data persistency", 81 | Destination: &dataDir, 82 | }, 83 | cli.StringFlag{ 84 | Name: "secret, s", 85 | Usage: "the password to encrypt data and manage JWT", 86 | Destination: &secret, 87 | }, 88 | cli.IntFlag{ 89 | Name: "maxtx, m", 90 | Value: 2048, 91 | Usage: "the max transactions in a block", 92 | Destination: &maxTxsPerBlock, 93 | }, 94 | cli.IntFlag{ 95 | Name: "maxtime, t", 96 | Value: 2000, 97 | Usage: "the time in milliseconds interval to generate a block", 98 | Destination: &maxTimeToGenerateBlock, 99 | }, 100 | cli.StringFlag{ 101 | Name: "porthttp, o", 102 | Value: "6899", 103 | Usage: "the port that the web api http server listens on", 104 | Destination: &portHttp, 105 | }, 106 | cli.IntFlag{ 107 | Name: "portP2p, p", 108 | Value: p2p.DefaultPort, 109 | Usage: "the port that the p2p node listens on", 110 | Destination: &portP2p, 111 | }, 112 | cli.StringFlag{ 113 | Name: "hostP2p, w", 114 | Value: "0.0.0.0", 115 | Usage: "the hostname/ip address that the p2p node binds to", 116 | Destination: &hostP2p, 117 | }, 118 | cli.StringFlag{ 119 | Name: "advertiseAddress, a", 120 | Value: "", 121 | Usage: "the public address of this node which is advertised on the ID sent to peers during a handshake protocol (optional)", 122 | Destination: &advertiseAddress, 123 | }, 124 | cli.StringFlag{ 125 | Name: "peerAddresses, e", 126 | Value: "", 127 | Usage: "the comma-separated address:port list of the peers (optional)", 128 | Destination: &peerAddresses, 129 | }, 130 | cli.StringFlag{ 131 | Name: "bulkLoading, b", 132 | Value: "false", 133 | Usage: "enable bulking loading API", 134 | Destination: &bulkLoading, 135 | }, 136 | cli.StringFlag{ 137 | Name: "loglevel, l", 138 | Value: "info", 139 | Usage: "the log levels: panic, fatal, error, warn, info, debug, trace", 140 | Destination: &loglevel, 141 | }, 142 | }, 143 | Action: func(c *cli.Context) error { 144 | switch level := loglevel; level { 145 | case "panic": 146 | log.SetLevel(log.PanicLevel) 147 | case "fatal": 148 | log.SetLevel(log.PanicLevel) 149 | case "error": 150 | log.SetLevel(log.ErrorLevel) 151 | case "warn": 152 | log.SetLevel(log.WarnLevel) 153 | case "info": 154 | log.SetLevel(log.InfoLevel) 155 | case "debug": 156 | log.SetLevel(log.DebugLevel) 157 | case "trace": 158 | log.SetLevel(log.TraceLevel) 159 | default: 160 | log.SetLevel(log.InfoLevel) 161 | } 162 | 163 | log.WithFields(log.Fields{ 164 | "path": dataDir, 165 | "maxtx": maxTxsPerBlock, 166 | "maxtime": maxTimeToGenerateBlock, 167 | "porthttp": portHttp, 168 | "portP2p": portP2p, 169 | "hostP2p": hostP2p, 170 | "advertiseAddress": advertiseAddress, 171 | "peerAddresses": peerAddresses, 172 | "bulkLoading": bulkLoading, 173 | "loglevel": loglevel, 174 | }).Info("configurations: ") 175 | 176 | if !funk.IsEmpty(peerAddresses) { 177 | peerAddressesArray = strings.Split(peerAddresses, ",") 178 | } 179 | // now start Blocace server 180 | server() 181 | return nil 182 | }, 183 | }, 184 | { 185 | Name: "keygen", 186 | Aliases: []string{"k"}, 187 | Usage: "generate and register an admin account", 188 | HelpName: "blocace keygen", 189 | Flags: []cli.Flag{ 190 | cli.StringFlag{ 191 | Name: "dir, d", 192 | Value: "data", 193 | Usage: "the path to the folder of data persistency", 194 | Destination: &dataDir, 195 | }, 196 | }, 197 | Action: func(c *cli.Context) error { 198 | keygen() 199 | return nil 200 | }, 201 | }, 202 | } 203 | 204 | err := app.Run(os.Args) 205 | if err != nil { 206 | log.Fatal(err) 207 | } 208 | } 209 | 210 | func server() { 211 | var bc *blockchain.Blockchain 212 | var r *pool.Receiver 213 | var dbFile = dataDir + filepath.Dir("/") + "blockchain.db" 214 | 215 | if _, err := os.Stat(dataDir); os.IsNotExist(err) { 216 | os.Mkdir(dataDir, os.ModePerm) 217 | } 218 | 219 | if blockchain.DbExists(dbFile) { 220 | log.Info("db file exists.") 221 | bc = blockchain.NewBlockchain(dbFile, dataDir) 222 | 223 | if !bc.IsComplete() { 224 | log.Errorf("local blockchain verification failed. exiting...") 225 | os.Exit(1) 226 | } 227 | } else { 228 | log.Info("cannot find the db file. creating new...") 229 | bc = blockchain.CreateBlockchain(dbFile, dataDir) 230 | generateAdminAccount(bc.Db) 231 | } 232 | 233 | p := p2p.NewP2P(bc, hostP2p, uint16(portP2p), advertiseAddress, peerAddressesArray...) 234 | p.SyncMappingsFromPeers() 235 | time.Sleep(200 * time.Millisecond) // wait for p2p connection to release before sending another request 236 | p.SyncAccountsFromPeers() 237 | time.Sleep(200 * time.Millisecond) 238 | p.SyncPeerBlockchains() 239 | 240 | r = pool.NewReceiver(p, maxTxsPerBlock, maxTimeToGenerateBlock) 241 | go r.Monitor() 242 | 243 | httpHandler := webapi.NewHTTPHandler(p.BlockchainForest, r, p, secret, version) 244 | router := mux.NewRouter() 245 | router.NotFoundHandler = http.HandlerFunc(webapi.ErrorHandler) 246 | router.Handle("/", httpHandler) 247 | router.HandleFunc("/jwt", httpHandler.HandleJWT).Methods("POST", "GET") 248 | router.HandleFunc("/jwt/challenge/{address}", httpHandler.JWTChallenge).Methods("GET") 249 | router.HandleFunc("/peers", httpHandler.HandlePeers).Methods("GET") // user 250 | router.HandleFunc("/info", httpHandler.HandleInfo).Methods("GET") // user 251 | router.HandleFunc("/block/{blockchainId}/{blockId}", httpHandler.HandleBlockInfo).Methods("GET") // user 252 | router.HandleFunc("/verification/{blockchainId}/{blockId}/{txId}", httpHandler.HandleMerklePath).Methods("GET") // user 253 | router.HandleFunc("/search/{collection}", httpHandler.HandleSearch).Methods("POST", "GET") // user 254 | router.HandleFunc("/document/{collection}", httpHandler.HandleTransaction).Methods("POST") // user 255 | router.HandleFunc("/collection", httpHandler.CollectionMappingCreation).Methods("POST") // admin 256 | router.HandleFunc("/collections", httpHandler.CollectionList).Methods("GET") // user 257 | router.HandleFunc("/collection/{name}", httpHandler.CollectionMappingGet).Methods("GET") // user 258 | router.HandleFunc("/account", httpHandler.AccountRegistration).Methods("POST") 259 | router.HandleFunc("/account/{address}", httpHandler.AccountUpdate).Methods("POST") // admin 260 | router.HandleFunc("/account/{address}", httpHandler.AccountGet).Methods("GET") // user 261 | router.HandleFunc("/setaccountpermission/{address}", httpHandler.SetAccountReadWrite).Methods("POST") // admin 262 | 263 | if bulkLoading == "true" { 264 | router.HandleFunc("/bulk/{collection}", httpHandler.HandleTransactionBulk).Methods("POST") // everyone 265 | } 266 | 267 | handler := cors.AllowAll().Handler(router) 268 | 269 | server := &http.Server{Addr: ":" + portHttp, Handler: handler} 270 | 271 | go func() { 272 | if err := server.ListenAndServe(); err == nil { 273 | log.Error(err) 274 | } 275 | }() 276 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 277 | defer cancel() 278 | 279 | sigs := make(chan os.Signal) 280 | done := make(chan bool) 281 | 282 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 283 | 284 | go func() { 285 | sig := <-sigs 286 | fmt.Println() 287 | fmt.Println(sig) 288 | 289 | done <- true 290 | }() 291 | 292 | log.Info("awaiting signal...") 293 | <-done 294 | 295 | // Release resources associated to node at the end of the program. 296 | p.Node.Close() 297 | 298 | if err := server.Shutdown(ctx); err != nil { 299 | log.Error(err) 300 | } 301 | log.Info("exiting...") 302 | } 303 | 304 | func keygen() { 305 | if blockchain.DbExists(dataDir + filepath.Dir("/") + "blockchain.db") { 306 | log.Info("db file exists. generating an admin keypair and registering an account...") 307 | db, err := bolt.Open(dataDir+filepath.Dir("/")+"blockchain.db", 0600, nil) 308 | if err != nil { 309 | log.Panic(err) 310 | } 311 | 312 | generateAdminAccount(db) 313 | 314 | } else { 315 | log.Panic("cannot find the db file. please run blocace server first to create the database") 316 | } 317 | } 318 | 319 | func generateAdminAccount(db *bolt.DB) { 320 | privKey, err := crypto.GenerateKey() 321 | if err != nil { 322 | log.Panic(err) 323 | } 324 | 325 | pubKey := privKey.PublicKey 326 | account := blockchain.Account{Role: blockchain.Role{Name: "admin"}, PublicKey: "04" + fmt.Sprintf("%x", pubKey.X) + fmt.Sprintf("%x", pubKey.Y)} 327 | addressBytes := []byte(crypto.PubkeyToAddress(pubKey).String()) 328 | 329 | result := account.Marshal() 330 | 331 | err = db.Update(func(dbtx *bolt.Tx) error { 332 | aBucket, _ := dbtx.CreateBucketIfNotExists([]byte(blockchain.AccountsBucket)) 333 | err := aBucket.Put(addressBytes, result) 334 | if err != nil { 335 | log.Error(err) 336 | } 337 | 338 | return nil 339 | }) 340 | 341 | if err != nil { 342 | log.Panic(err) 343 | } 344 | log.Info("the admin account has been created and registered successfully") 345 | 346 | fmt.Printf("\n####################\nPRIVATE KEY: %x\nWARNING: THIS PRIVATE KEY ONLY SHOWS ONCE. PLEASE SAVE IT NOW AND KEEP IT SAFE. YOU ARE THE ONLY PERSON THAT IS SUPPOSED TO OWN THIS KEY IN THE WORLD.\n####################\n\n", privKey.D) 347 | 348 | // priv, err := crypto.HexToECDSA(fmt.Sprintf("%x", privKey.D)) 349 | // if err != nil { 350 | // log.Error(err) 351 | // } 352 | // fmt.Printf("%x\n%x\n", priv.PublicKey.X, priv.PublicKey.Y) 353 | } 354 | -------------------------------------------------------------------------------- /p2p/accounts_p2p.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/codingpeasant/blocace/blockchain" 10 | ) 11 | 12 | // AccountsP2P represents all the accounts from a peer 13 | type AccountsP2P struct { 14 | Accounts map[string]blockchain.Account 15 | } 16 | 17 | // Marshal serializes AccountsP2P 18 | func (a AccountsP2P) Marshal() []byte { 19 | var result bytes.Buffer 20 | 21 | encoder := gob.NewEncoder(&result) 22 | err := encoder.Encode(a) 23 | if err != nil { 24 | log.Error(err) 25 | } 26 | 27 | return result.Bytes() 28 | } 29 | 30 | // unmarshalAccountsP2P deserializes encoded bytes to AccountsP2P object 31 | func unmarshalAccountsP2P(a []byte) (AccountsP2P, error) { 32 | var accountsP2p AccountsP2P 33 | 34 | decoder := gob.NewDecoder(bytes.NewReader(a)) 35 | err := decoder.Decode(&accountsP2p) 36 | if err != nil { 37 | log.Error(err) 38 | } 39 | 40 | return accountsP2p, err 41 | } 42 | -------------------------------------------------------------------------------- /p2p/block_p2p.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/codingpeasant/blocace/blockchain" 11 | ) 12 | 13 | // BlockP2P represents a block from a peer 14 | type BlockP2P struct { 15 | PeerId []byte 16 | Timestamp int64 17 | PrevBlockHash []byte 18 | Height uint64 19 | Hash []byte 20 | IsTip bool 21 | TotalTransactions int 22 | Transactions []blockchain.Transaction 23 | } 24 | 25 | // Marshal serializes BlockP2P 26 | func (b BlockP2P) Marshal() []byte { 27 | var result bytes.Buffer 28 | 29 | encoder := gob.NewEncoder(&result) 30 | err := encoder.Encode(b) 31 | if err != nil { 32 | log.Error(err) 33 | } 34 | 35 | return result.Bytes() 36 | } 37 | 38 | // MapToBlock verified each transaction in the BlockP2P on wire and convert it to blockchain.Block 39 | func (b BlockP2P) MapToBlock() (*blockchain.Block, error) { 40 | var transactions []*blockchain.Transaction 41 | 42 | // cannot use range here because the memory address for elements being iterated is always the same one 43 | for i := 0; i < len(b.Transactions); i++ { 44 | if bytes.Compare(b.Transactions[i].Signature, []byte{}) == 0 { // skipping for genisis transation and bulk loading 45 | transactions = append(transactions, &b.Transactions[i]) 46 | } else if blockchain.IsValidSig(b.Transactions[i].RawData, b.Transactions[i].PubKey, b.Transactions[i].Signature) { 47 | transactions = append(transactions, &b.Transactions[i]) 48 | } else { 49 | transactions = nil 50 | return nil, fmt.Errorf("transaction signature verification failed, abandon this block") 51 | } 52 | } 53 | 54 | return &blockchain.Block{Timestamp: b.Timestamp, PrevBlockHash: b.PrevBlockHash, 55 | Height: b.Height, Hash: b.Hash, TotalTransactions: b.TotalTransactions, Transactions: transactions}, nil 56 | } 57 | 58 | // unmarshalBlockP2P deserializes encoded bytes to BlockP2P object 59 | func unmarshalBlockP2P(b []byte) (BlockP2P, error) { 60 | var blockP2P BlockP2P 61 | 62 | decoder := gob.NewDecoder(bytes.NewReader(b)) 63 | err := decoder.Decode(&blockP2P) 64 | if err != nil { 65 | log.Error(err) 66 | } 67 | 68 | return blockP2P, err 69 | } 70 | -------------------------------------------------------------------------------- /p2p/blockchain_forest.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/boltdb/bolt" 14 | "github.com/codingpeasant/blocace/blockchain" 15 | "github.com/thoas/go-funk" 16 | 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | const peerBlockchainDir = "peers" 21 | 22 | // BlockchainForest defines the local and peer chains 23 | type BlockchainForest struct { 24 | Local *blockchain.Blockchain 25 | Peers map[string]*blockchain.Blockchain 26 | } 27 | 28 | // AddBlock persist the broadcasted or requested block from a peer to local peer blockchain db and index it 29 | func (b *BlockchainForest) AddBlock(blockP2p BlockP2P) { 30 | peerIdStr := fmt.Sprintf("%x", blockP2p.PeerId) 31 | 32 | block, err := blockP2p.MapToBlock() 33 | 34 | if err != nil { 35 | log.Error(err) 36 | return 37 | } 38 | 39 | if bytes.Compare(block.Hash, block.SetHash()) != 0 { 40 | log.Errorf("block hash verification failed, abandon this block") 41 | return 42 | } 43 | 44 | if b.Peers[peerIdStr] == nil { 45 | log.Infof("peer %s blockchain db not found, creating one...", peerIdStr) 46 | peerBlockchainsDbFile := b.Local.DataDir + filepath.Dir("/") + peerBlockchainDir + filepath.Dir("/") + fmt.Sprintf("%x", blockP2p.PeerId) + ".db" 47 | 48 | db, err := bolt.Open(peerBlockchainsDbFile, 0600, nil) 49 | if err != nil { 50 | log.Panic(err) 51 | } 52 | 53 | err = db.Update(func(dbtx *bolt.Tx) error { 54 | bBucket, err := dbtx.CreateBucket([]byte(blockchain.BlocksBucket)) 55 | if err != nil { 56 | log.Panic(err) 57 | } 58 | 59 | _, err = dbtx.CreateBucket([]byte(blockchain.TransactionsBucket)) 60 | if err != nil { 61 | log.Panic(err) 62 | } 63 | 64 | err = bBucket.Put([]byte("peerId"), blockP2p.PeerId) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | }) 71 | 72 | if blockP2p.IsTip { 73 | b.Peers[peerIdStr] = &blockchain.Blockchain{Tip: blockP2p.Hash, PeerId: blockP2p.PeerId, Db: db, Search: b.Local.Search, DataDir: b.Local.DataDir} 74 | } else { 75 | b.Peers[peerIdStr] = &blockchain.Blockchain{PeerId: blockP2p.PeerId, Db: db, Search: b.Local.Search, DataDir: b.Local.DataDir} 76 | } 77 | } else if bytes.Compare(b.Peers[peerIdStr].Tip, block.Hash) == 0 { 78 | log.Infof("peer %s tip is already up-to-date: %x", peerIdStr, b.Peers[peerIdStr].Tip) 79 | return 80 | } 81 | 82 | if blockP2p.IsTip { 83 | b.Peers[peerIdStr].Tip = blockP2p.Hash 84 | } 85 | 86 | _, err = block.Persist(b.Peers[peerIdStr].Db, blockP2p.IsTip) 87 | if err != nil { 88 | log.Error(err) 89 | } 90 | 91 | start := time.Now().UnixNano() 92 | log.Debugf("start indexing the block at %d for peer blockchain %s...", start, peerIdStr) 93 | b.Local.Search.IndexBlock(block, blockP2p.PeerId) 94 | end := time.Now().UnixNano() 95 | log.Debug("end indexing the block:" + strconv.FormatInt(end, 10) + ", duration:" + strconv.FormatInt((end-start)/1000000, 10) + "ms") 96 | } 97 | 98 | // GetBlock returns a local or peer block as requested 99 | func (b *BlockchainForest) GetBlock(peerId []byte, blockId []byte, blockOnly bool) BlockP2P { 100 | var block *blockchain.Block 101 | var blockP2P BlockP2P 102 | var blockchainDb *bolt.DB 103 | 104 | if bytes.Compare(peerId, b.Local.PeerId) == 0 { 105 | blockchainDb = b.Local.Db 106 | } else if !funk.IsEmpty(b.Peers[fmt.Sprintf("%x", peerId)]) { 107 | blockchainDb = b.Peers[fmt.Sprintf("%x", peerId)].Db 108 | } else { 109 | log.Warnf("peerId %x does not exist", peerId) 110 | return blockP2P 111 | } 112 | 113 | err := blockchainDb.View(func(dbtx *bolt.Tx) error { 114 | blockBucket := dbtx.Bucket([]byte(blockchain.BlocksBucket)) 115 | 116 | if blockBucket == nil { 117 | return errors.New("block bucket doesn't exist") 118 | } 119 | 120 | encodedBlock := blockBucket.Get(blockId) 121 | 122 | if encodedBlock == nil { 123 | return nil 124 | } 125 | block = blockchain.DeserializeBlock(encodedBlock) 126 | 127 | return nil 128 | }) 129 | 130 | if err != nil { 131 | log.Error(err) 132 | return blockP2P 133 | } 134 | 135 | if block == nil { 136 | return blockP2P 137 | } else if blockOnly { 138 | return BlockP2P{PeerId: peerId, Timestamp: block.Timestamp, PrevBlockHash: block.PrevBlockHash, Height: block.Height, Hash: block.Hash, 139 | TotalTransactions: block.TotalTransactions} 140 | } 141 | 142 | var transactions []blockchain.Transaction 143 | err = blockchainDb.View(func(dbtx *bolt.Tx) error { 144 | // Assume bucket exists and has keys 145 | c := dbtx.Bucket([]byte(blockchain.TransactionsBucket)).Cursor() 146 | 147 | prefix := block.Hash 148 | for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() { 149 | transactions = append(transactions, *blockchain.DeserializeTransaction(v)) 150 | } 151 | 152 | return nil 153 | }) 154 | 155 | if err != nil { 156 | log.Error(err) 157 | return blockP2P 158 | } 159 | 160 | return BlockP2P{PeerId: peerId, Timestamp: block.Timestamp, PrevBlockHash: block.PrevBlockHash, Height: block.Height, Hash: block.Hash, 161 | TotalTransactions: block.TotalTransactions, Transactions: transactions} 162 | } 163 | 164 | // NewBlockchainForest initializes the peer blockchains by reading existing dbs from peerBlockchainDir which will be created should not exist 165 | func NewBlockchainForest(bcLocal *blockchain.Blockchain) *BlockchainForest { 166 | peers := make(map[string]*blockchain.Blockchain) 167 | peerBlockchainsDirRoot := bcLocal.DataDir + filepath.Dir("/") + peerBlockchainDir 168 | 169 | if blockchain.DbExists(peerBlockchainsDirRoot) == false { 170 | log.Infof("did not find peer db dir %s, creating one...", peerBlockchainsDirRoot) 171 | err := os.MkdirAll(peerBlockchainsDirRoot, 0700) 172 | if err != nil { 173 | log.Fatal(err) 174 | } 175 | 176 | } else { 177 | log.Info("opening existing peer blockchains...") 178 | files, err := ioutil.ReadDir(peerBlockchainsDirRoot) 179 | if err != nil { 180 | log.Fatal(err) 181 | } 182 | 183 | // initialize all other peer blockchains than the local 184 | for _, file := range files { 185 | db, err := bolt.Open(peerBlockchainsDirRoot+filepath.Dir("/")+file.Name(), 0600, nil) 186 | if err != nil { 187 | log.Warnf("cannot open blockchain db %s: %s", peerBlockchainsDirRoot+filepath.Dir("/")+file.Name(), err.Error()) 188 | continue 189 | } 190 | 191 | var tip, peerId []byte 192 | err = db.View(func(dbtx *bolt.Tx) error { 193 | bBucket := dbtx.Bucket([]byte(blockchain.BlocksBucket)) 194 | tip = bBucket.Get([]byte("l")) 195 | 196 | return nil 197 | }) 198 | 199 | if err != nil { 200 | log.Warnf("cannot get tip of blockchain at %s: %s", peerBlockchainsDirRoot+filepath.Dir("/")+file.Name(), err.Error()) 201 | continue 202 | } 203 | 204 | err = db.View(func(dbtx *bolt.Tx) error { 205 | bBucket := dbtx.Bucket([]byte(blockchain.BlocksBucket)) 206 | peerId = bBucket.Get([]byte("peerId")) 207 | 208 | return nil 209 | }) 210 | 211 | if err != nil { 212 | log.Warnf("cannot get peerId of blockchain at %s: %s", peerBlockchainsDirRoot+filepath.Dir("/")+file.Name(), err.Error()) 213 | continue 214 | } 215 | peers[fmt.Sprintf("%x", peerId)] = &blockchain.Blockchain{Tip: tip, PeerId: peerId, Db: db, DataDir: bcLocal.DataDir} 216 | } 217 | } 218 | 219 | return &BlockchainForest{bcLocal, peers} 220 | } 221 | -------------------------------------------------------------------------------- /p2p/challenge_word_p2p.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // ChallengeWordP2P represents a challengeWord cache item from a peer 11 | type ChallengeWordP2P struct { 12 | ChallengeWord string 13 | Address string 14 | } 15 | 16 | // Marshal serializes ChallengeWordP2P 17 | func (a ChallengeWordP2P) Marshal() []byte { 18 | var result bytes.Buffer 19 | 20 | encoder := gob.NewEncoder(&result) 21 | err := encoder.Encode(a) 22 | if err != nil { 23 | log.Error(err) 24 | } 25 | 26 | return result.Bytes() 27 | } 28 | 29 | // unmarshalChallengeWordP2P deserializes encoded bytes to ChallengeWordP2P object 30 | func unmarshalChallengeWordP2P(a []byte) (ChallengeWordP2P, error) { 31 | var challengeWordP2P ChallengeWordP2P 32 | 33 | decoder := gob.NewDecoder(bytes.NewReader(a)) 34 | err := decoder.Decode(&challengeWordP2P) 35 | if err != nil { 36 | log.Error(err) 37 | } 38 | 39 | return challengeWordP2P, err 40 | } 41 | -------------------------------------------------------------------------------- /p2p/mappings_p2p.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/codingpeasant/blocace/blockchain" 10 | ) 11 | 12 | // MappingsP2P represents all the collection mappings (schemas) from a peer 13 | type MappingsP2P struct { 14 | Mappings map[string]blockchain.DocumentMapping 15 | } 16 | 17 | // Marshal serializes MappingsP2P 18 | func (a MappingsP2P) Marshal() []byte { 19 | var result bytes.Buffer 20 | 21 | encoder := gob.NewEncoder(&result) 22 | err := encoder.Encode(a) 23 | if err != nil { 24 | log.Error(err) 25 | } 26 | 27 | return result.Bytes() 28 | } 29 | 30 | // unmarshalMappingsP2P deserializes encoded bytes to MappingsP2P object 31 | func unmarshalMappingsP2P(a []byte) (MappingsP2P, error) { 32 | var mappingsP2p MappingsP2P 33 | 34 | decoder := gob.NewDecoder(bytes.NewReader(a)) 35 | err := decoder.Decode(&mappingsP2p) 36 | if err != nil { 37 | log.Error(err) 38 | } 39 | 40 | return mappingsP2p, err 41 | } 42 | -------------------------------------------------------------------------------- /p2p/p2p.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/hex" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/boltdb/bolt" 16 | "github.com/patrickmn/go-cache" 17 | "github.com/perlin-network/noise" 18 | "github.com/perlin-network/noise/kademlia" 19 | log "github.com/sirupsen/logrus" 20 | "github.com/thoas/go-funk" 21 | 22 | "github.com/codingpeasant/blocace/blockchain" 23 | ) 24 | 25 | var DefaultPort = 6091 26 | var PingIntervalInMs = 3000 27 | 28 | // P2P is the main object to handle networking-related messages 29 | type P2P struct { 30 | Node *noise.Node 31 | BlockchainForest *BlockchainForest 32 | ChallengeWordsCache *cache.Cache 33 | overlay *kademlia.Protocol 34 | Accounts map[string]blockchain.Account // used by http 35 | mappings map[string]blockchain.DocumentMapping 36 | } 37 | 38 | // BroadcastObject sends a serializable object to all the known peers 39 | func (p *P2P) BroadcastObject(object noise.Serializable) { 40 | // add the account(s) to local cache before broadcasting 41 | accountsToAdd, ok := object.(AccountsP2P) 42 | if ok { 43 | for address, account := range accountsToAdd.Accounts { 44 | p.Accounts[address] = account // update the cache 45 | } 46 | } 47 | 48 | // add the mapping(s) to local cache before broadcasting 49 | mappingsToAdd, ok := object.(MappingsP2P) 50 | if ok { 51 | for collection, mapping := range mappingsToAdd.Mappings { 52 | p.mappings[collection] = mapping // update the cache 53 | } 54 | } 55 | 56 | for _, id := range p.overlay.Table().Peers() { 57 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 58 | err := p.Node.SendMessage(ctx, id.Address, object) 59 | cancel() 60 | 61 | if err != nil { 62 | log.Errorf("failed to send object to %s(%s). Skipping... [error: %s]\n", 63 | id.Address, 64 | id.ID.String(), 65 | err, 66 | ) 67 | continue 68 | } 69 | } 70 | } 71 | 72 | // GetPeers returns currently know peers in json format 73 | func (p *P2P) GetPeers() []byte { 74 | var peers []noise.ID 75 | for _, peer := range p.overlay.Table().Peers() { 76 | peers = append(peers, peer) 77 | } 78 | peersJSON, _ := json.Marshal(peers) 79 | 80 | return peersJSON 81 | } 82 | 83 | // SyncAccountsFromPeers sends rpc to peers to sync the accounts 84 | func (p *P2P) SyncAccountsFromPeers() { 85 | for _, id := range p.overlay.Table().Peers() { 86 | sendAccountsRequest(p.Accounts, p.Node, id, p.BlockchainForest.Local) 87 | } 88 | } 89 | 90 | // SyncMappingsFromPeers sends rpc to peers to sync the mappings 91 | func (p *P2P) SyncMappingsFromPeers() { 92 | for _, id := range p.overlay.Table().Peers() { 93 | sendMappingsRequest(p.mappings, p.Node, id, p.BlockchainForest.Local.Search) 94 | } 95 | } 96 | 97 | // SyncPeerBlockchains sends rpc to all known peers to sync the peer blockchains to local 98 | func (p *P2P) SyncPeerBlockchains() { 99 | for _, id := range p.overlay.Table().Peers() { 100 | syncPeerBlockchain(p.Node, id, p.BlockchainForest, false) // startup sync, not reverse sync 101 | } 102 | } 103 | 104 | // NewP2P initializes the P2P node with messages and handlers 105 | func NewP2P(bc *blockchain.Blockchain, bindHost string, bindPort uint16, advertiseAddress string, connectionAddresses ...string) *P2P { 106 | accounts := initializeAccounts(bc.Db) 107 | mappings := initializeMappings(bc.Db) 108 | 109 | blockchainForest := NewBlockchainForest(bc) 110 | challengeWordsCache := cache.New(30*time.Second, 1*time.Minute) 111 | 112 | var p2pPrivKey noise.PrivateKey 113 | var p2pPrivKeyBytes []byte 114 | 115 | // make sure to reuse the priv key 116 | err := bc.Db.View(func(dbtx *bolt.Tx) error { 117 | bBucket := dbtx.Bucket([]byte(blockchain.BlocksBucket)) 118 | p2pPrivKeyBytes = bBucket.Get([]byte(blockchain.P2PPrivateKeyKey)) 119 | 120 | return nil 121 | }) 122 | 123 | if err != nil { 124 | log.Panic(err) 125 | } 126 | copy(p2pPrivKey[:], p2pPrivKeyBytes) 127 | 128 | // Create a new configured node. 129 | node, err := noise.NewNode( 130 | noise.WithNodeBindHost(net.ParseIP(bindHost)), 131 | noise.WithNodeBindPort(bindPort), 132 | noise.WithNodeAddress(advertiseAddress), 133 | noise.WithNodePrivateKey(p2pPrivKey), 134 | ) 135 | 136 | if err != nil { 137 | log.Panic(err) 138 | } 139 | 140 | // Register the chatMessage Go type to the node with an associated unmarshal function. 141 | node.RegisterMessage(RequestP2P{}, unmarshalRequestP2P) 142 | node.RegisterMessage(AccountsP2P{}, unmarshalAccountsP2P) 143 | node.RegisterMessage(MappingsP2P{}, unmarshalMappingsP2P) 144 | node.RegisterMessage(ChallengeWordP2P{}, unmarshalChallengeWordP2P) 145 | node.RegisterMessage(BlockP2P{}, unmarshalBlockP2P) 146 | 147 | // Register a message handler to the node. 148 | node.Handle(func(ctx noise.HandlerContext) error { 149 | if ctx.IsRequest() { 150 | obj, err := ctx.DecodeMessage() 151 | if err != nil { 152 | return err 153 | } 154 | 155 | requestP2P, ok := obj.(RequestP2P) 156 | if !ok { 157 | return nil 158 | } 159 | 160 | switch requestP2P.RequestType { 161 | case accountsRequestType: 162 | ctx.SendMessage(handleAccountsRequest(requestP2P, accounts)) 163 | accountsRequestReverse(requestP2P, accounts, node, ctx.ID(), blockchainForest.Local) // sync new accounts from remote 164 | case mappingsRequestType: 165 | ctx.SendMessage(handleMappingsRequest(requestP2P, mappings)) 166 | mappingsRequestReverse(requestP2P, mappings, node, ctx.ID(), blockchainForest.Local.Search) // sync new mappings from remote 167 | case blockRequestType: 168 | ctx.SendMessage(handleBlockRequest(requestP2P, blockchainForest)) 169 | // reversely sync the blockchain but don't reverse the reverse 170 | if requestP2P.RequestParameters["local"] == "tip" && requestP2P.RequestParameters["reverse"] != "reverse" { 171 | syncPeerBlockchain(node, ctx.ID(), blockchainForest, true) 172 | } 173 | default: 174 | log.Warnf("got unsupported RequestP2P request type: +%v", requestP2P) 175 | return nil 176 | } 177 | 178 | } else { 179 | obj, err := ctx.DecodeMessage() 180 | if err != nil { 181 | return err 182 | } 183 | 184 | switch objectP2p := obj.(type) { 185 | case AccountsP2P: 186 | for address, account := range objectP2p.Accounts { 187 | if funk.IsEmpty(accounts[address]) || accounts[address].LastModified < account.LastModified { 188 | accounts[address] = account // update the cache 189 | if err = bc.RegisterAccount([]byte(address), account); err != nil { 190 | return err 191 | } 192 | } 193 | } 194 | case MappingsP2P: 195 | for mappingName, mapping := range objectP2p.Mappings { 196 | if funk.IsEmpty(mappings[mappingName]) { 197 | mappings[mappingName] = mapping // update the cache 198 | if _, err = bc.Search.CreateMapping(mapping); err != nil { 199 | return err 200 | } 201 | } 202 | } 203 | case ChallengeWordP2P: 204 | if funk.IsEmpty(objectP2p.Address) && funk.IsEmpty(objectP2p.ChallengeWord) { 205 | challengeWordsCache.Set(objectP2p.ChallengeWord, objectP2p.Address, cache.DefaultExpiration) 206 | } 207 | case BlockP2P: 208 | log.Debugf("BlockFromPeer: %s(%s) > %+x; height: %d\n", ctx.ID().Address, ctx.ID().ID.String(), objectP2p.Hash, objectP2p.Height) 209 | blockchainForest.AddBlock(objectP2p) 210 | case RequestP2P: 211 | ctx.SendMessage(handleBlockRequest(objectP2p, blockchainForest)) 212 | default: 213 | return errors.New("cannot parse the object from peer: " + ctx.ID().ID.String()) 214 | } 215 | } 216 | 217 | return nil 218 | }) 219 | 220 | // Instantiate Kademlia. 221 | events := kademlia.Events{ 222 | OnPeerAdmitted: func(id noise.ID) { 223 | log.Infof("learned about a new peer %s (%s).\n", id.Address, id.ID.String()) 224 | }, 225 | OnPeerEvicted: func(id noise.ID) { 226 | log.Infof("forgotten a peer %s (%s).\n", id.Address, id.ID.String()) 227 | }, 228 | } 229 | 230 | overlay := kademlia.New(kademlia.WithProtocolEvents(events)) 231 | 232 | // Bind Kademlia to the node. 233 | node.Bind(overlay.Protocol()) 234 | 235 | // Have the node start listening for new peers. 236 | err = node.Listen() 237 | if err != nil { 238 | log.Panic(err) 239 | } 240 | 241 | if !funk.IsEmpty(connectionAddresses) { 242 | // Ping nodes to initially bootstrap and discover peers from. 243 | bootstrap(node, connectionAddresses) 244 | // Attempt to discover peers if we are bootstrapped to any nodes. 245 | discover(overlay) 246 | } else { 247 | log.Info("no peer address(es) provided, starting without trying to discover") 248 | } 249 | 250 | // remove stale peers 251 | go func() { 252 | ticker := time.NewTicker(time.Duration(PingIntervalInMs) * time.Millisecond) 253 | 254 | for range ticker.C { 255 | var peersAlive []noise.ID 256 | for _, id := range overlay.Table().Peers() { 257 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 258 | client, _ := node.Ping(ctx, id.Address) 259 | cancel() 260 | if client != nil { 261 | peersAlive = append(peersAlive, client.ID()) 262 | } 263 | } 264 | 265 | for _, id := range overlay.Table().Peers() { 266 | if !funk.Contains(peersAlive, id) { 267 | overlay.Table().Delete(id.ID) 268 | } 269 | } 270 | } 271 | }() 272 | 273 | return &P2P{Node: node, overlay: overlay, BlockchainForest: blockchainForest, ChallengeWordsCache: challengeWordsCache, Accounts: accounts, mappings: mappings} 274 | } 275 | 276 | // bootstrap pings and dials an array of network addresses which we may interact with and discover peers from. 277 | func bootstrap(node *noise.Node, addresses []string) { 278 | for _, addr := range addresses { 279 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 280 | _, err := node.Ping(ctx, addr) 281 | cancel() 282 | 283 | if err != nil { 284 | log.Warnf("failed to ping bootstrap node (%s). [%s]\n", addr, err) 285 | continue 286 | } 287 | } 288 | } 289 | 290 | // discover uses Kademlia to discover new peers from nodes we already are aware of. 291 | func discover(overlay *kademlia.Protocol) { 292 | ids := overlay.Discover() 293 | 294 | var str []string 295 | for _, id := range ids { 296 | str = append(str, fmt.Sprintf("%s (%s)", id.Address, id.ID.String())) 297 | } 298 | 299 | if len(ids) > 0 { 300 | log.Infof("discovered %d peer(s): [%v]\n", len(ids), strings.Join(str, ", ")) 301 | } else { 302 | log.Warn("did not discover any peers.") 303 | } 304 | } 305 | 306 | // initializeAccounts loads the accounts from disk to RAM 307 | func initializeAccounts(db *bolt.DB) map[string]blockchain.Account { 308 | accountMap := make(map[string]blockchain.Account) 309 | db.View(func(tx *bolt.Tx) error { 310 | // Assume bucket exists and has keys 311 | b := tx.Bucket([]byte(blockchain.AccountsBucket)) 312 | c := b.Cursor() 313 | 314 | for k, v := c.First(); k != nil; k, v = c.Next() { 315 | accountMap[fmt.Sprintf("%s", k)] = *blockchain.DeserializeAccount(v) 316 | } 317 | 318 | return nil 319 | }) 320 | return accountMap 321 | } 322 | 323 | // initializeMappings loads the mappings (schemas) from disk to RAM 324 | func initializeMappings(db *bolt.DB) map[string]blockchain.DocumentMapping { 325 | collectionMapings := make(map[string]blockchain.DocumentMapping) 326 | db.View(func(tx *bolt.Tx) error { 327 | // Assume bucket exists and has keys 328 | b := tx.Bucket([]byte(blockchain.CollectionsBucket)) 329 | c := b.Cursor() 330 | 331 | for k, v := c.First(); k != nil; k, v = c.Next() { 332 | collectionMapings[fmt.Sprintf("%s", k)] = *blockchain.DeserializeDocumentMapping(v) 333 | } 334 | 335 | return nil 336 | }) 337 | return collectionMapings 338 | } 339 | 340 | // handleAccountsRequest returns the new accounts which the peer doesn't have or has a older version of 341 | func handleAccountsRequest(request RequestP2P, accountsLocal map[string]blockchain.Account) AccountsP2P { 342 | accountsToSend := make(map[string]blockchain.Account) 343 | for address, account := range accountsLocal { 344 | if account.Role.Name == "admin" { 345 | continue 346 | } 347 | if funk.IsEmpty(request.RequestParameters[address]) { 348 | accountsToSend[address] = account 349 | } else { 350 | peerLastModified, err := strconv.ParseInt(request.RequestParameters[address], 10, 64) 351 | if err != nil { 352 | continue 353 | } 354 | if account.LastModified > peerLastModified { 355 | accountsToSend[address] = account 356 | } 357 | } 358 | } 359 | 360 | return AccountsP2P{Accounts: accountsToSend} 361 | } 362 | 363 | // handleMappingsRequest returns the new mappings which the peer doesn't have 364 | func handleMappingsRequest(request RequestP2P, mappingsLocal map[string]blockchain.DocumentMapping) MappingsP2P { 365 | mappingsToSend := make(map[string]blockchain.DocumentMapping) 366 | for collectionName, mapping := range mappingsLocal { 367 | if collectionName == "default" { 368 | continue 369 | } 370 | if funk.IsEmpty(request.RequestParameters[collectionName]) { 371 | mappingsToSend[collectionName] = mapping 372 | } 373 | } 374 | 375 | return MappingsP2P{Mappings: mappingsToSend} 376 | } 377 | 378 | // handleBlockRequest handles 1) local tip block; 2) local block; 3) peer block 379 | func handleBlockRequest(request RequestP2P, bf *BlockchainForest) BlockP2P { 380 | var blockToReturn BlockP2P 381 | if !funk.IsEmpty(request.RequestParameters["local"]) { 382 | requestValue := request.RequestParameters["local"] 383 | if requestValue == "tip" { 384 | blockToReturn = bf.GetBlock(bf.Local.PeerId, bf.Local.Tip, false) 385 | blockToReturn.IsTip = true // mark as tip for peer to process 386 | 387 | } else { 388 | blockId, err := hex.DecodeString(requestValue) 389 | if err != nil { 390 | log.Error(err) 391 | return blockToReturn 392 | } 393 | blockToReturn = bf.GetBlock(bf.Local.PeerId, blockId, false) 394 | } 395 | 396 | } else { 397 | for peerIdString, blockIdString := range request.RequestParameters { // should be only one round 398 | 399 | blockId, err := hex.DecodeString(blockIdString) 400 | peerId, err := hex.DecodeString(peerIdString) 401 | if err != nil { 402 | log.Error(err) 403 | return blockToReturn 404 | } 405 | blockToReturn = bf.GetBlock(peerId, blockId, false) 406 | } 407 | } 408 | 409 | return blockToReturn 410 | } 411 | 412 | // mappingsRequestReverse checks if a peer has mapping(s) that is new and request for them 413 | func mappingsRequestReverse(request RequestP2P, mappingsLocal map[string]blockchain.DocumentMapping, node *noise.Node, id noise.ID, search *blockchain.Search) { 414 | for _, mapping := range request.RequestParameters { 415 | if mapping == "default" { 416 | continue 417 | } 418 | 419 | if funk.IsEmpty(mappingsLocal[mapping]) { 420 | sendMappingsRequest(mappingsLocal, node, id, search) 421 | break 422 | } 423 | } 424 | } 425 | 426 | // accountsRequestReverse checks if a peer has accounts(s) that is new and request for them 427 | func accountsRequestReverse(request RequestP2P, accountsLocal map[string]blockchain.Account, node *noise.Node, id noise.ID, bcLocal *blockchain.Blockchain) { 428 | for address, peerLastModified := range request.RequestParameters { 429 | if funk.IsEmpty(accountsLocal[address]) { 430 | sendAccountsRequest(accountsLocal, node, id, bcLocal) 431 | break 432 | } else { 433 | peerLastModifiedLong, err := strconv.ParseInt(peerLastModified, 10, 64) 434 | if err != nil { 435 | continue 436 | } 437 | if peerLastModifiedLong > accountsLocal[address].LastModified { 438 | sendAccountsRequest(accountsLocal, node, id, bcLocal) 439 | break 440 | } 441 | } 442 | } 443 | } 444 | 445 | // syncPeerBlockchain sends rpc to a peer to sync the peer blockchain to local 446 | func syncPeerBlockchain(node *noise.Node, id noise.ID, bf *BlockchainForest, reverse bool) { 447 | log.Infof("start syncing blocks from peer %s (%s)...", id.Address, id.ID.String()) 448 | // first update the tip 449 | requestParameters := make(map[string]string) 450 | requestParameters["local"] = "tip" 451 | if reverse { 452 | requestParameters["reverse"] = "reverse" // distinguish between startup sync and reverse sync 453 | } 454 | 455 | previousBlockHash := sendBlockRequest(requestParameters, node, id, bf) 456 | 457 | // check blocks 458 | var previousBlock BlockP2P 459 | // until genesis block is reached or cannot find previous 460 | for bytes.Compare(previousBlockHash, []byte{}) != 0 { 461 | previousBlock = bf.GetBlock(id.ID[:], previousBlockHash, true) 462 | if !funk.IsEmpty(previousBlock) { 463 | previousBlockHash = previousBlock.PrevBlockHash 464 | } else { 465 | requestParameters["local"] = fmt.Sprintf("%x", previousBlockHash) 466 | previousBlockHash = sendBlockRequest(requestParameters, node, id, bf) 467 | } 468 | } 469 | log.Infof("finished syncing from peer %s (%s)", id.Address, id.ID.String()) 470 | } 471 | 472 | // sendMappingsRequest and update local mapping cache 473 | func sendMappingsRequest(mappingsLocal map[string]blockchain.DocumentMapping, node *noise.Node, id noise.ID, search *blockchain.Search) { 474 | requestParameters := make(map[string]string) 475 | for collectionName := range mappingsLocal { 476 | requestParameters[collectionName] = collectionName // collectionName:collectionName 477 | } 478 | 479 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 480 | mappingsFromPeerRes, err := node.RequestMessage(ctx, id.Address, RequestP2P{RequestType: mappingsRequestType, RequestParameters: requestParameters}) 481 | cancel() 482 | 483 | if err != nil { 484 | log.Errorf("failed to send mapping request message to %s(%s). Skipping... [error: %s]\n", 485 | id.Address, 486 | id.ID.String(), 487 | err, 488 | ) 489 | return 490 | } 491 | 492 | mappingsFromPeer, ok := mappingsFromPeerRes.(MappingsP2P) 493 | if !ok { 494 | log.Error("cannot parse mappings from peer: " + id.ID.String()) 495 | } 496 | 497 | for collectionName, mapping := range mappingsFromPeer.Mappings { 498 | if funk.IsEmpty(mappingsLocal[collectionName]) { 499 | mappingsLocal[collectionName] = mapping // update the cache 500 | log.Debugf("Collection: %s(%s) > %+v\n", id.Address, id.ID.String(), mapping) 501 | if _, err = search.CreateMapping(mapping); err != nil { 502 | log.Error(err) 503 | } 504 | } 505 | } 506 | 507 | } 508 | 509 | // sendAccountsRequest and update local accounts cache 510 | func sendAccountsRequest(accountsLocal map[string]blockchain.Account, node *noise.Node, id noise.ID, bcLocal *blockchain.Blockchain) { 511 | requestParameters := make(map[string]string) 512 | for address, account := range accountsLocal { 513 | if account.Role.Name != "admin" { 514 | requestParameters[address] = strconv.Itoa(int(account.LastModified)) // address:lastModified 515 | } 516 | } 517 | 518 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 519 | accountsFromPeerRes, err := node.RequestMessage(ctx, id.Address, RequestP2P{RequestType: accountsRequestType, RequestParameters: requestParameters}) 520 | cancel() 521 | 522 | if err != nil { 523 | log.Errorf("failed to send account request message to %s(%s). Skipping... [error: %s]\n", 524 | id.Address, 525 | id.ID.String(), 526 | err, 527 | ) 528 | } 529 | 530 | accountsFromPeer, ok := accountsFromPeerRes.(AccountsP2P) 531 | if !ok { 532 | log.Error("cannot parse accounts from peer: " + id.ID.String()) 533 | } 534 | 535 | for address, account := range accountsFromPeer.Accounts { 536 | if funk.IsEmpty(accountsLocal[address]) || accountsLocal[address].LastModified < account.LastModified { 537 | accountsLocal[address] = account // update the cache 538 | log.Debugf("Account: %s(%s) > %+v\n", id.Address, id.ID.String(), account) 539 | if err = bcLocal.RegisterAccount([]byte(address), account); err != nil { 540 | log.Error(err) 541 | } 542 | } 543 | } 544 | } 545 | 546 | // sendBlockRequest and update peer blockchain locally 547 | func sendBlockRequest(requestParameters map[string]string, node *noise.Node, id noise.ID, bf *BlockchainForest) []byte { 548 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 549 | blockFromPeerRes, err := node.RequestMessage(ctx, id.Address, RequestP2P{RequestType: blockRequestType, RequestParameters: requestParameters}) 550 | cancel() 551 | 552 | if err != nil { 553 | log.Warnf("failed to send block request message to %s(%s). retrying... [error: %s]\n", 554 | id.Address, 555 | id.ID.String(), 556 | err, 557 | ) 558 | return sendBlockRequest(requestParameters, node, id, bf) 559 | } 560 | 561 | blockFromPeer, ok := blockFromPeerRes.(BlockP2P) 562 | if !ok { 563 | log.Error("cannot parse block from peer: " + id.ID.String()) 564 | } 565 | log.Debugf("BlockFromPeer: %s(%s) > %+x; height: %d\n", id.Address, id.ID.String(), blockFromPeer.Hash, blockFromPeer.Height) 566 | 567 | bf.AddBlock(blockFromPeer) 568 | return blockFromPeer.PrevBlockHash 569 | } 570 | -------------------------------------------------------------------------------- /p2p/request_p2p.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const accountsRequestType = "accounts" // address:lastModified 11 | const mappingsRequestType = "mappings" // collecionName:collectionName 12 | const blockRequestType = "block" // peerId:blockId or local:[tip or blockId] (don't support multiple key-value pairs yet) 13 | 14 | // RequestP2P represents common p2p request body 15 | type RequestP2P struct { 16 | RequestType string 17 | RequestParameters map[string]string 18 | } 19 | 20 | // Marshal serializes RequestP2P 21 | func (a RequestP2P) Marshal() []byte { 22 | var result bytes.Buffer 23 | 24 | encoder := gob.NewEncoder(&result) 25 | err := encoder.Encode(a) 26 | if err != nil { 27 | log.Error(err) 28 | } 29 | 30 | return result.Bytes() 31 | } 32 | 33 | // unmarshalRequestP2P deserializes encoded bytes to RequestP2P object 34 | func unmarshalRequestP2P(a []byte) (RequestP2P, error) { 35 | var requestP2p RequestP2P 36 | 37 | decoder := gob.NewDecoder(bytes.NewReader(a)) 38 | err := decoder.Decode(&requestP2p) 39 | if err != nil { 40 | log.Error(err) 41 | } 42 | 43 | return requestP2p, err 44 | } 45 | -------------------------------------------------------------------------------- /pool/queue.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | ) 7 | 8 | const minQueueLen = 32 9 | 10 | type Queue struct { 11 | items map[int64]interface{} 12 | ids map[interface{}]int64 13 | buf []int64 14 | head, tail, count int 15 | mutex *sync.Mutex 16 | notEmpty *sync.Cond 17 | // You can subscribe to this channel to know whether queue is not empty 18 | NotEmpty chan struct{} 19 | } 20 | 21 | func NewQueue() *Queue { 22 | q := &Queue{ 23 | items: make(map[int64]interface{}), 24 | ids: make(map[interface{}]int64), 25 | buf: make([]int64, minQueueLen), 26 | mutex: &sync.Mutex{}, 27 | NotEmpty: make(chan struct{}, 1), 28 | } 29 | 30 | q.notEmpty = sync.NewCond(q.mutex) 31 | 32 | return q 33 | } 34 | 35 | // Removes all elements from queue 36 | func (q *Queue) Clean() { 37 | q.mutex.Lock() 38 | defer q.mutex.Unlock() 39 | 40 | q.items = make(map[int64]interface{}) 41 | q.ids = make(map[interface{}]int64) 42 | q.buf = make([]int64, minQueueLen) 43 | } 44 | 45 | // Returns the number of elements in queue 46 | func (q *Queue) Length() int { 47 | q.mutex.Lock() 48 | defer q.mutex.Unlock() 49 | 50 | return len(q.items) 51 | } 52 | 53 | // resizes the queue to fit exactly twice its current contents 54 | // this can result in shrinking if the queue is less than half-full 55 | func (q *Queue) resize() { 56 | newCount := q.count << 1 57 | 58 | if q.count < 2<<18 { 59 | newCount = newCount << 2 60 | } 61 | 62 | newBuf := make([]int64, newCount) 63 | 64 | if q.tail > q.head { 65 | copy(newBuf, q.buf[q.head:q.tail]) 66 | } else { 67 | n := copy(newBuf, q.buf[q.head:]) 68 | copy(newBuf[n:], q.buf[:q.tail]) 69 | } 70 | 71 | q.head = 0 72 | q.tail = q.count 73 | q.buf = newBuf 74 | } 75 | 76 | func (q *Queue) notify() { 77 | if len(q.items) > 0 { 78 | select { 79 | case q.NotEmpty <- struct{}{}: 80 | default: 81 | } 82 | } 83 | } 84 | 85 | // Adds one element at the back of the queue 86 | func (q *Queue) Append(elem interface{}) { 87 | q.mutex.Lock() 88 | defer q.mutex.Unlock() 89 | 90 | if q.count == len(q.buf) { 91 | q.resize() 92 | } 93 | 94 | id := q.newId() 95 | q.items[id] = elem 96 | q.ids[elem] = id 97 | q.buf[q.tail] = id 98 | // bitwise modulus 99 | q.tail = (q.tail + 1) & (len(q.buf) - 1) 100 | q.count++ 101 | 102 | q.notify() 103 | 104 | if q.count == 1 { 105 | q.notEmpty.Broadcast() 106 | } 107 | } 108 | 109 | func (q *Queue) newId() int64 { 110 | for { 111 | id := rand.Int63() 112 | _, ok := q.items[id] 113 | if id != 0 && !ok { 114 | return id 115 | } 116 | } 117 | } 118 | 119 | // Adds one element at the front of queue 120 | func (q *Queue) Prepend(elem interface{}) { 121 | q.mutex.Lock() 122 | defer q.mutex.Unlock() 123 | 124 | if q.count == len(q.buf) { 125 | q.resize() 126 | } 127 | 128 | q.head = (q.head - 1) & (len(q.buf) - 1) 129 | id := q.newId() 130 | q.items[id] = elem 131 | q.ids[elem] = id 132 | q.buf[q.head] = id 133 | // bitwise modulus 134 | q.count++ 135 | 136 | q.notify() 137 | 138 | if q.count == 1 { 139 | q.notEmpty.Broadcast() 140 | } 141 | } 142 | 143 | // Previews element at the front of queue 144 | func (q *Queue) Front() interface{} { 145 | q.mutex.Lock() 146 | defer q.mutex.Unlock() 147 | 148 | id := q.buf[q.head] 149 | if id != 0 { 150 | return q.items[id] 151 | } 152 | return nil 153 | } 154 | 155 | // Previews element at the back of queue 156 | func (q *Queue) Back() interface{} { 157 | q.mutex.Lock() 158 | defer q.mutex.Unlock() 159 | id := q.buf[(q.tail-1)&(len(q.buf)-1)] 160 | if id != 0 { 161 | return q.items[id] 162 | } 163 | return nil 164 | } 165 | 166 | func (q *Queue) pop() int64 { 167 | for { 168 | if q.count <= 0 { 169 | q.notEmpty.Wait() 170 | } 171 | 172 | // I have no idea why, but sometimes it's less than 0 173 | if q.count > 0 { 174 | break 175 | } 176 | } 177 | 178 | id := q.buf[q.head] 179 | q.buf[q.head] = 0 180 | 181 | // bitwise modulus 182 | q.head = (q.head + 1) & (len(q.buf) - 1) 183 | q.count-- 184 | if len(q.buf) > minQueueLen && (q.count<<1) == len(q.buf) { 185 | q.resize() 186 | } 187 | 188 | return id 189 | } 190 | 191 | // Pop removes and returns the element from the front of the queue. 192 | // If the queue is empty, it will block 193 | func (q *Queue) Pop() interface{} { 194 | q.mutex.Lock() 195 | defer q.mutex.Unlock() 196 | 197 | for { 198 | id := q.pop() 199 | 200 | item, ok := q.items[id] 201 | 202 | if ok { 203 | delete(q.ids, item) 204 | delete(q.items, id) 205 | q.notify() 206 | return item 207 | } 208 | } 209 | } 210 | 211 | // Removes one element from the queue 212 | func (q *Queue) Remove(elem interface{}) bool { 213 | q.mutex.Lock() 214 | defer q.mutex.Unlock() 215 | 216 | id, ok := q.ids[elem] 217 | if !ok { 218 | return false 219 | } 220 | delete(q.ids, elem) 221 | delete(q.items, id) 222 | return true 223 | } 224 | -------------------------------------------------------------------------------- /pool/queue_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestQueueSimple(t *testing.T) { 10 | q := NewQueue() 11 | 12 | for i := 0; i < minQueueLen; i++ { 13 | q.Append(i) 14 | } 15 | for i := 0; i < minQueueLen; i++ { 16 | x := q.Pop() 17 | if x != i { 18 | t.Error("remove", i, "had value", x) 19 | } 20 | } 21 | } 22 | 23 | func TestQueueSimplePrepend(t *testing.T) { 24 | q := NewQueue() 25 | 26 | for i := 0; i < minQueueLen; i++ { 27 | q.Prepend(i) 28 | } 29 | for i := minQueueLen - 1; i >= 0; i-- { 30 | x := q.Pop() 31 | if x != i { 32 | t.Error("remove", i, "had value", x) 33 | } 34 | } 35 | } 36 | 37 | func TestQueueManual(t *testing.T) { 38 | q := NewQueue() 39 | 40 | q.Append(1) 41 | q.Append(2) 42 | q.Prepend(4) 43 | 44 | if q.Pop() != 4 { 45 | t.Error("Invalid element") 46 | } 47 | 48 | q.Prepend(3) 49 | 50 | if q.Pop() != 3 { 51 | t.Error("Invalid element") 52 | } 53 | 54 | if q.Pop() != 1 { 55 | t.Error("Invalid element") 56 | } 57 | 58 | if q.Pop() != 2 { 59 | t.Error("Invalid element") 60 | } 61 | } 62 | 63 | func TestQueueWrapping(t *testing.T) { 64 | q := NewQueue() 65 | 66 | for i := 0; i < minQueueLen; i++ { 67 | q.Append(i) 68 | } 69 | for i := 0; i < 3; i++ { 70 | q.Pop() 71 | q.Append(minQueueLen + i) 72 | } 73 | 74 | for i := 0; i < minQueueLen; i++ { 75 | q.Pop() 76 | } 77 | } 78 | 79 | func TestQueueWrappingPrepend(t *testing.T) { 80 | q := NewQueue() 81 | 82 | for i := 0; i < minQueueLen; i++ { 83 | q.Prepend(i) 84 | } 85 | for i := 0; i < 3; i++ { 86 | q.Pop() 87 | q.Prepend(minQueueLen + i) 88 | } 89 | 90 | for i := 0; i < minQueueLen; i++ { 91 | q.Pop() 92 | } 93 | } 94 | 95 | func TestQueueThreadSafety(t *testing.T) { 96 | q := NewQueue() 97 | 98 | var wg sync.WaitGroup 99 | 100 | wg.Add(2) 101 | 102 | go func() { 103 | for i := 0; i < 10000; i++ { 104 | q.Append(i) 105 | } 106 | wg.Done() 107 | }() 108 | 109 | go func() { 110 | for i := 0; i < 10000; i++ { 111 | if q.Pop() != i { 112 | t.Errorf("Invalid returned index: %d", i) 113 | wg.Done() 114 | return 115 | } 116 | } 117 | wg.Done() 118 | }() 119 | 120 | wg.Wait() 121 | } 122 | 123 | func TestQueueThreadSafety2(t *testing.T) { 124 | q := NewQueue() 125 | 126 | var wg sync.WaitGroup 127 | 128 | wg.Add(2) 129 | 130 | go func() { 131 | for i := 0; i < 10000; i++ { 132 | q.Append(i) 133 | q.Prepend(i) 134 | } 135 | wg.Done() 136 | }() 137 | 138 | go func() { 139 | for i := 0; i < 20000; i++ { 140 | q.Pop() 141 | } 142 | wg.Done() 143 | }() 144 | 145 | wg.Wait() 146 | } 147 | 148 | func TestQueueLength(t *testing.T) { 149 | q := NewQueue() 150 | 151 | if q.Length() != 0 { 152 | t.Error("empty queue length not 0") 153 | } 154 | 155 | for i := 0; i < 1000; i++ { 156 | q.Append(i) 157 | if q.Length() != i+1 { 158 | t.Error("adding: queue with", i, "elements has length", q.Length()) 159 | } 160 | } 161 | for i := 0; i < 1000; i++ { 162 | q.Pop() 163 | if q.Length() != 1000-i-1 { 164 | t.Error("removing: queue with", 1000-i-i, "elements has length", q.Length()) 165 | } 166 | } 167 | } 168 | 169 | func TestQueueBlocking(t *testing.T) { 170 | q := NewQueue() 171 | 172 | var wg sync.WaitGroup 173 | 174 | wg.Add(1) 175 | go func() { 176 | q.Append(1) 177 | time.Sleep(1 * time.Second) 178 | q.Append(2) 179 | wg.Done() 180 | }() 181 | 182 | item := q.Pop() 183 | if item != 1 { 184 | t.Error("Returned invalid 1 element") 185 | } 186 | item2 := q.Pop() 187 | if item2 != 2 { 188 | t.Error("Returned invalid 2 element") 189 | } 190 | 191 | wg.Wait() 192 | } 193 | 194 | func assertPanics(t *testing.T, name string, f func()) { 195 | defer func() { 196 | if r := recover(); r == nil { 197 | t.Errorf("%s: didn't panic as expected", name) 198 | } 199 | }() 200 | 201 | f() 202 | } 203 | 204 | func TestFront(t *testing.T) { 205 | q := NewQueue() 206 | 207 | q.Append(1) 208 | q.Append(2) 209 | 210 | if q.Front() != 1 { 211 | t.Error("There should be 1 on front") 212 | } 213 | 214 | q.Pop() 215 | 216 | if q.Front() != 2 { 217 | t.Error("There should be 2 on front") 218 | } 219 | } 220 | 221 | func TestFrontEmpty(t *testing.T) { 222 | q := NewQueue() 223 | 224 | if q.Front() != nil { 225 | t.Error("There should be nil") 226 | } 227 | 228 | q.Append(1) 229 | q.Append(2) 230 | q.Prepend(3) 231 | q.Pop() 232 | q.Pop() 233 | q.Pop() 234 | 235 | if q.Front() != nil { 236 | t.Error("There should be nil") 237 | } 238 | } 239 | 240 | func TestBackEmpty(t *testing.T) { 241 | q := NewQueue() 242 | 243 | if q.Back() != nil { 244 | t.Error("There should be nil") 245 | } 246 | 247 | q.Append(1) 248 | q.Append(2) 249 | q.Prepend(3) 250 | q.Pop() 251 | q.Pop() 252 | q.Pop() 253 | 254 | if q.Back() != nil { 255 | t.Error("There should be nil") 256 | } 257 | } 258 | 259 | func TestBack(t *testing.T) { 260 | q := NewQueue() 261 | 262 | q.Append(1) 263 | q.Append(2) 264 | 265 | if q.Back() != 2 { 266 | t.Errorf("There should be 2 on back, there is %v", q.Back()) 267 | } 268 | 269 | q.Pop() 270 | 271 | if q.Back() != 2 { 272 | t.Errorf("There should be 2 on back, there is %v", q.Back()) 273 | } 274 | } 275 | 276 | func TestRemove(t *testing.T) { 277 | q := NewQueue() 278 | 279 | q.Append(1) 280 | q.Append(2) 281 | q.Append(3) 282 | q.Remove(2) 283 | 284 | if q.Length() != 2 { 285 | t.Errorf("Queue length should be 2, it is %d", q.Length()) 286 | } 287 | 288 | p := q.Pop() 289 | if p != 1 { 290 | t.Errorf("There should be 1 on pop, there is %v", p) 291 | } 292 | 293 | p = q.Pop() 294 | if p != 3 { 295 | t.Errorf("There should be 3 on pop, there is %v", p) 296 | } 297 | } 298 | 299 | func TestTestQueueClean(t *testing.T) { 300 | q := NewQueue() 301 | 302 | q.Append(4) 303 | q.Append(6) 304 | q.Clean() 305 | 306 | q.Append(1) 307 | q.Append(2) 308 | q.Append(3) 309 | q.Remove(2) 310 | 311 | if q.Length() != 2 { 312 | t.Errorf("Queue length should be 2, it is %d", q.Length()) 313 | } 314 | 315 | p := q.Pop() 316 | if p != 1 { 317 | t.Errorf("There should be 1 on pop, there is %v", p) 318 | } 319 | 320 | p = q.Pop() 321 | if p != 3 { 322 | t.Errorf("There should be 3 on pop, there is %v", p) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /pool/receiver.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/blevesearch/bleve/geo" 10 | "github.com/boltdb/bolt" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/codingpeasant/blocace/blockchain" 15 | "github.com/codingpeasant/blocace/p2p" 16 | ) 17 | 18 | // Receiver represents the front door for the incoming transactions 19 | type Receiver struct { 20 | transactionsBuffer *Queue 21 | p2p *p2p.P2P 22 | maxTxsPerBlock int 23 | maxTimeToGenerateBlock int 24 | } 25 | 26 | // Put a transaction in JSON format to a collection. Returns isValidSig, fieldErrorMapping, transationId, error 27 | func (r *Receiver) Put(rawData []byte, collection string, pubKey []byte, signature []byte, permittedAddresses []string) (bool, map[string]string, []byte, error) { 28 | isValidSig := blockchain.IsValidSig(rawData, pubKey, signature) 29 | 30 | if !isValidSig { 31 | return false, nil, nil, nil 32 | } 33 | 34 | fieldErrorMapping, err := r.checkMapping(rawData, collection) 35 | if err != nil { 36 | return true, nil, nil, err 37 | } else if fieldErrorMapping != nil { 38 | return true, fieldErrorMapping, nil, err 39 | } 40 | 41 | newTx := blockchain.NewTransaction(r.p2p.BlockchainForest.Local.PeerId, rawData, collection, pubKey, signature, permittedAddresses) 42 | r.transactionsBuffer.Append(newTx) 43 | 44 | return true, nil, newTx.ID, nil 45 | } 46 | 47 | // PutWithoutSignature a transaction in JSON format to a collection. Returns fieldErrorMapping, transationId, error 48 | // WARNING: this makes the document unverifiable 49 | func (r *Receiver) PutWithoutSignature(rawData []byte, collection string, permittedAddresses []string) (map[string]string, error) { 50 | fieldErrorMapping, err := r.checkMapping(rawData, collection) 51 | if err != nil { 52 | return nil, err 53 | } else if fieldErrorMapping != nil { 54 | return fieldErrorMapping, err 55 | } 56 | 57 | newTx := blockchain.NewTransaction(r.p2p.BlockchainForest.Local.PeerId, rawData, collection, nil, nil, permittedAddresses) 58 | r.transactionsBuffer.Append(newTx) 59 | 60 | return nil, nil 61 | } 62 | 63 | func (r *Receiver) generateBlock() { 64 | var candidateTxs []*blockchain.Transaction 65 | 66 | for i := 0; i < r.maxTxsPerBlock && r.transactionsBuffer.Length() > 0; i++ { 67 | tx, ok := interface{}(r.transactionsBuffer.Pop()).(*blockchain.Transaction) 68 | if ok { 69 | candidateTxs = append(candidateTxs, tx) 70 | } 71 | } 72 | 73 | if len(candidateTxs) > 0 { 74 | newBlockHash := r.p2p.BlockchainForest.Local.AddBlock(candidateTxs) 75 | r.p2p.BlockchainForest.GetBlock(r.p2p.BlockchainForest.Local.PeerId, newBlockHash, false) 76 | r.p2p.BroadcastObject(r.p2p.BlockchainForest.GetBlock(r.p2p.BlockchainForest.Local.PeerId, newBlockHash, false)) 77 | } 78 | } 79 | 80 | // Monitor creates a thread to monitor the transaction queue and generate block 81 | func (r *Receiver) Monitor() { 82 | ticker := time.NewTicker(time.Duration(r.maxTimeToGenerateBlock) * time.Millisecond) 83 | log.Infof("begin to monitor transactions every %d milliseconds...\n", r.maxTimeToGenerateBlock) 84 | 85 | for t := range ticker.C { 86 | if r.transactionsBuffer.Length() > 0 { 87 | log.Infof("generating a block at %s...\n", t) 88 | r.generateBlock() 89 | } 90 | } 91 | } 92 | 93 | func (r Receiver) checkMapping(rawData []byte, collection string) (map[string]string, error) { 94 | var rawDataJSON map[string]interface{} 95 | err := json.Unmarshal(rawData, &rawDataJSON) 96 | 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | var documentMapping *blockchain.DocumentMapping 102 | err = r.p2p.BlockchainForest.Local.Db.View(func(dbtx *bolt.Tx) error { 103 | b := dbtx.Bucket([]byte(blockchain.CollectionsBucket)) 104 | 105 | if b == nil { 106 | log.WithFields(log.Fields{ 107 | "method": "checkMapping()", 108 | }).Warn("bucket doesn't exist") 109 | return errors.New("collection doesn't exist") 110 | } 111 | 112 | encodedCollectionMapping := b.Get([]byte(collection)) 113 | if encodedCollectionMapping == nil { 114 | log.WithFields(log.Fields{ 115 | "method": "checkMapping()", 116 | }).Warn("collection doesn't exist") 117 | return errors.New("collection doesn't exist") 118 | } 119 | documentMapping = blockchain.DeserializeDocumentMapping(encodedCollectionMapping) 120 | 121 | return nil 122 | }) 123 | 124 | if err != nil { 125 | log.WithFields(log.Fields{ 126 | "method": "checkMapping()", 127 | }).Errorf("%s", err.Error()) 128 | return nil, err 129 | } 130 | 131 | valueMaps := make(map[string]interface{}) 132 | for field, value := range documentMapping.Fields { 133 | valueMap, _ := value.(map[string]interface{}) 134 | valueMaps[field] = valueMap["type"] 135 | } 136 | 137 | validationErrors := make(map[string]string) 138 | for field, value := range rawDataJSON { 139 | switch value := value.(type) { 140 | case string: 141 | if valueMaps[field] == nil { 142 | } else if valueMaps[field] == "text" { 143 | } else if valueMaps[field] == "datetime" { 144 | _, err := time.Parse(time.RFC3339, value) 145 | 146 | if err != nil { 147 | validationErrors[field] = "cannot parse as RFC3339 time format" 148 | } 149 | } else { 150 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 151 | } 152 | case float64: 153 | if valueMaps[field] == nil { 154 | } else if valueMaps[field] != "number" { 155 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 156 | } 157 | case bool: 158 | if valueMaps[field] == nil { 159 | } else if valueMaps[field] != "boolean" { 160 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 161 | } 162 | case []interface{}: 163 | if len(value) > 0 { 164 | switch element := value[0].(type) { 165 | case string: 166 | if valueMaps[field] == nil { 167 | } else if valueMaps[field] == "text" { 168 | } else if valueMaps[field] == "datetime" { 169 | _, err := time.Parse(time.RFC3339Nano, element) 170 | 171 | if err != nil { 172 | validationErrors[field] = "cannot parse as RFC3339 time format" 173 | } 174 | } else { 175 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 176 | } 177 | case float64: 178 | if valueMaps[field] == nil { 179 | } else if valueMaps[field] != "number" { 180 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 181 | } 182 | case bool: 183 | if valueMaps[field] == nil { 184 | } else if valueMaps[field] != "boolean" { 185 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 186 | } 187 | default: 188 | if valueMaps[field] == nil { 189 | } else if valueMaps[field] == "geopoint" { 190 | _, _, isGeoPoint := geo.ExtractGeoPoint(value) 191 | if !isGeoPoint { 192 | validationErrors[field] = "field type should be geopoint" 193 | } 194 | } else { 195 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 196 | } 197 | } 198 | } 199 | default: 200 | if valueMaps[field] == nil { 201 | } else if valueMaps[field] == "geopoint" { 202 | _, _, isGeoPoint := geo.ExtractGeoPoint(value) 203 | if !isGeoPoint { 204 | validationErrors[field] = "field type should be geopoint" 205 | } 206 | } else { 207 | validationErrors[field] = fmt.Sprintf("field type should be %s", valueMaps[field]) 208 | } 209 | } 210 | } 211 | 212 | if len(validationErrors) == 0 { 213 | return nil, nil 214 | } 215 | return validationErrors, nil 216 | } 217 | 218 | // NewReceiver creates an instance of Receiver 219 | func NewReceiver(p2p *p2p.P2P, maxTxsPerBlock int, maxTimeToGenerateBlock int) *Receiver { 220 | return &Receiver{transactionsBuffer: NewQueue(), p2p: p2p, maxTxsPerBlock: maxTxsPerBlock, maxTimeToGenerateBlock: maxTimeToGenerateBlock} 221 | } 222 | -------------------------------------------------------------------------------- /start_cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "No argument supplied. Usage $0 " 5 | exit 1 6 | fi 7 | 8 | if [ "$1" -gt 10 ]; then 9 | echo "Do not run too many nodes on a single machine." 10 | exit 1 11 | fi 12 | 13 | trap "exit" INT TERM ERR 14 | trap "kill 0" EXIT 15 | 16 | go get 17 | go build -ldflags="-s -w -X main.version=0.6.0" 18 | rm -rf ./data* 19 | 20 | for ((n = 0; n < $1; n++)); do 21 | echo -e "\e[96mPeer$n\033[0m" 22 | if [ "$n" -eq 0 ]; then 23 | ./blocace s --dir ./data$n --porthttp 6899 --portP2p 6091 -l debug & 24 | else 25 | ./blocace s --dir ./data$n --porthttp $(expr 6899 + $n) --portP2p $(expr 6091 + $n) --peerAddresses localhost:6091 -l debug & 26 | fi 27 | sleep 1 28 | done 29 | 30 | wait 31 | -------------------------------------------------------------------------------- /webapi/auth.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | jwt "github.com/dgrijalva/jwt-go" 9 | 10 | "github.com/codingpeasant/blocace/blockchain" 11 | ) 12 | 13 | // CustomClaims is the customized claim based on the standard JWT claims 14 | type CustomClaims struct { 15 | RoleName string `json:"roleName"` 16 | Address string `json:"address"` 17 | jwt.StandardClaims 18 | } 19 | 20 | // issueToken authenticates the user and issue a jwt 21 | func issueToken(address string, role blockchain.Role, secret string) (string, error) { 22 | signingKey := []byte(secret) 23 | claims := CustomClaims{ 24 | role.Name, 25 | address, 26 | jwt.StandardClaims{ 27 | IssuedAt: time.Now().Unix(), 28 | ExpiresAt: time.Now().Unix() + 600, 29 | Issuer: "blocace", 30 | Audience: "blocace user", 31 | }, 32 | } 33 | 34 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 35 | tokenString, err := token.SignedString(signingKey) 36 | return tokenString, err 37 | } 38 | 39 | // verifyToken verifies is a jwt is valid 40 | func verifyToken(tokenString string, secret string) (jwt.Claims, error) { 41 | signingKey := []byte(secret) 42 | 43 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 44 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 45 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 46 | } 47 | return signingKey, nil 48 | }) 49 | 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | if token.Valid { 55 | return token.Claims, nil 56 | } else if ve, ok := err.(*jwt.ValidationError); ok { 57 | if ve.Errors&jwt.ValidationErrorMalformed != 0 { 58 | return nil, errors.New("cannot validate the token") 59 | } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { 60 | // Token is either expired or not active yet 61 | return nil, errors.New("token expired") 62 | } else { 63 | return nil, errors.New("couldn't handle this token: " + err.Error()) 64 | } 65 | } else { 66 | return nil, errors.New("couldn't handle this token: " + err.Error()) 67 | } 68 | } 69 | --------------------------------------------------------------------------------