├── .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 |
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 |
45 | 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.
46 | 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.
47 | 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.
48 |
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 | User authenticates themself with a trusted account manager through email or phone verification code, and the manager responds with the identity seed Si .
56 | User chooses a very slow hash function Hs , e.g. argon2id, and generates the identity I = Hs (PIN || Si ) .
57 | User generates a random ephemeral seed Se , and stores the seed on its device securely.
58 | For each signer i in the network with public key Pi , user generates the ephemeral ei = Hs (I || Se || Pi ) .
59 | User sends signing requests (I, ei , nonce, grace) to each signer i and gathers enough partial signatures, then recover the final collective signature.
60 | User must repeat the process every a while to refresh the ephemeral grace period.
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
--------------------------------------------------------------------------------