├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── api ├── errors.go ├── http.go └── json.go ├── config ├── example.json ├── example.toml └── reader.go ├── crypto ├── aes.go ├── aes_test.go └── bls.go ├── go.mod ├── go.sum ├── keeper ├── guard.go └── guard_test.go ├── logger └── log.go ├── main.go ├── messenger ├── errors.go ├── interface.go └── mixin.go ├── sdk └── go │ ├── config.go │ ├── errors.go │ ├── http.go │ ├── secret.go │ ├── secret_test.go │ ├── tip.go │ └── tip_test.go ├── signer ├── README.md ├── board.go ├── bundle.go ├── message.go ├── node.go └── setup.go ├── store ├── badger.go ├── badger_test.go └── interface.go └── web ├── favicon.ico ├── index.html └── workflow.jpg /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.23.2 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | 24 | - name: Build 25 | run: go build -o tip . 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tip 2 | -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Throttled Identity Protocol 2 | 3 | Throttled Identity Protocol (TIP) is a decentralized key derivation protocol, which allows people to obtain a strong secret key through a very simple passphrase, e.g. a six-digit PIN. 4 | 5 | ## Mission and Overview 6 | 7 | Along with the rising of Bitcoin and other cryptocurrencies, the saying "not your keys, not your coins" has become well-known. That's true, very true and definitely true, that's the right and freedom Bitcoin has given people. Whoever can access the key can move the money, and nobody without the key is able to do that. 8 | 9 | That said it's better to manage your own Bitcoin private key than let your coins lie in some centralized exchanges. However, it's admitted that key management requires superior skills, which most people lack. And the result is people who own their keys lose the coins permanently due to various accidents, and those who opened a Coinbase account years ago can still get back their assets easily. 10 | 11 | The embarrassing result doesn't prove the security of centralized exchanges, yet exposes the drawbacks of key management. The key grants people the right to truly own their properties, but people lose money due to poor key management skills. People should not be blamed for that, it's the problem of the key itself. 12 | 13 | Bitcoin gives people the right and freedom to own their properties, and people deserve the convenience to manage their keys. Current private key or mnemonic phrase designs are over-complicated for people to keep properly. Instead of fearing the corruption of centralized financial institutions, people become slaves of the private key. 14 | 15 | It's what TIP strives to do. Let people truly own their coins with a six-digit PIN. This decentralized PIN is easy to remember for any person, doesn't require any special skills or hardware, and people can manage their coins with more confidence than ever. 16 | 17 | ## Protocol Design 18 | 19 | TIP involves three independent parties to make the protocol work. A decentralized signer network authenticates signing requests from the user, and throttles malicious attempts; A trusted account manager serves the user an identity seed, which typically authenticates the user by email or phone verification code; The user remembers a PIN and combines the identity seed from the account manager, then makes independent requests to enough signer network nodes, and finally derives their secret key. 20 | 21 | ### Decentralized Network Setup 22 | 23 | The decentralized signer network is launched cooperatively by many different entities. Specifically, those entities gather and reach a consensus to run some node software, those nodes interactively run a distributed key generation protocol. For TIP, the DKG is [threshold Boneh-Lynn-Shacham (BLS) signatures](https://en.wikipedia.org/wiki/Boneh%E2%80%93Lynn%E2%80%93Shacham). 24 | 25 | Assuming *n* entities agree to launch the network, they generate an asymmetric key pair respectively and configure their node software to include all the entities' public keys in a deterministic order. Then they boot the nodes to run a *t*-of-*n* (where _t = n * 2 / 3 + 1_) DKG protocol to set up a collective public key *P* and private key shares *si* respectively. 26 | 27 | After the DKG protocol finishes, all entities should share the public key *P* to ensure they hold the same one, keep their private key shares *si* cautiously, and should make professional backups. 28 | 29 | Finally, all entities should boot their node software to accept throttled signing requests from users. And again, they should safeguard the node servers and defend against all malicious attacks. 30 | 31 | This repository includes an implementation of the signer node software, for instructions please see the **signer** directory. 32 | 33 | ### Throttled Secret Derivation 34 | 35 | The network announces the configuration and signers list to the public or potential users and waits for signing requests. Each signer should throttle the requests based on the same restrictions. 36 | 37 | - **Identity.** This is the base factor for all restrictions, the identity should be a valid BLS public key, and a user should use the same identity for all signers. The signer checks the request and verifies the request signature against the public key, and the signer must reduce the request quota of this identity for any invalid signature. 38 | - **Ephemeral.** This parameter is a different random value for each signer but should remain unchanged for the same signer during the ephemeral grace period. If the ephemeral changes during the grace period, the signer must reduce the ephemeral requests quota of this identity. 39 | - **Nonce.** For each signing request, the user should increase the nonce during the ephemeral grace period. If the nonce is invalid during the grace period, the signer must reduce the ephemeral requests quota of this identity. 40 | 41 | After the signing request passes all throttle checks, the signer responds back a part of the *t*-of-*n* threshold BLS signature by signing the identity. Whenever the user collects *t* valid partials, they can recover the final collective signature and verify it with the collective public key. 42 | 43 | The final collective signature is the seed to the secret key of the user. Then it's up to the user to use different algorithms to generate their private key for Bitcoin or other usages. It doesn't need any further requests to use this secret key, and in case of a loss, the user can recover it by making the same requests. 44 | 45 | For details of the throttle restrictions, please see the **keeper** directory. 46 | 47 | ### Threshold Identity Generation 48 | 49 | The mission of TIP network is to let people truly own their coins by only remembering a 6-digit PIN, so they should not have the duty to store *identity*, *ephemeral* or *nonce*. They are capable of achieving this goal through the threshold identity generation process with the help from the trusted account manager. 50 | 51 | 1. User authenticates themself with a trusted account manager through email or phone verification code, and the manager responds with the identity seed *Si*. 52 | 2. User chooses a very slow hash function *Hs*, e.g. argon2id, and generates the identity *I = Hs(PIN || Si)*. 53 | 3. User generates a random ephemeral seed *Se*, and stores the seed on its device securely. 54 | 4. For each signer *i* in the network with public key *Pi*, user generates the ephemeral *ei = Hs(I || Se || Pi)*. 55 | 5. User sends signing requests *(I, ei, nonce, grace)* to each signer *i* and gathers enough partial signatures, then recover the final collective signature. 56 | 6. User must repeat the process every a while to refresh the ephemeral grace period. 57 | 58 | The identity seed should prohibit all impersonation, the on-device random ephemeral seed should prevent the account manager collude with some signer, and the ephemeral grace period allows the user to recover its secret key when the device is lost. 59 | 60 | Furthermore, the user can make their threshold identity generation more secure by cooperating with another user to combine their identity to increase the entropy especially when the account manager manages lots of identities. 61 | 62 | And finally, the user can just back up his seeds like any traditional key management process, and this backup is considered more secure against loss or theft. 63 | 64 | ## Network Evolution 65 | 66 | Once the decentralized signer network is launched, its signers should remain constant, no new entity is permitted to join the signers or replace an old signer because the DKG protocol remains valid only when all shares remain unchanged. But people need the network to become stronger, and that requires more entities to join the network. So TIP allows network evolution. 67 | 68 | Whenever a new entity is accepted to the network, either replacing an old signer or joining as a new one, an evolution happens. Indeed, an evolution starts a fresh DKG protocol in the same process as the previous evolution, but with different signers, thus resulting in absolutely different shares for each signer. It's noted that an entity leaving the network doesn't result in any evolution, because the remaining shares can still serve requests. 69 | 70 | In a new evolution, all signers should reference the number and the hash of the signer list from the previous evolution. After a new evolution starts, the previous evolution still works. For each signer in the new evolution, if it is a signer of the previous evolution, it must maintain its availability to serve signing requests to the previous evolution, otherwise it should be punished. 71 | 72 | Any user requests for the throttled secret derivation should include the evolution number to get the correct signature. And in any case of network changes, the user is assured of their key security due to various backups discussed in previous sections. 73 | 74 | ## Incentive and Punishment 75 | 76 | The code doesn't include any incentive or punishment for the entities running the signer node software. It's up to their consensus on their mission, either to serve their customers a better user experience, or charge a small key signing request fee, or they could make some tokens to do community development. 77 | 78 | ## Security 79 | 80 | All the cryptography libraries used in this repository are being developed and used by industry-leading institutions, notably the [drand project](https://github.com/drand/drand) and its league of entropy that includes Cloudflare, EPFL, Kudelski Security, Protocol Labs, Celo, UCL, and UIUC. 81 | 82 | The code has been audited by Certik, and the audit report can be found at https://github.com/MixinNetwork/audits. 83 | 84 | ## Contribution 85 | 86 | The project doesn't accept feature requests and welcomes all security improvement contributions. Shall you find any security issues, please email security@mixin.one before any public disclosures or pull requests. 87 | 88 | The core team highly values the contributions and provides at most a $100K bounty for any vulnerability report according to the severity. 89 | 90 | ## License 91 | 92 | The TIP project is licensed under Apache 2.0 terms. 93 | -------------------------------------------------------------------------------- /api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ErrUnknown = fmt.Errorf("server error") 7 | ErrInvalidAssignor = fmt.Errorf("invalid assignor") 8 | ErrTooManyRequest = fmt.Errorf("too many request") 9 | ) 10 | -------------------------------------------------------------------------------- /api/http.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/MixinNetwork/tip/logger" 10 | "github.com/MixinNetwork/tip/store" 11 | "github.com/drand/kyber" 12 | "github.com/drand/kyber/share" 13 | "github.com/drand/kyber/share/dkg" 14 | "github.com/unrolled/render" 15 | ) 16 | 17 | type Handler struct { 18 | store store.Storage 19 | conf *Configuration 20 | render *render.Render 21 | } 22 | 23 | type Configuration struct { 24 | Key kyber.Scalar `toml:"-"` 25 | Signers []dkg.Node `toml:"-"` 26 | Poly []kyber.Point `toml:"-"` 27 | Share *share.PriShare `toml:"-"` 28 | Port int `toml:"port"` 29 | } 30 | 31 | func NewServer(store store.Storage, conf *Configuration) *http.Server { 32 | hdr := &Handler{ 33 | store: store, 34 | render: render.New(), 35 | conf: conf, 36 | } 37 | server := &http.Server{ 38 | Addr: fmt.Sprintf(":%d", conf.Port), 39 | Handler: handleCORS(hdr), 40 | ReadTimeout: 10 * time.Second, 41 | WriteTimeout: 10 * time.Second, 42 | IdleTimeout: 120 * time.Second, 43 | } 44 | return server 45 | } 46 | 47 | func (hdr *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 48 | logger.Info(*r) 49 | defer handlePanic(w, r) 50 | 51 | if r.URL.Path != "/" { 52 | hdr.error(w, r, http.StatusNotFound) 53 | return 54 | } 55 | 56 | if r.Method == "POST" { 57 | hdr.handle(w, r) 58 | return 59 | } 60 | 61 | data, sig := info(hdr.conf.Key, hdr.conf.Signers, hdr.conf.Poly) 62 | hdr.json(w, r, http.StatusOK, map[string]interface{}{"data": data, "signature": sig, "version": "v0.2.0"}) 63 | } 64 | 65 | func (hdr *Handler) handle(w http.ResponseWriter, r *http.Request) { 66 | var body SignRequest 67 | err := json.NewDecoder(r.Body).Decode(&body) 68 | if err != nil { 69 | hdr.error(w, r, http.StatusBadRequest) 70 | return 71 | } 72 | switch body.Action { 73 | case "SIGN": 74 | data, sig, err := sign(hdr.conf.Key, hdr.store, &body, hdr.conf.Share) 75 | logger.Debug("api.sign", body.Identity, data, sig, err) 76 | if err == ErrTooManyRequest { 77 | hdr.error(w, r, http.StatusTooManyRequests) 78 | return 79 | } else if err == ErrInvalidAssignor { 80 | hdr.error(w, r, http.StatusForbidden) 81 | return 82 | } else if err != nil { 83 | hdr.error(w, r, http.StatusInternalServerError) 84 | return 85 | } 86 | hdr.json(w, r, http.StatusOK, map[string]interface{}{"data": data, "signature": sig}) 87 | case "WATCH": 88 | genesis, counter, err := watch(hdr.store, body.Watcher) 89 | if err != nil { 90 | hdr.error(w, r, http.StatusInternalServerError) 91 | return 92 | } 93 | hdr.json(w, r, http.StatusOK, map[string]interface{}{"genesis": genesis, "counter": counter}) 94 | default: 95 | hdr.error(w, r, http.StatusBadRequest) 96 | } 97 | } 98 | 99 | func (hdr *Handler) error(w http.ResponseWriter, r *http.Request, code int) { 100 | hdr.json(w, r, code, map[string]interface{}{"error": map[string]interface{}{ 101 | "code": code, 102 | "description": http.StatusText(code), 103 | }}) 104 | } 105 | 106 | func (hdr *Handler) json(w http.ResponseWriter, r *http.Request, code int, data interface{}) { 107 | id := r.Header.Get("X-Request-ID") 108 | logger.Info(r.Method, r.URL, id, code, data) 109 | _ = hdr.render.JSON(w, code, data) 110 | } 111 | 112 | func handleCORS(handler http.Handler) http.Handler { 113 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 114 | origin := r.Header.Get("Origin") 115 | if origin == "" { 116 | handler.ServeHTTP(w, r) 117 | return 118 | } 119 | w.Header().Set("Access-Control-Allow-Origin", origin) 120 | w.Header().Add("Access-Control-Allow-Headers", "Content-Type,X-Request-ID") 121 | w.Header().Set("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE") 122 | w.Header().Set("Access-Control-Max-Age", "600") 123 | if r.Method == "OPTIONS" { 124 | _ = render.New().JSON(w, http.StatusOK, map[string]interface{}{}) 125 | } else { 126 | handler.ServeHTTP(w, r) 127 | } 128 | }) 129 | } 130 | 131 | func handlePanic(_ http.ResponseWriter, _ *http.Request) { 132 | rcv := recover() 133 | if rcv == nil { 134 | return 135 | } 136 | err := fmt.Sprint(rcv) 137 | logger.Error(err) 138 | } 139 | -------------------------------------------------------------------------------- /api/json.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/MixinNetwork/tip/crypto" 11 | "github.com/MixinNetwork/tip/keeper" 12 | "github.com/MixinNetwork/tip/logger" 13 | "github.com/MixinNetwork/tip/store" 14 | "github.com/drand/kyber" 15 | "github.com/drand/kyber/pairing/bn256" 16 | "github.com/drand/kyber/share" 17 | "github.com/drand/kyber/share/dkg" 18 | "github.com/drand/kyber/sign/tbls" 19 | ) 20 | 21 | type SignRequest struct { 22 | Action string `json:"action"` 23 | Watcher string `json:"watcher"` 24 | Identity string `json:"identity"` 25 | Signature string `json:"signature"` 26 | Data string `json:"data"` 27 | } 28 | 29 | func info(key kyber.Scalar, sigrs []dkg.Node, poly []kyber.Point) (interface{}, string) { 30 | signers := make([]map[string]interface{}, len(sigrs)) 31 | for i, s := range sigrs { 32 | signers[i] = map[string]interface{}{ 33 | "index": s.Index, 34 | "identity": crypto.PublicKeyString(s.Public), 35 | } 36 | } 37 | commitments := make([]string, len(poly)) 38 | for i, c := range poly { 39 | commitments[i] = crypto.PublicKeyString(c) 40 | } 41 | id := crypto.PublicKey(key) 42 | data := map[string]interface{}{ 43 | "identity": crypto.PublicKeyString(id), 44 | "signers": signers, 45 | "commitments": commitments, 46 | } 47 | b, _ := json.Marshal(data) 48 | sig, _ := crypto.Sign(key, b) 49 | return data, hex.EncodeToString(sig) 50 | } 51 | 52 | func watch(store store.Storage, watcher string) (time.Time, int, error) { 53 | key, _ := hex.DecodeString(watcher) 54 | if len(key) != 32 { 55 | return time.Time{}, 0, fmt.Errorf("invalid watcher %s", watcher) 56 | } 57 | 58 | _, genesis, counter, err := store.Watch(key) 59 | return genesis, counter, err 60 | } 61 | 62 | func sign(key kyber.Scalar, store store.Storage, body *SignRequest, priv *share.PriShare) (interface{}, string, error) { 63 | res, err := keeper.Guard(store, key, body.Identity, body.Signature, body.Data) 64 | if err != nil { 65 | logger.Debug("keeper.Guard", body.Identity, body.Watcher, body.Signature, err) 66 | return nil, "", ErrUnknown 67 | } 68 | if res.Available < 1 { 69 | logger.Debug("keeper.Available", body.Identity, body.Watcher, body.Signature) 70 | return nil, "", ErrTooManyRequest 71 | } 72 | if watcher := hex.EncodeToString(res.Watcher); watcher != body.Watcher { 73 | logger.Debug("keeper.Watch", body.Identity, body.Watcher, body.Signature, watcher) 74 | return nil, "", ErrInvalidAssignor 75 | } 76 | 77 | scheme := tbls.NewThresholdSchemeOnG1(bn256.NewSuiteG2()) 78 | partial, err := scheme.Sign(priv, res.Assignor) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | genesis, counter, err := store.WriteSignRequest(res.Assignor, res.Watcher) 84 | if err != nil { 85 | logger.Debug("store.WriteSignRequest", err) 86 | return nil, "", ErrUnknown 87 | } 88 | 89 | buf := make([]byte, 8) 90 | binary.BigEndian.PutUint64(buf, res.Nonce) 91 | plain := append(buf, partial...) 92 | plain = append(plain, res.Assignor...) 93 | binary.BigEndian.PutUint64(buf, uint64(genesis.UnixNano())) 94 | plain = append(plain, buf...) 95 | binary.BigEndian.PutUint64(buf, uint64(counter)) 96 | plain = append(plain, buf...) 97 | cipher := crypto.EncryptECDH(res.Identity, key, plain) 98 | data := map[string]interface{}{ 99 | "cipher": hex.EncodeToString(cipher), 100 | } 101 | b, _ := json.Marshal(data) 102 | sig, _ := crypto.Sign(key, b) 103 | return data, hex.EncodeToString(sig), nil 104 | } 105 | -------------------------------------------------------------------------------- /config/example.json: -------------------------------------------------------------------------------- 1 | {"commitments":["5JYy3T2tYasdabAocvzp7SVvY7yc9geGwZXLa6MJEw9TLW7DzE8F3BjjsbbyRRCSvR5bzFb7vPt1s5Qv8WqVWq3Uuj1XKCzXK7rgUsAA2FnA5jsf9igKYZcpCX9v31fvjFj4L9F5TNVVDfzYeWZNcB8kE2e3XyyZN356yeY6X1LNVFrLsSfrju","5Jfx198vV4trL3LewJMDLdZFpLj8dqgz4XBzu5Mrafx9w1Ei7MisnJg98GM12hxNEddJCjuUyJZvcconvp79MzxnvYrq6bTJo67UBwQbnu2wAz3LMGs2zHG7uTXq7chHrKw2FgfVJKdVeMSCCCGfZm1hnpZdRGPT464jn749oF11SrvbrMn8QC","5JsXZiKcZtRH2c6Nd4kZZ3kJHka9XKFSXajREzoCezyNhHyfJsuVoDee2NRtKSHrx13igh6XUeZ6BFKjLGiqZBH46vaGUodDGY3RDG9SWoTJCyQa2cEEzy8L2yLWpg857ghH7mMNvhX3f928cyVKhuVMLdaKcD1GsK6vkAvKdXMMeAuS9Qy7ix"],"signers":[{"identity":"5HSkt6kGU4wF6um7Bz18mDNnPhWU1n2aKKPYJUxgcSYk91sVo73di4n4SLn8NouUQDxognzL4aNxrmRN2tuwvucCuP9tAZGgr6Aw3cEKaidwGQjo1JbKBaFA66DcQc4HWaej1CtE6amWZYT1AuPvpGApAYS45ewApYp3DsD1qPrpPbagjRbZfu","api":"http://127.0.0.1:7003"},{"identity":"5J6M1UHoQ4xkzsGifLSuLwNVQawdQQg5S6KhrSi79jMWBScogrEsHW2YVtqENTtsq3RqZqjwMagiA9Za6u97FiCYtXp465KaaJ3DUKi3mXbqwinwSyb8RSpJhM3EhgDJKWtU7spxe6YvEWvM69gcbTUTdHQoCiJPE3VdsTmAfGNRiqFxZ1grR3","api":"http://127.0.0.1:7001"},{"identity":"5JW5UpoTBdFS3XyTyisWm4rKMXKhcw97hZDJ4NWTHKKDboYDsA7ziS2d6drbN22iNqQQgFwktG2YmVq7UyMQynZM7RrMgJZRqd6xa2jAq1m2zenBFqqPD68raVLDZqvJUKPJ22sP2WApuBfaUZaZhkdw1yeUBTkou5C9heXdKg72MQNF69RzAA","api":"http://127.0.0.1:7002"},{"identity":"5JZj14hukDuaz3YKtkaHuXPLRCxZNiv1rgUPPdXAxY4T6tEtLnJm2A7EkLu5jvM6jvrXqTA9nmPBv5fsToPMRAy7b9JiFzgccuwgYRpQv6NkZBK2DZWDrJGPi7wvST3PzSDx9ZYcPpGQP5mvPQctgJCtxLHhMdKoDckf7s91baKt6xJ6JnMjsw","api":"http://127.0.0.1:7000"}]} 2 | -------------------------------------------------------------------------------- /config/example.toml: -------------------------------------------------------------------------------- 1 | [api] 2 | port = 7000 3 | 4 | [store] 5 | dir = "/tmp/tip" 6 | 7 | [messenger] 8 | user = "71b72e67-3636-473a-9ee4-db7ba3094057" 9 | session = "78cbc71b-840d-4e77-bd80-1a981f7d6b0f" 10 | key = "7MFAzjqB4JyEbYQG5kE1gv551mZZlwKvajVKHk32JVYBlPJ6CfvbSEclr6frnWsLfqpS7er6vbzTaIc2egomUw" 11 | buffer = 64 12 | conversation = "1241b00c-54f0-4f75-b91f-dabbccd1f80b" 13 | 14 | [node] 15 | key = "51b0f3a4428b36706de12a51325d46d94a980cee9c29841d60988f1f9c2d63fb" 16 | signers = [ 17 | "5JZj14hukDuaz3YKtkaHuXPLRCxZNiv1rgUPPdXAxY4T6tEtLnJm2A7EkLu5jvM6jvrXqTA9nmPBv5fsToPMRAy7b9JiFzgccuwgYRpQv6NkZBK2DZWDrJGPi7wvST3PzSDx9ZYcPpGQP5mvPQctgJCtxLHhMdKoDckf7s91baKt6xJ6JnMjsw", 18 | "5JW5UpoTBdFS3XyTyisWm4rKMXKhcw97hZDJ4NWTHKKDboYDsA7ziS2d6drbN22iNqQQgFwktG2YmVq7UyMQynZM7RrMgJZRqd6xa2jAq1m2zenBFqqPD68raVLDZqvJUKPJ22sP2WApuBfaUZaZhkdw1yeUBTkou5C9heXdKg72MQNF69RzAA", 19 | "5HSkt6kGU4wF6um7Bz18mDNnPhWU1n2aKKPYJUxgcSYk91sVo73di4n4SLn8NouUQDxognzL4aNxrmRN2tuwvucCuP9tAZGgr6Aw3cEKaidwGQjo1JbKBaFA66DcQc4HWaej1CtE6amWZYT1AuPvpGApAYS45ewApYp3DsD1qPrpPbagjRbZfu", 20 | "5J6M1UHoQ4xkzsGifLSuLwNVQawdQQg5S6KhrSi79jMWBScogrEsHW2YVtqENTtsq3RqZqjwMagiA9Za6u97FiCYtXp465KaaJ3DUKi3mXbqwinwSyb8RSpJhM3EhgDJKWtU7spxe6YvEWvM69gcbTUTdHQoCiJPE3VdsTmAfGNRiqFxZ1grR3" 21 | ] 22 | timeout = 10 23 | -------------------------------------------------------------------------------- /config/reader.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/MixinNetwork/tip/api" 10 | "github.com/MixinNetwork/tip/messenger" 11 | "github.com/MixinNetwork/tip/signer" 12 | "github.com/MixinNetwork/tip/store" 13 | "github.com/pelletier/go-toml" 14 | ) 15 | 16 | type Configuration struct { 17 | API *api.Configuration `toml:"api"` 18 | Messenger *messenger.MixinConfiguration `toml:"messenger"` 19 | Store *store.BadgerConfiguration `toml:"store"` 20 | Node *signer.Configuration `toml:"node"` 21 | } 22 | 23 | func ReadConfiguration(path string) (*Configuration, error) { 24 | if strings.HasPrefix(path, "~/") { 25 | usr, _ := user.Current() 26 | path = filepath.Join(usr.HomeDir, (path)[2:]) 27 | } 28 | f, err := os.ReadFile(path) 29 | if err != nil { 30 | return nil, err 31 | } 32 | var conf Configuration 33 | err = toml.Unmarshal(f, &conf) 34 | return &conf, err 35 | } 36 | -------------------------------------------------------------------------------- /crypto/aes.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | 8 | "github.com/drand/kyber" 9 | "github.com/drand/kyber/pairing/bn256" 10 | "github.com/drand/kyber/util/random" 11 | "golang.org/x/crypto/sha3" 12 | ) 13 | 14 | func ecdh(point kyber.Point, scalar kyber.Scalar) []byte { 15 | suite := bn256.NewSuiteG2() 16 | if point.Equal(suite.Point()) { 17 | r := suite.Scalar().Pick(random.New()) 18 | point = point.Mul(r, nil) 19 | } 20 | point = suite.Point().Mul(scalar, point) 21 | 22 | b := PublicKeyBytes(point) 23 | sum := sha3.Sum256(b) 24 | return sum[:] 25 | } 26 | 27 | func Decrypt(secret, b []byte) []byte { 28 | aes, _ := aes.NewCipher(secret) 29 | aead, _ := cipher.NewGCM(aes) 30 | nonce := b[:aead.NonceSize()] 31 | cipher := b[aead.NonceSize():] 32 | d, _ := aead.Open(nil, nonce, cipher, nil) 33 | return d 34 | } 35 | 36 | func Encrypt(secret, b []byte) []byte { 37 | aes, err := aes.NewCipher(secret) 38 | if err != nil { 39 | panic(err) 40 | } 41 | aead, err := cipher.NewGCM(aes) 42 | if err != nil { 43 | panic(err) 44 | } 45 | nonce := make([]byte, aead.NonceSize()) 46 | _, err = rand.Read(nonce) 47 | if err != nil { 48 | panic(err) 49 | } 50 | cipher := aead.Seal(nil, nonce, b, nil) 51 | return append(nonce, cipher...) 52 | } 53 | 54 | func DecryptECDH(pub kyber.Point, priv kyber.Scalar, b []byte) []byte { 55 | secret := ecdh(pub, priv) 56 | return Decrypt(secret, b) 57 | } 58 | 59 | func EncryptECDH(pub kyber.Point, priv kyber.Scalar, b []byte) []byte { 60 | secret := ecdh(pub, priv) 61 | return Encrypt(secret, b) 62 | } 63 | -------------------------------------------------------------------------------- /crypto/aes_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/drand/kyber/pairing/bn256" 7 | "github.com/drand/kyber/util/random" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDH(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | suite := bn256.NewSuiteG2() 15 | s1 := suite.Scalar().Pick(random.New()) 16 | p1 := suite.Point().Mul(s1, nil) 17 | s2 := suite.Scalar().Pick(random.New()) 18 | p2 := suite.Point().Mul(s2, nil) 19 | 20 | d1 := ecdh(p2, s1) 21 | d2 := ecdh(p1, s2) 22 | assert.Equal(d1, d2) 23 | 24 | d1 = ecdh(p1, s1) 25 | d2 = ecdh(p2, s2) 26 | assert.NotEqual(d1, d2) 27 | 28 | i1 := ecdh(bn256.NewSuiteG2().Point(), s1) 29 | i2 := ecdh(bn256.NewSuiteG2().Point(), s2) 30 | assert.NotEqual(i1, i2) 31 | } 32 | 33 | func TestEncDec(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | suite := bn256.NewSuiteG2() 37 | s1 := suite.Scalar().Pick(random.New()) 38 | p1 := suite.Point().Mul(s1, nil) 39 | s2 := suite.Scalar().Pick(random.New()) 40 | p2 := suite.Point().Mul(s2, nil) 41 | 42 | text := []byte("hello") 43 | b := EncryptECDH(p2, s1, text) 44 | assert.Len(b, 12+16+len(text)) 45 | dec := DecryptECDH(p1, s2, b) 46 | assert.Equal(text, dec) 47 | } 48 | -------------------------------------------------------------------------------- /crypto/bls.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/btcsuite/btcd/btcutil/base58" 8 | "github.com/drand/kyber" 9 | "github.com/drand/kyber/pairing/bn256" 10 | "github.com/drand/kyber/sign/bdn" 11 | ) 12 | 13 | const ( 14 | KeyVersion = 'T' 15 | ) 16 | 17 | func Sign(scalar kyber.Scalar, msg []byte) ([]byte, error) { 18 | scheme := bdn.NewSchemeOnG1(bn256.NewSuiteG2()) 19 | return scheme.Sign(scalar, msg) 20 | } 21 | 22 | func Verify(pub kyber.Point, msg, sig []byte) error { 23 | scheme := bdn.NewSchemeOnG1(bn256.NewSuiteG2()) 24 | return scheme.Verify(pub, msg, sig) 25 | } 26 | 27 | func PrivateKeyFromHex(s string) (kyber.Scalar, error) { 28 | seed, err := hex.DecodeString(s) 29 | if err != nil { 30 | return nil, err 31 | } 32 | suite := bn256.NewSuiteG2() 33 | scalar := suite.Scalar().SetBytes(seed) 34 | return scalar, nil 35 | } 36 | 37 | func PrivateKeyBytes(scalar kyber.Scalar) []byte { 38 | b, err := scalar.MarshalBinary() 39 | if err != nil { 40 | panic(err) 41 | } 42 | return b 43 | } 44 | 45 | func PublicKey(scalar kyber.Scalar) kyber.Point { 46 | suite := bn256.NewSuiteG2() 47 | return suite.Point().Mul(scalar, nil) 48 | } 49 | 50 | func PublicKeyString(point kyber.Point) string { 51 | b := PublicKeyBytes(point) 52 | return base58.CheckEncode(b, KeyVersion) 53 | } 54 | 55 | func PublicKeyBytes(point kyber.Point) []byte { 56 | b, err := point.MarshalBinary() 57 | if err != nil { 58 | panic(err) 59 | } 60 | return b 61 | } 62 | 63 | func PubKeyFromBytes(b []byte) (kyber.Point, error) { 64 | suite := bn256.NewSuiteG2() 65 | point := suite.G2().Point() 66 | err := point.UnmarshalBinary(b) 67 | return point, err 68 | } 69 | 70 | func PubKeyFromBase58(s string) (kyber.Point, error) { 71 | b, ver, err := base58.CheckDecode(s) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if ver != KeyVersion { 76 | return nil, fmt.Errorf("invalid version %d", ver) 77 | } 78 | return PubKeyFromBytes(b) 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MixinNetwork/tip 2 | 3 | go 1.23.2 4 | 5 | replace github.com/dgraph-io/badger/v4 => github.com/MixinNetwork/badger/v4 v4.3.1-F1 6 | 7 | require ( 8 | github.com/MixinNetwork/bot-api-go-client/v3 v3.8.6 9 | github.com/btcsuite/btcd/btcutil v1.1.6 10 | github.com/dgraph-io/badger/v4 v4.3.1 11 | github.com/drand/kyber v1.3.1 12 | github.com/fox-one/mixin-sdk-go v1.9.1 13 | github.com/gofrs/uuid/v5 v5.3.0 14 | github.com/pelletier/go-toml v1.9.5 15 | github.com/stretchr/testify v1.9.0 16 | github.com/unrolled/render v1.7.0 17 | github.com/urfave/cli/v2 v2.27.5 18 | golang.org/x/crypto v0.35.0 19 | ) 20 | 21 | require ( 22 | filippo.io/edwards25519 v1.1.0 // indirect 23 | github.com/MixinNetwork/go-number v0.1.1 // indirect 24 | github.com/MixinNetwork/mixin v0.18.15 // indirect 25 | github.com/btcsuite/btcutil v1.0.2 // indirect 26 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 27 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/dgraph-io/ristretto v1.0.0 // indirect 30 | github.com/dustin/go-humanize v1.0.1 // indirect 31 | github.com/fox-one/msgpack v1.0.0 // indirect 32 | github.com/fsnotify/fsnotify v1.6.0 // indirect 33 | github.com/go-resty/resty/v2 v2.10.0 // indirect 34 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 35 | github.com/gogo/protobuf v1.3.2 // indirect 36 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 37 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 38 | github.com/golang/protobuf v1.5.4 // indirect 39 | github.com/google/flatbuffers v24.3.25+incompatible // indirect 40 | github.com/gorilla/websocket v1.5.3 // indirect 41 | github.com/klauspost/compress v1.17.10 // indirect 42 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 43 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 44 | github.com/pkg/errors v0.9.1 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 47 | github.com/shopspring/decimal v1.4.0 // indirect 48 | github.com/vmihailenco/tagparser v0.1.2 // indirect 49 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 50 | github.com/zeebo/blake3 v0.2.4 // indirect 51 | go.dedis.ch/fixbuf v1.0.3 // indirect 52 | golang.org/x/net v0.36.0 // indirect 53 | golang.org/x/sync v0.8.0 // indirect 54 | golang.org/x/sys v0.30.0 // indirect 55 | google.golang.org/appengine v1.6.8 // indirect 56 | google.golang.org/protobuf v1.35.1 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= 3 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/MixinNetwork/badger/v4 v4.3.1-F1 h1:IxImp/PzrEWEzQtviDS6kvfv0e8o87SXMvHGn26/sfc= 7 | github.com/MixinNetwork/badger/v4 v4.3.1-F1/go.mod h1:Di5epo3QybOO+IxG5n+y6+3pDmckOdew/JCdfvjnNw0= 8 | github.com/MixinNetwork/bot-api-go-client/v3 v3.8.6 h1:u0nFlsp0LICWWAxUDS2uE0drxkQGfSaIgt6QD1n8jh8= 9 | github.com/MixinNetwork/bot-api-go-client/v3 v3.8.6/go.mod h1:h2C6c1ULWYR+UQEtQPAlUVFSD24kAGNOf3h+fhgfRFg= 10 | github.com/MixinNetwork/go-number v0.1.1 h1:Ui/xi0WGiBWI6cPrZaffB6q8lP7m2Zw0CXgOqLXb/3c= 11 | github.com/MixinNetwork/go-number v0.1.1/go.mod h1:4kaXQW9NOjjO3uZ5ehRVn3m+G+5ENGEKgiwfxea3zGQ= 12 | github.com/MixinNetwork/mixin v0.18.15 h1:F75II74rrSwWIWGg5L0KFUsDWXaKrAMROvedY33A85k= 13 | github.com/MixinNetwork/mixin v0.18.15/go.mod h1:3def81L2q9wZdjz45KMTP9kc8tcpwQQI4YquE1AyFc8= 14 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 15 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 16 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 17 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= 18 | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= 19 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 20 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 21 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 22 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 23 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= 24 | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= 25 | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= 26 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 27 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 28 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 29 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 30 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 31 | github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= 32 | github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= 33 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 34 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 35 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 36 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 37 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 38 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 39 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 40 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 41 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 42 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 45 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 46 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 47 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 48 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 49 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 52 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 54 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 55 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 56 | github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= 57 | github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= 58 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 59 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 60 | github.com/drand/kyber v1.3.1 h1:E0p6M3II+loMVwTlAp5zu4+GGZFNiRfq02qZxzw2T+Y= 61 | github.com/drand/kyber v1.3.1/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA= 62 | github.com/drand/kyber-bls12381 v0.3.1 h1:KWb8l/zYTP5yrvKTgvhOrk2eNPscbMiUOIeWBnmUxGo= 63 | github.com/drand/kyber-bls12381 v0.3.1/go.mod h1:H4y9bLPu7KZA/1efDg+jtJ7emKx+ro3PU7/jWUVt140= 64 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 65 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 66 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 67 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 68 | github.com/fox-one/mixin-sdk-go v1.9.1 h1:77zhBrNdKon3sU40N9Fj+7DbXynO/TvWevGYFrYT9dc= 69 | github.com/fox-one/mixin-sdk-go v1.9.1/go.mod h1:83Qjg0QXQ+4NDUUjarP/Jf98mFb/fh0Zqd850CHNAPw= 70 | github.com/fox-one/msgpack v1.0.0 h1:atr4La29WdMPCoddlRAPK2e1yhBJ2cEFF+2X93KY5Vs= 71 | github.com/fox-one/msgpack v1.0.0/go.mod h1:Gf/g5JQGPkB0JrQvfxCu8ZXm4jqXsCPe89mFe8i3vms= 72 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 73 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 74 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 75 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 76 | github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= 77 | github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= 78 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 79 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 80 | github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= 81 | github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 82 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 83 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 84 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 85 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 86 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 87 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 88 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 89 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 90 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 91 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 93 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 94 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 95 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 96 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 97 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 98 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 99 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 100 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 101 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 102 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 103 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 104 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 105 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 106 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 107 | github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= 108 | github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 109 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 110 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 111 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 112 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 113 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 115 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 116 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 117 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 118 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 119 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 120 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 121 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 122 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 123 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 124 | github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= 125 | github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 126 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 127 | github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= 128 | github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= 129 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 130 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 131 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 132 | github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= 133 | github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 134 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 135 | github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 136 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 137 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 138 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 139 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 140 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 141 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 142 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 143 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 144 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 145 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 146 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 147 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 148 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 149 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 150 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 151 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 152 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 153 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 154 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 155 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 156 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 157 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 158 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 159 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 160 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 161 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 162 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 163 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 164 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 165 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 166 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 167 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 168 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 169 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 170 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 171 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 172 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 173 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 174 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 175 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 176 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 177 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 178 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 179 | github.com/unrolled/render v1.7.0 h1:1yke01/tZiZpiXfUG+zqB+6fq3G4I+KDmnh0EhPq7So= 180 | github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI= 181 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 182 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 183 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 184 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 185 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 186 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 187 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 188 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 189 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 190 | github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= 191 | github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 192 | github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= 193 | github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= 194 | github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= 195 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 196 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 197 | go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= 198 | go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= 199 | go.dedis.ch/protobuf v1.0.11 h1:FTYVIEzY/bfl37lu3pR4lIj+F9Vp1jE8oh91VmxKgLo= 200 | go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= 201 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 202 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 203 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 204 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 205 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 206 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 207 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 208 | golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 209 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 210 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 211 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 212 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 213 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 214 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 215 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 216 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 217 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 218 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 219 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 220 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 221 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 222 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 223 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 224 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 225 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 226 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 227 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 228 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 229 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 230 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 231 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 232 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 233 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 234 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 235 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 236 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 237 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 238 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 239 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 240 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 241 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 242 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 248 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 249 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 250 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 251 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 252 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 253 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 254 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 255 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 262 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 263 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 264 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 266 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 267 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 268 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 269 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 270 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 271 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 272 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 273 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 274 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 275 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 276 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 277 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 278 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 279 | golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= 280 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 281 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 282 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 283 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 284 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 285 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 286 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 287 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 288 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 289 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 290 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 291 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 292 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 293 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 294 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 295 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 296 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 297 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 298 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 299 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 300 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 301 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 302 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 303 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 304 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 305 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 306 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 307 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 308 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 309 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 310 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 311 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 312 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 313 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 314 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 315 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 316 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 317 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 318 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 319 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 320 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 321 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 322 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 323 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 324 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 325 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 326 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 327 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 328 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 329 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 330 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 331 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 332 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 333 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 334 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 335 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 336 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 337 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 338 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 339 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 340 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 341 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 342 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 343 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 344 | nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= 345 | nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= 346 | -------------------------------------------------------------------------------- /keeper/guard.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "math/big" 11 | "time" 12 | 13 | "github.com/MixinNetwork/tip/crypto" 14 | "github.com/MixinNetwork/tip/logger" 15 | "github.com/MixinNetwork/tip/store" 16 | "github.com/drand/kyber" 17 | ) 18 | 19 | const ( 20 | EphemeralGracePeriod = time.Hour * 24 * 128 21 | EphemeralLimitWindow = time.Hour * 24 22 | EphemeralLimitQuota = 42 23 | SecretLimitWindow = time.Hour * 24 * 7 24 | SecretLimitQuota = 10 25 | ) 26 | 27 | type Response struct { 28 | Available int 29 | Nonce uint64 30 | Identity kyber.Point 31 | Assignor []byte 32 | Watcher []byte 33 | } 34 | 35 | func Guard(store store.Storage, priv kyber.Scalar, identity, signature, data string) (*Response, error) { 36 | b, err := base64.RawURLEncoding.DecodeString(data) 37 | if err != nil || len(b) == 0 { 38 | return nil, fmt.Errorf("invalid data %s", data) 39 | } 40 | pub, err := crypto.PubKeyFromBase58(identity) 41 | if err != nil { 42 | return nil, fmt.Errorf("invalid identity %s", identity) 43 | } 44 | b = crypto.DecryptECDH(pub, priv, b) 45 | 46 | var body body 47 | err = json.Unmarshal(b, &body) 48 | if err != nil { 49 | return nil, fmt.Errorf("invalid json %s", string(b)) 50 | } 51 | if body.Identity != identity { 52 | return nil, fmt.Errorf("invalid identity %s", identity) 53 | } 54 | var ab []byte 55 | if len(body.Assignee) > 0 { 56 | ab, err = checkAssignee(body.Assignee) 57 | if err != nil { 58 | return nil, err 59 | } 60 | } 61 | eb, valid := new(big.Int).SetString(body.Ephemeral, 16) 62 | if !valid { 63 | return nil, fmt.Errorf("invalid ephemeral %s", body.Ephemeral) 64 | } 65 | rb, _ := new(big.Int).SetString(body.Rotate, 16) 66 | sig, err := hex.DecodeString(signature) 67 | if err != nil { 68 | return nil, fmt.Errorf("invalid signature %s", signature) 69 | } 70 | 71 | assignee, err := store.ReadAssignee(crypto.PublicKeyBytes(pub)) 72 | if err != nil { 73 | return nil, err 74 | } else if pb := crypto.PublicKeyBytes(pub); assignee != nil && !bytes.Equal(assignee, pb) { 75 | lkey := append(pb, "SECRET"...) 76 | available, err := store.CheckLimit(lkey, SecretLimitWindow, SecretLimitQuota, true) 77 | logger.Debug("keeper.CheckLimit", "ASSIGNEE", true, hex.EncodeToString(assignee), hex.EncodeToString(pb), available, err) 78 | return &Response{Available: available}, err 79 | } 80 | 81 | assignor, err := store.ReadAssignor(crypto.PublicKeyBytes(pub)) 82 | if err != nil { 83 | return nil, err 84 | } else if assignor == nil { 85 | assignor = crypto.PublicKeyBytes(pub) 86 | } 87 | 88 | watcher, _ := hex.DecodeString(body.Watcher) 89 | if len(watcher) != 32 { 90 | return nil, fmt.Errorf("invalid watcher %s", body.Watcher) 91 | } 92 | oas, og, oc, err := store.Watch(watcher) 93 | logger.Debug("store.Watch", body.Watcher, hex.EncodeToString(oas), og, oc, err) 94 | if err != nil { 95 | return nil, fmt.Errorf("watch %x error %v", watcher, err) 96 | } 97 | if oas != nil && !bytes.Equal(oas, assignor) { 98 | lkey := append(oas, "SECRET"...) 99 | available, err := store.CheckLimit(lkey, SecretLimitWindow, SecretLimitQuota, true) 100 | logger.Debug("keeper.CheckLimit", "WATCHER", true, hex.EncodeToString(oas), hex.EncodeToString(assignor), available, err) 101 | return &Response{Available: available}, err 102 | } 103 | 104 | lkey := append(assignor, "EPHEMERAL"...) 105 | available, err := store.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 106 | if err != nil || available < 1 { 107 | logger.Debug("keeper.CheckLimit", "EPHEMERAL", false, hex.EncodeToString(assignor), available, err) 108 | return &Response{Available: available}, err 109 | } 110 | nonce, grace := uint64(body.Nonce), time.Duration(body.Grace) 111 | if grace < EphemeralGracePeriod { 112 | grace = EphemeralGracePeriod 113 | } 114 | valid, err = store.CheckEphemeralNonce(assignor, eb.Bytes(), nonce, grace) 115 | if err != nil { 116 | return nil, err 117 | } 118 | if !valid { 119 | available, err = store.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, true) 120 | logger.Debug("keeper.CheckLimit", "EPHEMERAL", true, hex.EncodeToString(assignor), available, err) 121 | return &Response{Available: available}, err 122 | } 123 | if rb != nil && rb.Sign() > 0 { 124 | err = store.RotateEphemeralNonce(assignor, rb.Bytes(), nonce) 125 | if err != nil { 126 | return nil, err 127 | } 128 | } 129 | 130 | lkey = append(assignor, "SECRET"...) 131 | available, err = store.CheckLimit(lkey, SecretLimitWindow, SecretLimitQuota, false) 132 | if err != nil || available < 1 { 133 | logger.Debug("keeper.CheckLimit", "SECRET", false, hex.EncodeToString(assignor), available, err) 134 | return &Response{Available: available}, err 135 | } 136 | err = checkSignature(pub, sig, eb, rb, nonce, uint64(grace), ab) 137 | if err == nil { 138 | if len(ab) > 0 { 139 | err := store.WriteAssignee(assignor, ab[:128]) 140 | logger.Debugf("store.WriteAssignee(%x, %x) => %v", assignor, ab[:128], err) 141 | if err != nil { 142 | return nil, err 143 | } 144 | } 145 | 146 | return &Response{ 147 | Available: available, 148 | Nonce: nonce, 149 | Identity: pub, 150 | Assignor: assignor, 151 | Watcher: watcher, 152 | }, nil 153 | } 154 | available, err = store.CheckLimit(lkey, SecretLimitWindow, SecretLimitQuota, true) 155 | logger.Debug("keeper.CheckLimit", "SECRET", true, hex.EncodeToString(assignor), available, err) 156 | return &Response{Available: available}, err 157 | } 158 | 159 | func checkSignature(pub kyber.Point, sig []byte, eb, rb *big.Int, nonce, grace uint64, ab []byte) error { 160 | if len(eb.Bytes()) > 32 || eb.Sign() <= 0 { 161 | return fmt.Errorf("invalid ephemeral %x", eb.Bytes()) 162 | } 163 | msg := crypto.PublicKeyBytes(pub) 164 | ebuf := make([]byte, 32) 165 | eb.FillBytes(ebuf) 166 | msg = append(msg, ebuf...) 167 | buf := make([]byte, 8) 168 | binary.BigEndian.PutUint64(buf, nonce) 169 | msg = append(msg, buf...) 170 | binary.BigEndian.PutUint64(buf, grace) 171 | msg = append(msg, buf...) 172 | if rb != nil && rb.Sign() > 0 && len(rb.Bytes()) <= 32 { 173 | rbuf := make([]byte, 32) 174 | rb.FillBytes(rbuf) 175 | msg = append(msg, rbuf...) 176 | } 177 | msg = append(msg, ab...) 178 | return crypto.Verify(pub, msg, sig) 179 | } 180 | 181 | func checkAssignee(as string) ([]byte, error) { 182 | ab, err := hex.DecodeString(as) 183 | if err != nil { 184 | return nil, fmt.Errorf("invalid assignee format %s", err) 185 | } 186 | if len(ab) != 192 { 187 | return nil, fmt.Errorf("invalid assignee format %d", len(as)) 188 | } 189 | ap, err := crypto.PubKeyFromBytes(ab[:128]) 190 | if err != nil { 191 | return nil, fmt.Errorf("invalid assignee public key %s", err) 192 | } 193 | return ab, crypto.Verify(ap, ab[:128], ab[128:]) 194 | } 195 | 196 | type body struct { 197 | // main identity public key to check signature 198 | Identity string `json:"identity"` 199 | 200 | // a new identity to represent the main identity 201 | Assignee string `json:"assignee"` 202 | 203 | // the ephemeral secret to authenticate 204 | Ephemeral string `json:"ephemeral"` 205 | 206 | // ephemeral grace period to maintain the secret valid, the grace 207 | // will be extended for each valid request, and if the grace expired 208 | // the ephemeral can be reset 209 | Grace int64 `json:"grace"` 210 | 211 | // ensure each request can only be used once 212 | Nonce int64 `json:"nonce"` 213 | 214 | // the ephemeral rotation allows user to use a new secret 215 | // e.g. when they cooperate with others to generate a non-random 216 | // ephemeral to replace their on-device random one 217 | Rotate string `json:"rotate"` 218 | 219 | // the key to watch the identity state 220 | Watcher string `json:"watcher"` 221 | } 222 | -------------------------------------------------------------------------------- /keeper/guard_test.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "encoding/json" 10 | "math/big" 11 | "os" 12 | "testing" 13 | "time" 14 | 15 | "github.com/MixinNetwork/tip/crypto" 16 | "github.com/MixinNetwork/tip/logger" 17 | "github.com/MixinNetwork/tip/store" 18 | "github.com/drand/kyber" 19 | "github.com/drand/kyber/pairing/bn256" 20 | "github.com/drand/kyber/util/random" 21 | "github.com/stretchr/testify/assert" 22 | ) 23 | 24 | func TestGuard(t *testing.T) { 25 | assert := assert.New(t) 26 | 27 | dir, _ := os.MkdirTemp("/tmp", "tip-keeper-test") 28 | conf := &store.BadgerConfiguration{Dir: dir} 29 | bs, _ := store.OpenBadger(context.Background(), conf) 30 | defer bs.Close() 31 | 32 | suite := bn256.NewSuiteBn256() 33 | signer := suite.Scalar().Pick(random.New()) 34 | node := crypto.PublicKey(signer) 35 | user := suite.Scalar().Pick(random.New()) 36 | userPub := crypto.PublicKey(user) 37 | identity := crypto.PublicKeyString(userPub) 38 | 39 | watcherSeed := make([]byte, 32) 40 | _, err := rand.Read(watcherSeed) 41 | assert.Nil(err) 42 | 43 | ephmr := crypto.PrivateKeyBytes(suite.Scalar().Pick(random.New())) 44 | epb := new(big.Int).SetBytes(ephmr).Bytes() 45 | grace := uint64(time.Hour * 24 * 128) 46 | for i := uint64(0); i < 10; i++ { 47 | signature, data := makeTestRequest(user, node, ephmr, nil, 1024+i, grace) 48 | res, err := Guard(bs, signer, identity, signature, data) 49 | assert.Nil(err) 50 | assert.Equal(SecretLimitQuota, res.Available) 51 | assert.Equal(1024+i, res.Nonce) 52 | key := crypto.PublicKeyBytes(crypto.PublicKey(user)) 53 | lkey := append(key, "EPHEMERAL"...) 54 | available, err := bs.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 55 | assert.Equal(EphemeralLimitQuota, available) 56 | assert.Nil(err) 57 | } 58 | 59 | // data should be base64 RawURLEncoding and none blank 60 | signature, _ := makeTestRequest(user, node, ephmr, nil, 1024, grace) 61 | _, err = Guard(bs, signer, identity, signature, "") 62 | assert.NotNil(err) 63 | 64 | // identity is not equal 65 | signature, data := makeTestRequestWithInvalidIdentity(user, node, ephmr, nil, 1039, grace, "", "", "") 66 | _, err = Guard(bs, signer, identity, signature, data) 67 | assert.NotNil(err) 68 | assert.Contains(err.Error(), "invalid identity ") 69 | 70 | // invalid nonce 71 | signature, data = makeTestRequest(user, node, ephmr, nil, 1024, grace) 72 | res, err := Guard(bs, signer, identity, signature, data) 73 | assert.Nil(err) 74 | assert.Equal(EphemeralLimitQuota-1, res.Available) 75 | assert.Nil(res.Watcher) 76 | key := crypto.PublicKeyBytes(crypto.PublicKey(user)) 77 | lkey := append(key, "EPHEMERAL"...) 78 | available, err := bs.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 79 | assert.Equal(EphemeralLimitQuota-1, available) 80 | assert.Nil(err) 81 | oas, _, counter, err := bs.Watch(watcherSeed) 82 | assert.Nil(err) 83 | assert.Equal(0, counter) 84 | assert.Nil(oas) 85 | 86 | // invalid encryption 87 | signature, data = makeTestRequest(user, crypto.PublicKey(user), ephmr, nil, 1034, grace) 88 | res, err = Guard(bs, signer, identity, signature, data) 89 | assert.Nil(res) 90 | assert.Contains(err.Error(), "invalid json ") 91 | key = crypto.PublicKeyBytes(crypto.PublicKey(user)) 92 | lkey = append(key, "EPHEMERAL"...) 93 | available, err = bs.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 94 | assert.Equal(EphemeralLimitQuota-1, available) 95 | assert.Nil(err) 96 | 97 | // invalid ephemeral 98 | signature, data = makeTestRequest(user, node, crypto.PublicKeyBytes(node), nil, 1034, grace) 99 | res, err = Guard(bs, signer, identity, signature, data) 100 | assert.Nil(err) 101 | assert.Equal(EphemeralLimitQuota-2, res.Available) 102 | assert.Nil(res.Watcher) 103 | key = crypto.PublicKeyBytes(crypto.PublicKey(user)) 104 | lkey = append(key, "EPHEMERAL"...) 105 | available, err = bs.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 106 | assert.Equal(EphemeralLimitQuota-2, available) 107 | assert.Nil(err) 108 | 109 | // invalid signature 110 | for i := 1; i < 6; i++ { 111 | _, data = makeTestRequest(user, node, ephmr, nil, uint64(1033+i), grace) 112 | res, err := Guard(bs, signer, identity, hex.EncodeToString(ephmr), data) 113 | assert.Nil(err) 114 | assert.Equal(res.Available, SecretLimitQuota-i) 115 | assert.Nil(res.Watcher) 116 | key = crypto.PublicKeyBytes(crypto.PublicKey(user)) 117 | lkey = append(key, "EPHEMERAL"...) 118 | available, err := bs.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 119 | assert.Equal(EphemeralLimitQuota-2, available) 120 | assert.Nil(err) 121 | lkey = append(key, "SECRET"...) 122 | available, err = bs.CheckLimit(lkey, SecretLimitWindow, SecretLimitQuota, false) 123 | assert.Equal(SecretLimitQuota-i, available) 124 | assert.Nil(err) 125 | } 126 | 127 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1039, grace, "", "", hex.EncodeToString(watcherSeed)) 128 | res, err = Guard(bs, signer, identity, signature, data) 129 | assert.Nil(err) 130 | assert.Equal(SecretLimitQuota-5, res.Available) 131 | assert.Equal(uint64(1039), res.Nonce) 132 | assert.NotNil(res.Watcher) 133 | key = crypto.PublicKeyBytes(crypto.PublicKey(user)) 134 | lkey = append(key, "EPHEMERAL"...) 135 | available, err = bs.CheckLimit(lkey, EphemeralLimitWindow, EphemeralLimitQuota, false) 136 | assert.Equal(EphemeralLimitQuota-2, available) 137 | assert.Nil(err) 138 | lkey = append(key, "SECRET"...) 139 | available, err = bs.CheckLimit(lkey, SecretLimitWindow, SecretLimitQuota, false) 140 | assert.Equal(SecretLimitQuota-5, available) 141 | assert.Nil(err) 142 | 143 | // invalid assignee 144 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1039, grace, identity, "", "") 145 | _, err = Guard(bs, signer, identity, signature, data) 146 | assert.NotNil(err) 147 | assert.Contains(err.Error(), "invalid assignee ") 148 | // valid assignee 149 | assignee := crypto.PublicKeyBytes(userPub) 150 | sig, err := crypto.Sign(user, assignee) 151 | assert.Nil(err) 152 | assignee = append(assignee, sig...) 153 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1039, grace, hex.EncodeToString(assignee), "", "") 154 | res, err = Guard(bs, signer, identity, signature, data) 155 | assert.Contains(err.Error(), "invalid watcher ") 156 | assert.Nil(res) 157 | assignee = crypto.PublicKeyBytes(userPub) 158 | sig, err = crypto.Sign(user, assignee) 159 | assert.Nil(err) 160 | assignee = append(assignee, sig...) 161 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1040, grace, hex.EncodeToString(assignee), "", hex.EncodeToString(watcherSeed)) 162 | res, err = Guard(bs, signer, identity, signature, data) 163 | assert.Nil(err) 164 | assert.NotNil(res) 165 | _, counter, err = bs.WriteSignRequest(res.Assignor, res.Watcher) 166 | assert.Nil(err) 167 | assert.Equal(1, counter) 168 | assignee, err = bs.ReadAssignee(crypto.PublicKeyBytes(userPub)) 169 | assert.Nil(err) 170 | assert.Len(assignee, 128) 171 | valid, err := bs.CheckEphemeralNonce(crypto.PublicKeyBytes(userPub), epb, 1040, time.Duration(grace)) 172 | assert.Nil(err) 173 | assert.False(valid) 174 | valid, err = bs.CheckEphemeralNonce(crypto.PublicKeyBytes(userPub), epb, 1041, time.Duration(grace)) 175 | assert.Nil(err) 176 | assert.True(valid) 177 | oas, _, counter, err = bs.Watch(watcherSeed) 178 | assert.Nil(err) 179 | assert.Equal(1, counter) 180 | assert.Equal(oas, crypto.PublicKeyBytes(userPub)) 181 | // valid existing assignee counter + 1 182 | assignee = crypto.PublicKeyBytes(userPub) 183 | sig, err = crypto.Sign(user, assignee) 184 | assert.Nil(err) 185 | assignee = append(assignee, sig...) 186 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1042, grace, hex.EncodeToString(assignee), "", hex.EncodeToString(watcherSeed)) 187 | res, err = Guard(bs, signer, identity, signature, data) 188 | assert.Nil(err) 189 | assert.Equal(SecretLimitQuota-5, res.Available) 190 | assert.NotNil(res.Watcher) 191 | _, counter, err = bs.WriteSignRequest(res.Assignor, res.Watcher) 192 | assert.Nil(err) 193 | assert.Equal(2, counter) 194 | assignee, err = bs.ReadAssignee(crypto.PublicKeyBytes(userPub)) 195 | assert.Nil(err) 196 | assert.Len(assignee, 128) 197 | valid, err = bs.CheckEphemeralNonce(crypto.PublicKeyBytes(userPub), epb, 1042, time.Duration(grace)) 198 | assert.Nil(err) 199 | assert.False(valid) 200 | valid, err = bs.CheckEphemeralNonce(crypto.PublicKeyBytes(userPub), epb, 1043, time.Duration(grace)) 201 | assert.Nil(err) 202 | assert.True(valid) 203 | oas, _, counter, err = bs.Watch(watcherSeed) 204 | assert.Nil(err) 205 | assert.Equal(2, counter) 206 | assert.Equal(oas, crypto.PublicKeyBytes(userPub)) 207 | // valid new assignee counter + 1 208 | newUser := suite.Scalar().Pick(random.New()) 209 | newUserPub := crypto.PublicKey(newUser) 210 | newIdentity := crypto.PublicKeyString(newUserPub) 211 | assignee = crypto.PublicKeyBytes(newUserPub) 212 | sig, err = crypto.Sign(newUser, assignee) 213 | assert.Nil(err) 214 | assignee = append(assignee, sig...) 215 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1045, grace, hex.EncodeToString(assignee), "", hex.EncodeToString(watcherSeed)) 216 | res, err = Guard(bs, signer, identity, signature, data) 217 | assert.Nil(err) 218 | assert.NotNil(res) 219 | // test user pin 220 | signature, data = makeTestRequestWithAssigneeAndRotation(newUser, node, ephmr, nil, 1046, grace, "", "", hex.EncodeToString(watcherSeed)) 221 | resNew, err := Guard(bs, signer, newIdentity, signature, data) 222 | assert.Nil(err) 223 | assert.NotNil(resNew) 224 | assert.Equal(res.Assignor, resNew.Assignor) 225 | _, _, counter, err = bs.Watch(watcherSeed) 226 | assert.Nil(err) 227 | assert.Equal(3, counter) 228 | _, counter, err = bs.WriteSignRequest(res.Assignor, res.Watcher) 229 | assert.Nil(err) 230 | assert.Equal(3, counter) 231 | _, _, counter, err = bs.Watch(watcherSeed) 232 | assert.Nil(err) 233 | assert.Equal(3, counter) 234 | // test user old pin 235 | signature, data = makeTestRequestWithAssigneeAndRotation(user, node, ephmr, nil, 1047, grace, "", "", hex.EncodeToString(watcherSeed)) 236 | res, err = Guard(bs, signer, identity, signature, data) 237 | assert.Nil(err) 238 | assert.Nil(res.Watcher) 239 | assert.Equal(SecretLimitQuota-6, res.Available) 240 | // test invalid watcher identity 241 | invalidUser := suite.Scalar().Pick(random.New()) 242 | invalidUserPub := crypto.PublicKey(invalidUser) 243 | invalidIdentity := crypto.PublicKeyString(invalidUserPub) 244 | signature, data = makeTestRequestWithAssigneeAndRotation(invalidUser, node, ephmr, nil, 1047, grace, "", "", hex.EncodeToString(watcherSeed)) 245 | res, err = Guard(bs, signer, invalidIdentity, signature, data) 246 | assert.Nil(err) 247 | assert.Nil(res.Watcher) 248 | assert.Equal(SecretLimitQuota-7, res.Available) 249 | oas, _, counter, err = bs.Watch(watcherSeed) 250 | assert.Nil(err) 251 | assert.Equal(3, counter) 252 | assert.Equal(oas, crypto.PublicKeyBytes(userPub)) 253 | signature, data = makeTestRequestWithAssigneeAndRotation(newUser, node, ephmr, nil, 1048, grace, "", "", hex.EncodeToString(watcherSeed)) 254 | res, err = Guard(bs, signer, newIdentity, signature, data) 255 | assert.Nil(err) 256 | assert.NotNil(res.Watcher) 257 | assert.Equal(SecretLimitQuota-7, res.Available) 258 | signature, data = makeTestRequestWithAssigneeAndRotation(invalidUser, node, ephmr, nil, 1050, grace, "", "", hex.EncodeToString(watcherSeed)) 259 | res, err = Guard(bs, signer, invalidIdentity, signature, data) 260 | assert.Nil(err) 261 | assert.Nil(res.Watcher) 262 | assert.Equal(SecretLimitQuota-8, res.Available) 263 | signature, data = makeTestRequestWithAssigneeAndRotation(newUser, node, ephmr, nil, 1051, grace, "", "", hex.EncodeToString(watcherSeed)) 264 | res, err = Guard(bs, signer, newIdentity, signature, data) 265 | assert.Nil(err) 266 | assert.NotNil(res.Watcher) 267 | assert.Equal(SecretLimitQuota-8, res.Available) 268 | oas, _, counter, err = bs.Watch(watcherSeed) 269 | assert.Nil(err) 270 | assert.Equal(3, counter) 271 | assert.Equal(oas, crypto.PublicKeyBytes(userPub)) 272 | // setup li pin 273 | liWatcher := make([]byte, 32) 274 | _, err = rand.Read(liWatcher) 275 | assert.Nil(err) 276 | li := suite.Scalar().Pick(random.New()) 277 | liPub := crypto.PublicKey(li) 278 | liIdentity := crypto.PublicKeyString(liPub) 279 | signature, data = makeTestRequestWithAssigneeAndRotation(li, node, ephmr, nil, 100, grace, "", "", hex.EncodeToString(liWatcher)) 280 | res, err = Guard(bs, signer, liIdentity, signature, data) 281 | assert.Nil(err) 282 | assert.NotNil(res) 283 | // update li' pin with wrong assignee 284 | signature, data = makeTestRequestWithAssigneeAndRotation(li, node, ephmr, nil, 105, grace, hex.EncodeToString(assignee), "", hex.EncodeToString(liWatcher)) 285 | _, err = Guard(bs, signer, liIdentity, signature, data) 286 | assert.NotNil(err) 287 | assert.Contains(err.Error(), "invalid assignor as is assignee") 288 | // update li pin 289 | liNew := suite.Scalar().Pick(random.New()) 290 | liNewPub := crypto.PublicKey(liNew) 291 | liNewIdentity := crypto.PublicKeyString(liNewPub) 292 | assignee = crypto.PublicKeyBytes(liNewPub) 293 | sig, err = crypto.Sign(liNew, assignee) 294 | assert.Nil(err) 295 | assignee = append(assignee, sig...) 296 | signature, data = makeTestRequestWithAssigneeAndRotation(li, node, ephmr, nil, 110, grace, hex.EncodeToString(assignee), "", hex.EncodeToString(watcherSeed)) 297 | res, err = Guard(bs, signer, liIdentity, signature, data) 298 | assert.Nil(err) 299 | assert.NotNil(res) 300 | // test li pin 301 | signature, data = makeTestRequestWithAssigneeAndRotation(liNew, node, ephmr, nil, 115, grace, "", "", hex.EncodeToString(liWatcher)) 302 | res, err = Guard(bs, signer, liNewIdentity, signature, data) 303 | assert.Nil(err) 304 | assert.NotNil(res) 305 | // pin should have watcher 306 | signature, data = makeTestRequestWithAssigneeAndRotation(liNew, node, ephmr, nil, 117, grace, "", "", "") 307 | res, err = Guard(bs, signer, liNewIdentity, signature, data) 308 | assert.Contains(err.Error(), "invalid watcher ") 309 | assert.Nil(res) 310 | // invalid ephmr 311 | ephmr = crypto.PrivateKeyBytes(suite.Scalar().Pick(random.New())) 312 | signature, data = makeTestRequestWithAssigneeAndRotation(liNew, node, ephmr, nil, 119, grace, "", "", hex.EncodeToString(liWatcher)) 313 | res, err = Guard(bs, signer, liNewIdentity, signature, data) 314 | assert.Nil(err) 315 | assert.Equal(EphemeralLimitQuota-1, res.Available) 316 | assert.Nil(res.Watcher) 317 | } 318 | 319 | func TestAssigneeAndRotation(t *testing.T) { 320 | assert := assert.New(t) 321 | 322 | dir, _ := os.MkdirTemp("/tmp", "tip-keeper-test") 323 | conf := &store.BadgerConfiguration{Dir: dir} 324 | bs, _ := store.OpenBadger(context.Background(), conf) 325 | defer bs.Close() 326 | 327 | suite := bn256.NewSuiteBn256() 328 | signer := suite.Scalar().Pick(random.New()) 329 | node := crypto.PublicKey(signer) 330 | 331 | u1 := suite.Scalar().Pick(random.New()) 332 | i1 := crypto.PublicKeyString(crypto.PublicKey(u1)) 333 | u2 := suite.Scalar().Pick(random.New()) 334 | i2 := crypto.PublicKeyString(crypto.PublicKey(u2)) 335 | u3 := suite.Scalar().Pick(random.New()) 336 | i3 := crypto.PublicKeyString(crypto.PublicKey(u3)) 337 | 338 | ephmr := crypto.PrivateKeyBytes(suite.Scalar().Pick(random.New())) 339 | grace := uint64(time.Hour * 24 * 128) 340 | signature, data := makeTestRequest(u1, node, ephmr, nil, 1024, grace) 341 | res, err := Guard(bs, signer, i1, signature, data) 342 | assert.Nil(err) 343 | assert.Equal(SecretLimitQuota, res.Available) 344 | assert.Equal(1024, int(res.Nonce)) 345 | 346 | ephmr = crypto.PrivateKeyBytes(suite.Scalar().Pick(random.New())) 347 | grace = uint64(time.Hour * 24 * 128) 348 | signature, data = makeTestRequest(u2, node, ephmr, nil, 1024, grace) 349 | res, err = Guard(bs, signer, i2, signature, data) 350 | assert.Nil(err) 351 | assert.Equal(SecretLimitQuota, res.Available) 352 | assert.Equal(1024, int(res.Nonce)) 353 | 354 | ephmr = crypto.PrivateKeyBytes(suite.Scalar().Pick(random.New())) 355 | grace = uint64(time.Hour * 24 * 128) 356 | signature, data = makeTestRequest(u3, node, ephmr, nil, 1024, grace) 357 | res, err = Guard(bs, signer, i3, signature, data) 358 | assert.Nil(err) 359 | assert.Equal(SecretLimitQuota, res.Available) 360 | assert.Equal(1024, int(res.Nonce)) 361 | } 362 | 363 | func makeTestRequest(user kyber.Scalar, signer kyber.Point, ephmr, rtt []byte, nonce, grace uint64) (string, string) { 364 | seed := make([]byte, 32) 365 | _, err := rand.Read(seed) 366 | if err != nil { 367 | panic(err) 368 | } 369 | return makeTestRequestWithAssigneeAndRotation(user, signer, ephmr, rtt, nonce, grace, "", "", hex.EncodeToString(seed)) 370 | } 371 | 372 | func makeTestRequestWithAssigneeAndRotation(user kyber.Scalar, signer kyber.Point, ephmr, rtt []byte, nonce, grace uint64, assignee, rotation, watcher string) (string, string) { 373 | logger.Debugf("rotation not tested %s", rotation) 374 | pkey := crypto.PublicKey(user) 375 | msg := crypto.PublicKeyBytes(pkey) 376 | msg = append(msg, ephmr...) 377 | buf := make([]byte, 8) 378 | binary.BigEndian.PutUint64(buf, nonce) 379 | msg = append(msg, buf...) 380 | binary.BigEndian.PutUint64(buf, grace) 381 | msg = append(msg, buf...) 382 | data := map[string]interface{}{ 383 | "identity": crypto.PublicKeyString(pkey), 384 | "ephemeral": hex.EncodeToString(ephmr), 385 | "nonce": nonce, 386 | "grace": grace, 387 | "watcher": watcher, 388 | } 389 | if rtt != nil { 390 | msg = append(msg, rtt[:]...) 391 | data["rotate"] = hex.EncodeToString(rtt) 392 | } 393 | if assignee != "" { 394 | buf, _ := hex.DecodeString(assignee) 395 | msg = append(msg, buf...) 396 | data["assignee"] = assignee 397 | } 398 | b, _ := json.Marshal(data) 399 | cipher := crypto.EncryptECDH(signer, user, b) 400 | sig, _ := crypto.Sign(user, msg) 401 | return hex.EncodeToString(sig), base64.RawURLEncoding.EncodeToString(cipher[:]) 402 | } 403 | 404 | func makeTestRequestWithInvalidIdentity(user kyber.Scalar, signer kyber.Point, ephmr, rtt []byte, nonce, grace uint64, assignee, rotation, watcher string) (string, string) { 405 | logger.Debugf("rotation and assignee not tested %s %s", rotation, assignee) 406 | pkey := crypto.PublicKey(user) 407 | msg := crypto.PublicKeyBytes(pkey) 408 | msg = append(msg, ephmr...) 409 | buf := make([]byte, 8) 410 | binary.BigEndian.PutUint64(buf, nonce) 411 | msg = append(msg, buf...) 412 | binary.BigEndian.PutUint64(buf, grace) 413 | msg = append(msg, buf...) 414 | suite := bn256.NewSuiteBn256() 415 | intruder := suite.Scalar().Pick(random.New()) 416 | intruderPub := crypto.PublicKey(intruder) 417 | data := map[string]interface{}{ 418 | "identity": crypto.PublicKeyString(intruderPub), 419 | "ephemeral": hex.EncodeToString(ephmr), 420 | "nonce": nonce, 421 | "grace": grace, 422 | "watcher": watcher, 423 | } 424 | if rtt != nil { 425 | msg = append(msg, rtt[:]...) 426 | data["rotate"] = hex.EncodeToString(rtt) 427 | } 428 | b, _ := json.Marshal(data) 429 | cipher := crypto.EncryptECDH(signer, user, b) 430 | sig, _ := crypto.Sign(user, msg) 431 | return hex.EncodeToString(sig), base64.RawURLEncoding.EncodeToString(cipher[:]) 432 | } 433 | -------------------------------------------------------------------------------- /logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | const ( 9 | ERROR = 1 10 | INFO = 2 11 | VERBOSE = 3 12 | DEBUG = 7 13 | ) 14 | 15 | var ( 16 | level = DEBUG 17 | ) 18 | 19 | func SetLevel(l int) { 20 | level = l 21 | } 22 | 23 | func Errorf(format string, v ...interface{}) { 24 | printfAtLevel(ERROR, format, v...) 25 | } 26 | 27 | func Error(v ...interface{}) { 28 | printAtLevel(ERROR, v...) 29 | } 30 | 31 | func Infof(format string, v ...interface{}) { 32 | printfAtLevel(INFO, format, v...) 33 | } 34 | 35 | func Info(v ...interface{}) { 36 | printAtLevel(INFO, v...) 37 | } 38 | 39 | func Verbosef(format string, v ...interface{}) { 40 | printfAtLevel(VERBOSE, format, v...) 41 | } 42 | 43 | func Verbose(v ...interface{}) { 44 | printAtLevel(VERBOSE, v...) 45 | } 46 | 47 | func Debugf(format string, v ...interface{}) { 48 | printfAtLevel(DEBUG, format, v...) 49 | } 50 | 51 | func Debug(v ...interface{}) { 52 | printAtLevel(DEBUG, v...) 53 | } 54 | 55 | func printfAtLevel(l int, format string, v ...interface{}) { 56 | if level < l { 57 | return 58 | } 59 | out := fmt.Sprintf(format, v...) 60 | log.Print(out) 61 | } 62 | 63 | func printAtLevel(l int, v ...interface{}) { 64 | if level < l { 65 | return 66 | } 67 | log.Println(v...) 68 | } 69 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | "github.com/MixinNetwork/tip/api" 12 | "github.com/MixinNetwork/tip/config" 13 | "github.com/MixinNetwork/tip/crypto" 14 | "github.com/MixinNetwork/tip/messenger" 15 | tip "github.com/MixinNetwork/tip/sdk/go" 16 | "github.com/MixinNetwork/tip/signer" 17 | "github.com/MixinNetwork/tip/store" 18 | "github.com/drand/kyber/pairing/bn256" 19 | "github.com/drand/kyber/sign/bdn" 20 | "github.com/drand/kyber/util/random" 21 | "github.com/fox-one/mixin-sdk-go" 22 | "github.com/urfave/cli/v2" 23 | ) 24 | 25 | func main() { 26 | app := &cli.App{ 27 | Name: "tip", 28 | Usage: "TIP (Throttled Identity PIN) is a decentralized key custodian.", 29 | Version: "0.3.0", 30 | EnableBashCompletion: true, 31 | Flags: []cli.Flag{ 32 | &cli.StringFlag{ 33 | Name: "config", 34 | Aliases: []string{"c"}, 35 | Value: "~/.tip/config.toml", 36 | Usage: "Configuration file path", 37 | }, 38 | }, 39 | Commands: []*cli.Command{ 40 | { 41 | Name: "signer", 42 | Usage: "Run the signer node", 43 | Action: runSigner, 44 | }, 45 | { 46 | Name: "setup", 47 | Usage: "Request a DKG setup", 48 | Action: requestSetup, 49 | Flags: []cli.Flag{ 50 | &cli.Uint64Flag{ 51 | Name: "nonce", 52 | Usage: "The nonce should match all other nodes", 53 | }, 54 | }, 55 | }, 56 | { 57 | Name: "key", 58 | Usage: "Generate a key pair", 59 | Action: genKey, 60 | }, 61 | { 62 | Name: "api", 63 | Usage: "Run the api node", 64 | Action: runAPI, 65 | }, 66 | { 67 | Name: "sign", 68 | Usage: "Request a signature", 69 | Action: requestSign, 70 | Flags: []cli.Flag{ 71 | &cli.StringFlag{ 72 | Name: "config", 73 | Usage: "The signers configuration", 74 | }, 75 | &cli.StringFlag{ 76 | Name: "key", 77 | Usage: "The identity key", 78 | }, 79 | &cli.StringFlag{ 80 | Name: "ephemeral", 81 | Usage: "The ephemeral seed", 82 | }, 83 | &cli.StringFlag{ 84 | Name: "rotate", 85 | Usage: "The ephemeral rotation", 86 | }, 87 | &cli.StringFlag{ 88 | Name: "assignee", 89 | Usage: "The identity assignee", 90 | }, 91 | &cli.StringFlag{ 92 | Name: "watcher", 93 | Usage: "The state watcher", 94 | }, 95 | &cli.Int64Flag{ 96 | Name: "nonce", 97 | Usage: "The nonce", 98 | }, 99 | }, 100 | }, 101 | }, 102 | } 103 | 104 | err := app.Run(os.Args) 105 | if err != nil { 106 | fmt.Println(err) 107 | } 108 | } 109 | 110 | func runSigner(c *cli.Context) error { 111 | ctx, cancel := context.WithCancel(context.Background()) 112 | defer cancel() 113 | 114 | cp := c.String("config") 115 | conf, err := config.ReadConfiguration(cp) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | store, err := store.OpenBadger(ctx, conf.Store) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | messenger, err := messenger.NewMixinMessenger(ctx, conf.Messenger) 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | node := signer.NewNode(ctx, cancel, store, messenger, conf.Node) 131 | return node.Run(ctx) 132 | } 133 | 134 | func runAPI(c *cli.Context) error { 135 | ctx := context.Background() 136 | 137 | cp := c.String("config") 138 | conf, err := config.ReadConfiguration(cp) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | store, err := store.OpenBadger(ctx, conf.Store) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | node := signer.NewNode(ctx, nil, store, nil, conf.Node) 149 | 150 | ac := conf.API 151 | ac.Key = node.GetKey() 152 | ac.Signers = node.GetSigners() 153 | ac.Poly = node.GetPoly() 154 | ac.Share = node.GetShare() 155 | server := api.NewServer(store, ac) 156 | return server.ListenAndServe() 157 | } 158 | 159 | func requestSetup(c *cli.Context) error { 160 | ctx := context.Background() 161 | 162 | nonce := c.Uint64("nonce") 163 | if nonce < 1024 { 164 | return fmt.Errorf("nonce too small") 165 | } 166 | 167 | cp := c.String("config") 168 | conf, err := config.ReadConfiguration(cp) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | key, err := crypto.PrivateKeyFromHex(conf.Node.Key) 174 | if err != nil { 175 | panic(conf.Node.Key) 176 | } 177 | 178 | s := &mixin.Keystore{ 179 | ClientID: conf.Messenger.UserId, 180 | SessionID: conf.Messenger.SessionId, 181 | PrivateKey: conf.Messenger.Key, 182 | } 183 | 184 | client, err := mixin.NewFromKeystore(s) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | msg := signer.MakeSetupMessage(ctx, key, nonce) 190 | mex := hex.EncodeToString(msg) 191 | data := base64.RawURLEncoding.EncodeToString(msg) 192 | fmt.Println(data, len(msg)) 193 | return client.SendMessage(ctx, &mixin.MessageRequest{ 194 | ConversationID: conf.Messenger.ConversationId, 195 | Category: mixin.MessageCategoryPlainText, 196 | MessageID: mixin.UniqueConversationID(mex, mex), 197 | Data: base64.RawURLEncoding.EncodeToString([]byte(data)), 198 | }) 199 | } 200 | 201 | func genKey(c *cli.Context) error { 202 | suite := bn256.NewSuiteG2() 203 | scalar := suite.Scalar().Pick(random.New()) 204 | point := suite.Point().Mul(scalar, nil) 205 | 206 | msg := []byte("tip") 207 | scheme := bdn.NewSchemeOnG1(suite) 208 | sig, err := scheme.Sign(scalar, msg) 209 | if err != nil { 210 | return err 211 | } 212 | err = scheme.Verify(point, msg, sig) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | pub := crypto.PublicKeyString(point) 218 | fmt.Println(scalar) 219 | fmt.Println(pub, err) 220 | return nil 221 | } 222 | 223 | func requestSign(c *cli.Context) error { 224 | f, err := os.ReadFile(c.String("config")) 225 | if err != nil { 226 | return err 227 | } 228 | conf, err := tip.LoadConfigurationJSON(string(f)) 229 | if err != nil { 230 | return err 231 | } 232 | client, _, err := tip.NewClient(conf) 233 | if err != nil { 234 | return err 235 | } 236 | grace := int64(time.Hour * 24 * 128) 237 | key := c.String("key") 238 | ephemeral := c.String("ephemeral") 239 | nonce := c.Int64("nonce") 240 | rotate := c.String("rotate") 241 | assignee := c.String("assignee") 242 | watcher := c.String("watcher") 243 | sig, evicted, err := client.Sign(key, ephemeral, nonce, grace, rotate, assignee, watcher) 244 | if err != nil { 245 | return err 246 | } 247 | fmt.Println(hex.EncodeToString(sig)) 248 | for _, sp := range evicted { 249 | fmt.Println(sp.Identity, sp.API) 250 | } 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /messenger/errors.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ErrorDone = newError("DONE") 7 | ) 8 | 9 | func newError(msg string) error { 10 | return fmt.Errorf("messenger error: %s", msg) 11 | } 12 | -------------------------------------------------------------------------------- /messenger/interface.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import "context" 4 | 5 | type Messenger interface { 6 | ReceiveMessage(context.Context) (string, []byte, error) 7 | SendMessage(ctx context.Context, receiver string, b []byte) error 8 | QueueMessage(ctx context.Context, receiver string, b []byte) error 9 | BroadcastMessage(ctx context.Context, b []byte) error 10 | BroadcastPlainMessage(ctx context.Context, text string) error 11 | } 12 | -------------------------------------------------------------------------------- /messenger/mixin.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "strings" 8 | "time" 9 | 10 | "github.com/MixinNetwork/bot-api-go-client/v3" 11 | "github.com/MixinNetwork/tip/logger" 12 | "github.com/fox-one/mixin-sdk-go" 13 | "github.com/gofrs/uuid/v5" 14 | ) 15 | 16 | type MixinConfiguration struct { 17 | UserId string `toml:"user"` 18 | SessionId string `toml:"session"` 19 | Key string `toml:"key"` 20 | Buffer int `toml:"buffer"` 21 | ConversationId string `toml:"conversation"` 22 | } 23 | 24 | type MixinMessenger struct { 25 | client *mixin.Client 26 | conf *MixinConfiguration 27 | conversationId string 28 | recv chan []byte 29 | send chan *mixin.MessageRequest 30 | } 31 | 32 | func NewMixinMessenger(ctx context.Context, conf *MixinConfiguration) (*MixinMessenger, error) { 33 | s := &mixin.Keystore{ 34 | ClientID: conf.UserId, 35 | SessionID: conf.SessionId, 36 | PrivateKey: conf.Key, 37 | } 38 | 39 | client, err := mixin.NewFromKeystore(s) 40 | if err != nil { 41 | return nil, err 42 | } 43 | mm := &MixinMessenger{ 44 | client: client, 45 | conf: conf, 46 | conversationId: conf.ConversationId, 47 | recv: make(chan []byte, conf.Buffer), 48 | send: make(chan *mixin.MessageRequest, conf.Buffer), 49 | } 50 | go mm.loopReceive(ctx) 51 | go mm.loopSend(ctx, time.Second, conf.Buffer) 52 | 53 | return mm, nil 54 | } 55 | 56 | func (mm *MixinMessenger) ReceiveMessage(ctx context.Context) (string, []byte, error) { 57 | select { 58 | case b := <-mm.recv: 59 | sender, err := uuid.FromBytes(b[:16]) 60 | if err != nil { 61 | panic(err) 62 | } 63 | return sender.String(), b[16:], nil 64 | case <-ctx.Done(): 65 | return "", nil, ErrorDone 66 | } 67 | } 68 | 69 | func (mm *MixinMessenger) BroadcastPlainMessage(ctx context.Context, data string) error { 70 | msg := &mixin.MessageRequest{ 71 | ConversationID: mm.conversationId, 72 | Category: mixin.MessageCategoryPlainText, 73 | MessageID: uniqueMessageId("", []byte(data)), 74 | Data: base64.RawURLEncoding.EncodeToString([]byte(data)), 75 | } 76 | return mm.client.SendMessage(ctx, msg) 77 | } 78 | 79 | func (mm *MixinMessenger) BroadcastMessage(ctx context.Context, b []byte) error { 80 | msg := mm.buildMessage("", b) 81 | return mm.client.SendMessage(ctx, msg) 82 | } 83 | 84 | func (mm *MixinMessenger) SendMessage(ctx context.Context, receiver string, b []byte) error { 85 | msg := mm.buildMessage(receiver, b) 86 | return mm.client.SendMessage(ctx, msg) 87 | } 88 | 89 | func (mm *MixinMessenger) QueueMessage(ctx context.Context, receiver string, b []byte) error { 90 | msg := mm.buildMessage(receiver, b) 91 | select { 92 | case mm.send <- msg: 93 | return nil 94 | case <-ctx.Done(): 95 | return ErrorDone 96 | } 97 | } 98 | 99 | func (mm *MixinMessenger) buildMessage(receiver string, b []byte) *mixin.MessageRequest { 100 | data := base64.RawURLEncoding.EncodeToString(b) 101 | return &mixin.MessageRequest{ 102 | ConversationID: mm.conversationId, 103 | RecipientID: receiver, 104 | Category: mixin.MessageCategoryPlainText, 105 | MessageID: uniqueMessageId(receiver, b), 106 | Data: base64.RawURLEncoding.EncodeToString([]byte(data)), 107 | } 108 | } 109 | 110 | func (mm *MixinMessenger) loopReceive(ctx context.Context) { 111 | for { 112 | blaze := bot.NewBlazeClient(mm.conf.UserId, mm.conf.SessionId, mm.conf.Key) 113 | err := blaze.Loop(context.Background(), mm) 114 | logger.Errorf("blaze.Loop %v\n", err) 115 | if ctx.Err() != nil { 116 | break 117 | } 118 | time.Sleep(3 * time.Second) 119 | } 120 | } 121 | 122 | func (mm *MixinMessenger) loopSend(ctx context.Context, period time.Duration, size int) { 123 | ticker := time.NewTicker(period) 124 | defer ticker.Stop() 125 | 126 | var batch []*mixin.MessageRequest 127 | filter := make(map[string]bool) 128 | for { 129 | select { 130 | case msg := <-mm.send: 131 | if filter[msg.MessageID] { 132 | continue 133 | } 134 | filter[msg.MessageID] = true 135 | batch = append(batch, msg) 136 | if len(batch) > size { 137 | err := mm.sendMessagesWithoutTimeout(ctx, batch) 138 | if err != nil { 139 | logger.Errorf("sendMessagesWithoutTimeout %s\n", err) 140 | } 141 | filter = make(map[string]bool) 142 | batch = nil 143 | } 144 | case <-ticker.C: 145 | if len(batch) > 0 { 146 | err := mm.sendMessagesWithoutTimeout(ctx, batch) 147 | if err != nil { 148 | logger.Errorf("sendMessagesWithoutTimeout %s\n", err) 149 | } 150 | filter = make(map[string]bool) 151 | batch = nil 152 | } 153 | } 154 | } 155 | } 156 | 157 | func (mm *MixinMessenger) OnMessage(ctx context.Context, msg bot.MessageView, userId string) error { 158 | if msg.Category != mixin.MessageCategoryPlainText { 159 | return nil 160 | } 161 | if msg.ConversationId != mm.conversationId { 162 | return nil 163 | } 164 | data, err := base64.RawURLEncoding.DecodeString(msg.DataBase64) 165 | if err != nil { 166 | return nil 167 | } 168 | data, err = base64.RawURLEncoding.DecodeString(string(data)) 169 | if err != nil { 170 | return nil 171 | } 172 | sender, err := uuid.FromString(msg.UserId) 173 | if err != nil { 174 | return nil 175 | } 176 | data = append(sender.Bytes(), data...) 177 | select { 178 | case mm.recv <- data: 179 | case <-ctx.Done(): 180 | } 181 | return nil 182 | } 183 | 184 | func (mm *MixinMessenger) OnAckReceipt(ctx context.Context, msg bot.MessageView, userId string) error { 185 | return nil 186 | } 187 | 188 | func (mm *MixinMessenger) SyncAck() bool { 189 | return true 190 | } 191 | 192 | func (mm *MixinMessenger) sendMessagesWithoutTimeout(ctx context.Context, batch []*mixin.MessageRequest) error { 193 | for { 194 | err := mm.client.SendMessages(ctx, batch) 195 | if err != nil && strings.Contains(err.Error(), "Client.Timeout exceeded") { 196 | continue 197 | } 198 | return err 199 | } 200 | } 201 | 202 | func uniqueMessageId(receiver string, b []byte) string { 203 | s := hex.EncodeToString(b) 204 | return mixin.UniqueConversationID(receiver, s) 205 | } 206 | -------------------------------------------------------------------------------- /sdk/go/config.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/MixinNetwork/tip/crypto" 7 | ) 8 | 9 | type signerPair struct { 10 | Identity string `json:"identity"` 11 | API string `json:"api"` 12 | } 13 | 14 | type Configuration struct { 15 | Commitments []string `json:"commitments"` 16 | Signers []*signerPair `json:"signers"` 17 | } 18 | 19 | func LoadConfigurationJSON(data string) (*Configuration, error) { 20 | var conf Configuration 21 | err := json.Unmarshal([]byte(data), &conf) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &conf, conf.validate() 26 | } 27 | 28 | func (conf *Configuration) validate() error { 29 | if len(conf.Commitments) != len(conf.Signers)*2/3+1 { 30 | return ErrInvalidConfiguration 31 | } 32 | for _, c := range conf.Commitments { 33 | _, err := crypto.PubKeyFromBase58(c) 34 | if err != nil { 35 | return ErrInvalidConfiguration 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /sdk/go/errors.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import "fmt" 4 | 5 | var ErrInvalidConfiguration = fmt.Errorf("invalid configuration") 6 | -------------------------------------------------------------------------------- /sdk/go/http.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/MixinNetwork/tip/crypto" 12 | ) 13 | 14 | var httpClient *http.Client 15 | 16 | func init() { 17 | httpClient = &http.Client{Timeout: 10 * time.Second} 18 | } 19 | 20 | type ResponseData struct { 21 | Commitments []string `json:"commitments,omitempty"` 22 | Identity string `json:"identity,omitempty"` 23 | Signers []struct { 24 | Identity string `json:"identity"` 25 | Index int `json:"index"` 26 | } `json:"signers,omitempty"` 27 | Cipher string `json:"cipher,omitempty"` 28 | } 29 | 30 | type Response struct { 31 | Error *struct { 32 | Code int `json:"code"` 33 | Description string `json:"description"` 34 | } `json:"error"` 35 | Data *ResponseData `json:"data"` 36 | Signature string `json:"signature"` 37 | } 38 | 39 | func request(sp *signerPair, method string, data []byte) (*ResponseData, error) { 40 | req, err := http.NewRequest(method, sp.API, bytes.NewReader(data)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | req.Close = true 45 | 46 | resp, err := httpClient.Do(req) 47 | if err != nil { 48 | return nil, err 49 | } 50 | defer resp.Body.Close() 51 | if resp.StatusCode != http.StatusOK { 52 | return nil, fmt.Errorf("error code %d", resp.StatusCode) 53 | } 54 | 55 | var body Response 56 | err = json.NewDecoder(resp.Body).Decode(&body) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if body.Error != nil { 61 | return nil, fmt.Errorf("error code %d", body.Error.Code) 62 | } 63 | 64 | sig, err := hex.DecodeString(body.Signature) 65 | if err != nil { 66 | return nil, err 67 | } 68 | pub, err := crypto.PubKeyFromBase58(sp.Identity) 69 | if err != nil { 70 | return nil, err 71 | } 72 | data, err = json.Marshal(body.Data) 73 | if err != nil { 74 | return nil, err 75 | } 76 | err = crypto.Verify(pub, data, sig) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return body.Data, nil 82 | } 83 | -------------------------------------------------------------------------------- /sdk/go/secret.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import "golang.org/x/crypto/argon2" 4 | 5 | func DeriveSecret(pin, seed string) []byte { 6 | return argon2.IDKey([]byte(pin), []byte(seed), 1024, 256*1024, 4, 64) 7 | } 8 | -------------------------------------------------------------------------------- /sdk/go/secret_test.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import "testing" 4 | 5 | func BenchmarkDeriveSecret(b *testing.B) { 6 | for i := 0; i < b.N; i++ { 7 | DeriveSecret("123456", "2e613adae4f0167255933a3ec1d97e0acdd38e46d319c348b7a3d709f23bae8f") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sdk/go/tip.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | 11 | "github.com/MixinNetwork/tip/crypto" 12 | "github.com/drand/kyber" 13 | "github.com/drand/kyber/pairing/bn256" 14 | "github.com/drand/kyber/share" 15 | "github.com/drand/kyber/sign/tbls" 16 | "golang.org/x/crypto/sha3" 17 | ) 18 | 19 | type Client struct { 20 | commitments []kyber.Point 21 | signers []*signerPair 22 | } 23 | 24 | func NewClient(conf *Configuration) (*Client, []*signerPair, error) { 25 | err := conf.validate() 26 | if err != nil { 27 | return nil, nil, err 28 | } 29 | 30 | cli := &Client{signers: conf.Signers} 31 | for _, c := range conf.Commitments { 32 | point, _ := crypto.PubKeyFromBase58(c) 33 | cli.commitments = append(cli.commitments, point) 34 | } 35 | 36 | var evicted []*signerPair 37 | for _, s := range conf.Signers { 38 | res, err := request(s, "GET", nil) 39 | if err != nil { 40 | evicted = append(evicted, s) 41 | continue 42 | } 43 | if res.Identity != s.Identity { 44 | evicted = append(evicted, s) 45 | continue 46 | } 47 | if len(res.Signers) != len(conf.Signers) { 48 | evicted = append(evicted, s) 49 | continue 50 | } 51 | for i, rs := range res.Signers { 52 | if conf.Signers[i].Identity != rs.Identity { 53 | evicted = append(evicted, s) 54 | break 55 | } 56 | } 57 | if len(res.Commitments) != len(conf.Commitments) { 58 | evicted = append(evicted, s) 59 | continue 60 | } 61 | for i, c := range res.Commitments { 62 | if conf.Commitments[i] != c { 63 | evicted = append(evicted, s) 64 | break 65 | } 66 | } 67 | } 68 | 69 | if sc := len(conf.Signers) - len(evicted); sc < len(conf.Commitments) { 70 | return nil, evicted, fmt.Errorf("not enough signers %d %d", sc, len(conf.Commitments)) 71 | } 72 | return cli, evicted, nil 73 | } 74 | 75 | func (c *Client) Sign(ks, ephemeral string, nonce, grace int64, rotate, assignee, watcher string) ([]byte, []*signerPair, error) { 76 | key, err := crypto.PrivateKeyFromHex(ks) 77 | if err != nil { 78 | return nil, nil, err 79 | } 80 | _, err = crypto.PrivateKeyFromHex(ephemeral) 81 | if err != nil { 82 | return nil, nil, err 83 | } 84 | if rotate != "" { 85 | _, err = crypto.PrivateKeyFromHex(rotate) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | } 90 | 91 | var assignor []byte 92 | var partials [][]byte 93 | var evicted []*signerPair 94 | pam := make(map[string][]byte) 95 | acm := make(map[string]int) 96 | for _, s := range c.signers { 97 | data := sign(key, s.Identity, ephemeral, uint64(nonce), uint64(grace), rotate, assignee, watcher) 98 | res, err := request(s, "POST", data) 99 | if err != nil { 100 | evicted = append(evicted, s) 101 | continue 102 | } 103 | enc, err := hex.DecodeString(res.Cipher) 104 | if err != nil { 105 | evicted = append(evicted, s) 106 | continue 107 | } 108 | if len(enc) < 32 { 109 | evicted = append(evicted, s) 110 | continue 111 | } 112 | pub, err := crypto.PubKeyFromBase58(s.Identity) 113 | if err != nil { 114 | panic(err) 115 | } 116 | dec := crypto.DecryptECDH(pub, key, enc) 117 | if len(dec) != 8+66+128+8+8 { 118 | evicted = append(evicted, s) 119 | continue 120 | } 121 | if uint64(nonce) != binary.BigEndian.Uint64(dec[:8]) { 122 | evicted = append(evicted, s) 123 | continue 124 | } 125 | p, a := dec[8:74], dec[74:202] 126 | as := hex.EncodeToString(a) 127 | pam[hex.EncodeToString(p)] = a 128 | acm[as] = acm[as] + 1 129 | } 130 | var amc int 131 | for a, c := range acm { 132 | if c <= amc { 133 | continue 134 | } 135 | assignor, _ = hex.DecodeString(a) 136 | amc = c 137 | } 138 | for p, a := range pam { 139 | if !bytes.Equal(a, assignor) { 140 | continue 141 | } 142 | partial, _ := hex.DecodeString(p) 143 | partials = append(partials, partial) 144 | } 145 | 146 | if len(partials) < len(c.commitments) { 147 | return nil, evicted, fmt.Errorf("not enough partials %d %d", len(partials), len(c.commitments)) 148 | } 149 | suite := bn256.NewSuiteG2() 150 | scheme := tbls.NewThresholdSchemeOnG1(bn256.NewSuiteG2()) 151 | poly := share.NewPubPoly(suite, suite.Point().Base(), c.commitments) 152 | sig, err := scheme.Recover(poly, assignor, partials, len(c.commitments), len(c.signers)) 153 | if err != nil { 154 | return nil, evicted, err 155 | } 156 | err = crypto.Verify(poly.Commit(), assignor, sig) 157 | if err != nil { 158 | return nil, evicted, err 159 | } 160 | return sig, evicted, nil 161 | } 162 | 163 | func sign(key kyber.Scalar, nodeId, ephemeral string, nonce, grace uint64, rotate, assignee, watcher string) []byte { 164 | pkey := crypto.PublicKey(key) 165 | esum := sha3.Sum256(append([]byte(ephemeral), nodeId...)) 166 | msg := crypto.PublicKeyBytes(pkey) 167 | msg = append(msg, esum[:]...) 168 | buf := make([]byte, 8) 169 | binary.BigEndian.PutUint64(buf, nonce) 170 | msg = append(msg, buf...) 171 | binary.BigEndian.PutUint64(buf, grace) 172 | msg = append(msg, buf...) 173 | data := map[string]interface{}{ 174 | "identity": crypto.PublicKeyString(pkey), 175 | "ephemeral": hex.EncodeToString(esum[:]), 176 | "watcher": watcher, 177 | "nonce": nonce, 178 | "grace": grace, 179 | } 180 | if rotate != "" { 181 | rsum := sha3.Sum256(append([]byte(rotate), nodeId...)) 182 | msg = append(msg, rsum[:]...) 183 | data["rotate"] = hex.EncodeToString(rsum[:]) 184 | } 185 | if len(assignee) > 0 { 186 | as, _ := crypto.PrivateKeyFromHex(assignee) 187 | ap := crypto.PublicKey(as) 188 | ab := crypto.PublicKeyBytes(ap) 189 | sig, _ := crypto.Sign(as, ab) 190 | ab = append(ab, sig...) 191 | msg = append(msg, ab...) 192 | data["assignee"] = hex.EncodeToString(ab) 193 | } 194 | b, _ := json.Marshal(data) 195 | spub, err := crypto.PubKeyFromBase58(nodeId) 196 | if err != nil { 197 | panic(err) 198 | } 199 | cipher := crypto.EncryptECDH(spub, key, b) 200 | sig, _ := crypto.Sign(key, msg) 201 | b, _ = json.Marshal(map[string]interface{}{ 202 | "action": "SIGN", 203 | "identity": crypto.PublicKeyString(pkey), 204 | "data": base64.RawURLEncoding.EncodeToString(cipher[:]), 205 | "signature": hex.EncodeToString(sig), 206 | "watcher": watcher, 207 | }) 208 | return b 209 | } 210 | -------------------------------------------------------------------------------- /sdk/go/tip_test.go: -------------------------------------------------------------------------------- 1 | package tip 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/MixinNetwork/tip/api" 14 | "github.com/MixinNetwork/tip/signer" 15 | "github.com/MixinNetwork/tip/store" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestTip(t *testing.T) { 20 | assert := assert.New(t) 21 | for _, port := range []int{7021, 7022, 7023, 7024} { 22 | go testRunServer(port) 23 | } 24 | 25 | time.Sleep(2 * time.Second) 26 | client, evicted, err := NewClient(testConfigurationJSON()) 27 | assert.Nil(err) 28 | assert.Len(evicted, 0) 29 | key := "8bee954d5315684caa46d78fb8456a165bdd0cb44643d335a6b15c21d8c1872b" 30 | ephemeral := "2b5a6b0cb9576ea218d081baa14d2cea82a6839165a29b3bdfc6ef8582b0ce5a" 31 | watcher := "2b5a6b0cb9576ea218d081baa14d2cea82a6839165a29b3bdfc6ef8582b0ce5a" 32 | grace := int64(time.Hour * 24 * 128) 33 | var nonce int64 = 123 34 | sig, evicted, err := client.Sign(key, ephemeral, nonce, grace, "", "", watcher) 35 | assert.Nil(err) 36 | assert.Len(evicted, 0) 37 | log.Println(hex.EncodeToString(sig)) 38 | 39 | nonce = 123 40 | sig, evicted, err = client.Sign(key, ephemeral, nonce, grace, "", "", watcher) 41 | assert.NotNil(err) 42 | assert.Len(evicted, 4) 43 | assert.Len(sig, 0) 44 | 45 | nonce = 1234 46 | sig, evicted, err = client.Sign(key, ephemeral, nonce, grace, "", "", watcher) 47 | assert.Nil(err) 48 | assert.Len(evicted, 0) 49 | assert.Equal("8258bc1a22db4865529d7c01a949d303e4d834d6fe79fcf746c6ad3fcb2ee37583975a034eea3ad08105c856f5c302ed02e2b11b71440d9e31da5b06097b691f", hex.EncodeToString(sig)) 50 | log.Println(hex.EncodeToString(sig)) 51 | } 52 | 53 | func testConfigurationJSON() *Configuration { 54 | return &Configuration{ 55 | Commitments: []string{ 56 | "5JJGU8uy7SsA7shL8eWZLiBHKaGUe61DSaT9WMKvmkuU3gJHK25yzgxqJtFqFLgE93b2AGjpF9xoUzyr2ypnPKABbsm7pUWdnLJFVMGSuCd7RP9fN67KH1ELgWER5ZGAiDyQZF67hGshCnSTHoMawnJ8QzBnjECSsmWgWQu6i8qwBY6C4ncWgX", 57 | "5JavWMMmNjfX2sCu3ixRksmd5npiZekjhW76dkn1wgToJzSouZgy7HaiKAw4CHenKzmU5Tn41JY3eJbK6TK1m328CwabL8u9ydKRKFBcqXbQHpdRc5DYUoqbJogqXdiR8LtEzBMVtKnMicK7RZpuY2QcoLqZNxQ3yW51eG7R4mexDoyfLLXf86", 58 | "5Jm9wNExYE3BcjYoHagvzPwWgK9WtAVf9JEwyQugDfT88GmZZr6Ztb9tALV4cYamauNunuVzaCmmZHmhK9yztGWyAKtoe6VeTfEdUzLBhXq3ZQznxxJrEbADKN1GZFmx7xcRVe7iL2AxuwDRkXcgBiTNL4afNLmQ3tiW3t8VnpwxBoxzahoSaY", 59 | }, 60 | Signers: []*signerPair{ 61 | { 62 | Identity: "5HrRVnj6PdfxKojB44te1XqhDSCexUxSognLi96SVx5B6VdnKkbyvUGcpkdQodg9rKgxM5v61ypmbJNGVWJTuacKUSZQfkq1mnc6P4XybemuXYmwSd5g2zkaArPc8VDTU5eEPuvgguSD8cnEgnMZzW7rJfaWoJU1DW6k2ujzUx15EjAG3WDTeG", 63 | API: "http://127.0.0.1:7022", 64 | }, 65 | { 66 | Identity: "5HzufHDbh8kUj3oBiYWeEe4wamNMmQ4BZ5uZULxGsyKYULpWLUdzzBb73EExRDgUxZD5vu6iA61ds7QGSjeCWazSmpXv7sMaHfizSnHjxeoEy1TumWVqGJhtAAYwAJPzUbTdyzEGz5r9hRSYFAmHkhwCwLi8BoSk8V2scv6r7LdfphGbXSWSAV", 67 | API: "http://127.0.0.1:7023", 68 | }, 69 | { 70 | Identity: "5JRrcBgsnUVr8D7tdTHX8nZAbkpPD4C5TS82KEbBMiV3inVp1vSu4gBwB1WwhQFguGbmkgrvA2vmtfY6GXhyFnh4SRoEQT2jVNTsk91pcPUaZ8nQcEdDAUjKXCTFi6TPDYPYPUsAK67kUXEtyNocsYUijKdF9pGRKUk92Rk7iRuJ3eqADYH7NB", 71 | API: "http://127.0.0.1:7021", 72 | }, 73 | { 74 | Identity: "5Jt4ztqknKHcAw13RALYx2mXT9qkKKJTvrU7W7HNcF7vGKxzh5tvSqQvrY4aZCVqzk46DV8X69qudryZsjyKjzLJMjyRMYiDoQY7WZvNk874cibXAoZrUbp7Eyc8DgNLnPycisLbNofh3iJpKMK2qpsQH7AsFkAMdhH8KLFoBGruTs1XcevoC1", 75 | API: "http://127.0.0.1:7024", 76 | }, 77 | }, 78 | } 79 | } 80 | 81 | func testRunServer(port int) { 82 | ctx := context.Background() 83 | store := testBadgerStore(port) 84 | if store == nil { 85 | panic(errors.New("store is nil")) 86 | } 87 | conf := testTipNode(port) 88 | if conf == nil { 89 | panic(errors.New("conf is nil")) 90 | } 91 | node := signer.NewNode(ctx, nil, store, nil, conf) 92 | ac := &api.Configuration{ 93 | Port: port, 94 | } 95 | ac.Key = node.GetKey() 96 | ac.Signers = node.GetSigners() 97 | ac.Poly = node.GetPoly() 98 | ac.Share = node.GetShare() 99 | server := api.NewServer(store, ac) 100 | err := server.ListenAndServe() 101 | if err != nil { 102 | panic(err) 103 | } 104 | } 105 | 106 | func testBadgerStore(port int) *store.BadgerStorage { 107 | path := fmt.Sprintf("tip%d", port) 108 | var share string 109 | switch port { 110 | case 7021: 111 | share = "000000028f36089865a5b1f36ed65ec8a6caa0082455a83b8469ed5c167f2700a0bb1264" 112 | case 7022: 113 | share = "00000000263e3d0c7a942e18a2206a791298346271a0a51eefc9178dc2d3714dec5e9469" 114 | case 7023: 115 | share = "00000001376ef740e9f5a867129d54a811dd256272fe46359dfe0680c27d327f8d02576b" 116 | case 7024: 117 | share = "000000030e296d4c585d3aca61ebaf6a0e56ec11288baf0ab2b659d78a7b661a782fe092" 118 | } 119 | if share == "" { 120 | return nil 121 | } 122 | 123 | dir, err := os.MkdirTemp("/tmp", path) 124 | if err != nil { 125 | panic(err) 126 | } 127 | conf := &store.BadgerConfiguration{ 128 | Dir: dir, 129 | } 130 | bs, err := store.OpenBadger(context.Background(), conf) 131 | if err != nil { 132 | panic(err) 133 | } 134 | shareBuf, _ := hex.DecodeString(share) 135 | publicBuf, _ := hex.DecodeString(polyPublic) 136 | err = bs.WritePoly(publicBuf, shareBuf) 137 | if err != nil { 138 | panic(err) 139 | } 140 | return bs 141 | } 142 | 143 | func testTipNode(port int) *signer.Configuration { 144 | key := "" 145 | switch port { 146 | case 7021: 147 | key = "2d3ef9158573d306210ad2579e78e2e99177542d8b1831c3828a40a556d66f35" 148 | case 7022: 149 | key = "6869e481b5ede57ec00504e1b76682aa62980cb0e46a3d9031bdb50acf8cb1c5" 150 | case 7023: 151 | key = "83f3daf28a106d20fb3a5dfa2a6f2822c76fccf7e88f16e9a31acb6d3f73c2c0" 152 | case 7024: 153 | key = "1d86753d770a1ced1103edb2ffd11728ee4ab6aed41094732c1748c72f2e181d" 154 | } 155 | if key == "" { 156 | return nil 157 | } 158 | return &signer.Configuration{ 159 | Key: key, 160 | Signers: signers, 161 | } 162 | } 163 | 164 | var signers = []string{ 165 | "5JRrcBgsnUVr8D7tdTHX8nZAbkpPD4C5TS82KEbBMiV3inVp1vSu4gBwB1WwhQFguGbmkgrvA2vmtfY6GXhyFnh4SRoEQT2jVNTsk91pcPUaZ8nQcEdDAUjKXCTFi6TPDYPYPUsAK67kUXEtyNocsYUijKdF9pGRKUk92Rk7iRuJ3eqADYH7NB", 166 | "5HrRVnj6PdfxKojB44te1XqhDSCexUxSognLi96SVx5B6VdnKkbyvUGcpkdQodg9rKgxM5v61ypmbJNGVWJTuacKUSZQfkq1mnc6P4XybemuXYmwSd5g2zkaArPc8VDTU5eEPuvgguSD8cnEgnMZzW7rJfaWoJU1DW6k2ujzUx15EjAG3WDTeG", 167 | "5HzufHDbh8kUj3oBiYWeEe4wamNMmQ4BZ5uZULxGsyKYULpWLUdzzBb73EExRDgUxZD5vu6iA61ds7QGSjeCWazSmpXv7sMaHfizSnHjxeoEy1TumWVqGJhtAAYwAJPzUbTdyzEGz5r9hRSYFAmHkhwCwLi8BoSk8V2scv6r7LdfphGbXSWSAV", 168 | "5Jt4ztqknKHcAw13RALYx2mXT9qkKKJTvrU7W7HNcF7vGKxzh5tvSqQvrY4aZCVqzk46DV8X69qudryZsjyKjzLJMjyRMYiDoQY7WZvNk874cibXAoZrUbp7Eyc8DgNLnPycisLbNofh3iJpKMK2qpsQH7AsFkAMdhH8KLFoBGruTs1XcevoC1", 169 | } 170 | 171 | const ( 172 | polyPublic = "596e6b811ba03ae0e2b3db8ae6f10ef5fb5493a2379751608b6a2e119def22af82dc26c672c5571631fc00f1943be08780d81272de4bf0ec8dfc3d9f37df7e3d1ec070e432c22789b742b16a5b551cf0148ef2ed32b85641a97b0da94035e67c06511a95ce015d750a16299a8ec4822603d0c91ad79a90ce865b7141f07729bd724e1b2e4cd7d550b80340d1dde642fed186774aed35941fd6a8e83950d3c1973090596cdc586297270210ec3d42c438e6e8c2894ac7eec297b4ad9a193494e130f47ebadbc7259c69deea91fc9a153e18304e209e8105932800e6f74ab75d78734fb8428da5735e45ab6bdc29e1816c33eb344ac8e679ab16e0af5e6c4b323d8195bc8edd1c0d2e078f5f5a0953163b87e1469213605e5af374309e197a0917753f944543beb8e7c6454d2d83fbac4f32d40386b3881548833297219f39a7b843678d1544b3c12f33535e4cfb81199488a8255c2547fe83e3b31adcd6172f858824132349a082d4db81e75747e82e3684e7c7965ff44d01ef48ca310ade5d6d" 173 | ) 174 | -------------------------------------------------------------------------------- /signer/README.md: -------------------------------------------------------------------------------- 1 | # Signer Node 2 | 3 | The signer runs a [threshold Boneh-Lynn-Shacham (BLS) signatures](https://en.wikipedia.org/wiki/Boneh%E2%80%93Lynn%E2%80%93Shacham) DKG, which generates a collective public key and a secret share for each node respectively. For *n* total signers, the threshold *t = n * 2 / 3 + 1*. 4 | 5 | Each signer node runs independently and doesn't have direct network connection with other nodes. They broadcast messages to a Mixin Messenger group chat which includes all the signers to exchange key generation information. 6 | 7 | ## Setup Key 8 | 9 | After all entities have reach a consensus, each entity should prepare a signer key and then forms a list of signer keys. To generate a key pair, run the command: 10 | 11 | ``` 12 | $ tip key 13 | 2bd462c1f02fa96234...3b6560fe308d5c6e74 14 | 5JhLbaTYCXbqFxibfX19GW...cTBg1UDpQ9xfMxXxtnzQS1 15 | ``` 16 | 17 | Then put the private key to `[node].key` section of **config/example.toml**, and share the public key with all other entities. After all entities make their public keys exchanged, they should sort the keys list in the same order and put them to `[node].signers`. 18 | 19 | ## Setup Messenger 20 | 21 | Go to the Mixin Messenger [developers dashboard](https://developers.mixin.one/dashboard) and create a bot for the signer, then in the secret section generate an Ed25519 session. Edit **config/example.toml**, and put `client_id` to `[messenger].user`, `session_id` to `[messenger].session`, and `private_key` to `[messenger].key`. 22 | 23 | Then all the entities should add all their bots to a Mixin Messenger group chat, then send the link `https://mixin.one/context` to the group chat and open it to obtain a UUID, which is the `[messenger].conversation` value. 24 | 25 | ## Run Signer DKG 26 | 27 | Change `[store].dir` to a secure and permanent directory, this is where the signer database resides. Then the **config/example.toml** is finished, put it to a proper path, e.g. ~/.tip/config.toml. 28 | 29 | ``` 30 | $ tip -c ~/.tip/config.toml signer 31 | ``` 32 | 33 | All entities should run the command above to prepare for the DKG process, and after all entities have started the node, all of them should run the command below. 34 | 35 | ``` 36 | $ tip -c ~/.tip/config.toml setup -nonce 887378 37 | ``` 38 | 39 | This command sends out the DKG setup signal to the Mixin Messenger group chat, and after enough signals received, the DKG starts. The `nonce` value must be a large number and all entities should use the same one. 40 | 41 | If the DKG finishes successfully, the node will exit with the output similar to below message. 42 | 43 | ``` 44 | runDKG 5cc8735afb....b34b4 000000035...f43fd402 45 | ``` 46 | 47 | The first and long hex is the commitments for the collective public key, and all entities should share it with others to ensure their nodes produce identical public key. The second and short hex is the private share, which should not be shared to anyone else, and must have a secure backup. 48 | 49 | If some node fails to produce the same public key, all the entities should remove the failed database and restart the DKG setup process until success. 50 | 51 | ## Run Signer API 52 | 53 | After the DKG process successfully, all nodes should start the signer API to accept signing requests from users. 54 | 55 | ``` 56 | $ tip -c ~/.tip/config.toml api 57 | ``` 58 | 59 | It's highly recommended to make a firewall and reverse proxy to hide the actual API server from public. 60 | -------------------------------------------------------------------------------- /signer/board.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/MixinNetwork/tip/logger" 7 | "github.com/MixinNetwork/tip/messenger" 8 | "github.com/drand/kyber" 9 | "github.com/drand/kyber/share/dkg" 10 | ) 11 | 12 | type Board struct { 13 | messenger messenger.Messenger 14 | nonce uint64 15 | deals chan dkg.DealBundle 16 | resps chan dkg.ResponseBundle 17 | justs chan dkg.JustificationBundle 18 | ctx context.Context 19 | key kyber.Scalar 20 | } 21 | 22 | func (node *Node) NewBoard(ctx context.Context, nonce uint64) *Board { 23 | return &Board{ 24 | messenger: node.messenger, 25 | nonce: nonce, 26 | deals: make(chan dkg.DealBundle), 27 | resps: make(chan dkg.ResponseBundle), 28 | justs: make(chan dkg.JustificationBundle), 29 | ctx: ctx, 30 | key: node.key, 31 | } 32 | } 33 | 34 | func (t *Board) PushDeals(db *dkg.DealBundle) { 35 | data := encodeDealBundle(db, t.nonce) 36 | msg := makeMessage(t.key, MessageActionDKGDeal, data) 37 | err := t.messenger.BroadcastMessage(t.ctx, msg) 38 | logger.Verbose("PushDeals", len(msg), err) 39 | } 40 | 41 | func (t *Board) IncomingDeal() <-chan dkg.DealBundle { 42 | return t.deals 43 | } 44 | 45 | func (t *Board) PushResponses(rb *dkg.ResponseBundle) { 46 | data := encodeResponseBundle(rb) 47 | msg := makeMessage(t.key, MessageActionDKGResponse, data) 48 | err := t.messenger.BroadcastMessage(t.ctx, msg) 49 | logger.Verbose("PushResponses", len(msg), err) 50 | } 51 | 52 | func (t *Board) IncomingResponse() <-chan dkg.ResponseBundle { 53 | return t.resps 54 | } 55 | 56 | func (t *Board) PushJustifications(jb *dkg.JustificationBundle) { 57 | data := encodeJustificationBundle(jb) 58 | msg := makeMessage(t.key, MessageActionDKGJustify, data) 59 | err := t.messenger.BroadcastMessage(t.ctx, msg) 60 | logger.Verbose("PushJustifications", len(msg), err) 61 | } 62 | 63 | func (t *Board) IncomingJustification() <-chan dkg.JustificationBundle { 64 | return t.justs 65 | } 66 | -------------------------------------------------------------------------------- /signer/bundle.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | 8 | "github.com/MixinNetwork/tip/crypto" 9 | "github.com/drand/kyber/pairing/bn256" 10 | "github.com/drand/kyber/share/dkg" 11 | ) 12 | 13 | func encodeJustificationBundle(jb *dkg.JustificationBundle) []byte { 14 | enc := NewEncoder() 15 | enc.WriteUint32(jb.DealerIndex) 16 | 17 | enc.WriteInt(len(jb.Justifications)) 18 | for _, j := range jb.Justifications { 19 | enc.WriteUint32(j.ShareIndex) 20 | b := crypto.PrivateKeyBytes(j.Share) 21 | enc.WriteFixedBytes(b) 22 | } 23 | 24 | enc.WriteFixedBytes(jb.SessionID) 25 | enc.WriteFixedBytes(jb.Signature) 26 | 27 | return enc.buf.Bytes() 28 | } 29 | 30 | func decodeJustificationBundle(b []byte) (*dkg.JustificationBundle, error) { 31 | jb := &dkg.JustificationBundle{} 32 | dec := NewDecoder(b) 33 | 34 | di, err := dec.ReadUint32() 35 | if err != nil { 36 | return nil, err 37 | } 38 | jb.DealerIndex = di 39 | 40 | jl, err := dec.ReadInt() 41 | if err != nil { 42 | return nil, err 43 | } 44 | suite := bn256.NewSuiteG2() 45 | for ; jl > 0; jl-- { 46 | si, err := dec.ReadUint32() 47 | if err != nil { 48 | return nil, err 49 | } 50 | b, err := dec.ReadBytes() 51 | if err != nil { 52 | return nil, err 53 | } 54 | scalar := suite.Scalar().SetBytes(b) 55 | jb.Justifications = append(jb.Justifications, dkg.Justification{ 56 | ShareIndex: si, 57 | Share: scalar, 58 | }) 59 | } 60 | 61 | sid, err := dec.ReadBytes() 62 | if err != nil { 63 | return nil, err 64 | } 65 | jb.SessionID = sid 66 | sig, err := dec.ReadBytes() 67 | if err != nil { 68 | return nil, err 69 | } 70 | jb.Signature = sig 71 | 72 | return jb, nil 73 | } 74 | 75 | func encodeResponseBundle(rb *dkg.ResponseBundle) []byte { 76 | enc := NewEncoder() 77 | enc.WriteUint32(rb.ShareIndex) 78 | 79 | enc.WriteInt(len(rb.Responses)) 80 | for _, r := range rb.Responses { 81 | enc.WriteUint32(r.DealerIndex) 82 | enc.WriteBool(r.Status) 83 | } 84 | 85 | enc.WriteFixedBytes(rb.SessionID) 86 | enc.WriteFixedBytes(rb.Signature) 87 | 88 | return enc.buf.Bytes() 89 | } 90 | 91 | func decodeResponseBundle(b []byte) (*dkg.ResponseBundle, error) { 92 | rb := &dkg.ResponseBundle{} 93 | dec := NewDecoder(b) 94 | 95 | si, err := dec.ReadUint32() 96 | if err != nil { 97 | return nil, err 98 | } 99 | rb.ShareIndex = si 100 | 101 | rl, err := dec.ReadInt() 102 | if err != nil { 103 | return nil, err 104 | } 105 | for ; rl > 0; rl-- { 106 | di, err := dec.ReadUint32() 107 | if err != nil { 108 | return nil, err 109 | } 110 | ss, err := dec.ReadBool() 111 | if err != nil { 112 | return nil, err 113 | } 114 | rb.Responses = append(rb.Responses, dkg.Response{ 115 | DealerIndex: di, 116 | Status: ss, 117 | }) 118 | } 119 | 120 | sid, err := dec.ReadBytes() 121 | if err != nil { 122 | return nil, err 123 | } 124 | rb.SessionID = sid 125 | sig, err := dec.ReadBytes() 126 | if err != nil { 127 | return nil, err 128 | } 129 | rb.Signature = sig 130 | 131 | return rb, nil 132 | } 133 | 134 | func encodeDealBundle(db *dkg.DealBundle, nonce uint64) []byte { 135 | enc := NewEncoder() 136 | enc.WriteUint64(nonce) 137 | 138 | enc.WriteUint32(db.DealerIndex) 139 | 140 | enc.WriteInt(len(db.Deals)) 141 | for _, d := range db.Deals { 142 | enc.WriteUint32(d.ShareIndex) 143 | enc.WriteFixedBytes(d.EncryptedShare) 144 | } 145 | 146 | enc.WriteInt(len(db.Public)) 147 | for _, p := range db.Public { 148 | b := crypto.PublicKeyBytes(p) 149 | enc.WriteFixedBytes(b) 150 | } 151 | 152 | enc.WriteFixedBytes(db.SessionID) 153 | enc.WriteFixedBytes(db.Signature) 154 | 155 | return enc.buf.Bytes() 156 | } 157 | 158 | func decodeDealBundle(b []byte) (uint64, *dkg.DealBundle, error) { 159 | db := &dkg.DealBundle{} 160 | dec := NewDecoder(b) 161 | 162 | nonce, err := dec.ReadUint64() 163 | if err != nil { 164 | return 0, nil, err 165 | } 166 | 167 | di, err := dec.ReadUint32() 168 | if err != nil { 169 | return 0, nil, err 170 | } 171 | db.DealerIndex = di 172 | 173 | dl, err := dec.ReadInt() 174 | if err != nil { 175 | return 0, nil, err 176 | } 177 | for ; dl > 0; dl-- { 178 | si, err := dec.ReadUint32() 179 | if err != nil { 180 | return 0, nil, err 181 | } 182 | es, err := dec.ReadBytes() 183 | if err != nil { 184 | return 0, nil, err 185 | } 186 | db.Deals = append(db.Deals, dkg.Deal{ 187 | ShareIndex: si, 188 | EncryptedShare: es, 189 | }) 190 | } 191 | 192 | pl, err := dec.ReadInt() 193 | if err != nil { 194 | return 0, nil, err 195 | } 196 | for ; pl > 0; pl-- { 197 | pb, err := dec.ReadBytes() 198 | if err != nil { 199 | return 0, nil, err 200 | } 201 | point, err := crypto.PubKeyFromBytes(pb) 202 | if err != nil { 203 | return 0, nil, err 204 | } 205 | db.Public = append(db.Public, point) 206 | } 207 | 208 | sid, err := dec.ReadBytes() 209 | if err != nil { 210 | return 0, nil, err 211 | } 212 | db.SessionID = sid 213 | sig, err := dec.ReadBytes() 214 | if err != nil { 215 | return 0, nil, err 216 | } 217 | db.Signature = sig 218 | 219 | return nonce, db, nil 220 | } 221 | 222 | type Decoder struct { 223 | buf *bytes.Reader 224 | } 225 | 226 | func NewDecoder(b []byte) *Decoder { 227 | return &Decoder{buf: bytes.NewReader(b)} 228 | } 229 | 230 | func (dec *Decoder) Read(b []byte) error { 231 | l, err := dec.buf.Read(b) 232 | if err != nil { 233 | return err 234 | } 235 | if l != len(b) { 236 | return fmt.Errorf("data short %d %d", l, len(b)) 237 | } 238 | return nil 239 | } 240 | 241 | func (dec *Decoder) ReadInt() (int, error) { 242 | d, err := dec.ReadUint32() 243 | return int(d), err 244 | } 245 | 246 | func (dec *Decoder) ReadUint32() (uint32, error) { 247 | var b [4]byte 248 | err := dec.Read(b[:]) 249 | if err != nil { 250 | return 0, err 251 | } 252 | d := binary.BigEndian.Uint32(b[:]) 253 | return d, nil 254 | } 255 | 256 | func (dec *Decoder) ReadUint64() (uint64, error) { 257 | var b [8]byte 258 | err := dec.Read(b[:]) 259 | if err != nil { 260 | return 0, err 261 | } 262 | d := binary.BigEndian.Uint64(b[:]) 263 | return d, nil 264 | } 265 | 266 | func (dec *Decoder) ReadBytes() ([]byte, error) { 267 | l, err := dec.ReadInt() 268 | if err != nil { 269 | return nil, err 270 | } 271 | if l == 0 { 272 | return nil, nil 273 | } 274 | b := make([]byte, l) 275 | err = dec.Read(b) 276 | return b, err 277 | } 278 | 279 | func (dec *Decoder) ReadBool() (bool, error) { 280 | b, err := dec.buf.ReadByte() 281 | return b == 1, err 282 | } 283 | 284 | type Encoder struct { 285 | buf *bytes.Buffer 286 | } 287 | 288 | func NewEncoder() *Encoder { 289 | return &Encoder{buf: new(bytes.Buffer)} 290 | } 291 | 292 | func (enc *Encoder) Write(b []byte) { 293 | l, err := enc.buf.Write(b) 294 | if err != nil { 295 | panic(err) 296 | } 297 | if l != len(b) { 298 | panic(b) 299 | } 300 | } 301 | 302 | func (enc *Encoder) WriteFixedBytes(b []byte) { 303 | enc.WriteInt(len(b)) 304 | enc.Write(b) 305 | } 306 | 307 | func (enc *Encoder) WriteInt(d int) { 308 | enc.WriteUint32(uint32(d)) 309 | } 310 | 311 | func (enc *Encoder) WriteUint32(d uint32) { 312 | b := uint32ToBytes(d) 313 | enc.Write(b) 314 | } 315 | 316 | func (enc *Encoder) WriteUint64(d uint64) { 317 | b := uint64ToBytes(d) 318 | enc.Write(b) 319 | } 320 | 321 | func (enc *Encoder) WriteBool(b bool) { 322 | if b { 323 | enc.buf.WriteByte(1) 324 | } else { 325 | enc.buf.WriteByte(0) 326 | } 327 | } 328 | 329 | func uint32ToBytes(d uint32) []byte { 330 | b := make([]byte, 4) 331 | binary.BigEndian.PutUint32(b, d) 332 | return b 333 | } 334 | 335 | func uint64ToBytes(d uint64) []byte { 336 | b := make([]byte, 8) 337 | binary.BigEndian.PutUint64(b, d) 338 | return b 339 | } 340 | -------------------------------------------------------------------------------- /signer/message.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "time" 8 | 9 | "github.com/MixinNetwork/tip/crypto" 10 | "github.com/drand/kyber" 11 | ) 12 | 13 | const ( 14 | MessageActionSetup = 7000 15 | MessageActionDKGDeal = 7001 16 | MessageActionDKGResponse = 7002 17 | MessageActionDKGJustify = 7003 18 | 19 | MessageSetupPeriodSeconds = 300 20 | ) 21 | 22 | type Message struct { 23 | Action int 24 | Sender string 25 | Data []byte 26 | Signature []byte 27 | } 28 | 29 | type SetupBundle struct { 30 | Nonce uint64 31 | Timestamp time.Time 32 | } 33 | 34 | func encodeSetupBundle(sb *SetupBundle) []byte { 35 | enc := NewEncoder() 36 | enc.WriteUint64(sb.Nonce) 37 | enc.WriteUint64(uint64(sb.Timestamp.UnixNano())) 38 | return enc.buf.Bytes() 39 | } 40 | 41 | func decodeSetupBundle(b []byte) (*SetupBundle, error) { 42 | sb := &SetupBundle{} 43 | dec := NewDecoder(b) 44 | 45 | no, err := dec.ReadUint64() 46 | if err != nil { 47 | return nil, err 48 | } 49 | sb.Nonce = no 50 | 51 | ts, err := dec.ReadUint64() 52 | if err != nil { 53 | return nil, err 54 | } 55 | sb.Timestamp = time.Unix(0, int64(ts)) 56 | return sb, nil 57 | } 58 | 59 | func MakeSetupMessage(ctx context.Context, key kyber.Scalar, nonce uint64) []byte { 60 | data := encodeSetupBundle(&SetupBundle{ 61 | Nonce: nonce, 62 | Timestamp: time.Now(), 63 | }) 64 | return makeMessage(key, MessageActionSetup, data) 65 | } 66 | 67 | func (node *Node) handleSetupMessage(ctx context.Context, msg *Message) error { 68 | sb, err := decodeSetupBundle(msg.Data) 69 | if err != nil { 70 | return err 71 | } 72 | var expired []string 73 | for k, v := range node.setupActions { 74 | if sb.Nonce < v.Nonce { 75 | return nil 76 | } 77 | if math.Abs(sb.Timestamp.Sub(v.Timestamp).Seconds()) > 300 { 78 | return nil 79 | } 80 | if sb.Nonce > v.Nonce { 81 | expired = append(expired, k) 82 | } 83 | } 84 | for _, k := range expired { 85 | delete(node.setupActions, k) 86 | } 87 | node.setupActions[msg.Sender] = sb 88 | if len(node.setupActions)+1 == len(node.signers) { 89 | err := node.setup(ctx, sb.Nonce) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func makeMessage(key kyber.Scalar, action int, data []byte) []byte { 98 | point := crypto.PublicKey(key) 99 | msg := &Message{ 100 | Action: action, 101 | Sender: crypto.PublicKeyString(point), 102 | Data: data, 103 | } 104 | b := encodeMessage(msg) 105 | sig, err := crypto.Sign(key, b) 106 | if err != nil { 107 | panic(err) 108 | } 109 | msg.Signature = sig 110 | return encodeMessage(msg) 111 | } 112 | 113 | func encodeMessage(m *Message) []byte { 114 | enc := NewEncoder() 115 | enc.WriteInt(m.Action) 116 | enc.WriteFixedBytes([]byte(m.Sender)) 117 | enc.WriteFixedBytes(m.Data) 118 | enc.WriteFixedBytes(m.Signature) 119 | return enc.buf.Bytes() 120 | } 121 | 122 | func decodeMessage(b []byte) (*Message, error) { 123 | msg := &Message{} 124 | dec := NewDecoder(b) 125 | 126 | an, err := dec.ReadInt() 127 | if err != nil { 128 | return nil, err 129 | } 130 | msg.Action = an 131 | 132 | sender, err := dec.ReadBytes() 133 | if err != nil { 134 | return nil, err 135 | } 136 | msg.Sender = string(sender) 137 | 138 | data, err := dec.ReadBytes() 139 | if err != nil { 140 | return nil, err 141 | } 142 | msg.Data = data 143 | 144 | sig, err := dec.ReadBytes() 145 | if err != nil { 146 | return nil, err 147 | } 148 | msg.Signature = sig 149 | 150 | return msg, nil 151 | } 152 | 153 | func (node *Node) verifyMessage(msg *Message) error { 154 | sender := node.checkSigner(msg.Sender) 155 | if sender == nil { 156 | return fmt.Errorf("unauthorized sender %s", msg.Sender) 157 | } 158 | b := encodeMessage(&Message{ 159 | Action: msg.Action, 160 | Sender: msg.Sender, 161 | Data: msg.Data, 162 | }) 163 | 164 | return crypto.Verify(sender, b, msg.Signature) 165 | } 166 | 167 | func (node *Node) checkSigner(sender string) kyber.Point { 168 | for _, s := range node.signers { 169 | if crypto.PublicKeyString(s.Public) == sender { 170 | return s.Public 171 | } 172 | } 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /signer/node.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/MixinNetwork/tip/crypto" 10 | "github.com/MixinNetwork/tip/logger" 11 | "github.com/MixinNetwork/tip/messenger" 12 | "github.com/MixinNetwork/tip/store" 13 | "github.com/drand/kyber" 14 | "github.com/drand/kyber/share" 15 | "github.com/drand/kyber/share/dkg" 16 | "golang.org/x/crypto/sha3" 17 | ) 18 | 19 | type Configuration struct { 20 | Key string `toml:"key"` 21 | Signers []string `toml:"signers"` 22 | } 23 | 24 | type Node struct { 25 | store store.Storage 26 | messenger messenger.Messenger 27 | 28 | setupActions map[string]*SetupBundle 29 | dkgStarted bool 30 | dkgDone context.CancelFunc 31 | board *Board 32 | 33 | key kyber.Scalar 34 | identity kyber.Point 35 | index int 36 | signers []dkg.Node 37 | phaser chan dkg.Phase 38 | counter int 39 | 40 | share *share.PriShare 41 | poly []kyber.Point 42 | } 43 | 44 | func NewNode(ctx context.Context, cancel context.CancelFunc, store store.Storage, messenger messenger.Messenger, conf *Configuration) *Node { 45 | node := &Node{ 46 | store: store, 47 | messenger: messenger, 48 | setupActions: make(map[string]*SetupBundle), 49 | dkgDone: cancel, 50 | phaser: make(chan dkg.Phase), 51 | index: -1, 52 | } 53 | scalar, err := crypto.PrivateKeyFromHex(conf.Key) 54 | if err != nil { 55 | panic(conf.Key) 56 | } 57 | node.key = scalar 58 | node.identity = crypto.PublicKey(scalar) 59 | var group []byte 60 | sort.Slice(conf.Signers, func(i, j int) bool { return conf.Signers[i] < conf.Signers[j] }) 61 | for i, s := range conf.Signers { 62 | point, err := crypto.PubKeyFromBase58(s) 63 | if err != nil { 64 | panic(s) 65 | } 66 | group = append(group, crypto.PublicKeyBytes(point)...) 67 | node.signers = append(node.signers, dkg.Node{ 68 | Index: uint32(i), 69 | Public: point, 70 | }) 71 | if node.identity.Equal(point) { 72 | node.index = i 73 | } 74 | } 75 | groupId := sha3.Sum256(group) 76 | valid, err := store.CheckPolyGroup(groupId[:]) 77 | if err != nil || !valid { 78 | panic(fmt.Errorf("Group check failed %v %v", valid, err)) 79 | } 80 | if node.index < 0 { 81 | panic(node.index) 82 | } 83 | 84 | logger.Infof("Idenity: %s\n", crypto.PublicKeyString(node.identity)) 85 | 86 | poly, err := store.ReadPolyPublic() 87 | if err != nil { 88 | panic(err) 89 | } else if len(poly) > 0 { 90 | logger.Infof("Poly public: %s\n", hex.EncodeToString(poly)) 91 | node.poly = unmarshalCommitments(poly) 92 | } 93 | 94 | priv, err := store.ReadPolyShare() 95 | if err != nil { 96 | panic(err) 97 | } else if len(priv) > 0 { 98 | logger.Infof("Poly share: %s\n", hex.EncodeToString(priv)) 99 | node.share = unmarshalPrivShare(priv) 100 | } 101 | return node 102 | } 103 | 104 | func (node *Node) GetKey() kyber.Scalar { 105 | return node.key 106 | } 107 | 108 | func (node *Node) GetSigners() []dkg.Node { 109 | return node.signers 110 | } 111 | 112 | func (node *Node) GetShare() *share.PriShare { 113 | return node.share 114 | } 115 | 116 | func (node *Node) GetPoly() []kyber.Point { 117 | return node.poly 118 | } 119 | 120 | func (node *Node) Run(ctx context.Context) error { 121 | if node.share != nil || node.poly != nil { 122 | return nil 123 | } 124 | for { 125 | _, b, err := node.messenger.ReceiveMessage(ctx) 126 | if err != nil { 127 | return err 128 | } 129 | msg, err := decodeMessage(b) 130 | if err != nil { 131 | logger.Errorf("msg decode error %d %s", len(b), err) 132 | continue 133 | } 134 | err = node.verifyMessage(msg) 135 | if err != nil { 136 | logger.Errorf("msg verify error %d %s", len(b), err) 137 | continue 138 | } 139 | switch msg.Action { 140 | case MessageActionSetup: 141 | err = node.handleSetupMessage(ctx, msg) 142 | logger.Verbose("SETUP", err) 143 | case MessageActionDKGDeal: 144 | nonce, db, err := decodeDealBundle(msg.Data) 145 | logger.Verbose("DEAL", nonce, err) 146 | if err != nil { 147 | continue 148 | } 149 | if !node.dkgStarted { 150 | err := node.setup(ctx, nonce) 151 | if err != nil { 152 | continue 153 | } 154 | } 155 | node.board.deals <- *db 156 | node.counter += 1 157 | logger.Verbose("DEAL COUNTER", node.counter) 158 | if node.counter+1 == len(node.signers) { 159 | node.phaser <- dkg.ResponsePhase 160 | node.counter = 0 161 | } 162 | case MessageActionDKGResponse: 163 | rb, err := decodeResponseBundle(msg.Data) 164 | logger.Verbose("RESPONSE", err) 165 | if err != nil || node.board == nil { 166 | continue 167 | } 168 | node.board.resps <- *rb 169 | node.counter += 1 170 | logger.Verbose("RESPONSE COUNTER", node.counter) 171 | if node.counter+1 == len(node.signers) { 172 | node.phaser <- dkg.JustifPhase 173 | node.counter = 0 174 | } 175 | case MessageActionDKGJustify: 176 | jb, err := decodeJustificationBundle(msg.Data) 177 | logger.Verbose("JUSTIFICATION", err) 178 | if err != nil || node.board == nil { 179 | continue 180 | } 181 | node.board.justs <- *jb 182 | node.counter += 1 183 | logger.Verbose("JUSTIFICATION COUNTER", node.counter) 184 | if node.counter+1 == len(node.signers) { 185 | node.phaser <- dkg.FinishPhase 186 | node.counter = 0 187 | } 188 | } 189 | } 190 | } 191 | 192 | func (node *Node) Threshold() int { 193 | return len(node.signers)*2/3 + 1 194 | } 195 | -------------------------------------------------------------------------------- /signer/setup.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "github.com/MixinNetwork/tip/crypto" 10 | "github.com/MixinNetwork/tip/logger" 11 | "github.com/drand/kyber" 12 | "github.com/drand/kyber/group/mod" 13 | "github.com/drand/kyber/pairing/bn256" 14 | "github.com/drand/kyber/share" 15 | "github.com/drand/kyber/share/dkg" 16 | "github.com/drand/kyber/sign/bdn" 17 | "golang.org/x/crypto/sha3" 18 | ) 19 | 20 | func (node *Node) setup(ctx context.Context, nonce uint64) error { 21 | if node.dkgStarted { 22 | return nil 23 | } 24 | node.dkgStarted = true 25 | 26 | priv, err := node.store.ReadPolyShare() 27 | if err != nil || priv != nil { 28 | return err 29 | } 30 | pub, err := node.store.ReadPolyPublic() 31 | if err != nil || pub != nil { 32 | return err 33 | } 34 | 35 | suite := bn256.NewSuiteG2() 36 | conf := &dkg.Config{ 37 | Suite: suite, 38 | Threshold: node.Threshold(), 39 | Longterm: node.key, 40 | Nonce: node.getNonce(nonce), 41 | Auth: bdn.NewSchemeOnG1(suite), 42 | FastSync: true, 43 | NewNodes: node.signers, 44 | } 45 | 46 | node.board = node.NewBoard(ctx, nonce) 47 | protocol, err := dkg.NewProtocol(conf, node.board, node, false) 48 | logger.Verbose("NewProtocol", protocol, err) 49 | if err != nil { 50 | return err 51 | } 52 | node.phaser <- dkg.DealPhase 53 | go func() { 54 | defer node.dkgDone() 55 | pub, priv, err = node.runDKG(ctx, protocol) 56 | logger.Verbose("runDKG", hex.EncodeToString(pub), hex.EncodeToString(priv), err) 57 | if err != nil { 58 | panic(err) 59 | } 60 | err = node.store.WritePoly(pub, priv) 61 | if err != nil { 62 | panic(err) 63 | } 64 | }() 65 | return nil 66 | } 67 | 68 | func (node *Node) NextPhase() chan dkg.Phase { 69 | return node.phaser 70 | } 71 | 72 | func (node *Node) runDKG(_ context.Context, protocol *dkg.Protocol) ([]byte, []byte, error) { 73 | resCh := protocol.WaitEnd() 74 | optRes := <-resCh 75 | if optRes.Error != nil { 76 | return nil, nil, optRes.Error 77 | } 78 | res := optRes.Result 79 | if i := res.Key.PriShare().I; i != node.index { 80 | return nil, nil, fmt.Errorf("private share index malformed %d %d", node.index, i) 81 | } 82 | priv := marshalPrivShare(res.Key.PriShare()) 83 | pub := marshalCommitments(res.Key.Commitments()) 84 | return pub, priv, nil 85 | } 86 | 87 | func unmarshalPrivShare(b []byte) *share.PriShare { 88 | var ps share.PriShare 89 | ps.V = mod.NewInt64(0, bn256.Order).SetBytes(b[4:]) 90 | ps.I = int(binary.BigEndian.Uint32(b[:4])) 91 | return &ps 92 | } 93 | 94 | func marshalPrivShare(ps *share.PriShare) []byte { 95 | var buf [4]byte 96 | binary.BigEndian.PutUint32(buf[:], uint32(ps.I)) 97 | b := crypto.PrivateKeyBytes(ps.V) 98 | return append(buf[:], b...) 99 | } 100 | 101 | func unmarshalCommitments(b []byte) []kyber.Point { 102 | var commits []kyber.Point 103 | for i, l := 0, len(b)/128; i < l; i++ { 104 | point, err := crypto.PubKeyFromBytes(b[i*128 : (i+1)*128]) 105 | if err != nil { 106 | panic(err) 107 | } 108 | commits = append(commits, point) 109 | } 110 | return commits 111 | } 112 | 113 | func marshalCommitments(commits []kyber.Point) []byte { 114 | var data []byte 115 | for _, p := range commits { 116 | b := crypto.PublicKeyBytes(p) 117 | data = append(data, b...) 118 | } 119 | return data 120 | } 121 | 122 | func (node *Node) getNonce(nonce uint64) []byte { 123 | var data []byte 124 | for _, s := range node.signers { 125 | b := crypto.PublicKeyBytes(s.Public) 126 | data = append(data, b...) 127 | } 128 | var buf [8]byte 129 | binary.BigEndian.PutUint64(buf[:], nonce) 130 | data = append(data, buf[:]...) 131 | sum := sha3.Sum256(data) 132 | return sum[:] 133 | } 134 | -------------------------------------------------------------------------------- /store/badger.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/dgraph-io/badger/v4" 11 | ) 12 | 13 | const ( 14 | badgerKeyPolyGroup = "POLY#GROUP" 15 | badgerKeyPolyPublic = "POLY#PUBLIC" 16 | badgerKeyPolyShare = "POLY#SHARE" 17 | 18 | badgerKeyPrefixAssignee = "ASSIGNEE#" 19 | badgerKeyPrefixAssignor = "ASSIGNOR#" 20 | badgerKeyPrefixWatcher = "WATCHER#" 21 | badgerKeyPrefixLimit = "LIMIT#" 22 | badgerKeyPrefixNonce = "NONCE#" 23 | badgerKeyPrefixGenesis = "GENESIS#" 24 | badgerKeyPrefixCounter = "COUNTER#" 25 | maxUint64 = ^uint64(0) 26 | ) 27 | 28 | type BadgerConfiguration struct { 29 | Dir string `toml:"dir"` 30 | } 31 | 32 | type BadgerStorage struct { 33 | db *badger.DB 34 | } 35 | 36 | func (bs *BadgerStorage) CheckLimit(key []byte, window time.Duration, quota uint32, increase bool) (int, error) { 37 | now := uint64(time.Now().UnixNano()) 38 | if now >= maxUint64/2 || now <= uint64(window) { 39 | panic(time.Now()) 40 | } 41 | now = maxUint64 - now 42 | threshold := now + uint64(window) 43 | available := quota 44 | 45 | prefix := append([]byte(badgerKeyPrefixLimit), key...) 46 | err := bs.db.Update(func(txn *badger.Txn) error { 47 | opts := badger.DefaultIteratorOptions 48 | opts.PrefetchValues = false 49 | opts.Prefix = prefix 50 | it := txn.NewIterator(opts) 51 | defer it.Close() 52 | 53 | for it.Seek(prefix); available > 0 && it.ValidForPrefix(prefix); it.Next() { 54 | ts := it.Item().Key()[len(prefix):] 55 | if binary.BigEndian.Uint64(ts) > threshold { 56 | break 57 | } 58 | available-- 59 | } 60 | if available == 0 || !increase { 61 | return nil 62 | } 63 | 64 | available-- 65 | buf := uint64ToBytes(now) 66 | entry := badger.NewEntry(append(prefix, buf...), []byte{1}) 67 | entry = entry.WithTTL(window * 2) 68 | return txn.SetEntry(entry) 69 | }) 70 | return int(available), err 71 | } 72 | 73 | func (bs *BadgerStorage) CheckEphemeralNonce(key, ephemeral []byte, nonce uint64, grace time.Duration) (bool, error) { 74 | var valid bool 75 | now := time.Now().UnixNano() 76 | val := uint64ToBytes(uint64(now)) 77 | val = append(val, ephemeral...) 78 | buf := uint64ToBytes(nonce) 79 | val = append(val, buf...) 80 | key = append([]byte(badgerKeyPrefixNonce), key...) 81 | err := bs.db.Update(func(txn *badger.Txn) error { 82 | item, err := txn.Get(key) 83 | if err == badger.ErrKeyNotFound { 84 | valid = true 85 | return txn.Set(key, val) 86 | } else if err != nil { 87 | return err 88 | } 89 | v, err := item.ValueCopy(nil) 90 | if err != nil { 91 | return err 92 | } 93 | old := binary.BigEndian.Uint64(v[:8]) 94 | if old+uint64(grace) < uint64(now) { 95 | valid = true 96 | return txn.Set(key, val) 97 | } 98 | if !bytes.Equal(v[8:len(v)-8], ephemeral) { 99 | return nil 100 | } 101 | old = binary.BigEndian.Uint64(v[len(v)-8:]) 102 | if old >= nonce { 103 | return nil 104 | } 105 | valid = true 106 | return txn.Set(key, val) 107 | }) 108 | return valid, err 109 | } 110 | 111 | func (bs *BadgerStorage) RotateEphemeralNonce(key, ephemeral []byte, nonce uint64) error { 112 | now := time.Now().UnixNano() 113 | key = append([]byte(badgerKeyPrefixNonce), key...) 114 | 115 | val := uint64ToBytes(uint64(now)) 116 | val = append(val, ephemeral...) 117 | 118 | buf := uint64ToBytes(nonce) 119 | val = append(val, buf...) 120 | 121 | return bs.db.Update(func(txn *badger.Txn) error { 122 | return txn.Set(key, val) 123 | }) 124 | } 125 | 126 | func (bs *BadgerStorage) CheckPolyGroup(group []byte) (bool, error) { 127 | var valid bool 128 | key := []byte(badgerKeyPolyGroup) 129 | err := bs.db.Update(func(txn *badger.Txn) error { 130 | item, err := txn.Get(key) 131 | if err == badger.ErrKeyNotFound { 132 | valid = true 133 | return txn.Set(key, group) 134 | } else if err != nil { 135 | return err 136 | } 137 | old, err := item.ValueCopy(nil) 138 | if err != nil { 139 | return err 140 | } 141 | if bytes.Equal(old, group) { 142 | valid = true 143 | } 144 | return nil 145 | }) 146 | return valid, err 147 | } 148 | 149 | func (bs *BadgerStorage) ReadPolyShare() ([]byte, error) { 150 | txn := bs.db.NewTransaction(false) 151 | defer txn.Discard() 152 | 153 | item, err := txn.Get([]byte(badgerKeyPolyShare)) 154 | if err == badger.ErrKeyNotFound { 155 | return nil, nil 156 | } 157 | if err != nil { 158 | return nil, err 159 | } 160 | return item.ValueCopy(nil) 161 | } 162 | 163 | func (bs *BadgerStorage) ReadPolyPublic() ([]byte, error) { 164 | txn := bs.db.NewTransaction(false) 165 | defer txn.Discard() 166 | 167 | item, err := txn.Get([]byte(badgerKeyPolyPublic)) 168 | if err == badger.ErrKeyNotFound { 169 | return nil, nil 170 | } 171 | if err != nil { 172 | return nil, err 173 | } 174 | return item.ValueCopy(nil) 175 | } 176 | 177 | func (bs *BadgerStorage) WritePoly(public, share []byte) error { 178 | return bs.db.Update(func(txn *badger.Txn) error { 179 | err := txn.Set([]byte(badgerKeyPolyPublic), public) 180 | if err != nil { 181 | return err 182 | } 183 | return txn.Set([]byte(badgerKeyPolyShare), share) 184 | }) 185 | } 186 | 187 | func (bs *BadgerStorage) WriteAssignee(key []byte, assignee []byte) error { 188 | return bs.db.Update(func(txn *badger.Txn) error { 189 | if oa, err := readKey(txn, badgerKeyPrefixAssignee, key); err != nil { 190 | return err 191 | } else if oa != nil { 192 | rk := append([]byte(badgerKeyPrefixAssignor), oa...) 193 | err = txn.Delete(rk) 194 | if err != nil { 195 | return err 196 | } 197 | } 198 | 199 | if !bytes.Equal(key, assignee) { 200 | old, err := readKey(txn, badgerKeyPrefixAssignee, assignee) 201 | if err != nil { 202 | return err 203 | } else if old != nil { 204 | return fmt.Errorf("invalid assignee as is assignee") 205 | } 206 | old, err = readKey(txn, badgerKeyPrefixAssignor, assignee) 207 | if err != nil { 208 | return err 209 | } else if old != nil { 210 | return fmt.Errorf("invalid assignor as is assignee") 211 | } 212 | } 213 | 214 | lk := append([]byte(badgerKeyPrefixAssignee), key...) 215 | err := txn.Set(lk, assignee) 216 | if err != nil { 217 | return err 218 | } 219 | rk := append([]byte(badgerKeyPrefixAssignor), assignee...) 220 | err = txn.Set(rk, key) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | var counter uint64 226 | cb, err := readKey(txn, badgerKeyPrefixCounter, key) 227 | if err != nil { 228 | return err 229 | } else if cb != nil { 230 | counter = binary.BigEndian.Uint64(cb) 231 | } 232 | ck := append([]byte(badgerKeyPrefixCounter), key...) 233 | cv := uint64ToBytes(counter + 1) 234 | return txn.Set(ck, cv) 235 | }) 236 | } 237 | 238 | func (bs *BadgerStorage) ReadAssignee(key []byte) ([]byte, error) { 239 | txn := bs.db.NewTransaction(false) 240 | defer txn.Discard() 241 | 242 | return readKey(txn, badgerKeyPrefixAssignee, key) 243 | } 244 | 245 | func (bs *BadgerStorage) ReadAssignor(key []byte) ([]byte, error) { 246 | txn := bs.db.NewTransaction(false) 247 | defer txn.Discard() 248 | 249 | return readKey(txn, badgerKeyPrefixAssignor, key) 250 | } 251 | 252 | func (bs *BadgerStorage) Watch(key []byte) ([]byte, time.Time, int, error) { 253 | txn := bs.db.NewTransaction(false) 254 | defer txn.Discard() 255 | 256 | assignor, err := readKey(txn, badgerKeyPrefixWatcher, key) 257 | if err != nil { 258 | return nil, time.Time{}, 0, err 259 | } else if assignor == nil { 260 | return nil, time.Time{}, 0, nil 261 | } 262 | 263 | gb, err := readKey(txn, badgerKeyPrefixGenesis, assignor) 264 | if err != nil { 265 | return assignor, time.Time{}, 0, err 266 | } 267 | genesis := time.Unix(0, int64(binary.BigEndian.Uint64(gb))) 268 | 269 | cb, err := readKey(txn, badgerKeyPrefixCounter, assignor) 270 | if err != nil { 271 | return assignor, time.Time{}, 0, err 272 | } 273 | counter := int(binary.BigEndian.Uint64(cb)) 274 | 275 | return assignor, genesis, counter, nil 276 | } 277 | 278 | func (bs *BadgerStorage) WriteSignRequest(assignor, watcher []byte) (time.Time, int, error) { 279 | if len(assignor) == 0 || len(watcher) == 0 { 280 | return time.Time{}, 0, fmt.Errorf("invalid assignor %x or watcher %x", assignor, watcher) 281 | } 282 | var counter int 283 | var genesis time.Time 284 | err := bs.db.Update(func(txn *badger.Txn) error { 285 | cb, err := readKey(txn, badgerKeyPrefixCounter, assignor) 286 | if err != nil { 287 | return err 288 | } else if cb != nil { 289 | counter = int(binary.BigEndian.Uint64(cb)) 290 | } else { 291 | // counter means the number of key an identity has used in the node, thus 292 | // the first counter returned is 1 after an identity assignor created. 293 | // counter is only increased whenever a new key created, i.e. WriteAssignee 294 | counter = 1 295 | } 296 | 297 | old, err := readKey(txn, badgerKeyPrefixGenesis, assignor) 298 | if err != nil { 299 | return err 300 | } else if old != nil { 301 | genesis = time.Unix(0, int64(binary.BigEndian.Uint64(old))) 302 | } else { 303 | genesis = time.Now() 304 | } 305 | 306 | key := append([]byte(badgerKeyPrefixGenesis), assignor...) 307 | val := uint64ToBytes(uint64(genesis.UnixNano())) 308 | err = txn.Set(key, val) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | key = append([]byte(badgerKeyPrefixCounter), assignor...) 314 | val = uint64ToBytes(uint64(counter)) 315 | err = txn.Set(key, val) 316 | if err != nil { 317 | return err 318 | } 319 | 320 | old, err = readKey(txn, badgerKeyPrefixWatcher, watcher) 321 | if err != nil { 322 | return err 323 | } else if old != nil && !bytes.Equal(old, assignor) { 324 | return fmt.Errorf("invalid watcher %x", watcher) 325 | } 326 | key = append([]byte(badgerKeyPrefixWatcher), watcher...) 327 | return txn.Set(key, assignor) 328 | }) 329 | return genesis, counter, err 330 | } 331 | 332 | func OpenBadger(ctx context.Context, conf *BadgerConfiguration) (*BadgerStorage, error) { 333 | db, err := badger.Open(badger.DefaultOptions(conf.Dir)) 334 | if err != nil { 335 | return nil, err 336 | } 337 | return &BadgerStorage{ 338 | db: db, 339 | }, nil 340 | } 341 | 342 | func (bs *BadgerStorage) Close() { 343 | bs.db.Close() 344 | } 345 | 346 | func readKey(txn *badger.Txn, prefix string, key []byte) ([]byte, error) { 347 | key = append([]byte(prefix), key...) 348 | item, err := txn.Get(key) 349 | if err == badger.ErrKeyNotFound { 350 | return nil, nil 351 | } 352 | if err != nil { 353 | return nil, err 354 | } 355 | return item.ValueCopy(nil) 356 | } 357 | 358 | func uint64ToBytes(i uint64) []byte { 359 | buf := make([]byte, 8) 360 | binary.BigEndian.PutUint64(buf, i) 361 | return buf 362 | } 363 | -------------------------------------------------------------------------------- /store/badger_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/MixinNetwork/tip/crypto" 11 | "github.com/drand/kyber/pairing/bn256" 12 | "github.com/drand/kyber/util/random" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestBadgerLimit(t *testing.T) { 17 | assert := assert.New(t) 18 | bs := testBadgerStore() 19 | defer bs.Close() 20 | 21 | key := []byte("limit-check-test") 22 | available, err := bs.CheckLimit(key, time.Second*3, 3, true) 23 | assert.Nil(err) 24 | assert.Equal(2, available) 25 | available, err = bs.CheckLimit(key, time.Second*3, 3, true) 26 | assert.Nil(err) 27 | assert.Equal(1, available) 28 | available, err = bs.CheckLimit(key, time.Second*3, 5, true) 29 | assert.Nil(err) 30 | assert.Equal(2, available) 31 | available, err = bs.CheckLimit(key, time.Second*3, 5, true) 32 | assert.Nil(err) 33 | assert.Equal(1, available) 34 | available, err = bs.CheckLimit(key, time.Second*3, 5, true) 35 | assert.Nil(err) 36 | assert.Equal(0, available) 37 | available, err = bs.CheckLimit(key, time.Second*3, 5, true) 38 | assert.Nil(err) 39 | assert.Equal(0, available) 40 | available, err = bs.CheckLimit(key, time.Second*3, 5, true) 41 | assert.Nil(err) 42 | assert.Equal(0, available) 43 | available, err = bs.CheckLimit(key, time.Second*3, 5, true) 44 | assert.Nil(err) 45 | assert.Equal(0, available) 46 | } 47 | 48 | func TestBadgerNonce(t *testing.T) { 49 | assert := assert.New(t) 50 | bs := testBadgerStore() 51 | defer bs.Close() 52 | 53 | key := []byte("nonce-check-test-key") 54 | nonce := []byte("nonce-check-test-value") 55 | res, err := bs.CheckEphemeralNonce(key, nonce, 0, time.Second) 56 | assert.Nil(err) 57 | assert.True(res) 58 | res, err = bs.CheckEphemeralNonce(key, nonce, 0, time.Second) 59 | assert.Nil(err) 60 | assert.False(res) 61 | res, err = bs.CheckEphemeralNonce(key, nonce, 1, time.Second) 62 | assert.Nil(err) 63 | assert.True(res) 64 | res, err = bs.CheckEphemeralNonce(key, append(nonce, 1), 2, time.Second) 65 | assert.Nil(err) 66 | assert.False(res) 67 | res, err = bs.CheckEphemeralNonce(key, append(nonce, 1), 3, time.Second) 68 | assert.Nil(err) 69 | assert.False(res) 70 | time.Sleep(time.Second) 71 | res, err = bs.CheckEphemeralNonce(key, append(nonce, 1), 0, time.Second) 72 | assert.Nil(err) 73 | assert.True(res) 74 | } 75 | 76 | func TestBadgerPolyGroup(t *testing.T) { 77 | assert := assert.New(t) 78 | bs := testBadgerStore() 79 | defer bs.Close() 80 | 81 | valid, err := bs.CheckPolyGroup([]byte("group")) 82 | assert.Nil(err) 83 | assert.True(valid) 84 | 85 | valid, err = bs.CheckPolyGroup([]byte("group")) 86 | assert.Nil(err) 87 | assert.True(valid) 88 | 89 | valid, err = bs.CheckPolyGroup([]byte("invalid")) 90 | assert.Nil(err) 91 | assert.False(valid) 92 | 93 | valid, err = bs.CheckPolyGroup([]byte("group")) 94 | assert.Nil(err) 95 | assert.True(valid) 96 | } 97 | 98 | func TestBadgerAssignee(t *testing.T) { 99 | assert := assert.New(t) 100 | bs := testBadgerStore() 101 | defer bs.Close() 102 | 103 | a, b, c := []byte{1}, []byte{2}, []byte{3} 104 | err := bs.WriteAssignee(a, b) 105 | assert.Nil(err) 106 | available, err := bs.CheckLimit(a, time.Second*3, 3, true) 107 | assert.Nil(err) 108 | assert.Equal(2, available) 109 | err = bs.WriteAssignee(a, b) 110 | assert.Nil(err) 111 | res, err := bs.CheckEphemeralNonce(a, a, 0, time.Second) 112 | assert.Nil(err) 113 | assert.True(res) 114 | err = bs.WriteAssignee(a, b) 115 | assert.Nil(err) 116 | ee, err := bs.ReadAssignee(a) 117 | assert.Nil(err) 118 | assert.Equal(b, ee) 119 | or, err := bs.ReadAssignor(a) 120 | assert.Nil(err) 121 | assert.Nil(or) 122 | or, err = bs.ReadAssignor(b) 123 | assert.Nil(err) 124 | assert.Equal(a, or) 125 | res, err = bs.CheckEphemeralNonce(a, a, 0, time.Second) 126 | assert.Nil(err) 127 | assert.False(res) 128 | res, err = bs.CheckEphemeralNonce(b, a, 0, time.Second) 129 | assert.Nil(err) 130 | assert.True(res) 131 | res, err = bs.CheckEphemeralNonce(b, b, 1, time.Second) 132 | assert.Nil(err) 133 | assert.False(res) 134 | res, err = bs.CheckEphemeralNonce(b, a, 1, time.Second) 135 | assert.Nil(err) 136 | assert.True(res) 137 | res, err = bs.CheckEphemeralNonce(c, c, 0, time.Second) 138 | assert.Nil(err) 139 | assert.True(res) 140 | err = bs.WriteAssignee(a, c) 141 | assert.Nil(err) 142 | ee, err = bs.ReadAssignee(a) 143 | assert.Nil(err) 144 | assert.Equal(c, ee) 145 | or, err = bs.ReadAssignor(a) 146 | assert.Nil(err) 147 | assert.Nil(or) 148 | or, err = bs.ReadAssignor(b) 149 | assert.Nil(err) 150 | assert.Nil(or) 151 | or, err = bs.ReadAssignor(c) 152 | assert.Nil(err) 153 | assert.Equal(or, a) 154 | res, err = bs.CheckEphemeralNonce(c, a, 1, time.Second) 155 | assert.Nil(err) 156 | assert.False(res) 157 | res, err = bs.CheckEphemeralNonce(c, c, 1, time.Second) 158 | assert.Nil(err) 159 | assert.True(res) 160 | res, err = bs.CheckEphemeralNonce(b, a, 2, time.Second) 161 | assert.Nil(err) 162 | assert.True(res) 163 | err = bs.WriteAssignee(a, a) 164 | assert.Nil(err) 165 | ee, err = bs.ReadAssignee(a) 166 | assert.Nil(err) 167 | assert.Equal(a, ee) 168 | or, err = bs.ReadAssignor(a) 169 | assert.Nil(err) 170 | assert.Equal(a, or) 171 | or, err = bs.ReadAssignor(b) 172 | assert.Nil(err) 173 | assert.Nil(or) 174 | or, err = bs.ReadAssignor(c) 175 | assert.Nil(err) 176 | assert.Nil(or) 177 | } 178 | 179 | func TestBadgerWatch(t *testing.T) { 180 | assert := assert.New(t) 181 | bs := testBadgerStore() 182 | defer bs.Close() 183 | 184 | suite := bn256.NewSuiteBn256() 185 | user := suite.Scalar().Pick(random.New()) 186 | identity := crypto.PublicKeyBytes(crypto.PublicKey(user)) 187 | watcher := make([]byte, 32) 188 | rand.Read(watcher) 189 | genesis, counter, err := bs.WriteSignRequest(identity, watcher) 190 | assert.Nil(err) 191 | assert.Equal(1, counter) 192 | assert.True(genesis.Add(time.Minute).After(time.Now())) 193 | oas, genesisExist, counterExist, err := bs.Watch(watcher) 194 | assert.Nil(err) 195 | assert.Equal(counter, counterExist) 196 | assert.True(genesis.Equal(genesisExist)) 197 | assert.Equal(identity, oas) 198 | genesis, counter, err = bs.WriteSignRequest(identity, watcher) 199 | assert.Nil(err) 200 | assert.Equal(1, counter) 201 | assert.True(genesis.Add(time.Minute).After(time.Now())) 202 | oas, genesisExist, counterExist, err = bs.Watch(watcher) 203 | assert.Nil(err) 204 | assert.Equal(counter, counterExist) 205 | assert.True(genesis.Equal(genesisExist)) 206 | assert.Equal(identity, oas) 207 | err = bs.WriteAssignee(identity, identity) 208 | assert.Nil(err) 209 | oas, genesisExist, counterExist, err = bs.Watch(watcher) 210 | assert.Nil(err) 211 | assert.Equal(2, counterExist) 212 | assert.True(genesis.Equal(genesisExist)) 213 | assert.Equal(identity, oas) 214 | } 215 | 216 | func testBadgerStore() *BadgerStorage { 217 | dir, err := os.MkdirTemp("/tmp", "tip-badger-test") 218 | if err != nil { 219 | panic(err) 220 | } 221 | conf := &BadgerConfiguration{ 222 | Dir: dir, 223 | } 224 | bs, err := OpenBadger(context.Background(), conf) 225 | if err != nil { 226 | panic(err) 227 | } 228 | return bs 229 | } 230 | -------------------------------------------------------------------------------- /store/interface.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "time" 4 | 5 | type Storage interface { 6 | CheckPolyGroup(group []byte) (bool, error) 7 | ReadPolyPublic() ([]byte, error) 8 | ReadPolyShare() ([]byte, error) 9 | WritePoly(public, share []byte) error 10 | 11 | WriteAssignee(key []byte, assignee []byte) error 12 | ReadAssignor(key []byte) ([]byte, error) 13 | ReadAssignee(key []byte) ([]byte, error) 14 | CheckLimit(key []byte, window time.Duration, quota uint32, increase bool) (int, error) 15 | CheckEphemeralNonce(key, ephemeral []byte, nonce uint64, grace time.Duration) (bool, error) 16 | RotateEphemeralNonce(key, ephemeral []byte, nonce uint64) error 17 | WriteSignRequest(key, watcher []byte) (time.Time, int, error) 18 | Watch(key []byte) ([]byte, time.Time, int, error) 19 | } 20 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MixinNetwork/tip/30da76aaffff42682d45a77a6a1f75600ca02dab/web/favicon.ico -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Throttled Identity Protocol (TIP) 6 | 7 | 8 | 9 | 21 | 22 | 23 |
24 |
25 |

Throttled Identity Protocol

26 |

Throttled Identity Protocol (TIP) is a decentralized key derivation protocol, which allows people to obtain a strong secret key through a very simple passphrase, e.g. a six-digit PIN.

27 |

Mission and Overview

28 |

Along with the rising of Bitcoin and other cryptocurrencies, the saying "not your keys, not your coins" has become well-known. That's true, very true and definitely true, that's the right and freedom Bitcoin has given people. Whoever can access the key can move the money, and nobody without the key is able to do that.

29 |

That said it's better to manage your own Bitcoin private key than let your coins lie in some centralized exchanges. However, it's admitted that key management requires superior skills, which most people lack. And the result is people who own their keys lose the coins permanently due to various accidents, and those who opened a Coinbase account years ago can still get back their assets easily.

30 |

The embarrassing result doesn't prove the security of centralized exchanges, yet exposes the drawbacks of key management. The key grants people the right to truly own their properties, but people lose money due to poor key management skills. People should not be blamed for that, it's the problem of the key itself.

31 |

Bitcoin gives people the right and freedom to own their properties, and people deserve the convenience to manage their keys. Current private key or mnemonic phrase designs are over-complicated for people to keep properly. Instead of fearing the corruption of centralized financial institutions, people become slaves of the private key.

32 |

It's what TIP strives to do. Let people truly own their coins with a six-digit PIN. This decentralized PIN is easy to remember for any person, doesn't require any special skills or hardware, and people can manage their coins with more confidence than ever.

33 |

Protocol Design

34 |

TIP involves three independent parties to make the protocol work. A decentralized signer network authenticates signing requests from the user, and throttles malicious attempts; A trusted account manager serves the user an identity seed, which typically authenticates the user by email or phone verification code; The user remembers a PIN and combines the identity seed from the account manager, then makes independent requests to enough signer network nodes, and finally derives their secret key.

35 |

protocol design workflow diagram

36 |

Decentralized Network Setup

37 |

The decentralized signer network is launched cooperatively by many different entities. Specifically, those entities gather and reach a consensus to run some node software, those nodes interactively run a distributed key generation protocol. For TIP, the DKG is threshold Boneh-Lynn-Shacham (BLS) signatures.

38 |

Assuming n entities agree to launch the network, they generate an asymmetric key pair respectively and configure their node software to include all the entities' public keys in a deterministic order. Then they boot the nodes to run a t-of-n (where t = n * 2 / 3 + 1) DKG protocol to set up a collective public key P and private key shares si respectively.

39 |

After the DKG protocol finishes, all entities should share the public key P to ensure they hold the same one, keep their private key shares si cautiously, and should make professional backups.

40 |

Finally, all entities should boot their node software to accept throttled signing requests from users. And again, they should safeguard the node servers and defend against all malicious attacks.

41 |

This repository includes an implementation of the signer node software, for instructions please see the signer directory.

42 |

Throttled Secret Derivation

43 |

The network announces the configuration and signers list to the public or potential users and waits for signing requests. Each signer should throttle the requests based on the same restrictions.

44 | 49 |

After the signing request passes all throttle checks, the signer responds back a part of the t-of-n threshold BLS signature by signing the identity. Whenever the user collects t valid partials, they can recover the final collective signature and verify it with the collective public key.

50 |

The final collective signature is the seed to the secret key of the user. Then it's up to the user to use different algorithms to generate their private key for Bitcoin or other usages. It doesn't need any further requests to use this secret key, and in case of a loss, the user can recover it by making the same requests.

51 |

For details of the throttle restrictions, please see the keeper directory.

52 |

Threshold Identity Generation

53 |

The mission of TIP network is to let people truly own their coins by only remembering a 6-digit PIN, so they should not have the duty to store identity, ephemeral or nonce. They are capable of achieving this goal through the threshold identity generation process with the help from the trusted account manager.

54 |
    55 |
  1. User authenticates themself with a trusted account manager through email or phone verification code, and the manager responds with the identity seed Si.
  2. 56 |
  3. User chooses a very slow hash function Hs, e.g. argon2id, and generates the identity I = Hs(PIN || Si).
  4. 57 |
  5. User generates a random ephemeral seed Se, and stores the seed on its device securely.
  6. 58 |
  7. For each signer i in the network with public key Pi, user generates the ephemeral ei = Hs(I || Se || Pi).
  8. 59 |
  9. User sends signing requests (I, ei, nonce, grace) to each signer i and gathers enough partial signatures, then recover the final collective signature.
  10. 60 |
  11. User must repeat the process every a while to refresh the ephemeral grace period.
  12. 61 |
62 |

The identity seed should prohibit all impersonation, the on-device random ephemeral seed should prevent the account manager collude with some signer, and the ephemeral grace period allows the user to recover its secret key when the device is lost.

63 |

Furthermore, the user can make their threshold identity generation more secure by cooperating with another user to combine their identity to increase the entropy especially when the account manager manages lots of identities.

64 |

And finally, the user can just back up his seeds like any traditional key management process, and this backup is considered more secure against loss or theft.

65 |

Network Evolution

66 |

Once the decentralized signer network is launched, its signers should remain constant, no new entity is permitted to join the signers or replace an old signer because the DKG protocol remains valid only when all shares remain unchanged. But people need the network to become stronger, and that requires more entities to join the network. So TIP allows network evolution.

67 |

Whenever a new entity is accepted to the network, either replacing an old signer or joining as a new one, an evolution happens. Indeed, an evolution starts a fresh DKG protocol in the same process as the previous evolution, but with different signers, thus resulting in absolutely different shares for each signer. It's noted that an entity leaving the network doesn't result in any evolution, because the remaining shares can still serve requests.

68 |

In a new evolution, all signers should reference the number and the hash of the signer list from the previous evolution. After a new evolution starts, the previous evolution still works. For each signer in the new evolution, if it is a signer of the previous evolution, it must maintain its availability to serve signing requests to the previous evolution, otherwise it should be punished.

69 |

Any user requests for the throttled secret derivation should include the evolution number to get the correct signature. And in any case of network changes, the user is assured of their key security due to various backups discussed in previous sections.

70 |

Incentive and Punishment

71 |

The code doesn't include any incentive or punishment for the entities running the signer node software. It's up to their consensus on their mission, either to serve their customers a better user experience, or charge a small key signing request fee, or they could make some tokens to do community development.

72 |

Security

73 |

All the cryptography libraries used in this repository are being developed and used by industry-leading institutions, notably the drand project and its league of entropy that includes Cloudflare, EPFL, Kudelski Security, Protocol Labs, Celo, UCL, and UIUC.

74 |

The code has been audited by Certik, and the audit report can be found at https://github.com/MixinNetwork/audits.

75 |

Contribution

76 |

The project doesn't accept feature requests and welcomes all security improvement contributions. Shall you find any security issues, please email security@mixin.one before any public disclosures or pull requests.

77 |

The core team highly values the contributions and provides at most a $100K bounty for any vulnerability report according to the severity.

78 |

Code and License

79 |

The TIP implementation https://github.com/MixinNetwork/tip is released under Apache 2.0 license.

80 |
81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /web/workflow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MixinNetwork/tip/30da76aaffff42682d45a77a6a1f75600ca02dab/web/workflow.jpg --------------------------------------------------------------------------------