-
Notifications
You must be signed in to change notification settings - Fork 38
/
notary.go
510 lines (417 loc) · 13.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
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
package client
import (
"errors"
"fmt"
"strings"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"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"
"go.uber.org/zap"
)
type (
notary struct {
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
alphabetSource AlphabetKeys // source of alphabet node keys to prepare witness
notary util.Uint160
proxy util.Uint160
}
notaryCfg struct {
txValidTime, roundTime, fallbackTime uint32
alphabetSource AlphabetKeys
}
AlphabetKeys func() (keys.PublicKeys, error)
NotaryOption func(*notaryCfg)
)
const (
defaultNotaryValidTime = 50
defaultNotaryRoundTime = 100
defaultNotaryFallbackTime = 40
notaryBalanceOfMethod = "balanceOf"
setDesignateMethod = "designateAsRole"
notaryBalanceErrMsg = "can't fetch notary balance"
notaryNotEnabledPanicMsg = "notary support was not enabled on this client"
)
var errUnexpectedItems = errors.New("invalid number of NEO VM arguments on stack")
func defaultNotaryConfig(c *Client) *notaryCfg {
return ¬aryCfg{
txValidTime: defaultNotaryValidTime,
roundTime: defaultNotaryRoundTime,
fallbackTime: defaultNotaryFallbackTime,
alphabetSource: c.Committee,
}
}
// EnableNotarySupport creates notary structure in client that provides
// ability for client to get alphabet keys from committee or provided source
// and use proxy contract script hash to create tx for notary contract.
func (c *Client) EnableNotarySupport(proxy util.Uint160, opts ...NotaryOption) error {
cfg := defaultNotaryConfig(c)
for _, opt := range opts {
opt(cfg)
}
notaryContract, err := c.client.GetNativeContractHash(nativenames.Notary)
if err != nil {
return fmt.Errorf("can't get notary contract script hash: %w", err)
}
c.notary = ¬ary{
notary: notaryContract,
proxy: proxy,
txValidTime: cfg.txValidTime,
roundTime: cfg.roundTime,
fallbackTime: cfg.fallbackTime,
alphabetSource: cfg.alphabetSource,
}
return nil
}
// NotaryEnabled returns true if notary support was enabled in this instance
// of client by calling `EnableNotarySupport()`. Otherwise returns false.
func (c *Client) NotaryEnabled() bool {
return c.notary != 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.
//
// This function must be invoked after `EnableNotarySupport()` otherwise it
// throws panic.
func (c *Client) DepositNotary(amount fixedn.Fixed8, delta uint32) (util.Uint256, error) {
if c.notary == nil {
panic(notaryNotEnabledPanicMsg)
}
bc, err := c.client.GetBlockCount()
if err != nil {
return util.Uint256{}, fmt.Errorf("can't get blockchain height: %w", err)
}
txHash, err := c.client.TransferNEP17(
c.acc,
c.notary.notary,
c.gas,
int64(amount),
0,
[]interface{}{c.acc.PrivateKey().GetScriptHash(), int64(bc + delta)},
nil,
)
if err != nil {
return util.Uint256{}, fmt.Errorf("can't make notary deposit: %w", err)
}
c.logger.Debug("notary deposit invoke",
zap.Int64("amount", int64(amount)),
zap.Uint32("expire_at", bc+delta),
zap.Stringer("tx_hash", txHash.Reverse()))
return txHash, nil
}
// GetNotaryDeposit returns deposit of client's account in notary contract.
// Notary support should be enabled in client to use this function.
//
// This function must be invoked after `EnableNotarySupport()` otherwise it
// throws panic.
func (c *Client) GetNotaryDeposit() (int64, error) {
if c.notary == nil {
panic(notaryNotEnabledPanicMsg)
}
sh := c.acc.PrivateKey().PublicKey().GetScriptHash()
items, err := c.TestInvoke(c.notary.notary, notaryBalanceOfMethod, sh)
if err != nil {
return 0, fmt.Errorf("%v: %w", notaryBalanceErrMsg, err)
}
if len(items) != 1 {
return 0, fmt.Errorf("%v: %w", notaryBalanceErrMsg, errUnexpectedItems)
}
bigIntDeposit, err := items[0].TryInteger()
if err != nil {
return 0, fmt.Errorf("%v: %w", notaryBalanceErrMsg, err)
}
return bigIntDeposit.Int64(), nil
}
// UpdateNotaryList updates list of notary nodes in designate contract. Requires
// committee multi signature.
//
// This function must be invoked after `EnableNotarySupport()` otherwise it
// throws panic.
func (c *Client) UpdateNotaryList(list keys.PublicKeys) error {
if c.notary == nil {
panic(notaryNotEnabledPanicMsg)
}
return c.notaryInvokeAsCommittee(c.designate,
setDesignateMethod,
noderoles.P2PNotary,
list,
)
}
// UpdateNeoFSAlphabetList updates list of alphabet nodes in designate contract.
// As for side chain list should contain all inner ring nodes.
// Requires committee multi signature.
//
// This function must be invoked after `EnableNotarySupport()` otherwise it
// throws panic.
func (c *Client) UpdateNeoFSAlphabetList(list keys.PublicKeys) error {
if c.notary == nil {
panic(notaryNotEnabledPanicMsg)
}
return c.notaryInvokeAsCommittee(c.designate,
setDesignateMethod,
noderoles.NeoFSAlphabet,
list,
)
}
// NotaryInvoke 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.
//
// This function must be invoked after `EnableNotarySupport()` otherwise it
// throws panic.
func (c *Client) NotaryInvoke(contract util.Uint160, method string, args ...interface{}) error {
if c.notary == nil {
panic(notaryNotEnabledPanicMsg)
}
return c.notaryInvoke(false, contract, method, args...)
}
func (c *Client) notaryInvokeAsCommittee(contract util.Uint160, method string, args ...interface{}) error {
return c.notaryInvoke(true, contract, method, args...)
}
func (c *Client) notaryInvoke(committee bool, contract util.Uint160, method string, args ...interface{}) error {
alphabetList, err := c.notary.alphabetSource() // prepare arguments for test invocation
if err != nil {
return err
}
_, n := mn(alphabetList, committee)
u8n := uint8(n)
cosigners, err := c.notaryCosigners(alphabetList, committee)
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
}
// check invocation state
if test.State != HaltState {
return &NotHaltStateError{state: test.State, exception: test.FaultException}
}
// 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(alphabetList, committee)
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,
}
// 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 && !alreadyOnChainError(err) {
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, committee bool) ([]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, committee)
multisigScript, err := sc.CreateMultiSigRedeemScript(m, ir)
if err != nil {
return nil, fmt.Errorf("can't create ir multisig redeem script: %w", err)
}
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().SignHashable(uint32(c.client.GetNetwork()), tx)...,
),
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) notaryMultisigAccount(ir []*keys.PublicKey, committee bool) (*wallet.Account, error) {
m, _ := mn(ir, committee)
multisigAccount := wallet.NewAccountFromPrivateKey(c.acc.PrivateKey())
err := multisigAccount.ConvertMultisig(m, ir)
if err != nil {
return nil, fmt.Errorf("can't make inner ring multisig wallet: %w", err)
}
return multisigAccount, nil
}
func (c *Client) notaryTxValidationLimit() (uint32, error) {
bc, err := c.client.GetBlockCount()
if err != nil {
return 0, fmt.Errorf("can't get current blockchain height: %w", err)
}
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). If committee is
// true, returns M as N/2+1.
func mn(ir []*keys.PublicKey, committee bool) (m int, n int) {
n = len(ir)
if committee {
m = n/2 + 1
} else {
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
}
}
// WithAlphabetSource returns a notary support option for client
// that specifies function to return list of alphabet node keys.
// By default notary subsystem uses committee as a source. This is
// valid for side chain but notary in main chain should override it.
func WithAlphabetSource(t AlphabetKeys) NotaryOption {
return func(c *notaryCfg) {
c.alphabetSource = t
}
}
const alreadyOnChainErrorMessage = "already on chain"
// Neo RPC node can return `core.ErrInvalidAttribute` error with
// `conflicting transaction <> is already on chain` message. This
// error is expected and ignored. As soon as main tx persisted on
// chain everything is fine. This happens because notary contract
// requires 5 out of 7 signatures to send main tx, thus last two
// notary requests may be processed after main tx appeared on chain.
func alreadyOnChainError(err error) bool {
return strings.Contains(err.Error(), alreadyOnChainErrorMessage)
}