-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchainverify.go
270 lines (232 loc) · 7.52 KB
/
chainverify.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package client
import (
"bytes"
"fmt"
"sort"
"time"
"github.com/mit-dci/go-bverify/utils"
"github.com/mit-dci/go-bverify/logging"
btcwire "github.com/mit-dci/go-bverify/bitcoin/wire"
"github.com/mit-dci/go-bverify/wire"
"github.com/tidwall/buntdb"
)
// loadStuff loads the last server commitment into memory
func (c *Client) loadStuff() error {
// First, we try to get the last commitment hash from the database
hash := [32]byte{}
err := c.db.View(func(tx *buntdb.Tx) error {
lastCommitHash, err := tx.Get("commitment-last")
if err != nil {
return err
}
copy(hash[:], []byte(lastCommitHash))
return nil
})
if err != nil {
// If the error is a not found, we'll just return and leave the
// lastServerCommitment empty.
if err != buntdb.ErrNotFound {
// Something else went wrong, return the error
return err
}
return nil
}
// Fetch the commitment details of the last known commitment
c.lastServerCommitment, err = c.getCommitment(hash[:])
return err
}
// getCommitment retrieves the commitment details of a single commitment
// from the database by its hash
func (c *Client) getCommitment(hash []byte) (*wire.Commitment, error) {
var comm *wire.Commitment
err := c.db.View(func(tx *buntdb.Tx) error {
b, err := tx.Get(fmt.Sprintf("commitment-%x", hash))
if err != nil {
return err
}
comm = wire.CommitmentFromBytes([]byte(b))
return nil
})
if err != nil {
return nil, err
}
return comm, nil
}
// verifyCommitment will check a commitment's validity. It will verify the
// validity of the commitment transaction
func (c *Client) verifyCommitment(comm *wire.Commitment) error {
logging.Debugf("Verifying commitment %x (block %s)", comm.Commitment, comm.IncludedInBlock.String())
logging.Debugf("Transaction is %s", comm.TxHash.String())
// First and foremost, check if the block specified by the server is actually
// known to us in the header chain.
retry := 3
var header *btcwire.BlockHeader
var err error
// Start a retry loop. If the block isn't found right away, ask for headers
// and try again
for {
header, err = c.GetBlockHeaderByHash(comm.IncludedInBlock)
if err != nil {
retry--
if retry == 0 {
return err
}
// First try to sync headers and then try again
c.SPVAskHeaders()
for {
if c.SPVSynced() {
break
}
time.Sleep(1 * time.Second)
}
} else {
break
}
}
logging.Debugf("Found the block specified in the commitment in our header chain")
// Now that we've found the header, we should verify the provided merkle proof
// actually checks out with the block's merkle root.
checksOut := comm.MerkleProof.Check(comm.TxHash, &header.MerkleRoot)
if !checksOut {
return fmt.Errorf("Merkle proof is incorrect")
} else {
logging.Debugf("Merkle proof is correct")
}
// This already looks very good, but in order to prove non-equivocation (the server
// might have made _more_ commitments than just this one. In order to check that,
// we check that the first input to the transaction is actually output 1 (the change)
// of the last commitment transaction. This is not needed for the "maiden" commitment
// which always has the hash 523e59cfc5235b915dc89de188d87449453b083a8b7d97c1ee64d875da403361
tx := btcwire.NewMsgTx(1)
tx.Deserialize(bytes.NewBuffer(comm.RawTx))
if bytes.Equal(comm.Commitment[:], utils.MaidenHash()) {
logging.Debugf("Skipping the TXO chain check since this is the first commitment")
} else {
logging.Debugf("Previous outpoint expected: [%x/1] - First input: [%x/%d]", c.lastServerCommitment.TxHash[:], tx.TxIn[0].PreviousOutPoint.Hash[:], tx.TxIn[0].PreviousOutPoint.Index)
if tx.TxIn[0].PreviousOutPoint.Index != 1 || !tx.TxIn[0].PreviousOutPoint.Hash.IsEqual(c.lastServerCommitment.TxHash) {
return fmt.Errorf("Commitment transaction's first input is not the change output of the last commitment. This breaks the chain and is invalid.")
}
}
logging.Debugf("Everything checks out, this commitment is valid!")
return nil
}
// ClearCommitments delets the client-side cache of the server commitments. This will
// trigger a resynchronization of the commitments and proofs.
func (c *Client) ClearCommitments() error {
keys := make([]string, 0)
err := c.db.View(func(tx *buntdb.Tx) error {
return tx.AscendRange("", "commitment-", "commitment.", func(key, value string) bool {
keys = append(keys, key)
return true
})
})
if err != nil {
return err
}
return c.db.Update(func(tx *buntdb.Tx) error {
for _, k := range keys {
_, err := tx.Delete(k)
if err != nil {
return err
}
}
return nil
})
}
func (c *Client) getAllCommitments() ([]*wire.Commitment, error) {
returnVal := make([]*wire.Commitment, 0)
err := c.db.View(func(tx *buntdb.Tx) error {
return tx.AscendRange("", "commitment-", "commitment.", func(key, value string) bool {
if key != "commitment-last" {
returnVal = append(returnVal, wire.CommitmentFromBytes([]byte(value)))
}
return true
})
})
if err != nil {
return nil, err
}
sort.Slice(returnVal, func(i, j int) bool {
return returnVal[i].TriggeredAtBlockHeight < returnVal[j].TriggeredAtBlockHeight
})
return returnVal, nil
}
// saveCommitment writes the details of a commitment to our client-side database
func (c *Client) saveCommitment(comm *wire.Commitment) error {
err := c.db.Update(func(dtx *buntdb.Tx) error {
key := fmt.Sprintf("commitment-%x", comm.Commitment)
_, _, err := dtx.Set(key, string(comm.Bytes()), nil)
if err != nil {
return err
}
// Store a mapping from block to commitment if we want to look up
// the reverse
key = fmt.Sprintf("block-%x", comm.IncludedInBlock[:])
_, _, err = dtx.Set(key, string(comm.Commitment[:]), nil)
if err != nil {
return err
}
// Also store the last commitment
_, _, err = dtx.Set("commitment-last", string(comm.Commitment[:]), nil)
return err
})
if err != nil {
return err
}
// Set the commitment as the last one
c.lastServerCommitment = comm
return nil
}
// verifyLoop is the full-client's main loop that will check validity of both the
// commitment transaction and the commitment proofs. It does this every 20 seconds
func (c *Client) verifyLoop() {
for {
if c.SPVSynced() {
lastCommitHash := [32]byte{}
if c.lastServerCommitment != nil {
copy(lastCommitHash[:], c.lastServerCommitment.Commitment[:])
}
// Fetch server commitments since our last known commitment
hist, err := c.GetCommitmentHistory(lastCommitHash)
if err != nil {
panic(err)
}
logging.Debugf("Got %d commitments", len(hist))
// For each commitment, verify if it's correct and then save it
retry := false
for _, comm := range hist {
err = c.verifyCommitment(comm)
if err != nil {
// If the block is not found, it could mean the commitment
// transaction reorged. We'll get the updated blockhash
// when we ask the server again - once the server has
// processed the reorg.
if err.Error() == "Block not found" {
logging.Warnf("The server says commitment %x is in block %s (tx %s), but we don't have that. Retry (could be reorg-related)", comm.Commitment[:], comm.IncludedInBlock.String(), comm.TxHash.String())
time.Sleep(time.Second * 20)
retry = true
break
}
panic(err)
}
err = c.saveCommitment(comm)
if err != nil {
panic(err)
}
}
if retry {
continue
}
// If we got new commitments, we should also update proofs
if len(hist) > 0 {
err = c.updateProofs()
if err != nil {
panic(err)
}
}
time.Sleep(time.Second * 20)
} else {
time.Sleep(time.Second * 1)
}
}
}