forked from goat-systems/go-tezos
-
Notifications
You must be signed in to change notification settings - Fork 0
/
operations.go
531 lines (439 loc) · 15.8 KB
/
operations.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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
package gotezos
import (
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"time"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/nacl/secretbox"
"github.com/Messer4/base58check"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/pbkdf2"
)
var (
// How many Transactions per batch are injected. I recommend 100. Now 30 for easier testing
batchSize = 100
// For (de)constructing addresses
tz1 = []byte{6, 161, 159}
edsk = []byte{43, 246, 78, 7}
edsk2 = []byte{13, 15, 58, 7}
edpk = []byte{13, 15, 37, 217}
edesk = []byte{7, 90, 60, 179, 41}
)
// CreateBatchPayment forges batch payments and returns them ready to inject to a Tezos RPC. PaymentFee must be expressed in mutez.
func (gt *GoTezos) CreateBatchPayment(payments []Payment, wallet Wallet, paymentFee int, gaslimit int, storageLimit int) ([]string, error) {
var operationSignatures []string
// Get current branch head
blockHead, err := gt.GetChainHead()
if err != nil {
return operationSignatures, err
}
// Get the counter for the payment address and increment it
counter, err := gt.getAddressCounter(wallet.Address)
if err != nil {
return operationSignatures, err
}
counter++
// Split our slice of []Payment into batches
batches := gt.splitPaymentIntoBatches(payments)
operationSignatures = make([]string, len(batches))
for k := range batches {
// Convert (ie: forge) each 'Payment' into an actual Tezos transfer operation
operationBytes, operationContents, newCounter, err := gt.forgeOperationBytes(blockHead.Hash, counter, wallet, batches[k], paymentFee, gaslimit, storageLimit)
if err != nil {
return operationSignatures, err
}
counter = newCounter
// Sign gt batch of operations with the secret key; return that signature
edsig, err := gt.signOperationBytes(operationBytes, wallet)
if err != nil {
return operationSignatures, err
}
// Extract and decode the bytes of the signature
decodedSignature := gt.decodeSignature(edsig)
decodedSignature = decodedSignature[10:(len(decodedSignature))]
// The signed bytes of gt batch
fullOperation := operationBytes + decodedSignature
// We can validate gt batch against the node for any errors
if err := gt.preApplyOperations(operationContents, edsig, blockHead); err != nil {
return operationSignatures, fmt.Errorf("CreateBatchPayment failed to Pre-Apply: %s", err)
}
// Add the signature (raw operation bytes & signature of operations) of gt batch of transfers to the returnning slice
// gt will be used to POST to /injection/operation
operationSignatures[k] = fullOperation
}
return operationSignatures, nil
}
// CreateWallet returns Wallet with the mnemonic and password provided
func (gt *GoTezos) CreateWallet(mnenomic string, password string) (Wallet, error) {
// Copied from https://github.com/tyler-smith/go-bip39/blob/dbb3b84ba2ef14e894f5e33d6c6e43641e665738/bip39.go#L268
seed := pbkdf2.Key([]byte(mnenomic), []byte("mnemonic"+password), 2048, 32, sha512.New)
privKey := ed25519.NewKeyFromSeed(seed)
pubKey := privKey.Public().(ed25519.PublicKey)
pubKeyBytes := []byte(pubKey)
signKp := KeyPair{PrivKey: privKey, PubKey: pubKeyBytes}
address, err := gt.generatePublicHash(pubKeyBytes)
if err != nil {
return Wallet{}, err
}
wallet := Wallet{
Address: address,
Mnemonic: mnenomic,
Kp: signKp,
Seed: seed,
Sk: gt.b58cencode(privKey, edsk),
Pk: gt.b58cencode(pubKeyBytes, edpk),
}
return wallet, nil
}
// ImportWallet returns an imported Wallet
func (gt *GoTezos) ImportWallet(address, public, secret string) (Wallet, error) {
var wallet Wallet
var signKP KeyPair
// Sanity check
secretLength := len(secret)
if secret[:4] != "edsk" || (secretLength != 98 && secretLength != 54) {
return wallet, fmt.Errorf("import Wallet Error: The provided secret does not conform to known patterns")
}
// Determine if 'secret' is an actual secret key or a seed
if secretLength == 98 {
// A full secret key
decodedSecretKey := gt.b58cdecode(secret, edsk)
// Public key is last 32 of decoded secret, re-encoded as edpk
publicKey := decodedSecretKey[32:]
signKP.PubKey = []byte(publicKey)
signKP.PrivKey = []byte(secret)
wallet.Sk = secret
} else if secretLength == 54 {
// "secret" is actually a seed
decodedSeed := gt.b58cdecode(secret, edsk2)
//signSeed := sodium.SignSeed{Bytes: decodedSeed}
// Reconstruct keypair from seed
privKey := ed25519.NewKeyFromSeed(decodedSeed)
pubKey := privKey.Public().(ed25519.PublicKey)
signKP.PrivKey = privKey
signKP.PubKey = []byte(pubKey)
wallet.Sk = gt.b58cencode(signKP.PrivKey, edsk)
} else {
return wallet, fmt.Errorf("import Wallet Error: Secret key is not the correct length")
}
wallet.Kp = signKP
// Generate public address from public key
generatedAddress, err := gt.generatePublicHash(signKP.PubKey)
if err != nil {
return wallet, fmt.Errorf("Import Wallet Error: %s", err)
}
if generatedAddress != address {
return wallet, fmt.Errorf("import Wallet Error: Reconstructed address '%s' and provided address '%s' do not match", generatedAddress, address)
}
wallet.Address = generatedAddress
// Genrate and check public key
generatedPublicKey := gt.b58cencode(signKP.PubKey, edpk)
if generatedPublicKey != public {
return wallet, fmt.Errorf("import Wallet Error: Reconstructed Pkh '%s' and provided Pkh '%s' do not match", generatedPublicKey, public)
}
wallet.Pk = generatedPublicKey
return wallet, nil
}
// ImportEncryptedWallet imports an encrypted wallet using password provided by caller.
// Caller should remove any 'encrypted:' scheme prefix.
func (gt *GoTezos) ImportEncryptedWallet(pw, encKey string) (Wallet, error) {
var wallet Wallet
// Check if user copied 'encrypted:' scheme prefix
if encKey[:5] != "edesk" || len(encKey) != 88 {
return wallet, fmt.Errorf("importEncryptedWallet: encrypted secret key does not conform to known patterns")
}
// Convert key from base58 to []byte
b58c, err := base58check.Decode(encKey)
if err != nil {
return wallet, err
}
// Strip off prefix and extract parts
esb := b58c[len(edesk):]
salt := esb[:8]
esm := esb[8:] // encrypted key
// Convert string pw to []byte
passWd := []byte(pw)
// Derive a key from password, salt and number of iterations
key := pbkdf2.Key(passWd, salt, 32768, 32, sha512.New)
var byteKey [32]byte
for i := range key {
byteKey[i] = key[i]
}
var out []byte
var emptyNonceBytes [24]byte
unencSecret, ok := secretbox.Open(out, esm, &emptyNonceBytes, &byteKey)
if !ok {
return wallet, fmt.Errorf("incorrect password for encrypted key")
}
privKey := ed25519.NewKeyFromSeed(unencSecret)
pubKey := privKey.Public().(ed25519.PublicKey)
pubKeyBytes := []byte(pubKey)
signKP := KeyPair{PrivKey: privKey, PubKey: pubKeyBytes}
// public key & secret key
wallet.Kp = signKP
wallet.Sk = gt.b58cencode(signKP.PrivKey, edsk)
wallet.Pk = gt.b58cencode(signKP.PubKey, edpk)
// Generate public address from public key
generatedAddress, err := gt.generatePublicHash(signKP.PubKey)
if err != nil {
return wallet, fmt.Errorf("importEncryptedWallet: %s", err)
}
wallet.Address = generatedAddress
return wallet, nil
}
//Sign previously forged Operation bytes using secret key of wallet
func (gt *GoTezos) signOperationBytes(operationBytes string, wallet Wallet) (string, error) {
//Prefixes
edsigByte := []byte{9, 245, 205, 134, 18}
watermark := []byte{3}
opBytes, err := hex.DecodeString(operationBytes)
if err != nil {
return "", fmt.Errorf("Unable to sign operation bytes: %s", err)
}
opBytes = append(watermark, opBytes...)
// Generic hash of 32 bytes
genericHash, err := blake2b.New(32, []byte{})
// Write operation bytes to hash
i, err := genericHash.Write(opBytes)
if i != len(opBytes) || err != nil {
return "", fmt.Errorf("Unable to write operations to generic hash")
}
finalHash := genericHash.Sum([]byte{})
// Sign the finalized generic hash of operations and b58 encode
sig := ed25519.Sign(wallet.Kp.PrivKey, finalHash)
//sig := sodium.Bytes(finalHash).SignDetached(wallet.Kp.PrivKey)
edsig := gt.b58cencode(sig, edsigByte)
return edsig, nil
}
func (gt *GoTezos) generatePublicHash(publicKey []byte) (string, error) {
hash, err := blake2b.New(20, []byte{})
hash.Write(publicKey)
if err != nil {
return "", fmt.Errorf("Unable to write public key to generic hash: %v", err)
}
return gt.b58cencode(hash.Sum(nil), tz1), nil
}
func (gt *GoTezos) forgeOperationBytes(branchHash string, counter int, wallet Wallet, batch []Payment, paymentFee int, gaslimit int, storageLimit int) (string, Conts, int, error) {
var contents Conts
var combinedOps []TransOp
//left here to display how to reveal a new wallet (needs funds to be revealed!)
/**
combinedOps = append(combinedOps, TransOp{Kind: "reveal", PublicKey: wallet.pk , Source: wallet.address, Fee: "0", GasLimit: "127", StorageLimit: "0", Counter: strCounter})
counter++
**/
for k := range batch {
if batch[k].Amount > 0 || len(batch[k].Parameters) > 0 {
operation := TransOp{
Kind: "transaction",
Source: wallet.Address,
Fee: strconv.Itoa(paymentFee),
GasLimit: strconv.Itoa(gaslimit),
StorageLimit: strconv.Itoa(storageLimit),
Amount: strconv.FormatFloat(roundPlus(batch[k].Amount, 0), 'f', -1, 64),
Destination: batch[k].Address,
Counter: strconv.Itoa(counter),
Parameters: batch[k].Parameters,
}
combinedOps = append(combinedOps, operation)
counter++
}
}
contents.Contents = combinedOps
contents.Branch = branchHash
var opBytes string
forge := "/chains/main/blocks/head/helpers/forge/operations"
output, err := gt.PostResponse(forge, contents.String())
if err != nil {
return "", contents, counter, fmt.Errorf("POST-Forge Operation Error: %s", err)
}
err = json.Unmarshal(output.Bytes, &opBytes)
if err != nil {
return "", contents, counter, fmt.Errorf("Forge Operation Error: %s", err)
}
return opBytes, contents, counter, nil
}
// Pre-apply an operation, or batch of operations, to a Tezos node to ensure correctness
func (gt *GoTezos) preApplyOperations(paymentOperations Conts, signature string, blockHead Block) error {
// Create a full transfer request
var transfer Transfer
transfer.Signature = signature
transfer.Contents = paymentOperations.Contents
transfer.Branch = blockHead.Hash
transfer.Protocol = blockHead.Protocol
// RPC says outer element must be JSON array
var transfers = []Transfer{transfer}
// Convert object to JSON string
transfersOp, err := json.Marshal(transfers)
if err != nil {
return err
}
if gt.debug {
fmt.Println("\n== preApplyOperations Submit:", string(transfersOp))
}
// POST the JSON to the RPC
preApplyResp, err := gt.PostResponse("/chains/main/blocks/head/helpers/preapply/operations", string(transfersOp))
if err != nil {
return err
}
if gt.debug {
fmt.Println("\n== preApplyOperations Result:", string(preApplyResp.Bytes))
}
return nil
}
// InjectOperation injects an signed operation string and returns the response
func (gt *GoTezos) InjectOperation(op string) ([]byte, error) {
post := "/injection/operation"
jsonBytes, err := json.Marshal(op)
if err != nil {
return nil, err
}
resp, err := gt.PostResponse(post, string(jsonBytes))
if err != nil {
return resp.Bytes, err
}
return resp.Bytes, nil
}
//Getting the Counter of an address from the RPC
func (gt *GoTezos) getAddressCounter(address string) (int, error) {
rpc := "/chains/main/blocks/head/context/contracts/" + address + "/counter"
resp, err := gt.GetResponse(rpc, "{}")
if err != nil {
return 0, err
}
rtnStr, err := unmarshalString(resp.Bytes)
if err != nil {
return 0, err
}
counter, err := strconv.Atoi(rtnStr)
return counter, err
}
func (gt *GoTezos) splitPaymentIntoBatches(rewards []Payment) [][]Payment {
var batches [][]Payment
for i := 0; i < len(rewards); i += batchSize {
end := i + batchSize
if end > len(rewards) {
end = len(rewards)
}
batches = append(batches, rewards[i:end])
}
return batches
}
//Helper function to return the decoded signature
func (gt *GoTezos) decodeSignature(sig string) string {
decBytes, err := base58check.Decode(sig)
if err != nil {
fmt.Println(err.Error())
return ""
}
return hex.EncodeToString(decBytes)
}
//Helper Function to get the right format for wallet.
func (gt *GoTezos) b58cencode(payload []byte, prefix []byte) string {
n := make([]byte, (len(prefix) + len(payload)))
for k := range prefix {
n[k] = prefix[k]
}
for l := range payload {
n[l+len(prefix)] = payload[l]
}
b58c := base58check.Encode(n)
return b58c
}
func (gt *GoTezos) b58cdecode(payload string, prefix []byte) []byte {
b58c, _ := base58check.Decode(payload)
return b58c[len(prefix):]
}
//Helper Functions to round float64
func roundPlus(f float64, places int) float64 {
shift := math.Pow(10, float64(places))
return round(f*shift) / shift
}
func round(f float64) float64 {
return math.Floor(f + .5)
}
func getRFC3339NowTimestamp() string {
return time.Now().UTC().Format(time.RFC3339)
}
// WatchOperationReceipt watches for maximum an hour every newly mined blocks and if it finds the wanted operation,
// it writes its receipt on a dedicated channel
func (gt *GoTezos) WatchOperationReceipt(watchedOpHash string, receiptChannel chan<- StructOperations, logChannel chan<- error) {
// init variables and constants
const predecessorsLength = 8 // number of predecessor blocks to query from known chain heads
var processedBlocks []string // list of processed operation hashes
var minDate = getRFC3339NowTimestamp() // min_date parameter when querying chain heads
// loop every minute during maximum an hour (operations can be considered lost after this time)
for i := 0; i < 60; i++ {
// sleep a minute
time.Sleep(time.Minute)
// get block hashes for known heads since minDate
rpc := "/chains/main/blocks?min_date=" + minDate + "&length=" + strconv.Itoa(predecessorsLength)
resp, err := gt.GetResponse(rpc, "{}")
if err != nil {
logChannel <- errors.New("Could not get block hashes: " + err.Error())
continue
}
var blockHashes RawBlockHashes
blockHashes, err = blockHashes.UnmarshalJSON(resp.Bytes)
if err != nil {
logChannel <- errors.New("Could not unmarshal block hashes: " + err.Error())
continue
}
// if no heads are known since minDate, try again in a minute
if len(blockHashes) < 1 {
continue
}
// update minDate for next iteration
minDate = getRFC3339NowTimestamp()
// if more than one head fit the query, try again in a minute with an updated minDate value.
// for that reason, predecessorsLength should be strictly larger than 1.
if len(blockHashes) > 1 {
continue
}
// process each block
BlockLoop:
for _, blockHash := range blockHashes[0] {
// exclude already processed blocks
for _, processedBlock := range processedBlocks {
if blockHash == processedBlock {
continue BlockLoop
}
}
// get operation hashes for a given block
opHashesSlice, err := gt.GetBlockRawOperationHashes(blockHash)
if err != nil {
continue
}
// look for watched operation hash
for listOffset, opHashes := range opHashesSlice {
for operationOffset, opHash := range opHashes {
if opHash == watchedOpHash {
// fetch operation receipt
var op StructOperations
rpc := "/chains/main/blocks/" + blockHash + "/operations/" +
strconv.Itoa(listOffset) + "/" + strconv.Itoa(operationOffset)
resp, err := gt.GetResponse(rpc, "{}")
if err != nil {
logChannel <- errors.New("Could not get operation: " + err.Error())
return
}
op, err = op.UnmarshalJSON(resp.Bytes)
if err != nil {
logChannel <- errors.New("Could not decode operation: " + err.Error())
return
}
// write the receipt on the result channel and return
receiptChannel <- op
return
}
}
}
// update processedBlocks
processedBlocks = append(processedBlocks, blockHash)
}
}
}