├── .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 |
2 |
3 |
4 |
5 | [](https://godoc.org/github.com/codingpeasant/blocace) [](https://goreportcard.com/report/github.com/codingpeasant/blocace) [](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 | 
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 |
--------------------------------------------------------------------------------