-
Notifications
You must be signed in to change notification settings - Fork 38
/
notary.go
465 lines (375 loc) · 11.6 KB
/
notary.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
package client
import (
"crypto/elliptic"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type (
notary struct {
// extra fee to check witness of proxy contract
// neo-go does not have an option to calculate it exactly right now
extraVerifyFee int64
txValidTime uint32 // minimum amount of blocks when mainTx will be valid
roundTime uint32 // extra amount of blocks to synchronize sidechain height diff of inner ring nodes
fallbackTime uint32 // amount of blocks before fallbackTx will be sent
notary util.Uint160
proxy util.Uint160
netmap util.Uint160
}
notaryCfg struct {
txValidTime, roundTime, fallbackTime uint32
}
NotaryOption func(*notaryCfg)
)
const (
defaultNotaryValidTime = 50
defaultNotaryRoundTime = 100
defaultNotaryFallbackTime = 40
innerRingListMethod = "innerRingList"
notaryBalanceOfMethod = "balanceOf"
notaryBalanceErrMsg = "can't fetch notary balance"
)
var (
errNotaryNotEnabled = errors.New("notary support was not enabled on this client")
errInvalidIR = errors.New("invalid inner ring list from netmap contract")
errUnexpectedItems = errors.New("invalid number of NEO VM arguments on stack")
)
func defaultNotaryConfig() *notaryCfg {
return ¬aryCfg{
txValidTime: defaultNotaryValidTime,
roundTime: defaultNotaryRoundTime,
fallbackTime: defaultNotaryFallbackTime,
}
}
// EnableNotarySupport creates notary structure in client that provides
// ability for client to get inner ring list from netmap contract and
// use proxy contract script hash to create tx for notary contract.
func (c *Client) EnableNotarySupport(proxy, netmap util.Uint160, opts ...NotaryOption) error {
cfg := defaultNotaryConfig()
for _, opt := range opts {
opt(cfg)
}
notaryContract, err := c.client.GetNativeContractHash(nativenames.Notary)
if err != nil {
return errors.Wrap(err, "can't get notary contract script hash")
}
c.notary = ¬ary{
notary: notaryContract,
proxy: proxy,
netmap: netmap,
txValidTime: cfg.txValidTime,
roundTime: cfg.roundTime,
fallbackTime: cfg.fallbackTime,
}
return nil
}
// DepositNotary calls notary deposit method. Deposit is required to operate
// with notary contract. It used by notary contract in to produce fallback tx
// if main tx failed to create. Deposit isn't last forever, so it should
// be called periodically. Notary support should be enabled in client to
// use this function.
func (c *Client) DepositNotary(amount fixedn.Fixed8, delta uint32) error {
if c.notary == nil {
return errNotaryNotEnabled
}
bc, err := c.client.GetBlockCount()
if err != nil {
return errors.Wrap(err, "can't get blockchain height")
}
txHash, err := c.client.TransferNEP17(
c.acc,
c.notary.notary,
c.gas,
int64(amount),
0,
[]interface{}{c.acc.PrivateKey().GetScriptHash(), int64(bc + delta)},
)
if err != nil {
return errors.Wrap(err, "can't make notary deposit")
}
c.logger.Debug("notary deposit invoke",
zap.Int64("amount", int64(amount)),
zap.Uint32("expire_at", bc+delta),
zap.Stringer("tx_hash", txHash))
return nil
}
// GetNotaryDeposit returns deposit of client's account in notary contract.
// Notary support should be enabled in client to use this function.
func (c *Client) GetNotaryDeposit() (int64, error) {
if c.notary == nil {
return 0, errNotaryNotEnabled
}
sh := c.acc.PrivateKey().PublicKey().GetScriptHash()
items, err := c.TestInvoke(c.notary.notary, notaryBalanceOfMethod, sh)
if err != nil {
return 0, errors.Wrap(err, notaryBalanceErrMsg)
}
if len(items) != 1 {
return 0, errors.Wrap(errUnexpectedItems, notaryBalanceErrMsg)
}
bigIntDeposit, err := items[0].TryInteger()
if err != nil {
return 0, errors.Wrap(err, notaryBalanceErrMsg)
}
return bigIntDeposit.Int64(), nil
}
// Invoke invokes contract method by sending tx to notary contract in
// blockchain. Fallback tx is a `RET`. Notary support should be enabled
// in client to use this function.
//
// Supported args types: int64, string, util.Uint160, []byte and bool.
func (c *Client) NotaryInvoke(contract util.Uint160, method string, args ...interface{}) error {
if c.notary == nil {
return errNotaryNotEnabled
}
// prepare arguments for test invocation
irList, err := c.notaryInnerRingList()
if err != nil {
return err
}
_, n := mn(irList)
u8n := uint8(n)
cosigners, err := c.notaryCosigners(irList)
if err != nil {
return err
}
params, err := invocationParams(args...)
if err != nil {
return err
}
// make test invocation of the method
test, err := c.client.InvokeFunction(contract, method, params, cosigners)
if err != nil {
return err
}
// if test invocation failed, then return error
if len(test.Script) == 0 {
return errEmptyInvocationScript
}
// after test invocation we build main multisig transaction
multiaddrAccount, err := c.notaryMultisigAccount(irList)
if err != nil {
return err
}
until, err := c.notaryTxValidationLimit()
if err != nil {
return err
}
// prepare main tx
mainTx := &transaction.Transaction{
Nonce: 1,
SystemFee: test.GasConsumed,
ValidUntilBlock: until,
Script: test.Script,
Attributes: []transaction.Attribute{
{
Type: transaction.NotaryAssistedT,
Value: &transaction.NotaryAssisted{NKeys: u8n},
},
},
Signers: cosigners,
Network: c.client.GetNetwork(),
}
// calculate notary fee
notaryFee, err := c.client.CalculateNotaryFee(u8n)
if err != nil {
return err
}
// add network fee for cosigners
err = c.client.AddNetworkFee(
mainTx,
notaryFee,
c.notaryAccounts(multiaddrAccount)...,
)
if err != nil {
return err
}
// define witnesses
mainTx.Scripts = c.notaryWitnesses(multiaddrAccount, mainTx)
resp, err := c.client.SignAndPushP2PNotaryRequest(mainTx,
[]byte{byte(opcode.RET)},
-1,
0,
c.notary.fallbackTime,
c.acc)
if err != nil {
return err
}
c.logger.Debug("notary request invoked",
zap.String("method", method),
zap.Stringer("tx_hash", resp.Hash().Reverse()))
return nil
}
func (c *Client) notaryCosigners(ir []*keys.PublicKey) ([]transaction.Signer, error) {
s := make([]transaction.Signer, 0, 3)
// first we have proxy contract signature, as it will pay for the execution
s = append(s, transaction.Signer{
Account: c.notary.proxy,
Scopes: transaction.None,
})
// then we have inner ring multiaddress signature
m, _ := mn(ir)
multisigScript, err := sc.CreateMultiSigRedeemScript(m, ir)
if err != nil {
return nil, errors.Wrap(err, "can't create ir multisig redeem script")
}
s = append(s, transaction.Signer{
Account: hash.Hash160(multisigScript),
Scopes: transaction.Global,
})
// last one is a placeholder for notary contract signature
s = append(s, transaction.Signer{
Account: c.notary.notary,
Scopes: transaction.None,
})
return s, nil
}
func (c *Client) notaryAccounts(multiaddr *wallet.Account) []*wallet.Account {
if multiaddr == nil {
return nil
}
a := make([]*wallet.Account, 0, 3)
// first we have proxy account, as it will pay for the execution
a = append(a, &wallet.Account{
Contract: &wallet.Contract{
Deployed: true,
},
})
// then we have inner ring multiaddress account
a = append(a, multiaddr)
// last one is a placeholder for notary contract account
a = append(a, &wallet.Account{
Contract: &wallet.Contract{},
})
return a
}
func (c *Client) notaryWitnesses(multiaddr *wallet.Account, tx *transaction.Transaction) []transaction.Witness {
if multiaddr == nil || tx == nil {
return nil
}
w := make([]transaction.Witness, 0, 3)
// first we have empty proxy witness, because notary will execute `Verify`
// method on the proxy contract to check witness
w = append(w, transaction.Witness{
InvocationScript: []byte{},
VerificationScript: []byte{},
})
// then we have inner ring multiaddress witness
w = append(w, transaction.Witness{
InvocationScript: append(
[]byte{byte(opcode.PUSHDATA1), 64},
multiaddr.PrivateKey().Sign(tx.GetSignedPart())...,
),
VerificationScript: multiaddr.GetVerificationScript(),
})
// last one is a placeholder for notary contract witness
w = append(w, transaction.Witness{
InvocationScript: append(
[]byte{byte(opcode.PUSHDATA1), 64},
make([]byte, 64)...,
),
VerificationScript: []byte{},
})
return w
}
func (c *Client) notaryInnerRingList() ([]*keys.PublicKey, error) {
data, err := c.TestInvoke(c.notary.netmap, innerRingListMethod)
if err != nil {
return nil, errors.Wrap(err, "test invoke error")
}
if len(data) == 0 {
return nil, errors.Wrap(errInvalidIR, "test invoke returned empty stack")
}
prms, err := ArrayFromStackItem(data[0])
if err != nil {
return nil, errors.Wrap(err, "test invoke returned non array element")
}
res := make([]*keys.PublicKey, 0, len(prms))
for i := range prms {
nodePrms, err := ArrayFromStackItem(prms[i])
if err != nil {
return nil, errors.Wrap(err, "inner ring node structure is not an array")
}
if len(nodePrms) == 0 {
return nil, errors.Wrap(errInvalidIR, "inner ring node structure is empty array")
}
rawKey, err := BytesFromStackItem(nodePrms[0])
if err != nil {
return nil, errors.Wrap(err, "inner ring public key is not slice of bytes")
}
key, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256())
if err != nil {
return nil, errors.Wrap(err, "can't parse inner ring public key bytes")
}
res = append(res, key)
}
return res, nil
}
func (c *Client) notaryMultisigAccount(ir []*keys.PublicKey) (*wallet.Account, error) {
m, _ := mn(ir)
multisigAccount := wallet.NewAccountFromPrivateKey(c.acc.PrivateKey())
err := multisigAccount.ConvertMultisig(m, ir)
if err != nil {
return nil, errors.Wrap(err, "can't make inner ring multisig wallet")
}
return multisigAccount, nil
}
func (c *Client) notaryTxValidationLimit() (uint32, error) {
bc, err := c.client.GetBlockCount()
if err != nil {
return 0, errors.Wrap(err, "can't get current blockchain height")
}
min := bc + c.notary.txValidTime
rounded := (min/c.notary.roundTime + 1) * c.notary.roundTime
return rounded, nil
}
func invocationParams(args ...interface{}) ([]sc.Parameter, error) {
params := make([]sc.Parameter, 0, len(args))
for i := range args {
param, err := toStackParameter(args[i])
if err != nil {
return nil, err
}
params = append(params, param)
}
return params, nil
}
// mn returns M and N multi signature numbers. For NeoFS N is a length of
// inner ring list, and M is a 2/3+1 of it (like in dBFT).
func mn(ir []*keys.PublicKey) (m int, n int) {
n = len(ir)
m = n*2/3 + 1
return
}
// WithTxValidTime returns a notary support option for client
// that specifies minimum amount of blocks when mainTx will be valid.
func WithTxValidTime(t uint32) NotaryOption {
return func(c *notaryCfg) {
c.txValidTime = t
}
}
// WithRoundTime returns a notary support option for client
// that specifies extra blocks to synchronize side chain
// height diff of inner ring nodes.
func WithRoundTime(t uint32) NotaryOption {
return func(c *notaryCfg) {
c.roundTime = t
}
}
// WithFallbackTime returns a notary support option for client
// that specifies amount of blocks before fallbackTx will be sent.
// Should be less than TxValidTime.
func WithFallbackTime(t uint32) NotaryOption {
return func(c *notaryCfg) {
c.fallbackTime = t
}
}